diff --git a/modules/proxy/config.m4 b/modules/proxy/config.m4 index f131ee66111..7746b09569f 100644 --- a/modules/proxy/config.m4 +++ b/modules/proxy/config.m4 @@ -16,6 +16,7 @@ APACHE_MODULE(proxy, Apache proxy module, $proxy_objs, , $proxy_mods_enable) proxy_connect_objs="mod_proxy_connect.lo" proxy_ftp_objs="mod_proxy_ftp.lo" proxy_http_objs="mod_proxy_http.lo" +proxy_fcgi_objs="mod_proxy_fcgi.lo" proxy_ajp_objs="mod_proxy_ajp.lo ajp_header.lo ajp_link.lo ajp_msg.lo" proxy_balancer_objs="mod_proxy_balancer.lo" @@ -26,6 +27,7 @@ case "$host" in proxy_connect_objs="$proxy_connect_objs mod_proxy.la" proxy_ftp_objs="$proxy_ftp_objs mod_proxy.la" proxy_http_objs="$proxy_http_objs mod_proxy.la" + proxy_fcgi_objs="$proxy_fcgi_objs mod_proxy.la" proxy_ajp_objs="$proxy_ajp_objs mod_proxy.la" proxy_balancer_objs="$proxy_balancer_objs mod_proxy.la" ;; @@ -34,6 +36,7 @@ esac APACHE_MODULE(proxy_connect, Apache proxy CONNECT module, $proxy_connect_objs, , $proxy_mods_enable) APACHE_MODULE(proxy_ftp, Apache proxy FTP module, $proxy_ftp_objs, , $proxy_mods_enable) APACHE_MODULE(proxy_http, Apache proxy HTTP module, $proxy_http_objs, , $proxy_mods_enable) +APACHE_MODULE(proxy_fcgi, Apache proxy FastCGI module, $proxy_fcgi_objs, , $proxy_mods_enable) APACHE_MODULE(proxy_ajp, Apache proxy AJP module, $proxy_ajp_objs, , $proxy_mods_enable) APACHE_MODULE(proxy_balancer, Apache proxy BALANCER module, $proxy_balancer_objs, , $proxy_mods_enable) diff --git a/modules/proxy/fcgi_protocol.h b/modules/proxy/fcgi_protocol.h new file mode 100644 index 00000000000..34e6714dd96 --- /dev/null +++ b/modules/proxy/fcgi_protocol.h @@ -0,0 +1,107 @@ +/* Copyright 2005 The Apache Software Foundation or its licensors, as + * applicable. + * + * Licensed 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. + */ + +/** + * @file fcgi_protocol.h + * @brief FastCGI protocol defines + * + * @addtogroup FCGI_defines + * @{ + */ + +#ifndef FCGI_PROTOCOL_H +#define FCGI_PROTOCOL_H + + +#define FCGI_VERSION 1 + +#define FCGI_BEGIN_REQUEST 1 +#define FCGI_ABORT_REQUEST 2 +#define FCGI_END_REQUEST 3 +#define FCGI_PARAMS 4 +#define FCGI_STDIN 5 +#define FCGI_STDOUT 6 +#define FCGI_STDERR 7 +#define FCGI_DATA 8 +#define FCGI_GET_VALUES 9 +#define FCGI_GET_VALUES_RESULT 10 +#define FCGI_UNKNOWN_TYPE 11 +#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE) + +typedef struct { + unsigned char version; + unsigned char type; + unsigned char requestIdB1; + unsigned char requestIdB0; + unsigned char contentLengthB1; + unsigned char contentLengthB0; + unsigned char paddingLength; + unsigned char reserved; +} fcgi_header; + +#define FCGI_HDR_VERSION_OFFSET 0 +#define FCGI_HDR_TYPE_OFFSET 1 +#define FCGI_HDR_REQUEST_ID_B1_OFFSET 2 +#define FCGI_HDR_REQUEST_ID_B0_OFFSET 3 +#define FCGI_HDR_CONTENT_LEN_B1_OFFSET 4 +#define FCGI_HDR_CONTENT_LEN_B0_OFFSET 5 +#define FCGI_HDR_PADDING_LEN_OFFSET 6 +#define FCGI_HDR_RESERVED_OFFSET 7 + +#define FCGI_BRB_ROLEB1_OFFSET 0 +#define FCGI_BRB_ROLEB0_OFFSET 1 +#define FCGI_BRB_FLAGS_OFFSET 2 +#define FCGI_BRB_RESERVED0_OFFSET 3 +#define FCGI_BRB_RESERVED1_OFFSET 4 +#define FCGI_BRB_RESERVED2_OFFSET 5 +#define FCGI_BRB_RESERVED3_OFFSET 6 +#define FCGI_BRB_RESERVED4_OFFSET 7 + +/* + * Number of bytes in a fcgi_header. Future versions of the protocol + * will not reduce this number. + */ +#define FCGI_HEADER_LEN 8 + +/* + * Mask for flags component of FCGI_BeginRequestBody + */ +#define FCGI_KEEP_CONN 1 + +/* + * Values for role component of FCGI_BeginRequestBody + */ +#define FCGI_RESPONDER 1 +#define FCGI_AUTHORIZER 2 +#define FCGI_FILTER 3 + +typedef struct { + unsigned char roleB1; + unsigned char roleB0; + unsigned char flags; + unsigned char reserved[5]; +} fcgi_begin_request_body; + +/* + * Maximum size of the allowed environment. + */ +#define FCGI_MAX_ENV_SIZE 65535 + +/* #define FCGI_DUMP_ENV_VARS */ + + +#endif /* FCGI_PROTOCOL_H */ +/** @} */ diff --git a/modules/proxy/mod_proxy_balancer.c b/modules/proxy/mod_proxy_balancer.c index b8cba775b63..de1a15a476c 100644 --- a/modules/proxy/mod_proxy_balancer.c +++ b/modules/proxy/mod_proxy_balancer.c @@ -75,6 +75,9 @@ static int proxy_balancer_canon(request_rec *r, char *url) r->filename = apr_pstrcat(r->pool, "proxy:balancer://", host, "/", path, (search) ? "?" : "", (search) ? search : "", NULL); + + r->path_info = apr_pstrcat(r->pool, "/", path, NULL); + return OK; } diff --git a/modules/proxy/mod_proxy_fcgi.c b/modules/proxy/mod_proxy_fcgi.c new file mode 100644 index 00000000000..51fb90533a3 --- /dev/null +++ b/modules/proxy/mod_proxy_fcgi.c @@ -0,0 +1,993 @@ +/* Copyright 2005 The Apache Software Foundation or its licensors, as + * applicable. + * + * Licensed 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 "fcgi_protocol.h" + +module AP_MODULE_DECLARE_DATA proxy_fcgi_module; + +/* + * The below 3 functions serve to map the FCGI structs + * back and forth between an 8 byte array. We do this to avoid + * any potential padding issues when we send or read these + * structures. + * + * NOTE: These have specific internal knowledge of the + * layout of the fcgi_header and fcgi_begin_request_body + * structs! + */ +static void fcgi_header_to_array(fcgi_header *h, unsigned char a[]) +{ + a[FCGI_HDR_VERSION_OFFSET] = h->version; + a[FCGI_HDR_TYPE_OFFSET] = h->type; + a[FCGI_HDR_REQUEST_ID_B1_OFFSET] = h->requestIdB1; + a[FCGI_HDR_REQUEST_ID_B0_OFFSET] = h->requestIdB0; + a[FCGI_HDR_CONTENT_LEN_B1_OFFSET] = h->contentLengthB1; + a[FCGI_HDR_CONTENT_LEN_B0_OFFSET] = h->contentLengthB0; + a[FCGI_HDR_PADDING_LEN_OFFSET] = h->paddingLength; + a[FCGI_HDR_RESERVED_OFFSET] = h->reserved; +} + +static void fcgi_header_from_array(fcgi_header *h, unsigned char a[]) +{ + h->version = a[FCGI_HDR_VERSION_OFFSET]; + h->type = a[FCGI_HDR_TYPE_OFFSET]; + h->requestIdB1 = a[FCGI_HDR_REQUEST_ID_B1_OFFSET]; + h->requestIdB0 = a[FCGI_HDR_REQUEST_ID_B0_OFFSET]; + h->contentLengthB1 = a[FCGI_HDR_CONTENT_LEN_B1_OFFSET]; + h->contentLengthB0 = a[FCGI_HDR_CONTENT_LEN_B0_OFFSET]; + h->paddingLength = a[FCGI_HDR_PADDING_LEN_OFFSET]; + h->reserved = a[FCGI_HDR_RESERVED_OFFSET]; +} + +static void fcgi_begin_request_body_to_array(fcgi_begin_request_body *h, + unsigned char a[]) +{ + a[FCGI_BRB_ROLEB1_OFFSET] = h->roleB1; + a[FCGI_BRB_ROLEB0_OFFSET] = h->roleB0; + a[FCGI_BRB_FLAGS_OFFSET] = h->flags; + a[FCGI_BRB_RESERVED0_OFFSET] = h->reserved[0]; + a[FCGI_BRB_RESERVED1_OFFSET] = h->reserved[1]; + a[FCGI_BRB_RESERVED2_OFFSET] = h->reserved[2]; + a[FCGI_BRB_RESERVED3_OFFSET] = h->reserved[3]; + a[FCGI_BRB_RESERVED4_OFFSET] = h->reserved[4]; +} + +/* + * 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, *path; + apr_port_t port = 8000; + + if (strncasecmp(url, "fcgi://", 7) == 0) { + url += 5; + } + else { + return DECLINED; + } + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, + "proxy: FCGI: 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, + "error parsing URL %s: %s", url, err); + return HTTP_BAD_REQUEST; + } + + apr_snprintf(sport, sizeof(sport), ":%d", port); + + if (ap_strchr_c(host, ':')) { + /* if literal IPv6 address */ + host = apr_pstrcat(r->pool, "[", host, "]", NULL); + } + + path = ap_proxy_canonenc(r->pool, url, strlen(url), enc_path, 0, + r->proxyreq); + if (path == NULL) + return HTTP_BAD_REQUEST; + + r->filename = apr_pstrcat(r->pool, "proxy:fcgi://", host, sport, "/", + path, NULL); + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "proxy: FCGI: set r->filename to %s", r->filename); + + r->path_info = apr_pstrcat(r->pool, "/", path, NULL); + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "proxy: FCGI: set r->path_info to %s", r->path_info); + + return OK; +} + +/* + * Fill in a fastcgi request header with the following type, request id, + * content length, and padding length. + * + * The header array must be at least FCGI_HEADER_LEN bytes long. + */ +static void fill_in_header(fcgi_header *header, + unsigned char type, + apr_uint16_t request_id, + apr_uint16_t content_len, + unsigned char padding_len) +{ + header->version = 1; + + header->type = type; + + header->requestIdB1 = ((request_id >> 8) & 0xff); + header->requestIdB0 = ((request_id) & 0xff); + + header->contentLengthB1 = ((content_len >> 8) & 0xff); + header->contentLengthB0 = ((content_len) & 0xff); + + header->paddingLength = padding_len; + + header->reserved = 0; +} + +/* 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, + int blocking) +{ + apr_status_t rv = APR_SUCCESS, arv; + apr_size_t written = 0, to_write = 0; + int i, offset; + apr_interval_time_t old_timeout; + apr_socket_t *s = conn->sock; + + if (!blocking) { + arv = apr_socket_timeout_get(s, &old_timeout); + if (arv != APR_SUCCESS) { + return arv; + } + arv = apr_socket_timeout_set(s, 0); + if (arv != APR_SUCCESS) { + return arv; + } + } + + 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 += n; + break; + } + } + } + } + + conn->worker->s->transferred += written; + *len = written; + + if (!blocking) { + arv = apr_socket_timeout_set(s, old_timeout); + if ((arv != APR_SUCCESS) && (rv == APR_SUCCESS)) { + return arv; + } + } + 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 send_begin_request(proxy_conn_rec *conn, int request_id) +{ + struct iovec vec[2]; + fcgi_header header; + unsigned char farray[FCGI_HEADER_LEN]; + fcgi_begin_request_body brb; + unsigned char abrb[FCGI_HEADER_LEN]; + apr_size_t len; + + fill_in_header(&header, FCGI_BEGIN_REQUEST, request_id, sizeof(abrb), 0); + + brb.roleB1 = ((FCGI_RESPONDER >> 8) & 0xff); + brb.roleB0 = ((FCGI_RESPONDER) & 0xff); + brb.flags = FCGI_KEEP_CONN; + + fcgi_header_to_array(&header, farray); + fcgi_begin_request_body_to_array(&brb, abrb); + + vec[0].iov_base = farray; + vec[0].iov_len = sizeof(farray); + vec[1].iov_base = abrb; + vec[1].iov_len = sizeof(abrb); + + return send_data(conn, vec, 2, &len, 1); +} + +static apr_status_t send_environment(proxy_conn_rec *conn, request_rec *r, + int request_id) +{ + const apr_array_header_t *envarr; + const apr_table_entry_t *elts; + struct iovec vec[2]; + fcgi_header header; + unsigned char farray[FCGI_HEADER_LEN]; + apr_size_t bodylen, envlen; + char *body, *itr; + apr_status_t rv; + apr_size_t len; + int i, numenv; + + ap_add_common_vars(r); + ap_add_cgi_vars(r); + + /* XXX are there any FastCGI specific env vars we need to send? */ + + bodylen = envlen = 0; + + /* 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; + + for (i = 0; i < envarr->nelts; ++i) { + apr_size_t keylen, vallen; + + if (! elts[i].key) { + continue; + } + + keylen = strlen(elts[i].key); + + if (keylen >> 7 == 0) { + envlen += 1; + } + else { + envlen += 4; + } + + envlen += keylen; + + vallen = strlen(elts[i].val); + +#ifdef FCGI_DUMP_ENV_VARS + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "proxy: FCGI: sending env var '%s' value '%s'", + elts[i].key, elts[i].val); +#endif + + if (vallen >> 7 == 0) { + envlen += 1; + } + else { + envlen += 4; + } + + envlen += vallen; + + if (envlen > FCGI_MAX_ENV_SIZE) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "proxy: FCGI: truncating environment to %d bytes and %d elements", + (int)bodylen, i); + break; + } + + bodylen = envlen; + } + + numenv = i; + + body = apr_pcalloc(r->pool, bodylen); + + itr = body; + + for (i = 0; i < numenv; ++i) { + apr_size_t keylen, vallen; + + if (! elts[i].key) { + continue; + } + + keylen = strlen(elts[i].key); + + if (keylen >> 7 == 0) { + itr[0] = keylen & 0xff; + itr += 1; + } + else { + itr[0] = ((keylen >> 24) & 0xff) | 0x80; + itr[1] = ((keylen >> 16) & 0xff); + itr[2] = ((keylen >> 8) & 0xff); + itr[3] = ((keylen) & 0xff); + itr += 4; + } + + vallen = strlen(elts[i].val); + + if (vallen >> 7 == 0) { + itr[0] = vallen & 0xff; + itr += 1; + } + else { + itr[0] = ((vallen >> 24) & 0xff) | 0x80; + itr[1] = ((vallen >> 16) & 0xff); + itr[2] = ((vallen >> 8) & 0xff); + itr[3] = ((vallen) & 0xff); + itr += 4; + } + + memcpy(itr, elts[i].key, keylen); + itr += keylen; + + memcpy(itr, elts[i].val, vallen); + itr += vallen; + } + + fill_in_header(&header, FCGI_PARAMS, request_id, bodylen, 0); + fcgi_header_to_array(&header, farray); + + vec[0].iov_base = farray; + vec[0].iov_len = sizeof(farray); + vec[1].iov_base = body; + vec[1].iov_len = bodylen; + + rv = send_data(conn, vec, 2, &len, 1); + if (rv) { + return rv; + } + + fill_in_header(&header, FCGI_PARAMS, request_id, 0, 0); + fcgi_header_to_array(&header, farray); + + vec[0].iov_base = farray; + vec[0].iov_len = sizeof(farray); + + return send_data(conn, vec, 1, &len, 1); +} + +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 parse the script headers in the response from the back end fastcgi + * server. Assumes that the contents of READBUF have already been added to + * the end of OB. STATE holds the current header parsing state for this + * request. + * + * Returns -1 on error, 0 if it can't find the end of the headers, and 1 if + * it found the end of the headers and scans them successfully. */ +static int handle_headers(request_rec *r, + int *state, + char *readbuf, + apr_bucket_brigade *ob) +{ + conn_rec *c = r->connection; + const char *itr = readbuf; + + while (*itr) { + 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) { + int status = ap_scan_script_header_err_brigade(r, ob, NULL); + if (status != OK) { + apr_bucket *b; + + r->status = status; + + apr_brigade_cleanup(ob); + + b = apr_bucket_eos_create(c->bucket_alloc); + + APR_BRIGADE_INSERT_TAIL(ob, b); + + ap_pass_brigade(r->output_filters, ob); + + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "proxy: FCGI: Error parsing script headers"); + + return -1; + } + else { + return 1; + } + } + + return 0; +} + +static void dump_header_to_log(request_rec *r, unsigned char fheader[], + apr_size_t length) +{ +#ifdef FCGI_DUMP_HEADERS + apr_size_t posn = 0; + char asc_line[20]; + char hex_line[60]; + int i = 0; + + memset(asc_line, 0, sizeof(asc_line)); + memset(hex_line, 0, sizeof(hex_line)); + + while (posn < length) { + unsigned char c = fheader[posn]; + char hexval[3]; + + if (i >= 20) { + i = 0; + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, + "HEADER: %s %s", asc_line, hex_line); + + memset(asc_line, 0, sizeof(asc_line)); + memset(hex_line, 0, sizeof(hex_line)); + } + + if (isprint(c)) { + asc_line[i] = c; + } + else { + asc_line[i] = '.'; + } + + if ((c >> 4) >= 10) { + hex_line[i * 3] = 'a' + ((c >> 4) - 10); + } + else { + hex_line[i * 3] = '0' + (c >> 4); + } + + if ((c & 0x0F) >= 10) { + hex_line[i * 3 + 1] = 'a' + ((c & 0x0F) - 10); + } + else { + hex_line[i * 3 + 1] = '0' + (c & 0xF); + } + + hex_line[i * 3 + 2] = ' '; + + i++; + posn++; + } + + if (i != 1) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "HEADER: %s %s", + asc_line, hex_line); + } + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "HEADER: -EOH-"); +#endif +} + +static apr_status_t dispatch(proxy_conn_rec *conn, request_rec *r, + int request_id) +{ + apr_bucket_brigade *ib, *ob; + int seen_end_of_headers = 0, done = 0; + apr_status_t rv = APR_SUCCESS; + conn_rec *c = r->connection; + struct iovec vec[2]; + fcgi_header header; + unsigned char farray[FCGI_HEADER_LEN]; + apr_pollfd_t pfd; + int header_state = HDR_STATE_READING_HEADERS; + apr_pool_t *setaside_pool; + + apr_pool_create(&setaside_pool, r->pool); + + pfd.desc_type = APR_POLL_SOCKET; + pfd.desc.s = conn->sock; + pfd.p = r->pool; + pfd.reqevents = APR_POLLIN | APR_POLLOUT; + + 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 = conn->worker->timeout; + apr_size_t len; + int n; + + /* We need SOME kind of timeout here, or virtually anything will + * cause timeout errors. */ + if (! conn->worker->timeout_set) { + timeout = apr_time_from_sec(30); + } + + rv = apr_poll(&pfd, 1, &n, timeout); + if (rv != APR_SUCCESS) { + break; + } + + if (pfd.rtnevents & APR_POLLOUT) { + char writebuf[AP_IOBUFSIZE]; + apr_size_t writebuflen; + int last_stdin = 0; + + rv = ap_get_brigade(r->input_filters, ib, + AP_MODE_READBYTES, APR_BLOCK_READ, + sizeof(writebuf)); + if (rv != APR_SUCCESS) { + break; + } + + if (APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(ib))) { + last_stdin = 1; + } + + writebuflen = sizeof(writebuf); + + rv = apr_brigade_flatten(ib, writebuf, &writebuflen); + + apr_brigade_cleanup(ib); + + if (rv != APR_SUCCESS) { + break; + } + + fill_in_header(&header, FCGI_STDIN, request_id, + (apr_uint16_t) writebuflen, 0); + fcgi_header_to_array(&header, farray); + + vec[0].iov_base = farray; + vec[0].iov_len = sizeof(farray); + vec[1].iov_base = writebuf; + vec[1].iov_len = writebuflen; + + rv = send_data(conn, vec, 2, &len, 0); + if (rv != APR_SUCCESS) { + break; + } + + if (last_stdin) { + pfd.reqevents = APR_POLLIN; /* Done with input data */ + + fill_in_header(&header, FCGI_STDIN, request_id, 0, 0); + fcgi_header_to_array(&header, farray); + + vec[0].iov_base = farray; + vec[0].iov_len = sizeof(farray); + + rv = send_data(conn, vec, 1, &len, 1); + } + } + + if (pfd.rtnevents & APR_POLLIN) { + /* readbuf has one byte on the end that is always 0, so it's + * able to work with a strstr when we search for the end of + * the headers, even if we fill the entire length in the recv. */ + char readbuf[AP_IOBUFSIZE + 1]; + apr_size_t readbuflen; + apr_size_t clen; + int rid, type; + apr_bucket *b; + char plen; + + memset(readbuf, 0, sizeof(readbuf)); + memset(farray, 0, sizeof(farray)); + + /* First, we grab the header... */ + readbuflen = FCGI_HEADER_LEN; + + rv = get_data(conn, (char *) farray, &readbuflen); + if (rv != APR_SUCCESS) { + break; + } + + dump_header_to_log(r, farray, readbuflen); + + if (readbuflen != FCGI_HEADER_LEN) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "proxy: FCGI: Failed to read entire header " + "got %d wanted %d", + readbuflen, FCGI_HEADER_LEN); + rv = APR_EINVAL; + break; + } + + fcgi_header_from_array(&header, farray); + + if (header.version != FCGI_VERSION) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "proxy: FCGI: Got bogus version %d", + (int) header.version); + rv = APR_EINVAL; + break; + } + + type = header.type; + + rid = header.requestIdB1 << 8; + rid |= header.requestIdB0; + + if (rid != request_id) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "proxy: FCGI: Got bogus rid %d, expected %d", + rid, request_id); + rv = APR_EINVAL; + break; + } + + clen = header.contentLengthB1 << 8; + clen |= header.contentLengthB0; + + plen = header.paddingLength; + +recv_again: + if (clen > sizeof(readbuf) - 1) { + readbuflen = sizeof(readbuf) - 1; + } 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, readbuf, &readbuflen); + if (rv != APR_SUCCESS) { + break; + } + readbuf[readbuflen] = 0; + } + + switch (type) { + case FCGI_STDOUT: + if (clen != 0) { + b = apr_bucket_transient_create(readbuf, + readbuflen, + c->bucket_alloc); + + APR_BRIGADE_INSERT_TAIL(ob, b); + + if (! seen_end_of_headers) { + int st = handle_headers(r, &header_state, readbuf, ob); + + if (st == 1) { + seen_end_of_headers = 1; + + rv = ap_pass_brigade(r->output_filters, ob); + if (rv != APR_SUCCESS) { + break; + } + + apr_brigade_cleanup(ob); + + apr_pool_clear(setaside_pool); + } + else if (st == -1) { + rv = APR_EINVAL; + break; + } + 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 + */ + rv = ap_pass_brigade(r->output_filters, ob); + if (rv != APR_SUCCESS) { + break; + } + 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? */ + + b = apr_bucket_eos_create(c->bucket_alloc); + + APR_BRIGADE_INSERT_TAIL(ob, b); + + rv = ap_pass_brigade(r->output_filters, ob); + if (rv != APR_SUCCESS) { + break; + } + + /* XXX Why don't we cleanup here? (logic from AJP) */ + } + break; + + case FCGI_STDERR: + /* TODO: Should probably clean up this logging a bit... */ + if (clen) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "proxy: FCGI: Got error '%s'", readbuf); + } + + if (clen > readbuflen) { + clen -= readbuflen; + goto recv_again; + } + break; + + case FCGI_END_REQUEST: + done = 1; + break; + + default: + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "proxy: FCGI: Got bogus record %d", type); + break; + } + + if (plen) { + readbuflen = plen; + + rv = get_data(conn, readbuf, &readbuflen); + if (rv != APR_SUCCESS) { + break; + } + } + } + } + + apr_brigade_destroy(ib); + apr_brigade_destroy(ob); + + 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) +{ + /* Request IDs are arbitrary numbers that we assign to a + * single request. This would allow multiplex/pipelinig 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. */ + int request_id = 1; + apr_status_t rv; + + /* Step 1: Send FCGI_BEGIN_REQUEST */ + rv = send_begin_request(conn, request_id); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, r->server, + "proxy: FCGI: Failed Writing Request to %s:", + server_portstr); + conn->close = 1; + return HTTP_SERVICE_UNAVAILABLE; + } + + /* Step 2: Send Enviroment via FCGI_PARAMS */ + rv = send_environment(conn, r, request_id); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, r->server, + "proxy: FCGI: 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, r, request_id); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, r->server, + "proxy: FCGI: Error dispatching request to %s:", + server_portstr); + conn->close = 1; + return HTTP_SERVICE_UNAVAILABLE; + } + + return OK; +} + +#define FCGI_SCHEME "FCGI" + +/* + * 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; + + proxy_dir_conf *dconf = ap_get_module_config(r->per_dir_config, + &proxy_module); + + apr_pool_t *p = r->pool; + + apr_uri_t *uri = apr_palloc(r->pool, sizeof(*uri)); + + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "proxy: FCGI: url: %s proxyname: %s proxyport: %d", + url, proxyname, proxyport); + + if (strncasecmp(url, "fcgi://", 7) == 0) { + url += 5; + } + else { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, + "proxy: FCGI: declining URL %s", url); + return DECLINED; + } + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, + "proxy: FCGI: serving URL %s", url); + + /* Create space for state information */ + if (! backend) { + status = ap_proxy_acquire_connection(FCGI_SCHEME, &backend, worker, + r->server); + if (status != OK) { + if (backend) { + backend->close_on_recycle = 1; + ap_proxy_release_connection(FCGI_SCHEME, backend, r->server); + } + return status; + } + } + + backend->is_ssl = 0; + + /* XXX Setting close_on_recycle to 0 is a great way to end up with + * timeouts at this point, since we lack good ways to manage the + * back end fastcgi processes. This should be revisited when we + * have a better story on that part of things. */ + + backend->close_on_recycle = 1; + + /* Step One: Determine Who To Connect To */ + status = ap_proxy_determine_connection(p, r, conf, worker, backend, + uri, &url, proxyname, proxyport, + server_portstr, + sizeof(server_portstr)); + if (status != OK) { + goto cleanup; + } + + /* Step Two: Make the Connection */ + if (ap_proxy_connect_backend(FCGI_SCHEME, backend, worker, r->server)) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "proxy: FCGI: 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); + +cleanup: + /* Do not close the socket */ + ap_proxy_release_connection(FCGI_SCHEME, backend, r->server); + return status; +} + +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); +} + +module AP_MODULE_DECLARE_DATA proxy_fcgi_module = { + STANDARD20_MODULE_STUFF, + NULL, /* create per-directory config structure */ + NULL, /* merge per-directory config structures */ + NULL, /* create per-server config structure */ + NULL, /* merge per-server config structures */ + NULL, /* command apr_table_t */ + register_hooks /* register hooks */ +}; + diff --git a/support/Makefile.in b/support/Makefile.in index 53672e2583a..670e465d9dc 100644 --- a/support/Makefile.in +++ b/support/Makefile.in @@ -3,7 +3,7 @@ DISTCLEAN_TARGETS = apxs apachectl dbmmanage log_server_status \ CLEAN_TARGETS = suexec -PROGRAMS = htpasswd htdigest rotatelogs logresolve ab checkgid htdbm htcacheclean httxt2dbm +PROGRAMS = htpasswd htdigest rotatelogs logresolve ab checkgid htdbm htcacheclean httxt2dbm fcgistarter TARGETS = $(PROGRAMS) PROGRAM_LDADD = $(UTIL_LDFLAGS) $(PROGRAM_DEPENDENCIES) $(EXTRA_LIBS) $(AP_LIBS) @@ -70,3 +70,6 @@ httxt2dbm_OBJECTS = httxt2dbm.lo httxt2dbm: $(httxt2dbm_OBJECTS) $(LINK) $(httxt2dbm_LTFLAGS) $(httxt2dbm_OBJECTS) $(PROGRAM_LDADD) +fcgistarter_OBJECTS = fcgistarter.lo +fcgistarter: $(fcgistarter_OBJECTS) + $(LINK) $(fcgistarter_LTFLAGS) $(fcgistarter_OBJECTS) $(PROGRAM_LDADD) diff --git a/support/fcgistarter.c b/support/fcgistarter.c new file mode 100644 index 00000000000..ad9b9e54906 --- /dev/null +++ b/support/fcgistarter.c @@ -0,0 +1,212 @@ +/* Copyright 2006 The Apache Software Foundation or its licensors, as + * applicable. + * + * Licensed 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 +#include +#include +#include +#include +#include + +#if APR_HAVE_STDLIB_H +#include /* For EXIT_SUCCESS, EXIT_FAILURE */ +#endif + +#if APR_HAVE_UNISTD_H +#include /* For execl */ +#endif + +static const char *usage_message = + "usage: fcgistarter -c -p [-i -N ]\n" + "\n" + "If an interface is not specified, any available will be used.\n"; + +static void usage() +{ + fprintf(stderr, "%s", usage_message); + + exit(EXIT_FAILURE); +} + +static void exit_error(apr_status_t rv, const char *func) +{ + char buffer[1024]; + + fprintf(stderr, + "%s: %s\n", + func, + apr_strerror(rv, buffer, sizeof(buffer))); + + exit(EXIT_FAILURE); +} + +int main(int argc, const char *argv[]) +{ + apr_file_t *infd, *skwrapper; + apr_sockaddr_t *skaddr; + apr_procattr_t *pattr; + apr_getopt_t *gopt; + apr_socket_t *skt; + apr_pool_t *pool; + apr_status_t rv; + apr_proc_t proc; + + + /* Command line arguments */ + int num_to_start = 1, port = 0; + const char *interface = NULL; + const char *command = NULL; + + apr_initialize(); + + atexit(apr_terminate); + + apr_pool_create(&pool, NULL); + + rv = apr_getopt_init(&gopt, pool, argc, argv); + if (rv) { + return EXIT_FAILURE; + } + + for (;;) { + const char *arg; + char opt; + + rv = apr_getopt(gopt, "c:p:i:N:", &opt, &arg); + if (APR_STATUS_IS_EOF(rv)) { + break; + } else if (rv) { + usage(); + } else { + switch (opt) { + case 'c': + command = arg; + break; + + case 'p': + port = atoi(arg); + if (! port) { + usage(); + } + break; + + case 'i': + interface = arg; + break; + + case 'N': + num_to_start = atoi(arg); + if (! num_to_start) { + usage(); + } + break; + + default: + break; + } + } + } + + if (! command || ! port) { + usage(); + } + + rv = apr_socket_create(&skt, APR_INET, SOCK_STREAM, APR_PROTO_TCP, pool); + if (rv) { + exit_error(rv, "apr_socket_create"); + } + + rv = apr_sockaddr_info_get(&skaddr, interface, APR_UNSPEC, port, 0, pool); + if (rv) { + exit_error(rv, "apr_sockaddr_info_get"); + } + + rv = apr_socket_bind(skt, skaddr); + if (rv) { + exit_error(rv, "apr_socket_bind"); + } + + rv = apr_socket_listen(skt, 1024); + if (rv) { + exit_error(rv, "apr_socket_listen"); + } + + while (--num_to_start >= 0) { + rv = apr_proc_fork(&proc, pool); + if (rv == APR_INCHILD) { + apr_os_file_t oft = 0; + apr_os_sock_t oskt; + + rv = apr_proc_detach(APR_PROC_DETACH_DAEMONIZE); + if (rv) { + exit_error(rv, "apr_proc_detach"); + } + +#if defined(WIN32) || defined(OS2) || defined(NETWARE) +#error "Please implement me." +#else + + /* Ok, so we need a file that has file descriptor 0 (which + * FastCGI wants), but points to our socket. This isn't really + * possible in APR, so we cheat a bit. I have no idea how to + * do this on a non-unix platform, so for now this is platform + * specific. Ick. + * + * Note that this has to happen post-detach, otherwise fd 0 + * gets closed during apr_proc_detach and it's all for nothing. + * + * Unfortunately, doing this post detach means we have no way + * to let anyone know if there's a problem at this point :( */ + + rv = apr_os_file_put(&infd, &oft, APR_READ | APR_WRITE, pool); + if (rv) { + exit(EXIT_FAILURE); + } + + rv = apr_os_sock_get(&oskt, skt); + if (rv) { + exit(EXIT_FAILURE); + } + + rv = apr_os_file_put(&skwrapper, &oskt, APR_READ | APR_WRITE, + pool); + if (rv) { + exit(EXIT_FAILURE); + } + + rv = apr_file_dup2(infd, skwrapper, pool); + if (rv) { + exit(EXIT_FAILURE); + } + + /* XXX Can't use apr_proc_create because there's no way to get + * infd into the procattr without going through another dup2, + * which means by the time it gets to the fastcgi process it + * is no longer fd 0, so it doesn't work. Sigh. */ + + execl(command, NULL); +#endif + } else if (rv == APR_INPARENT) { + if (num_to_start == 0) { + apr_socket_close(skt); + } + } else { + exit_error(rv, "apr_proc_fork"); + } + } + + return EXIT_SUCCESS; +}