Browse Source

[core] consolidate dynamic handler response parse

- consolidate dynamic handler HTTP response parsing code
- reduce string copies for CGI, FastCGI, SCGI, proxy response headers
- let read() signal EOF or EAGAIN instead of ioctl FIONREAD 0-data-ready
personal/stbuehler/mod-csrf
Glenn Strauss 4 years ago
parent
commit
0a635fc8be
  1. 607
      src/http-header-glue.c
  2. 423
      src/mod_cgi.c
  3. 350
      src/mod_fastcgi.c
  4. 282
      src/mod_proxy.c
  5. 332
      src/mod_scgi.c
  6. 19
      src/response.h
  7. 2
      tests/mod-cgi.t

607
src/http-header-glue.c

@ -670,12 +670,11 @@ void http_response_send_file (server *srv, connection *con, buffer *path) {
}
}
void http_response_xsendfile (server *srv, connection *con, buffer *path, const array *xdocroot) {
static void http_response_xsendfile (server *srv, connection *con, buffer *path, const array *xdocroot) {
const int status = con->http_status;
int valid = 1;
con->file_started = 1;
/* reset Content-Length, if set by backend
* Content-Length might later be set to size of X-Sendfile static file,
* determined by open(), fstat() to reduces race conditions if the file
@ -727,6 +726,143 @@ void http_response_xsendfile (server *srv, connection *con, buffer *path, const
}
}
static void http_response_xsendfile2(server *srv, connection *con, const buffer *value, const array *xdocroot) {
const char *pos = value->ptr;
buffer *b = srv->tmp_buf;
const int status = con->http_status;
/* reset Content-Length, if set by backend */
if (con->parsed_response & HTTP_CONTENT_LENGTH) {
data_string *ds = (data_string *)
array_get_element(con->response.headers, "Content-Length");
if (ds) buffer_reset(ds->value);
con->parsed_response &= ~HTTP_CONTENT_LENGTH;
con->response.content_length = -1;
}
while (*pos) {
const char *filename, *range;
stat_cache_entry *sce;
off_t begin_range, end_range, range_len;
while (' ' == *pos) pos++;
if (!*pos) break;
filename = pos;
if (NULL == (range = strchr(pos, ' '))) {
/* missing range */
log_error_write(srv, __FILE__, __LINE__, "ss",
"Couldn't find range after filename:", filename);
con->http_status = 502;
break;
}
buffer_copy_string_len(b, filename, range - filename);
/* find end of range */
for (pos = ++range; *pos && *pos != ' ' && *pos != ','; pos++) ;
buffer_urldecode_path(b);
buffer_path_simplify(b, b);
if (con->conf.force_lowercase_filenames) {
buffer_to_lower(b);
}
if (xdocroot->used) {
size_t i, xlen = buffer_string_length(b);
for (i = 0; i < xdocroot->used; ++i) {
data_string *ds = (data_string *)xdocroot->data[i];
size_t dlen = buffer_string_length(ds->value);
if (dlen <= xlen
&& (!con->conf.force_lowercase_filenames
? 0 == memcmp(b->ptr, ds->value->ptr, dlen)
: 0 == strncasecmp(b->ptr, ds->value->ptr, dlen))) {
break;
}
}
if (i == xdocroot->used) {
log_error_write(srv, __FILE__, __LINE__, "SBs",
"X-Sendfile2 (", b,
") not under configured x-sendfile-docroot(s)");
con->http_status = 403;
break;
}
}
if (HANDLER_ERROR == stat_cache_get_entry(srv, con, b, &sce)) {
log_error_write(srv, __FILE__, __LINE__, "sb", "send-file error: "
"couldn't get stat_cache entry for X-Sendfile2:",
b);
con->http_status = 404;
break;
} else if (!S_ISREG(sce->st.st_mode)) {
log_error_write(srv, __FILE__, __LINE__, "sb",
"send-file error: wrong filetype for X-Sendfile2:",
b);
con->http_status = 502;
break;
}
/* found the file */
/* parse range */
end_range = sce->st.st_size - 1;
{
char *rpos = NULL;
errno = 0;
begin_range = strtoll(range, &rpos, 10);
if (errno != 0 || begin_range < 0 || rpos == range)
goto range_failed;
if ('-' != *rpos++) goto range_failed;
if (rpos != pos) {
range = rpos;
end_range = strtoll(range, &rpos, 10);
if (errno != 0 || end_range < 0 || rpos == range)
goto range_failed;
}
if (rpos != pos) goto range_failed;
goto range_success;
range_failed:
log_error_write(srv, __FILE__, __LINE__, "ss",
"Couldn't decode range after filename:", filename);
con->http_status = 502;
break;
range_success: ;
}
/* no parameters accepted */
while (*pos == ' ') pos++;
if (*pos != '\0' && *pos != ',') {
con->http_status = 502;
break;
}
range_len = end_range - begin_range + 1;
if (range_len < 0) {
con->http_status = 502;
break;
}
if (range_len != 0) {
if (0 != http_chunk_append_file_range(srv, con, b,
begin_range, range_len)) {
con->http_status = 502;
break;
}
}
if (*pos == ',') pos++;
}
if (con->http_status >= 400 && status < 300) {
con->mode = DIRECT;
} else if (0 != status && 200 != status) {
con->http_status = status;
}
}
void http_response_backend_error (server *srv, connection *con) {
UNUSED(srv);
if (con->file_started) {
@ -762,6 +898,471 @@ void http_response_backend_done (server *srv, connection *con) {
}
static handler_t http_response_process_local_redir(server *srv, connection *con, size_t blen) {
/* [RFC3875] The Common Gateway Interface (CGI) Version 1.1
* [RFC3875] 6.2.2 Local Redirect Response
*
* The CGI script can return a URI path and query-string
* ('local-pathquery') for a local resource in a Location header field.
* This indicates to the server that it should reprocess the request
* using the path specified.
*
* local-redir-response = local-Location NL
*
* The script MUST NOT return any other header fields or a message-body,
* and the server MUST generate the response that it would have produced
* in response to a request containing the URL
*
* scheme "://" server-name ":" server-port local-pathquery
*
* (Might not have begun to receive body yet, but do skip local-redir
* if we already have started receiving a response body (blen > 0))
* (Also, while not required by the RFC, do not send local-redir back
* to same URL, since CGI should have handled it internally if it
* really wanted to do that internally)
*/
/* con->http_status >= 300 && con->http_status < 400) */
size_t ulen = buffer_string_length(con->uri.path);
data_string *ds = (data_string *)
array_get_element(con->response.headers, "Location");
if (NULL != ds
&& ds->value->ptr[0] == '/'
&& (0 != strncmp(ds->value->ptr, con->uri.path->ptr, ulen)
|| (ds->value->ptr[ulen] != '\0'
&& ds->value->ptr[ulen] != '/'
&& ds->value->ptr[ulen] != '?'))
&& 0 == blen
&& !(con->parsed_response & HTTP_STATUS) /*no "Status" or NPH response*/
&& 1 == con->response.headers->used) {
if (++con->loops_per_request > 5) {
log_error_write(srv, __FILE__, __LINE__, "sb",
"too many internal loops while processing request:",
con->request.orig_uri);
con->http_status = 500; /* Internal Server Error */
con->mode = DIRECT;
return HANDLER_FINISHED;
}
buffer_copy_buffer(con->request.uri, ds->value);
if (con->request.content_length) {
if (con->request.content_length
!= con->request_content_queue->bytes_in) {
con->keep_alive = 0;
}
con->request.content_length = 0;
chunkqueue_reset(con->request_content_queue);
}
if (con->http_status != 307 && con->http_status != 308) {
/* Note: request body (if any) sent to initial dynamic handler
* and is not available to the internal redirect */
con->request.http_method = HTTP_METHOD_GET;
}
/*(caller must reset request as follows)*/
/*connection_response_reset(srv, con);*/ /*(sets con->http_status = 0)*/
/*plugins_call_connection_reset(srv, con);*/
return HANDLER_COMEBACK;
}
return HANDLER_GO_ON;
}
static int http_response_process_headers(server *srv, connection *con, http_response_opts *opts, buffer *hdrs) {
char *ns;
const char *s;
int line = 0;
for (s = hdrs->ptr; NULL != (ns = strchr(s, '\n')); s = ns + 1, ++line) {
const char *key, *value;
int key_len;
data_string *ds;
/* strip the \n */
ns[0] = '\0';
if (ns > s && ns[-1] == '\r') ns[-1] = '\0';
if (0 == line && 0 == strncmp(s, "HTTP/1.", 7)) {
/* non-parsed headers ... we parse them anyway */
if ((s[7] == '1' || s[7] == '0') && s[8] == ' ') {
/* after the space should be a status code for us */
int status = strtol(s+9, NULL, 10);
if (status >= 100 && status < 1000) {
con->parsed_response |= HTTP_STATUS;
con->http_status = status;
} /* else we expected 3 digits and didn't get them */
}
if (0 == con->http_status) {
log_error_write(srv, __FILE__, __LINE__, "ss",
"invalid HTTP status line:", s);
con->http_status = 502; /* Bad Gateway */
con->mode = DIRECT;
return -1;
}
continue;
}
/* parse the headers */
key = s;
if (NULL == (value = strchr(s, ':'))) {
/* we expect: "<key>: <value>\r\n" */
continue;
}
key_len = value - key;
do { ++value; } while (*value == ' ' || *value == '\t'); /* skip LWS */
if (opts->authorizer) {
if (0 == con->http_status || 200 == con->http_status) {
if (key_len == 6 && 0 == strncasecmp(key, "Status", key_len)) {
int status = strtol(value, NULL, 10);
if (status >= 100 && status < 1000) {
con->http_status = status;
} else {
con->http_status = 502; /* Bad Gateway */
break;
}
} else if (key_len > 9
&& 0==strncasecmp(key, CONST_STR_LEN("Variable-"))) {
ds = (data_string *)
array_get_unused_element(con->environment, TYPE_STRING);
if (NULL == ds) ds = data_string_init();
buffer_copy_string_len(ds->key, key + 9, key_len - 9);
buffer_copy_string(ds->value, value);
array_insert_unique(con->environment, (data_unset *)ds);
}
continue;
}
}
switch(key_len) {
case 4:
if (0 == strncasecmp(key, "Date", key_len)) {
con->parsed_response |= HTTP_DATE;
}
break;
case 6:
if (0 == strncasecmp(key, "Status", key_len)) {
int status;
if (opts->backend == BACKEND_PROXY) break; /*(pass w/o parse)*/
status = strtol(value, NULL, 10);
if (status >= 100 && status < 1000) {
con->http_status = status;
con->parsed_response |= HTTP_STATUS;
} else {
con->http_status = 502;
con->mode = DIRECT;
}
continue; /* do not send Status to client */
}
break;
case 8:
if (0 == strncasecmp(key, "Location", key_len)) {
con->parsed_response |= HTTP_LOCATION;
}
break;
case 10:
if (0 == strncasecmp(key, "Connection", key_len)) {
if (opts->backend == BACKEND_PROXY) continue;
con->response.keep_alive =
(0 == strcasecmp(value, "Keep-Alive")) ? 1 : 0;
con->parsed_response |= HTTP_CONNECTION;
}
break;
case 14:
if (0 == strncasecmp(key, "Content-Length", key_len)) {
con->response.content_length = strtoul(value, NULL, 10);
con->parsed_response |= HTTP_CONTENT_LENGTH;
}
break;
case 17:
if (0 == strncasecmp(key, "Transfer-Encoding", key_len)) {
if (opts->backend == BACKEND_PROXY) continue;
con->parsed_response |= HTTP_TRANSFER_ENCODING;
}
break;
default:
break;
}
ds = (data_string *)
array_get_unused_element(con->response.headers, TYPE_STRING);
if (NULL == ds) ds = data_response_init();
buffer_copy_string_len(ds->key, key, key_len);
buffer_copy_string(ds->value, value);
array_insert_unique(con->response.headers, (data_unset *)ds);
}
/* CGI/1.1 rev 03 - 7.2.1.2 */
/* (proxy requires Status-Line, so never true for proxy)*/
if ((con->parsed_response & HTTP_LOCATION) &&
!(con->parsed_response & HTTP_STATUS)) {
con->http_status = 302;
}
return 0;
}
handler_t http_response_parse_headers(server *srv, connection *con, http_response_opts *opts, buffer *b) {
/**
* possible formats of response headers:
*
* proxy or NPH (non-parsed headers):
*
* HTTP/1.0 200 Ok\n
* Header: Value\n
* \n
*
* CGI:
*
* Header: Value\n
* Status: 200\n
* \n
*
* and different mixes of \n and \r\n combinations
*
* Some users also forget about CGI and just send a response
* and hope we handle it. No headers, no header-content separator
*/
int is_nph = (0 == strncmp(b->ptr, "HTTP/1.", 7)); /*nph (non-parsed hdrs)*/
int is_header_end = 0;
size_t last_eol = 0;
size_t i = 0, header_len = buffer_string_length(b);
const char *bstart;
size_t blen;
if (b->ptr[0] == '\n' || (b->ptr[0] == '\r' && b->ptr[1] == '\n')) {
/* no HTTP headers */
i = (b->ptr[0] == '\n') ? 1 : 2;
is_header_end = 1;
} else if (is_nph || b->ptr[(i = strcspn(b->ptr, ":\n"))] == ':') {
/* HTTP headers */
++i;
for (char *c; NULL != (c = strchr(b->ptr+i, '\n')); ++i) {
i = (uintptr_t)(c - b->ptr);
/**
* check if we saw a \n(\r)?\n sequence
*/
if (last_eol > 0 &&
((i - last_eol == 1) ||
(i - last_eol == 2 && b->ptr[i - 1] == '\r'))) {
is_header_end = 1;
break;
}
last_eol = i;
}
} else if (opts->backend == BACKEND_CGI) {
/* no HTTP headers, but a body (special-case for CGI compat) */
/* no colon found; does not appear to be HTTP headers */
if (0 != http_chunk_append_buffer(srv, con, b)) {
return HANDLER_ERROR;
}
con->http_status = 200; /* OK */
con->file_started = 1;
return HANDLER_GO_ON;
} else {
/* invalid response headers */
con->http_status = 502; /* Bad Gateway */
con->mode = DIRECT;
return HANDLER_FINISHED;
}
if (!is_header_end) {
/*(reuse MAX_HTTP_REQUEST_HEADER as max size
* for response headers from backends)*/
if (header_len > MAX_HTTP_REQUEST_HEADER) {
log_error_write(srv, __FILE__, __LINE__, "sb",
"response headers too large for", con->uri.path);
con->http_status = 502; /* Bad Gateway */
con->mode = DIRECT;
return HANDLER_FINISHED;
}
return HANDLER_GO_ON;
}
/* the body starts after the EOL */
bstart = b->ptr + (i + 1);
blen = header_len - (i + 1);
/* strip the last \r?\n */
if (i > 0 && (b->ptr[i - 1] == '\r')) {
i--;
}
buffer_string_set_length(b, i);
if (opts->backend == BACKEND_PROXY && !is_nph) {
/* invalid response Status-Line from HTTP proxy */
con->http_status = 502; /* Bad Gateway */
con->mode = DIRECT;
return HANDLER_FINISHED;
}
if (0 != http_response_process_headers(srv, con, opts, b)) {
return HANDLER_ERROR;
}
con->file_started = 1;
if (opts->authorizer
&& (con->http_status == 0 || con->http_status == 200)) {
return HANDLER_GO_ON;
}
if (con->mode == DIRECT) {
return HANDLER_FINISHED;
}
if (opts->local_redir && con->http_status >= 300 && con->http_status < 400){
/*(con->parsed_response & HTTP_LOCATION)*/
handler_t rc = http_response_process_local_redir(srv, con, blen);
if (rc != HANDLER_GO_ON) return rc;
}
if (opts->xsendfile_allow) {
data_string *ds;
/* X-Sendfile2 is deprecated; historical for fastcgi */
if (opts->backend == BACKEND_FASTCGI
&& NULL != (ds = (data_string *) array_get_element(con->response.headers, "X-Sendfile2"))) {
http_response_xsendfile2(srv, con, ds->value, opts->xsendfile_docroot);
buffer_reset(ds->value); /*(do not send to client)*/
return HANDLER_FINISHED;
} else if (NULL != (ds = (data_string *) array_get_element(con->response.headers, "X-Sendfile"))
|| (opts->backend == BACKEND_FASTCGI /* X-LIGHTTPD-send-file is deprecated; historical for fastcgi */
&& NULL != (ds = (data_string *) array_get_element(con->response.headers, "X-LIGHTTPD-send-file")))) {
http_response_xsendfile(srv, con, ds->value, opts->xsendfile_docroot);
buffer_reset(ds->value); /*(do not send to client)*/
return HANDLER_FINISHED;
}
}
if (blen > 0) {
if (0 != http_chunk_append_mem(srv, con, bstart, blen)) {
return HANDLER_ERROR;
}
}
return HANDLER_GO_ON;
}
handler_t http_response_read(server *srv, connection *con, http_response_opts *opts, buffer *b, int fd, int *fde_ndx) {
while (1) {
ssize_t n;
size_t avail = buffer_string_space(b);
unsigned int toread = 4096;
#if !defined(_WIN32) && !defined(__CYGWIN__)
if (0 == ioctl(fd, FIONREAD, (int *)&toread)) {
if (avail < toread) {
if (toread < 4096)
toread = 4096;
else if (toread > MAX_READ_LIMIT)
toread = MAX_READ_LIMIT;
}
else if (0 == toread) {
#if 0
return (fdevent_event_get_interest(srv->ev, fd) & FDEVENT_IN)
? HANDLER_FINISHED /* read finished */
: HANDLER_GO_ON; /* optimistic read; data not ready */
#else
if (!(fdevent_event_get_interest(srv->ev, fd) & FDEVENT_IN))
return HANDLER_GO_ON; /* optimistic read; data not ready */
toread = 4096; /* let read() below indicate if EOF or EAGAIN */
#endif
}
}
#endif
if (con->conf.stream_response_body & FDEVENT_STREAM_RESPONSE_BUFMIN) {
off_t cqlen = chunkqueue_length(con->write_queue);
if (cqlen + (off_t)toread > 65536 - 4096) {
if (!con->is_writable) {
/*(defer removal of FDEVENT_IN interest since
* connection_state_machine() might be able to send data
* immediately, unless !con->is_writable, where
* connection_state_machine() might not loop back to call
* mod_proxy_handle_subrequest())*/
fdevent_event_clr(srv->ev, fde_ndx, fd, FDEVENT_IN);
}
if (cqlen >= 65536-1) return HANDLER_GO_ON;
toread = 65536 - 1 - (unsigned int)cqlen;
}
}
if (avail < toread) {
avail = toread;
buffer_string_prepare_append(b, avail);
}
n = read(fd, b->ptr+buffer_string_length(b), avail);
if (0 == n) {
return HANDLER_FINISHED; /* read finished */
}
else if (n < 0) {
switch (errno) {
case EAGAIN:
#ifdef EWOULDBLOCK
#if EWOULDBLOCK != EAGAIN
case EWOULDBLOCK:
#endif
#endif
case EINTR:
return HANDLER_GO_ON;
default:
log_error_write(srv, __FILE__, __LINE__, "ssdd",
"read():", strerror(errno), con->fd, fd);
return HANDLER_ERROR;
}
}
buffer_commit(b, (size_t)n);
if (con->file_started == 0) {
/* split header from body */
handler_t rc = http_response_parse_headers(srv, con, opts, b);
if (rc != HANDLER_GO_ON) return rc;
/* accumulate response in b until headers completed (or error) */
if (con->file_started) buffer_string_set_length(b, 0);
} else {
if (0 != http_chunk_append_buffer(srv, con, b)) {
/* error writing to tempfile;
* truncate response or send 500 if nothing sent yet */
return HANDLER_ERROR;
}
buffer_string_set_length(b, 0);
}
if ((con->conf.stream_response_body & FDEVENT_STREAM_RESPONSE_BUFMIN)
&& chunkqueue_length(con->write_queue) > 65536 - 4096) {
if (!con->is_writable) {
/*(defer removal of FDEVENT_IN interest since
* connection_state_machine() might be able to send
* data immediately, unless !con->is_writable, where
* connection_state_machine() might not loop back to
* call the subrequest handler)*/
fdevent_event_clr(srv->ev, fde_ndx, fd, FDEVENT_IN);
}
break;
}
}
return HANDLER_GO_ON;
}
int http_cgi_headers (server *srv, connection *con, http_cgi_opts *opts, http_cgi_header_append_cb cb, void *vdata) {
/* CGI-SPEC 6.1.2, FastCGI spec 6.3 and SCGI spec */

423
src/mod_cgi.c

@ -75,8 +75,6 @@ typedef struct {
PLUGIN_DATA;
buffer_pid_t cgi_pid;
buffer *parse_response;
plugin_config **config_storage;
plugin_config conf;
@ -93,8 +91,8 @@ typedef struct {
plugin_data *plugin_data; /* dumb pointer */
buffer *response;
buffer *response_header;
buffer *cgi_handler; /* dumb pointer */
http_response_opts opts;
plugin_config conf;
} handler_ctx;
@ -104,7 +102,6 @@ static handler_ctx * cgi_handler_ctx_init(void) {
force_assert(hctx);
hctx->response = buffer_init();
hctx->response_header = buffer_init();
hctx->fd = -1;
hctx->fdtocgi = -1;
@ -113,13 +110,9 @@ static handler_ctx * cgi_handler_ctx_init(void) {
static void cgi_handler_ctx_free(handler_ctx *hctx) {
buffer_free(hctx->response);
buffer_free(hctx->response_header);
free(hctx);
}
enum {FDEVENT_HANDLED_UNSET, FDEVENT_HANDLED_FINISHED, FDEVENT_HANDLED_NOT_FINISHED, FDEVENT_HANDLED_COMEBACK, FDEVENT_HANDLED_ERROR};
INIT_FUNC(mod_cgi_init) {
plugin_data *p;
@ -127,8 +120,6 @@ INIT_FUNC(mod_cgi_init) {
force_assert(p);
p->parse_response = buffer_init();
return p;
}
@ -157,8 +148,6 @@ FREE_FUNC(mod_cgi_free) {
if (r->ptr) free(r->ptr);
buffer_free(p->parse_response);
free(p);
return HANDLER_GO_ON;
@ -285,376 +274,6 @@ static int cgi_pid_del(server *srv, plugin_data *p, pid_t pid) {
return 0;
}
static int cgi_response_parse(server *srv, connection *con, plugin_data *p, buffer *in) {
char *ns;
const char *s;
int line = 0;
UNUSED(srv);
buffer_copy_buffer(p->parse_response, in);
for (s = p->parse_response->ptr;
NULL != (ns = strchr(s, '\n'));
s = ns + 1, line++) {
const char *key, *value;
int key_len;
data_string *ds;
/* strip the \n */
ns[0] = '\0';
if (ns > s && ns[-1] == '\r') ns[-1] = '\0';
if (line == 0 &&
0 == strncmp(s, "HTTP/1.", 7)) {
/* non-parsed header ... we parse them anyway */
if ((s[7] == '1' ||
s[7] == '0') &&
s[8] == ' ') {
int status;
/* after the space should be a status code for us */
status = strtol(s+9, NULL, 10);
if (status >= 100 &&
status < 1000) {
/* we expected 3 digits and didn't got them */
con->parsed_response |= HTTP_STATUS;
con->http_status = status;
}
}
} else {
/* parse the headers */
key = s;
if (NULL == (value = strchr(s, ':'))) {
/* we expect: "<key>: <value>\r\n" */
continue;
}
key_len = value - key;
value += 1;
/* skip LWS */
while (*value == ' ' || *value == '\t') value++;
if (NULL == (ds = (data_string *)array_get_unused_element(con->response.headers, TYPE_STRING))) {
ds = data_response_init();
}
buffer_copy_string_len(ds->key, key, key_len);
buffer_copy_string(ds->value, value);
array_insert_unique(con->response.headers, (data_unset *)ds);
switch(key_len) {
case 4:
if (0 == strncasecmp(key, "Date", key_len)) {
con->parsed_response |= HTTP_DATE;
}
break;
case 6:
if (0 == strncasecmp(key, "Status", key_len)) {
int status = strtol(value, NULL, 10);
if (status >= 100 && status < 1000) {
con->http_status = status;
con->parsed_response |= HTTP_STATUS;
} else {
con->http_status = 502;
}
/* do not send Status to client */
buffer_reset(ds->value);
}
break;
case 8:
if (0 == strncasecmp(key, "Location", key_len)) {
con->parsed_response |= HTTP_LOCATION;
}
break;
case 10:
if (0 == strncasecmp(key, "Connection", key_len)) {
con->response.keep_alive = (0 == strcasecmp(value, "Keep-Alive")) ? 1 : 0;
con->parsed_response |= HTTP_CONNECTION;
}
break;
case 14:
if (0 == strncasecmp(key, "Content-Length", key_len)) {
con->response.content_length = strtoul(value, NULL, 10);
con->parsed_response |= HTTP_CONTENT_LENGTH;
}
break;
case 17:
if (0 == strncasecmp(key, "Transfer-Encoding", key_len)) {
con->parsed_response |= HTTP_TRANSFER_ENCODING;
}
break;
default:
break;
}
}
}
/* CGI/1.1 rev 03 - 7.2.1.2 */
if ((con->parsed_response & HTTP_LOCATION) &&
!(con->parsed_response & HTTP_STATUS)) {
con->http_status = 302;
}
return 0;
}
static int cgi_demux_response(server *srv, handler_ctx *hctx) {
plugin_data *p = hctx->plugin_data;
connection *con = hctx->remote_conn;
while(1) {
int n;
int toread;
#if defined(__WIN32)
buffer_string_prepare_copy(hctx->response, 4 * 1024);
#else
if (ioctl(hctx->fd, FIONREAD, &toread) || toread <= 4*1024) {
buffer_string_prepare_copy(hctx->response, 4 * 1024);
} else {
if (toread > MAX_READ_LIMIT) toread = MAX_READ_LIMIT;
buffer_string_prepare_copy(hctx->response, toread);
}
#endif
if (-1 == (n = read(hctx->fd, hctx->response->ptr, hctx->response->size - 1))) {
if (errno == EAGAIN || errno == EINTR) {
/* would block, wait for signal */
return FDEVENT_HANDLED_NOT_FINISHED;
}
/* error */
log_error_write(srv, __FILE__, __LINE__, "sdd", strerror(errno), con->fd, hctx->fd);
return FDEVENT_HANDLED_ERROR;
}
if (n == 0) {
/* read finished */
return FDEVENT_HANDLED_FINISHED;
}
buffer_commit(hctx->response, n);
/* split header from body */
if (con->file_started == 0) {
int is_header = 0;
int is_header_end = 0;
size_t last_eol = 0;
size_t i, header_len;
buffer_append_string_buffer(hctx->response_header, hctx->response);
/**
* we have to handle a few cases:
*
* nph:
*
* HTTP/1.0 200 Ok\n
* Header: Value\n
* \n
*
* CGI:
* Header: Value\n
* Status: 200\n
* \n
*
* and different mixes of \n and \r\n combinations
*
* Some users also forget about CGI and just send a response and hope
* we handle it. No headers, no header-content seperator
*
*/
/* nph (non-parsed headers) */
if (0 == strncmp(hctx->response_header->ptr, "HTTP/1.", 7)) is_header = 1;
header_len = buffer_string_length(hctx->response_header);
for (i = 0; !is_header_end && i < header_len; i++) {
char c = hctx->response_header->ptr[i];
switch (c) {
case ':':
/* we found a colon
*
* looks like we have a normal header
*/
is_header = 1;
break;
case '\n':
/* EOL */
if (is_header == 0) {
/* we got a EOL but we don't seem to got a HTTP header */
is_header_end = 1;
break;
}
/**
* check if we saw a \n(\r)?\n sequence
*/
if (last_eol > 0 &&
((i - last_eol == 1) ||
(i - last_eol == 2 && hctx->response_header->ptr[i - 1] == '\r'))) {
is_header_end = 1;
break;
}
last_eol = i;
break;
}
}
if (is_header_end) {
if (!is_header) {
/* no header, but a body */
if (0 != http_chunk_append_buffer(srv, con, hctx->response_header)) {
return FDEVENT_HANDLED_ERROR;
}
if (0 == con->http_status) con->http_status = 200; /* OK */
} else {
const char *bstart;
size_t blen;
/* the body starts after the EOL */
bstart = hctx->response_header->ptr + i;
blen = header_len - i;
/**
* i still points to the char after the terminating EOL EOL
*
* put it on the last \n again
*/
i--;
/* string the last \r?\n */
if (i > 0 && (hctx->response_header->ptr[i - 1] == '\r')) {
i--;
}
buffer_string_set_length(hctx->response_header, i);
/* parse the response header */
cgi_response_parse(srv, con, p, hctx->response_header);
/* [RFC3875] 6.2.2 Local Redirect Response
*
* The CGI script can return a URI path and query-string
* ('local-pathquery') for a local resource in a Location header field.
* This indicates to the server that it should reprocess the request
* using the path specified.
*
* local-redir-response = local-Location NL
*
* The script MUST NOT return any other header fields or a message-body,
* and the server MUST generate the response that it would have produced
* in response to a request containing the URL
*
* scheme "://" server-name ":" server-port local-pathquery
*
* (Might not have begun to receive body yet, but do skip local-redir
* if we already have started receiving a response body (blen > 0))
* (Also, while not required by the RFC, do not send local-redir back
* to same URL, since CGI should have handled it internally if it
* really wanted to do that internally)
*/
if (hctx->conf.local_redir && con->http_status >= 300 && con->http_status < 400) {
/*(con->parsed_response & HTTP_LOCATION)*/
size_t ulen = buffer_string_length(con->uri.path);
data_string *ds;
if (NULL != (ds = (data_string *) array_get_element(con->response.headers, "Location"))
&& ds->value->ptr[0] == '/'
&& (0 != strncmp(ds->value->ptr, con->uri.path->ptr, ulen)
|| (ds->value->ptr[ulen] != '\0' && ds->value->ptr[ulen] != '/' && ds->value->ptr[ulen] != '?'))
&& 0 == blen
&& !(con->parsed_response & HTTP_STATUS) /* no "Status" or NPH response line */
&& 1 == con->response.headers->used) {
if (++con->loops_per_request > 5) {
log_error_write(srv, __FILE__, __LINE__, "sb", "too many internal loops while processing request:", con->request.orig_uri);
con->http_status = 500; /* Internal Server Error */
con->mode = DIRECT;
return FDEVENT_HANDLED_FINISHED;
}
buffer_copy_buffer(con->request.uri, ds->value);
if (con->request.content_length) {
if (con->request.content_length != con->request_content_queue->bytes_in) {
con->keep_alive = 0;
}
con->request.content_length = 0;
chunkqueue_reset(con->request_content_queue);
}
if (con->http_status != 307 && con->http_status != 308) {
/* Note: request body (if any) sent to initial dynamic handler
* and is not available to the internal redirect */
con->request.http_method = HTTP_METHOD_GET;
}
connection_response_reset(srv, con); /*(includes con->http_status = 0)*/
plugins_call_connection_reset(srv, con);
return FDEVENT_HANDLED_COMEBACK;
}
}
if (hctx->conf.xsendfile_allow) {
data_string *ds;
if (NULL != (ds = (data_string *) array_get_element(con->response.headers, "X-Sendfile"))) {
http_response_xsendfile(srv, con, ds->value, hctx->conf.xsendfile_docroot);
return FDEVENT_HANDLED_FINISHED;
}
}
if (blen > 0) {
if (0 != http_chunk_append_mem(srv, con, bstart, blen)) {
return FDEVENT_HANDLED_ERROR;
}
}
}
con->file_started = 1;
} else {
/*(reuse MAX_HTTP_REQUEST_HEADER as max size for response headers from backends)*/
if (header_len > MAX_HTTP_REQUEST_HEADER) {
log_error_write(srv, __FILE__, __LINE__, "sb", "response headers too large for", con->uri.path);
con->http_status = 502; /* Bad Gateway */
con->mode = DIRECT;
return FDEVENT_HANDLED_FINISHED;
}
}
} else {
if (0 != http_chunk_append_buffer(srv, con, hctx->response)) {
return FDEVENT_HANDLED_ERROR;
}
if ((con->conf.stream_response_body & FDEVENT_STREAM_RESPONSE_BUFMIN)
&& chunkqueue_length(con->write_queue) > 65536 - 4096) {
if (!con->is_writable) {
/*(defer removal of FDEVENT_IN interest since
* connection_state_machine() might be able to send data
* immediately, unless !con->is_writable, where
* connection_state_machine() might not loop back to call
* mod_cgi_handle_subrequest())*/
fdevent_event_clr(srv->ev, &(hctx->fde_ndx), hctx->fd, FDEVENT_IN);
}
break;
}
}
#if 0
log_error_write(srv, __FILE__, __LINE__, "ddss", con->fd, hctx->fd, connection_get_state(con->state), b->ptr);
#endif
}
return FDEVENT_HANDLED_NOT_FINISHED;
}
static void cgi_connection_close_fdtocgi(server *srv, handler_ctx *hctx) {
/*(closes only hctx->fdtocgi)*/
@ -805,30 +424,23 @@ static handler_t cgi_handle_fdevent_send (server *srv, void *ctx, int revents) {
static int cgi_recv_response(server *srv, handler_ctx *hctx) {
switch (cgi_demux_response(srv, hctx)) {
case FDEVENT_HANDLED_NOT_FINISHED:
break;
case FDEVENT_HANDLED_FINISHED:
/* we are done */
#if 0
log_error_write(srv, __FILE__, __LINE__, "ddss", con->fd, hctx->fd, connection_get_state(con->state), "finished");
#endif
switch (http_response_read(srv, hctx->remote_conn, &hctx->opts,
hctx->response, hctx->fd, &hctx->fde_ndx)) {
default:
return HANDLER_GO_ON;
case HANDLER_ERROR:
http_response_backend_error(srv, hctx->remote_conn);
/* fall through */
case HANDLER_FINISHED:
cgi_connection_close(srv, hctx);
/* if we get a IN|HUP and have read everything don't exec the close twice */
return HANDLER_FINISHED;
case FDEVENT_HANDLED_COMEBACK:
case HANDLER_COMEBACK:
/* hctx->conf.local_redir */
connection_response_reset(srv, hctx->remote_conn); /*(includes con->http_status = 0)*/
plugins_call_connection_reset(srv, hctx->remote_conn);
/*cgi_connection_close(srv, hctx);*//*(already cleaned up and hctx is now invalid)*/
return HANDLER_COMEBACK;
case FDEVENT_HANDLED_ERROR:
log_error_write(srv, __FILE__, __LINE__, "s", "demuxer failed: ");
cgi_connection_close(srv, hctx);
return HANDLER_FINISHED;
}
return HANDLER_GO_ON;
}
@ -856,10 +468,10 @@ static handler_t cgi_handle_fdevent(server *srv, void *ctx, int revents) {
rc = cgi_recv_response(srv,hctx);/*(might invalidate hctx)*/
} while (rc == HANDLER_GO_ON); /*(unless HANDLER_GO_ON)*/
return rc; /* HANDLER_FINISHED or HANDLER_COMEBACK or HANDLER_ERROR */
} else if (!buffer_string_is_empty(hctx->response_header)) {
} else if (!buffer_string_is_empty(hctx->response)) {
/* unfinished header package which is a body in reality */
con->file_started = 1;
if (0 != http_chunk_append_buffer(srv, con, hctx->response_header)) {
if (0 != http_chunk_append_buffer(srv, con, hctx->response)) {
cgi_connection_close(srv, hctx);
return HANDLER_ERROR;
}
@ -1403,6 +1015,11 @@ URIHANDLER_FUNC(cgi_is_handled) {
hctx->plugin_data = p;
hctx->cgi_handler = cgi_handler;
memcpy(&hctx->conf, &p->conf, sizeof(plugin_config));
hctx->opts.backend = BACKEND_CGI;
hctx->opts.authorizer = 0;
hctx->opts.local_redir = hctx->conf.local_redir;
hctx->opts.xsendfile_allow = hctx->conf.xsendfile_allow;
hctx->opts.xsendfile_docroot = hctx->conf.xsendfile_docroot;
con->plugin_ctx[p->id] = hctx;
con->mode = p->id;
}

350
src/mod_fastcgi.c

@ -342,6 +342,7 @@ typedef struct {
int request_id;
int send_content_body;
http_response_opts opts;
plugin_config conf;
connection *remote_conn; /* dumb pointer */
@ -457,7 +458,7 @@ static handler_ctx * handler_ctx_init(void) {
hctx->fde_ndx = -1;
hctx->response_header = buffer_init();
/*hctx->response_header = buffer_init();*//*(allocated when needed)*/
hctx->request_id = 0;
hctx->fcgi_mode = FCGI_RESPONDER;
@ -1751,6 +1752,8 @@ static handler_t fcgi_reconnect(server *srv, handler_ctx *hctx) {
fcgi_host_assign(srv, hctx, hctx->host);
hctx->request_id = 0;
hctx->opts.xsendfile_allow = hctx->host->xsendfile_allow;
hctx->opts.xsendfile_docroot = hctx->host->xsendfile_docroot;
fcgi_set_state(srv, hctx, FCGI_STATE_INIT);
return HANDLER_COMEBACK;
}
@ -2054,257 +2057,6 @@ static int fcgi_create_env(server *srv, handler_ctx *hctx, int request_id) {
return 0;
}
static int fcgi_response_parse(server *srv, connection *con, plugin_data *p, buffer *in) {
char *s, *ns;
handler_ctx *hctx = con->plugin_ctx[p->id];
fcgi_extension_host *host= hctx->host;
int have_sendfile2 = 0;
off_t sendfile2_content_length = 0;
UNUSED(srv);
/* search for \n */
for (s = in->ptr; NULL != (ns = strchr(s, '\n')); s = ns + 1) {
char *key, *value;
int key_len;
/* a good day. Someone has read the specs and is sending a \r\n to us */
if (ns > in->ptr &&
*(ns-1) == '\r') {
*(ns-1) = '\0';
}
ns[0] = '\0';
key = s;
if (NULL == (value = strchr(s, ':'))) {
/* we expect: "<key>: <value>\n" */
continue;
}
key_len = value - key;
value++;
/* strip WS */
while (*value == ' ' || *value == '\t') value++;
if (hctx->fcgi_mode != FCGI_AUTHORIZER ||
!(con->http_status == 0 ||
con->http_status == 200)) {
/* authorizers shouldn't affect the response headers sent back to the client */
/* don't forward Status: */
if (key_len != sizeof("Status")-1 || 0 != strncasecmp(key, "Status", key_len)) {
data_string *ds;
if (NULL == (ds = (data_string *)array_get_unused_element(con->response.headers, TYPE_STRING))) {
ds = data_response_init();
}
buffer_copy_string_len(ds->key, key, key_len);
buffer_copy_string(ds->value, value);
array_insert_unique(con->response.headers, (data_unset *)ds);
}
}
if (hctx->fcgi_mode == FCGI_AUTHORIZER &&
key_len > 9 &&
0 == strncasecmp(key, CONST_STR_LEN("Variable-"))) {
data_string *ds;
if (NULL == (ds = (data_string *)array_get_unused_element(con->environment, TYPE_STRING))) {
ds = data_response_init();
}
buffer_copy_string_len(ds->key, key + 9, key_len - 9);
buffer_copy_string(ds->value, value);
array_insert_unique(con->environment, (data_unset *)ds);
}
switch(key_len) {
case 4:
if (0 == strncasecmp(key, "Date", key_len)) {
con->parsed_response |= HTTP_DATE;
}
break;
case 6:
if (0 == strncasecmp(key, "Status", key_len)) {
int status = strtol(value, NULL, 10);
if (status >= 100 && status < 1000) {
con->http_status = status;
con->parsed_response |= HTTP_STATUS;
} else {
con->http_status = 502;
}
}
break;
case 8:
if (0 == strncasecmp(key, "Location", key_len)) {
con->parsed_response |= HTTP_LOCATION;
}
break;
case 10:
if (0 == strncasecmp(key, "Connection", key_len)) {
con->response.keep_alive = (0 == strcasecmp(value, "Keep-Alive")) ? 1 : 0;
con->parsed_response |= HTTP_CONNECTION;
}
break;
case 11:
if (host->xsendfile_allow && 0 == strncasecmp(key, "X-Sendfile2", key_len) && hctx->send_content_body) {
char *pos = value;
have_sendfile2 = 1;
while (*pos) {
char *filename, *range;
stat_cache_entry *sce;
off_t begin_range, end_range, range_len;
while (' ' == *pos) pos++;
if (!*pos) break;
filename = pos;
if (NULL == (range = strchr(pos, ' '))) {
/* missing range */
if (hctx->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "ss", "Couldn't find range after filename:", filename);
}
return 502;
}
buffer_copy_string_len(srv->tmp_buf, filename, range - filename);
/* find end of range */
for (pos = ++range; *pos && *pos != ' ' && *pos != ','; pos++) ;
buffer_urldecode_path(srv->tmp_buf);
buffer_path_simplify(srv->tmp_buf, srv->tmp_buf);
if (con->conf.force_lowercase_filenames) {
buffer_to_lower(srv->tmp_buf);
}
if (host->xsendfile_docroot->used) {
size_t i, xlen = buffer_string_length(srv->tmp_buf);
for (i = 0; i < host->xsendfile_docroot->used; ++i) {
data_string *ds = (data_string *)host->xsendfile_docroot->data[i];
size_t dlen = buffer_string_length(ds->value);
if (dlen <= xlen
&& (!con->conf.force_lowercase_filenames
? 0 == memcmp(srv->tmp_buf->ptr, ds->value->ptr, dlen)
: 0 == strncasecmp(srv->tmp_buf->ptr, ds->value->ptr, dlen))) {
break;
}
}
if (i == host->xsendfile_docroot->used) {
log_error_write(srv, __FILE__, __LINE__, "SBs",
"X-Sendfile2 (", srv->tmp_buf,
") not under configured x-sendfile-docroot(s)");
return 403;
}
}
if (HANDLER_ERROR == stat_cache_get_entry(srv, con, srv->tmp_buf, &sce)) {
if (hctx->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "sb",
"send-file error: couldn't get stat_cache entry for X-Sendfile2:",
srv->tmp_buf);
}
return 404;
} else if (!S_ISREG(sce->st.st_mode)) {
if (hctx->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "sb",
"send-file error: wrong filetype for X-Sendfile2:",
srv->tmp_buf);
}
return 502;
}
/* found the file */
/* parse range */
end_range = sce->st.st_size - 1;
{
char *rpos = NULL;
errno = 0;
begin_range = strtoll(range, &rpos, 10);
if (errno != 0 || begin_range < 0 || rpos == range) goto range_failed;
if ('-' != *rpos++) goto range_failed;
if (rpos != pos) {
range = rpos;
end_range = strtoll(range, &rpos, 10);
if (errno != 0 || end_range < 0 || rpos == range) goto range_failed;
}
if (rpos != pos) goto range_failed;
goto range_success;
range_failed:
if (hctx->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "ss", "Couldn't decode range after filename:", filename);
}
return 502;
range_success: ;
}
/* no parameters accepted */
while (*pos == ' ') pos++;
if (*pos != '\0' && *pos != ',') return 502;
range_len = end_range - begin_range + 1;
if (range_len < 0) return 502;
if (range_len != 0) {
if (0 != http_chunk_append_file_range(srv, con, srv->tmp_buf, begin_range, range_len)) {
return 502;
}
}
sendfile2_content_length += range_len;
if (*pos == ',') pos++;
}
}
break;
case 14:
if (0 == strncasecmp(key, "Content-Length", key_len)) {
con->response.content_length = strtoul(value, NULL, 10);
con->parsed_response |= HTTP_CONTENT_LENGTH;
if (con->response.content_length < 0) con->response.content_length = 0;
}
break;
case 17:
if (0 == strncasecmp(key, "Transfer-Encoding", key_len)) {
con->parsed_response |= HTTP_TRANSFER_ENCODING;
}
break;
default:
break;
}
}
if (have_sendfile2) {
data_string *dcls;
/* fix content-length */
if (NULL == (dcls = (data_string *)array_get_unused_element(con->response.headers, TYPE_STRING))) {
dcls = data_response_init();
}
buffer_copy_string_len(dcls->key, "Content-Length", sizeof("Content-Length")-1);
buffer_copy_int(dcls->value, sendfile2_content_length);
array_replace(con->response.headers, (data_unset *)dcls);
con->parsed_response |= HTTP_CONTENT_LENGTH;
con->response.content_length = sendfile2_content_length;
return 200;
}
/* CGI/1.1 rev 03 - 7.2.1.2 */
if ((con->parsed_response & HTTP_LOCATION) &&
!(con->parsed_response & HTTP_STATUS)) {
con->http_status = 302;
}
return 0;
}
typedef struct {
buffer *b;
unsigned int len;
@ -2394,13 +2146,11 @@ static int fastcgi_get_packet(server *srv, handler_ctx *hctx, fastcgi_response_p
static int fcgi_demux_response(server *srv, handler_ctx *hctx) {
int fin = 0;
int toread, ret;
int toread;
ssize_t r = 0;
plugin_data *p = hctx->plugin_data;
connection *con = hctx->remote_conn;
int fcgi_fd = hctx->fd;
fcgi_extension_host *host= hctx->host;
fcgi_proc *proc = hctx->proc;
/*
@ -2415,6 +2165,10 @@ static int fcgi_demux_response(server *srv, handler_ctx *hctx) {
"unexpected end-of-file (perhaps the fastcgi process died):",
fcgi_fd);
return -1;
} else if (0 == toread) {
if (!(fdevent_event_get_interest(srv->ev, hctx->fd) & FDEVENT_IN))
return HANDLER_GO_ON; /* optimistic read; data not ready */
toread = 4096; /* let read() below indicate if EOF or EAGAIN */
}
#else
toread = 4096;
@ -2478,76 +2232,28 @@ static int fcgi_demux_response(server *srv, handler_ctx *hctx) {
/* is the header already finished */
if (0 == con->file_started) {
char *c;
data_string *ds;
/* search for header terminator
*
* if we start with \r\n check if last packet terminated with \r\n
* if we start with \n check if last packet terminated with \n
* search for \r\n\r\n
* search for \n\n
*/
buffer_append_string_buffer(hctx->response_header, packet.b);
if (NULL != (c = buffer_search_string_len(hctx->response_header, CONST_STR_LEN("\r\n\r\n")))) {
char *hend = c + 4; /* header end == body start */