| /* 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 "mod_proxy.h" |
| #include "util_fcgi.h" |
| #include "util_script.h" |
| #include "ap_expr.h" |
| |
| module AP_MODULE_DECLARE_DATA proxy_fcgi_module; |
| |
| typedef struct { |
| ap_expr_info_t *cond; |
| ap_expr_info_t *subst; |
| const char *envname; |
| } sei_entry; |
| |
| typedef struct { |
| int need_dirwalk; |
| } fcgi_req_config_t; |
| |
| /* We will assume FPM, but still differentiate */ |
| typedef enum { |
| BACKEND_DEFAULT_UNKNOWN = 0, |
| BACKEND_FPM, |
| BACKEND_GENERIC, |
| } fcgi_backend_t; |
| |
| |
| #define FCGI_MAY_BE_FPM(dconf) \ |
| (dconf && \ |
| ((dconf->backend_type == BACKEND_DEFAULT_UNKNOWN) || \ |
| (dconf->backend_type == BACKEND_FPM))) |
| |
| typedef struct { |
| fcgi_backend_t backend_type; |
| apr_array_header_t *env_fixups; |
| } fcgi_dirconf_t; |
| |
| /* |
| * Canonicalise http-like URLs. |
| * scheme is the scheme for the URL |
| * url is the URL starting with the first '/' |
| * def_port is the default port for this scheme. |
| */ |
| static int proxy_fcgi_canon(request_rec *r, char *url) |
| { |
| char *host, sport[7]; |
| const char *err; |
| char *path; |
| apr_port_t port, def_port; |
| fcgi_req_config_t *rconf = NULL; |
| const char *pathinfo_type = NULL; |
| |
| if (ap_cstr_casecmpn(url, "fcgi:", 5) == 0) { |
| url += 5; |
| } |
| else { |
| return DECLINED; |
| } |
| |
| port = def_port = ap_proxy_port_of_scheme("fcgi"); |
| |
| ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, |
| "canonicalising URL %s", url); |
| err = ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port); |
| if (err) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01059) |
| "error parsing URL %s: %s", url, err); |
| return HTTP_BAD_REQUEST; |
| } |
| |
| if (port != def_port) |
| apr_snprintf(sport, sizeof(sport), ":%d", port); |
| else |
| sport[0] = '\0'; |
| |
| if (ap_strchr_c(host, ':')) { |
| /* if literal IPv6 address */ |
| host = apr_pstrcat(r->pool, "[", host, "]", NULL); |
| } |
| |
| if (apr_table_get(r->notes, "proxy-nocanon") |
| || apr_table_get(r->notes, "proxy-noencode")) { |
| path = url; /* this is the raw/encoded path */ |
| } |
| else { |
| core_dir_config *d = ap_get_core_module_config(r->per_dir_config); |
| int flags = d->allow_encoded_slashes && !d->decode_encoded_slashes ? PROXY_CANONENC_NOENCODEDSLASHENCODING : 0; |
| |
| path = ap_proxy_canonenc_ex(r->pool, url, strlen(url), enc_path, flags, |
| r->proxyreq); |
| if (!path) { |
| return HTTP_BAD_REQUEST; |
| } |
| } |
| /* |
| * If we have a raw control character or a ' ' in nocanon path, |
| * correct encoding was missed. |
| */ |
| if (path == url && *ap_scan_vchar_obstext(path)) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10414) |
| "To be forwarded path contains control " |
| "characters or spaces"); |
| return HTTP_FORBIDDEN; |
| } |
| |
| r->filename = apr_pstrcat(r->pool, "proxy:fcgi://", host, sport, "/", |
| path, NULL); |
| |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01060) |
| "set r->filename to %s", r->filename); |
| |
| rconf = ap_get_module_config(r->request_config, &proxy_fcgi_module); |
| if (rconf == NULL) { |
| rconf = apr_pcalloc(r->pool, sizeof(fcgi_req_config_t)); |
| ap_set_module_config(r->request_config, &proxy_fcgi_module, rconf); |
| } |
| |
| if (NULL != (pathinfo_type = apr_table_get(r->subprocess_env, "proxy-fcgi-pathinfo"))) { |
| /* It has to be on disk for this to work */ |
| if (!strcasecmp(pathinfo_type, "full")) { |
| rconf->need_dirwalk = 1; |
| ap_unescape_url_keep2f(path, 0); |
| } |
| else if (!strcasecmp(pathinfo_type, "first-dot")) { |
| char *split = ap_strchr(path, '.'); |
| if (split) { |
| char *slash = ap_strchr(split, '/'); |
| if (slash) { |
| r->path_info = apr_pstrdup(r->pool, slash); |
| ap_unescape_url_keep2f(r->path_info, 0); |
| *slash = '\0'; /* truncate path */ |
| } |
| } |
| } |
| else if (!strcasecmp(pathinfo_type, "last-dot")) { |
| char *split = ap_strrchr(path, '.'); |
| if (split) { |
| char *slash = ap_strchr(split, '/'); |
| if (slash) { |
| r->path_info = apr_pstrdup(r->pool, slash); |
| ap_unescape_url_keep2f(r->path_info, 0); |
| *slash = '\0'; /* truncate path */ |
| } |
| } |
| } |
| else { |
| /* before proxy-fcgi-pathinfo had multi-values. This requires the |
| * the FCGI server to fixup PATH_INFO because it's the entire path |
| */ |
| r->path_info = apr_pstrcat(r->pool, "/", path, NULL); |
| if (!strcasecmp(pathinfo_type, "unescape")) { |
| ap_unescape_url_keep2f(r->path_info, 0); |
| } |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01061) |
| "set r->path_info to %s", r->path_info); |
| } |
| } |
| |
| return OK; |
| } |
| |
| |
| /* |
| ProxyFCGISetEnvIf "reqenv('PATH_INFO') =~ m#/foo(\d+)\.php$#" COVENV1 "$1" |
| ProxyFCGISetEnvIf "reqenv('PATH_INFO') =~ m#/foo(\d+)\.php$#" PATH_INFO "/foo.php" |
| ProxyFCGISetEnvIf "reqenv('PATH_TRANSLATED') =~ m#(/.*foo)(\d+)(.*)#" PATH_TRANSLATED "$1$3" |
| */ |
| static apr_status_t fix_cgivars(request_rec *r, fcgi_dirconf_t *dconf) |
| { |
| sei_entry *entries; |
| const char *err, *src; |
| int i = 0, rc = 0; |
| ap_regmatch_t regm[AP_MAX_REG_MATCH]; |
| |
| entries = (sei_entry *) dconf->env_fixups->elts; |
| for (i = 0; i < dconf->env_fixups->nelts; i++) { |
| sei_entry *entry = &entries[i]; |
| |
| rc = ap_expr_exec_re(r, entry->cond, AP_MAX_REG_MATCH, regm, &src, &err); |
| if (rc < 0) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10241) |
| "fix_cgivars: Condition eval returned %d: %s", |
| rc, err); |
| return APR_EGENERAL; |
| } |
| else if (rc == 0) { |
| continue; /* evaluated false */ |
| } |
| |
| if (entry->envname[0] == '!') { |
| apr_table_unset(r->subprocess_env, entry->envname+1); |
| } |
| else { |
| const char *val = ap_expr_str_exec_re(r, entry->subst, AP_MAX_REG_MATCH, regm, &src, &err); |
| if (err) { |
| ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(03514) |
| "Error evaluating expression for replacement of %s: '%s'", |
| entry->envname, err); |
| continue; |
| } |
| if (APLOGrtrace4(r)) { |
| const char *oldval = apr_table_get(r->subprocess_env, entry->envname); |
| ap_log_rerror(APLOG_MARK, APLOG_TRACE4, 0, r, |
| "fix_cgivars: override %s from '%s' to '%s'", |
| entry->envname, oldval, val); |
| |
| } |
| apr_table_setn(r->subprocess_env, entry->envname, val); |
| } |
| } |
| return APR_SUCCESS; |
| } |
| |
| /* Wrapper for apr_socket_sendv that handles updating the worker stats. */ |
| static apr_status_t send_data(proxy_conn_rec *conn, |
| struct iovec *vec, |
| int nvec, |
| apr_size_t *len) |
| { |
| apr_status_t rv = APR_SUCCESS; |
| apr_size_t written = 0, to_write = 0; |
| int i, offset; |
| apr_socket_t *s = conn->sock; |
| |
| for (i = 0; i < nvec; i++) { |
| to_write += vec[i].iov_len; |
| } |
| |
| offset = 0; |
| while (to_write) { |
| apr_size_t n = 0; |
| rv = apr_socket_sendv(s, vec + offset, nvec - offset, &n); |
| if (rv != APR_SUCCESS) { |
| break; |
| } |
| if (n > 0) { |
| written += n; |
| if (written >= to_write) |
| break; /* short circuit out */ |
| for (i = offset; i < nvec; ) { |
| if (n >= vec[i].iov_len) { |
| offset++; |
| n -= vec[i++].iov_len; |
| } else { |
| vec[i].iov_len -= n; |
| vec[i].iov_base = (char *) vec[i].iov_base + n; |
| break; |
| } |
| } |
| } |
| } |
| |
| conn->worker->s->transferred += written; |
| *len = written; |
| |
| return rv; |
| } |
| |
| /* Wrapper for apr_socket_recv that handles updating the worker stats. */ |
| static apr_status_t get_data(proxy_conn_rec *conn, |
| char *buffer, |
| apr_size_t *buflen) |
| { |
| apr_status_t rv = apr_socket_recv(conn->sock, buffer, buflen); |
| |
| if (rv == APR_SUCCESS) { |
| conn->worker->s->read += *buflen; |
| } |
| |
| return rv; |
| } |
| |
| static apr_status_t get_data_full(proxy_conn_rec *conn, |
| char *buffer, |
| apr_size_t buflen) |
| { |
| apr_size_t readlen; |
| apr_size_t cumulative_len = 0; |
| apr_status_t rv; |
| |
| do { |
| readlen = buflen - cumulative_len; |
| rv = get_data(conn, buffer + cumulative_len, &readlen); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| cumulative_len += readlen; |
| } while (cumulative_len < buflen); |
| |
| return APR_SUCCESS; |
| } |
| |
| static apr_status_t send_begin_request(proxy_conn_rec *conn, |
| apr_uint16_t request_id) |
| { |
| struct iovec vec[2]; |
| ap_fcgi_header header; |
| unsigned char farray[AP_FCGI_HEADER_LEN]; |
| ap_fcgi_begin_request_body brb; |
| unsigned char abrb[AP_FCGI_HEADER_LEN]; |
| apr_size_t len; |
| |
| ap_fcgi_fill_in_header(&header, AP_FCGI_BEGIN_REQUEST, request_id, |
| sizeof(abrb), 0); |
| |
| ap_fcgi_fill_in_request_body(&brb, AP_FCGI_RESPONDER, |
| ap_proxy_connection_reusable(conn) |
| ? AP_FCGI_KEEP_CONN : 0); |
| |
| ap_fcgi_header_to_array(&header, farray); |
| ap_fcgi_begin_request_body_to_array(&brb, abrb); |
| |
| vec[0].iov_base = (void *)farray; |
| vec[0].iov_len = sizeof(farray); |
| vec[1].iov_base = (void *)abrb; |
| vec[1].iov_len = sizeof(abrb); |
| |
| return send_data(conn, vec, 2, &len); |
| } |
| |
| static apr_status_t send_environment(proxy_conn_rec *conn, request_rec *r, |
| apr_pool_t *temp_pool, |
| apr_uint16_t request_id) |
| { |
| const apr_array_header_t *envarr; |
| const apr_table_entry_t *elts; |
| struct iovec vec[2]; |
| ap_fcgi_header header; |
| unsigned char farray[AP_FCGI_HEADER_LEN]; |
| char *body; |
| apr_status_t rv; |
| apr_size_t avail_len, len, required_len; |
| int next_elem, starting_elem; |
| fcgi_req_config_t *rconf = ap_get_module_config(r->request_config, &proxy_fcgi_module); |
| fcgi_dirconf_t *dconf = ap_get_module_config(r->per_dir_config, &proxy_fcgi_module); |
| |
| if (rconf) { |
| if (rconf->need_dirwalk) { |
| ap_directory_walk(r); |
| } |
| } |
| |
| /* Strip proxy: prefixes */ |
| if (r->filename) { |
| char *newfname = NULL; |
| |
| if (!strncmp(r->filename, "proxy:balancer://", 17)) { |
| newfname = apr_pstrdup(r->pool, r->filename+17); |
| } |
| |
| if (!FCGI_MAY_BE_FPM(dconf)) { |
| if (!strncmp(r->filename, "proxy:fcgi://", 13)) { |
| /* If we strip this under FPM, and any internal redirect occurs |
| * on PATH_INFO, FPM may use PATH_TRANSLATED instead of |
| * SCRIPT_FILENAME (a la mod_fastcgi + Action). |
| */ |
| newfname = apr_pstrdup(r->pool, r->filename+13); |
| } |
| /* Query string in environment only */ |
| if (newfname && r->args && *r->args) { |
| char *qs = strrchr(newfname, '?'); |
| if (qs && !strcmp(qs+1, r->args)) { |
| *qs = '\0'; |
| } |
| } |
| } |
| |
| if (newfname) { |
| newfname = ap_strchr(newfname, '/'); |
| r->filename = newfname; |
| } |
| } |
| |
| ap_add_common_vars(r); |
| ap_add_cgi_vars(r); |
| |
| /* XXX are there any FastCGI specific env vars we need to send? */ |
| |
| /* Give admins final option to fine-tune env vars */ |
| if (APR_SUCCESS != (rv = fix_cgivars(r, dconf))) { |
| return rv; |
| } |
| |
| /* XXX mod_cgi/mod_cgid use ap_create_environment here, which fills in |
| * the TZ value specially. We could use that, but it would mean |
| * parsing the key/value pairs back OUT of the allocated env array, |
| * not to mention allocating a totally useless array in the first |
| * place, which would suck. */ |
| |
| envarr = apr_table_elts(r->subprocess_env); |
| elts = (const apr_table_entry_t *) envarr->elts; |
| |
| if (APLOGrtrace8(r)) { |
| int i; |
| |
| for (i = 0; i < envarr->nelts; ++i) { |
| ap_log_rerror(APLOG_MARK, APLOG_TRACE8, 0, r, APLOGNO(01062) |
| "sending env var '%s' value '%s'", |
| elts[i].key, elts[i].val); |
| } |
| } |
| |
| /* Send envvars over in as many FastCGI records as it takes, */ |
| next_elem = 0; /* starting with the first one */ |
| |
| avail_len = 16 * 1024; /* our limit per record, which could have been up |
| * to AP_FCGI_MAX_CONTENT_LEN |
| */ |
| |
| while (next_elem < envarr->nelts) { |
| starting_elem = next_elem; |
| required_len = ap_fcgi_encoded_env_len(r->subprocess_env, |
| avail_len, |
| &next_elem); |
| |
| if (!required_len) { |
| if (next_elem < envarr->nelts) { |
| ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, |
| APLOGNO(02536) "couldn't encode envvar '%s' in %" |
| APR_SIZE_T_FMT " bytes", |
| elts[next_elem].key, avail_len); |
| /* skip this envvar and continue */ |
| ++next_elem; |
| continue; |
| } |
| /* only an unused element at the end of the array */ |
| break; |
| } |
| |
| body = apr_palloc(temp_pool, required_len); |
| rv = ap_fcgi_encode_env(r, r->subprocess_env, body, required_len, |
| &starting_elem); |
| /* we pre-compute, so we can't run out of space */ |
| ap_assert(rv == APR_SUCCESS); |
| /* compute and encode must be in sync */ |
| ap_assert(starting_elem == next_elem); |
| |
| ap_fcgi_fill_in_header(&header, AP_FCGI_PARAMS, request_id, |
| (apr_uint16_t)required_len, 0); |
| ap_fcgi_header_to_array(&header, farray); |
| |
| vec[0].iov_base = (void *)farray; |
| vec[0].iov_len = sizeof(farray); |
| vec[1].iov_base = body; |
| vec[1].iov_len = required_len; |
| |
| rv = send_data(conn, vec, 2, &len); |
| apr_pool_clear(temp_pool); |
| |
| if (rv) { |
| return rv; |
| } |
| } |
| |
| /* Envvars sent, so say we're done */ |
| ap_fcgi_fill_in_header(&header, AP_FCGI_PARAMS, request_id, 0, 0); |
| ap_fcgi_header_to_array(&header, farray); |
| |
| vec[0].iov_base = (void *)farray; |
| vec[0].iov_len = sizeof(farray); |
| |
| return send_data(conn, vec, 1, &len); |
| } |
| |
| enum { |
| HDR_STATE_READING_HEADERS, |
| HDR_STATE_GOT_CR, |
| HDR_STATE_GOT_CRLF, |
| HDR_STATE_GOT_CRLFCR, |
| HDR_STATE_GOT_LF, |
| HDR_STATE_DONE_WITH_HEADERS |
| }; |
| |
| /* Try to find the end of the script headers in the response from the back |
| * end fastcgi server. STATE holds the current header parsing state for this |
| * request. |
| * |
| * Returns 0 if it can't find the end of the headers, and 1 if it found the |
| * end of the headers. */ |
| static int handle_headers(request_rec *r, int *state, |
| const char *readbuf, apr_size_t readlen) |
| { |
| const char *itr = readbuf; |
| |
| while (readlen--) { |
| if (*itr == '\r') { |
| switch (*state) { |
| case HDR_STATE_GOT_CRLF: |
| *state = HDR_STATE_GOT_CRLFCR; |
| break; |
| |
| default: |
| *state = HDR_STATE_GOT_CR; |
| break; |
| } |
| } |
| else if (*itr == '\n') { |
| switch (*state) { |
| case HDR_STATE_GOT_LF: |
| *state = HDR_STATE_DONE_WITH_HEADERS; |
| break; |
| |
| case HDR_STATE_GOT_CR: |
| *state = HDR_STATE_GOT_CRLF; |
| break; |
| |
| case HDR_STATE_GOT_CRLFCR: |
| *state = HDR_STATE_DONE_WITH_HEADERS; |
| break; |
| |
| default: |
| *state = HDR_STATE_GOT_LF; |
| break; |
| } |
| } |
| else { |
| *state = HDR_STATE_READING_HEADERS; |
| } |
| |
| if (*state == HDR_STATE_DONE_WITH_HEADERS) |
| break; |
| |
| ++itr; |
| } |
| |
| if (*state == HDR_STATE_DONE_WITH_HEADERS) { |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| static apr_status_t dispatch(proxy_conn_rec *conn, proxy_dir_conf *conf, |
| request_rec *r, apr_pool_t *setaside_pool, |
| apr_uint16_t request_id, const char **err, |
| int *bad_request, int *has_responded, |
| apr_bucket_brigade *input_brigade) |
| { |
| apr_bucket_brigade *ib, *ob; |
| int seen_end_of_headers = 0, done = 0, ignore_body = 0; |
| apr_status_t rv = APR_SUCCESS; |
| int script_error_status = HTTP_OK; |
| conn_rec *c = r->connection; |
| struct iovec vec[2]; |
| ap_fcgi_header header; |
| unsigned char farray[AP_FCGI_HEADER_LEN]; |
| apr_pollfd_t pfd; |
| apr_pollfd_t *flushpoll = NULL; |
| apr_int32_t flushpoll_fd; |
| int header_state = HDR_STATE_READING_HEADERS; |
| char stack_iobuf[AP_IOBUFSIZE]; |
| apr_size_t iobuf_size = AP_IOBUFSIZE; |
| char *iobuf = stack_iobuf; |
| |
| *err = NULL; |
| if (conn->worker->s->io_buffer_size_set) { |
| iobuf_size = conn->worker->s->io_buffer_size; |
| iobuf = apr_palloc(r->pool, iobuf_size); |
| } |
| |
| pfd.desc_type = APR_POLL_SOCKET; |
| pfd.desc.s = conn->sock; |
| pfd.p = r->pool; |
| pfd.reqevents = APR_POLLIN | APR_POLLOUT; |
| |
| if (conn->worker->s->flush_packets == flush_auto) { |
| flushpoll = apr_pcalloc(r->pool, sizeof(apr_pollfd_t)); |
| flushpoll->reqevents = APR_POLLIN; |
| flushpoll->desc_type = APR_POLL_SOCKET; |
| flushpoll->desc.s = conn->sock; |
| } |
| |
| ib = apr_brigade_create(r->pool, c->bucket_alloc); |
| ob = apr_brigade_create(r->pool, c->bucket_alloc); |
| |
| while (! done) { |
| apr_interval_time_t timeout; |
| apr_size_t len; |
| int n; |
| |
| /* We need SOME kind of timeout here, or virtually anything will |
| * cause timeout errors. */ |
| apr_socket_timeout_get(conn->sock, &timeout); |
| |
| rv = apr_poll(&pfd, 1, &n, timeout); |
| if (rv != APR_SUCCESS) { |
| if (APR_STATUS_IS_EINTR(rv)) { |
| continue; |
| } |
| *err = "polling"; |
| break; |
| } |
| |
| if (pfd.rtnevents & APR_POLLOUT) { |
| apr_size_t to_send, writebuflen; |
| int last_stdin = 0; |
| char *iobuf_cursor; |
| |
| if (APR_BRIGADE_EMPTY(input_brigade)) { |
| rv = ap_get_brigade(r->input_filters, ib, |
| AP_MODE_READBYTES, APR_BLOCK_READ, |
| iobuf_size); |
| } |
| else { |
| apr_bucket *e; |
| APR_BRIGADE_CONCAT(ib, input_brigade); |
| rv = apr_brigade_partition(ib, iobuf_size, &e); |
| if (rv == APR_SUCCESS) { |
| while (e != APR_BRIGADE_SENTINEL(ib) |
| && APR_BUCKET_IS_METADATA(e)) { |
| e = APR_BUCKET_NEXT(e); |
| } |
| apr_brigade_split_ex(ib, e, input_brigade); |
| } |
| else if (rv == APR_INCOMPLETE) { |
| rv = APR_SUCCESS; |
| } |
| } |
| if (rv != APR_SUCCESS) { |
| *err = "reading input brigade"; |
| *bad_request = 1; |
| break; |
| } |
| |
| if (APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(ib))) { |
| last_stdin = 1; |
| } |
| |
| writebuflen = iobuf_size; |
| |
| rv = apr_brigade_flatten(ib, iobuf, &writebuflen); |
| |
| apr_brigade_cleanup(ib); |
| |
| if (rv != APR_SUCCESS) { |
| *err = "flattening brigade"; |
| break; |
| } |
| |
| to_send = writebuflen; |
| iobuf_cursor = iobuf; |
| while (to_send > 0) { |
| int nvec = 0; |
| apr_size_t write_this_time; |
| |
| write_this_time = |
| to_send < AP_FCGI_MAX_CONTENT_LEN ? to_send : AP_FCGI_MAX_CONTENT_LEN; |
| |
| ap_fcgi_fill_in_header(&header, AP_FCGI_STDIN, request_id, |
| (apr_uint16_t)write_this_time, 0); |
| ap_fcgi_header_to_array(&header, farray); |
| |
| vec[nvec].iov_base = (void *)farray; |
| vec[nvec].iov_len = sizeof(farray); |
| ++nvec; |
| if (writebuflen) { |
| vec[nvec].iov_base = iobuf_cursor; |
| vec[nvec].iov_len = write_this_time; |
| ++nvec; |
| } |
| |
| rv = send_data(conn, vec, nvec, &len); |
| if (rv != APR_SUCCESS) { |
| *err = "sending stdin"; |
| break; |
| } |
| |
| to_send -= write_this_time; |
| iobuf_cursor += write_this_time; |
| } |
| if (rv != APR_SUCCESS) { |
| break; |
| } |
| |
| if (last_stdin) { |
| pfd.reqevents = APR_POLLIN; /* Done with input data */ |
| |
| /* signal EOF (empty FCGI_STDIN) */ |
| ap_fcgi_fill_in_header(&header, AP_FCGI_STDIN, request_id, |
| 0, 0); |
| ap_fcgi_header_to_array(&header, farray); |
| |
| vec[0].iov_base = (void *)farray; |
| vec[0].iov_len = sizeof(farray); |
| |
| rv = send_data(conn, vec, 1, &len); |
| if (rv != APR_SUCCESS) { |
| *err = "sending empty stdin"; |
| break; |
| } |
| } |
| } |
| |
| if (pfd.rtnevents & APR_POLLIN) { |
| apr_size_t readbuflen; |
| apr_uint16_t clen, rid; |
| apr_bucket *b; |
| unsigned char plen; |
| unsigned char type, version; |
| int mayflush = 0; |
| |
| /* First, we grab the header... */ |
| rv = get_data_full(conn, (char *) farray, AP_FCGI_HEADER_LEN); |
| if (rv != APR_SUCCESS) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01067) |
| "Failed to read FastCGI header"); |
| break; |
| } |
| |
| ap_log_rdata(APLOG_MARK, APLOG_TRACE8, r, "FastCGI header", |
| farray, AP_FCGI_HEADER_LEN, 0); |
| |
| ap_fcgi_header_fields_from_array(&version, &type, &rid, |
| &clen, &plen, farray); |
| |
| if (version != AP_FCGI_VERSION_1) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01068) |
| "Got bogus version %d", (int)version); |
| rv = APR_EINVAL; |
| break; |
| } |
| |
| if (rid != request_id) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01069) |
| "Got bogus rid %d, expected %d", |
| rid, request_id); |
| rv = APR_EINVAL; |
| break; |
| } |
| |
| recv_again: |
| if (clen > iobuf_size) { |
| readbuflen = iobuf_size; |
| } else { |
| readbuflen = clen; |
| } |
| |
| /* Now get the actual data. Yes it sucks to do this in a second |
| * recv call, this will eventually change when we move to real |
| * nonblocking recv calls. */ |
| if (readbuflen != 0) { |
| rv = get_data(conn, iobuf, &readbuflen); |
| if (rv != APR_SUCCESS) { |
| *err = "reading response body"; |
| break; |
| } |
| } |
| |
| switch (type) { |
| case AP_FCGI_STDOUT: |
| if (clen != 0) { |
| b = apr_bucket_transient_create(iobuf, |
| readbuflen, |
| c->bucket_alloc); |
| |
| APR_BRIGADE_INSERT_TAIL(ob, b); |
| |
| if (! seen_end_of_headers) { |
| int st = handle_headers(r, &header_state, |
| iobuf, readbuflen); |
| |
| if (st == 1) { |
| int status; |
| seen_end_of_headers = 1; |
| |
| status = ap_scan_script_header_err_brigade_ex(r, ob, |
| NULL, APLOG_MODULE_INDEX); |
| |
| /* FCGI has its own body framing mechanism which we don't |
| * match against any provided Content-Length, so let the |
| * core determine C-L vs T-E based on what's actually sent. |
| */ |
| if (!apr_table_get(r->subprocess_env, AP_TRUST_CGILIKE_CL_ENVVAR)) |
| apr_table_unset(r->headers_out, "Content-Length"); |
| apr_table_unset(r->headers_out, "Transfer-Encoding"); |
| |
| /* suck in all the rest */ |
| if (status != OK) { |
| apr_bucket *tmp_b; |
| apr_brigade_cleanup(ob); |
| tmp_b = apr_bucket_eos_create(c->bucket_alloc); |
| APR_BRIGADE_INSERT_TAIL(ob, tmp_b); |
| |
| *has_responded = 1; |
| r->status = status; |
| rv = ap_pass_brigade(r->output_filters, ob); |
| if (rv != APR_SUCCESS) { |
| *err = "passing headers brigade to output filters"; |
| break; |
| } |
| else if (status == HTTP_NOT_MODIFIED |
| || status == HTTP_PRECONDITION_FAILED) { |
| /* Special 'status' cases handled: |
| * 1) HTTP 304 response MUST NOT contain |
| * a message-body, ignore it. |
| * 2) HTTP 412 response. |
| * The break is not added since there might |
| * be more bytes to read from the FCGI |
| * connection. Even if the message-body is |
| * ignored (and the EOS bucket has already |
| * been sent) we want to avoid subsequent |
| * bogus reads. */ |
| ignore_body = 1; |
| } |
| else { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01070) |
| "Error parsing script headers"); |
| rv = APR_EINVAL; |
| break; |
| } |
| } |
| |
| if (ap_proxy_should_override(conf, r->status) && ap_is_initial_req(r)) { |
| /* |
| * set script_error_status to discard |
| * everything after the headers |
| */ |
| script_error_status = r->status; |
| /* |
| * prevent ap_die() from treating this as a |
| * recursive error, initially: |
| */ |
| r->status = HTTP_OK; |
| } |
| |
| if (script_error_status == HTTP_OK |
| && !APR_BRIGADE_EMPTY(ob) && !ignore_body) { |
| /* Send the part of the body that we read while |
| * reading the headers. |
| */ |
| *has_responded = 1; |
| rv = ap_pass_brigade(r->output_filters, ob); |
| if (rv != APR_SUCCESS) { |
| *err = "passing brigade to output filters"; |
| break; |
| } |
| mayflush = 1; |
| } |
| apr_brigade_cleanup(ob); |
| |
| apr_pool_clear(setaside_pool); |
| } |
| else { |
| /* We're still looking for the end of the |
| * headers, so this part of the data will need |
| * to persist. */ |
| apr_bucket_setaside(b, setaside_pool); |
| } |
| } else { |
| /* we've already passed along the headers, so now pass |
| * through the content. we could simply continue to |
| * setaside the content and not pass until we see the |
| * 0 content-length (below, where we append the EOS), |
| * but that could be a huge amount of data; so we pass |
| * along smaller chunks |
| */ |
| if (script_error_status == HTTP_OK && !ignore_body) { |
| *has_responded = 1; |
| rv = ap_pass_brigade(r->output_filters, ob); |
| if (rv != APR_SUCCESS) { |
| *err = "passing brigade to output filters"; |
| break; |
| } |
| mayflush = 1; |
| } |
| apr_brigade_cleanup(ob); |
| } |
| |
| /* If we didn't read all the data, go back and get the |
| * rest of it. */ |
| if (clen > readbuflen) { |
| clen -= readbuflen; |
| goto recv_again; |
| } |
| } else { |
| /* XXX what if we haven't seen end of the headers yet? */ |
| |
| if (script_error_status == HTTP_OK) { |
| b = apr_bucket_eos_create(c->bucket_alloc); |
| APR_BRIGADE_INSERT_TAIL(ob, b); |
| |
| *has_responded = 1; |
| rv = ap_pass_brigade(r->output_filters, ob); |
| if (rv != APR_SUCCESS) { |
| *err = "passing brigade to output filters"; |
| break; |
| } |
| } |
| |
| /* XXX Why don't we cleanup here? (logic from AJP) */ |
| } |
| break; |
| |
| case AP_FCGI_STDERR: |
| /* TODO: Should probably clean up this logging a bit... */ |
| if (clen) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01071) |
| "Got error '%.*s'", (int)readbuflen, iobuf); |
| } |
| |
| if (clen > readbuflen) { |
| clen -= readbuflen; |
| goto recv_again; |
| } |
| break; |
| |
| case AP_FCGI_END_REQUEST: |
| done = 1; |
| break; |
| |
| default: |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01072) |
| "Got bogus record %d", type); |
| break; |
| } |
| /* Leave on above switch's inner error. */ |
| if (rv != APR_SUCCESS) { |
| break; |
| } |
| |
| if (plen) { |
| rv = get_data_full(conn, iobuf, plen); |
| if (rv != APR_SUCCESS) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(02537) |
| "Error occurred reading padding"); |
| break; |
| } |
| } |
| |
| if (mayflush && ((conn->worker->s->flush_packets == flush_on) || |
| ((conn->worker->s->flush_packets == flush_auto) && |
| (apr_poll(flushpoll, 1, &flushpoll_fd, |
| conn->worker->s->flush_wait) == APR_TIMEUP)))) { |
| apr_bucket* flush_b = apr_bucket_flush_create(r->connection->bucket_alloc); |
| APR_BRIGADE_INSERT_TAIL(ob, flush_b); |
| rv = ap_pass_brigade(r->output_filters, ob); |
| if (rv != APR_SUCCESS) { |
| *err = "passing headers brigade to output filters"; |
| break; |
| } |
| mayflush = 0; |
| } |
| } |
| } |
| |
| apr_brigade_destroy(ib); |
| apr_brigade_destroy(ob); |
| |
| if (script_error_status != HTTP_OK) { |
| ap_die(script_error_status, r); /* send ErrorDocument */ |
| *has_responded = 1; |
| } |
| |
| return rv; |
| } |
| |
| /* |
| * process the request and write the response. |
| */ |
| static int fcgi_do_request(apr_pool_t *p, request_rec *r, |
| proxy_conn_rec *conn, |
| conn_rec *origin, |
| proxy_dir_conf *conf, |
| apr_uri_t *uri, |
| char *url, char *server_portstr, |
| apr_bucket_brigade *input_brigade) |
| { |
| /* Request IDs are arbitrary numbers that we assign to a |
| * single request. This would allow multiplex/pipelining of |
| * multiple requests to the same FastCGI connection, but |
| * we don't support that, and always use a value of '1' to |
| * keep things simple. */ |
| apr_uint16_t request_id = 1; |
| apr_status_t rv; |
| apr_pool_t *temp_pool; |
| const char *err; |
| int bad_request = 0, |
| has_responded = 0; |
| |
| /* Step 1: Send AP_FCGI_BEGIN_REQUEST */ |
| rv = send_begin_request(conn, request_id); |
| if (rv != APR_SUCCESS) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01073) |
| "Failed Writing Request to %s:", server_portstr); |
| conn->close = 1; |
| return HTTP_SERVICE_UNAVAILABLE; |
| } |
| |
| apr_pool_create(&temp_pool, r->pool); |
| apr_pool_tag(temp_pool, "proxy_fcgi_do_request"); |
| |
| /* Step 2: Send Environment via FCGI_PARAMS */ |
| rv = send_environment(conn, r, temp_pool, request_id); |
| if (rv != APR_SUCCESS) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01074) |
| "Failed writing Environment to %s:", server_portstr); |
| conn->close = 1; |
| return HTTP_SERVICE_UNAVAILABLE; |
| } |
| |
| /* Step 3: Read records from the back end server and handle them. */ |
| rv = dispatch(conn, conf, r, temp_pool, request_id, |
| &err, &bad_request, &has_responded, |
| input_brigade); |
| if (rv != APR_SUCCESS) { |
| /* If the client aborted the connection during retrieval or (partially) |
| * sending the response, don't return a HTTP_SERVICE_UNAVAILABLE, since |
| * this is not a backend problem. */ |
| if (r->connection->aborted) { |
| ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, |
| "The client aborted the connection."); |
| conn->close = 1; |
| return OK; |
| } |
| |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01075) |
| "Error dispatching request to %s: %s%s%s", |
| server_portstr, |
| err ? "(" : "", |
| err ? err : "", |
| err ? ")" : ""); |
| conn->close = 1; |
| if (has_responded) { |
| return AP_FILTER_ERROR; |
| } |
| if (bad_request) { |
| return ap_map_http_request_error(rv, HTTP_BAD_REQUEST); |
| } |
| if (APR_STATUS_IS_TIMEUP(rv)) { |
| return HTTP_GATEWAY_TIME_OUT; |
| } |
| return HTTP_SERVICE_UNAVAILABLE; |
| } |
| |
| return OK; |
| } |
| |
| #define FCGI_SCHEME "FCGI" |
| |
| #define MAX_MEM_SPOOL 16384 |
| |
| /* |
| * This handles fcgi:(dest) URLs |
| */ |
| static int proxy_fcgi_handler(request_rec *r, proxy_worker *worker, |
| proxy_server_conf *conf, |
| char *url, const char *proxyname, |
| apr_port_t proxyport) |
| { |
| int status; |
| char server_portstr[32]; |
| conn_rec *origin = NULL; |
| proxy_conn_rec *backend = NULL; |
| apr_bucket_brigade *input_brigade; |
| apr_off_t input_bytes = 0; |
| apr_uri_t *uri; |
| |
| proxy_dir_conf *dconf = ap_get_module_config(r->per_dir_config, |
| &proxy_module); |
| |
| apr_pool_t *p = r->pool; |
| |
| |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01076) |
| "url: %s proxyname: %s proxyport: %d", |
| url, proxyname, proxyport); |
| |
| if (ap_cstr_casecmpn(url, "fcgi:", 5) != 0) { |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01077) "declining URL %s", url); |
| return DECLINED; |
| } |
| |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01078) "serving URL %s", url); |
| |
| /* Create space for state information */ |
| status = ap_proxy_acquire_connection(FCGI_SCHEME, &backend, worker, |
| r->server); |
| if (status != OK) { |
| if (backend) { |
| backend->close = 1; |
| ap_proxy_release_connection(FCGI_SCHEME, backend, r->server); |
| } |
| return status; |
| } |
| |
| backend->is_ssl = 0; |
| |
| /* Step One: Determine Who To Connect To */ |
| uri = apr_palloc(p, sizeof(*uri)); |
| status = ap_proxy_determine_connection(p, r, conf, worker, backend, |
| uri, &url, proxyname, proxyport, |
| server_portstr, |
| sizeof(server_portstr)); |
| if (status != OK) { |
| goto cleanup; |
| } |
| |
| /* We possibly reuse input data prefetched in previous call(s), e.g. for a |
| * balancer fallback scenario. |
| */ |
| apr_pool_userdata_get((void **)&input_brigade, "proxy-fcgi-input", p); |
| if (input_brigade == NULL) { |
| const char *old_te = apr_table_get(r->headers_in, "Transfer-Encoding"); |
| const char *old_cl = NULL; |
| if (old_te) { |
| apr_table_unset(r->headers_in, "Content-Length"); |
| } |
| else { |
| old_cl = apr_table_get(r->headers_in, "Content-Length"); |
| } |
| |
| input_brigade = apr_brigade_create(p, r->connection->bucket_alloc); |
| apr_pool_userdata_setn(input_brigade, "proxy-fcgi-input", NULL, p); |
| |
| /* Prefetch (nonlocking) the request body so to increase the chance |
| * to get the whole (or enough) body and determine Content-Length vs |
| * chunked or spooled. By doing this before connecting or reusing the |
| * backend, we want to minimize the delay between this connection is |
| * considered alive and the first bytes sent (should the client's link |
| * be slow or some input filter retain the data). This is a best effort |
| * to prevent the backend from closing (from under us) what it thinks is |
| * an idle connection, hence to reduce to the minimum the unavoidable |
| * local is_socket_connected() vs remote keepalive race condition. |
| */ |
| status = ap_proxy_prefetch_input(r, backend, input_brigade, |
| APR_NONBLOCK_READ, &input_bytes, |
| MAX_MEM_SPOOL); |
| if (status != OK) { |
| goto cleanup; |
| } |
| |
| /* |
| * The request body is streamed by default, using either C-L or |
| * chunked T-E, like this: |
| * |
| * The whole body (including no body) was received on prefetch, i.e. |
| * the input brigade ends with EOS => C-L = input_bytes. |
| * |
| * C-L is known and reliable, i.e. only protocol filters in the input |
| * chain thus none should change the body => use C-L from client. |
| * |
| * The administrator has not "proxy-sendcl" which prevents T-E => use |
| * T-E and chunks. |
| * |
| * Otherwise we need to determine and set a content-length, so spool |
| * the entire request body to memory/temporary file (MAX_MEM_SPOOL), |
| * such that we finally know its length => C-L = input_bytes. |
| */ |
| if (!APR_BRIGADE_EMPTY(input_brigade) |
| && APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(input_brigade))) { |
| /* The whole thing fit, so our decision is trivial, use the input |
| * bytes for the Content-Length. If we expected no body, and read |
| * no body, do not set the Content-Length. |
| */ |
| if (old_cl || old_te || input_bytes) { |
| apr_table_setn(r->headers_in, "Content-Length", |
| apr_off_t_toa(p, input_bytes)); |
| if (old_te) { |
| apr_table_unset(r->headers_in, "Transfer-Encoding"); |
| } |
| } |
| } |
| else if (old_cl && r->input_filters == r->proto_input_filters) { |
| /* Streaming is possible by preserving the existing C-L */ |
| } |
| else if (!apr_table_get(r->subprocess_env, "proxy-sendcl")) { |
| /* Streaming is possible using T-E: chunked */ |
| } |
| else { |
| /* No streaming, C-L is the only option so spool to memory/file */ |
| apr_bucket_brigade *tmp_bb; |
| apr_off_t remaining_bytes = 0; |
| |
| AP_DEBUG_ASSERT(MAX_MEM_SPOOL >= input_bytes); |
| tmp_bb = apr_brigade_create(p, r->connection->bucket_alloc); |
| status = ap_proxy_spool_input(r, backend, tmp_bb, &remaining_bytes, |
| MAX_MEM_SPOOL - input_bytes); |
| if (status != OK) { |
| goto cleanup; |
| } |
| |
| APR_BRIGADE_CONCAT(input_brigade, tmp_bb); |
| input_bytes += remaining_bytes; |
| |
| apr_table_setn(r->headers_in, "Content-Length", |
| apr_off_t_toa(p, input_bytes)); |
| if (old_te) { |
| apr_table_unset(r->headers_in, "Transfer-Encoding"); |
| } |
| } |
| } |
| |
| /* This scheme handler does not reuse connections by default, to |
| * avoid tying up a fastcgi that isn't expecting to work on |
| * parallel requests. But if the user went out of their way to |
| * type the default value of disablereuse=off, we'll allow it. |
| */ |
| backend->close = 1; |
| if (worker->s->disablereuse_set && !worker->s->disablereuse) { |
| backend->close = 0; |
| } |
| |
| /* Step Two: Make the Connection */ |
| if (ap_proxy_check_connection(FCGI_SCHEME, backend, r->server, 0, |
| PROXY_CHECK_CONN_EMPTY) |
| && ap_proxy_connect_backend(FCGI_SCHEME, backend, worker, |
| r->server)) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01079) |
| "failed to make connection to backend: %s", |
| backend->hostname); |
| status = HTTP_SERVICE_UNAVAILABLE; |
| goto cleanup; |
| } |
| |
| /* Step Three: Process the Request */ |
| status = fcgi_do_request(p, r, backend, origin, dconf, uri, url, |
| server_portstr, input_brigade); |
| |
| cleanup: |
| ap_proxy_release_connection(FCGI_SCHEME, backend, r->server); |
| return status; |
| } |
| |
| static void *fcgi_create_dconf(apr_pool_t *p, char *path) |
| { |
| fcgi_dirconf_t *a; |
| |
| a = (fcgi_dirconf_t *)apr_pcalloc(p, sizeof(fcgi_dirconf_t)); |
| a->backend_type = BACKEND_DEFAULT_UNKNOWN; |
| a->env_fixups = apr_array_make(p, 20, sizeof(sei_entry)); |
| |
| return a; |
| } |
| |
| static void *fcgi_merge_dconf(apr_pool_t *p, void *basev, void *overridesv) |
| { |
| fcgi_dirconf_t *a, *base, *over; |
| |
| a = (fcgi_dirconf_t *)apr_pcalloc(p, sizeof(fcgi_dirconf_t)); |
| base = (fcgi_dirconf_t *)basev; |
| over = (fcgi_dirconf_t *)overridesv; |
| |
| a->backend_type = (over->backend_type != BACKEND_DEFAULT_UNKNOWN) |
| ? over->backend_type |
| : base->backend_type; |
| a->env_fixups = apr_array_append(p, base->env_fixups, over->env_fixups); |
| return a; |
| } |
| |
| static const char *cmd_servertype(cmd_parms *cmd, void *in_dconf, |
| const char *val) |
| { |
| fcgi_dirconf_t *dconf = in_dconf; |
| |
| if (!strcasecmp(val, "GENERIC")) { |
| dconf->backend_type = BACKEND_GENERIC; |
| } |
| else if (!strcasecmp(val, "FPM")) { |
| dconf->backend_type = BACKEND_FPM; |
| } |
| else { |
| return "ProxyFCGIBackendType requires one of the following arguments: " |
| "'GENERIC', 'FPM'"; |
| } |
| |
| return NULL; |
| } |
| |
| |
| static const char *cmd_setenv(cmd_parms *cmd, void *in_dconf, |
| const char *arg1, const char *arg2, |
| const char *arg3) |
| { |
| fcgi_dirconf_t *dconf = in_dconf; |
| const char *err; |
| sei_entry *new; |
| const char *envvar = arg2; |
| |
| new = apr_array_push(dconf->env_fixups); |
| new->cond = ap_expr_parse_cmd(cmd, arg1, 0, &err, NULL); |
| if (err) { |
| return apr_psprintf(cmd->pool, "Could not parse expression \"%s\": %s", |
| arg1, err); |
| } |
| |
| if (envvar[0] == '!') { |
| /* Unset mode. */ |
| if (arg3) { |
| return apr_psprintf(cmd->pool, "Third argument (\"%s\") is not " |
| "allowed when using ProxyFCGISetEnvIf's unset " |
| "mode (%s)", arg3, envvar); |
| } |
| else if (!envvar[1]) { |
| /* i.e. someone tried to give us a name of just "!" */ |
| return "ProxyFCGISetEnvIf: \"!\" is not a valid variable name"; |
| } |
| |
| new->subst = NULL; |
| } |
| else { |
| /* Set mode. */ |
| if (!arg3) { |
| /* A missing expr-value should be treated as empty. */ |
| arg3 = ""; |
| } |
| |
| new->subst = ap_expr_parse_cmd(cmd, arg3, AP_EXPR_FLAG_STRING_RESULT, &err, NULL); |
| if (err) { |
| return apr_psprintf(cmd->pool, "Could not parse expression \"%s\": %s", |
| arg3, err); |
| } |
| } |
| |
| new->envname = envvar; |
| |
| return NULL; |
| } |
| static void register_hooks(apr_pool_t *p) |
| { |
| proxy_hook_scheme_handler(proxy_fcgi_handler, NULL, NULL, APR_HOOK_FIRST); |
| proxy_hook_canon_handler(proxy_fcgi_canon, NULL, NULL, APR_HOOK_FIRST); |
| } |
| |
| static const command_rec command_table[] = { |
| AP_INIT_TAKE1("ProxyFCGIBackendType", cmd_servertype, NULL, OR_FILEINFO, |
| "Specify the type of FastCGI server: 'Generic', 'FPM'"), |
| AP_INIT_TAKE23("ProxyFCGISetEnvIf", cmd_setenv, NULL, OR_FILEINFO, |
| "expr-condition env-name expr-value"), |
| { NULL } |
| }; |
| |
| AP_DECLARE_MODULE(proxy_fcgi) = { |
| STANDARD20_MODULE_STUFF, |
| fcgi_create_dconf, /* create per-directory config structure */ |
| fcgi_merge_dconf, /* merge per-directory config structures */ |
| NULL, /* create per-server config structure */ |
| NULL, /* merge per-server config structures */ |
| command_table, /* command apr_table_t */ |
| register_hooks /* register hooks */ |
| }; |