lighttpd1.4/src/mod_cgi.c

1560 lines
42 KiB
C
Raw Normal View History

#include "first.h"
#include "server.h"
#include "stat_cache.h"
#include "keyvalue.h"
#include "log.h"
#include "connections.h"
#include "joblist.h"
#include "response.h"
#include "http_chunk.h"
#include "plugin.h"
#include <sys/types.h>
#include "sys-mmap.h"
#ifdef __WIN32
# include <winsock2.h>
#else
# include <sys/socket.h>
# include <sys/wait.h>
# include <netinet/in.h>
# include <arpa/inet.h>
#endif
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <fdevent.h>
#include <signal.h>
#include <ctype.h>
#include <assert.h>
#include <stdio.h>
#include <fcntl.h>
static int pipe_cloexec(int pipefd[2]) {
#ifdef HAVE_PIPE2
if (0 == pipe2(pipefd, O_CLOEXEC)) return 0;
#endif
return 0 == pipe(pipefd)
#ifdef FD_CLOEXEC
&& 0 == fcntl(pipefd[0], F_SETFD, FD_CLOEXEC)
&& 0 == fcntl(pipefd[1], F_SETFD, FD_CLOEXEC)
#endif
? 0
: -1;
}
enum {EOL_UNSET, EOL_N, EOL_RN};
typedef struct {
char **ptr;
size_t size;
size_t used;
} char_array;
typedef struct {
pid_t *ptr;
size_t used;
size_t size;
} buffer_pid_t;
typedef struct {
array *cgi;
unsigned short execute_x_only;
unsigned short local_redir;
unsigned short xsendfile_allow;
array *xsendfile_docroot;
} plugin_config;
typedef struct {
PLUGIN_DATA;
buffer_pid_t cgi_pid;
buffer *parse_response;
plugin_config **config_storage;
plugin_config conf;
} plugin_data;
typedef struct {
pid_t pid;
int fd;
int fdtocgi;
int fde_ndx; /* index into the fd-event buffer */
int fde_ndx_tocgi; /* index into the fd-event buffer */
connection *remote_conn; /* dumb pointer */
plugin_data *plugin_data; /* dumb pointer */
buffer *response;
buffer *response_header;
buffer *cgi_handler; /* dumb pointer */
plugin_config conf;
} handler_ctx;
static handler_ctx * cgi_handler_ctx_init(void) {
handler_ctx *hctx = calloc(1, sizeof(*hctx));
force_assert(hctx);
hctx->response = buffer_init();
hctx->response_header = buffer_init();
hctx->fd = -1;
hctx->fdtocgi = -1;
return hctx;
}
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;
p = calloc(1, sizeof(*p));
force_assert(p);
p->parse_response = buffer_init();
return p;
}
FREE_FUNC(mod_cgi_free) {
plugin_data *p = p_d;
buffer_pid_t *r = &(p->cgi_pid);
UNUSED(srv);
if (p->config_storage) {
size_t i;
for (i = 0; i < srv->config_context->used; i++) {
plugin_config *s = p->config_storage[i];
if (NULL == s) continue;
array_free(s->cgi);
array_free(s->xsendfile_docroot);
free(s);
}
free(p->config_storage);
}
if (r->ptr) free(r->ptr);
buffer_free(p->parse_response);
free(p);
return HANDLER_GO_ON;
}
SETDEFAULTS_FUNC(mod_fastcgi_set_defaults) {
plugin_data *p = p_d;
size_t i = 0;
config_values_t cv[] = {
{ "cgi.assign", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
{ "cgi.execute-x-only", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 1 */
{ "cgi.x-sendfile", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 2 */
{ "cgi.x-sendfile-docroot", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 3 */
{ "cgi.local-redir", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 4 */
{ NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET}
};
if (!p) return HANDLER_ERROR;
p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
force_assert(p->config_storage);
for (i = 0; i < srv->config_context->used; i++) {
data_config const* config = (data_config const*)srv->config_context->data[i];
plugin_config *s;
s = calloc(1, sizeof(plugin_config));
force_assert(s);
s->cgi = array_init();
s->execute_x_only = 0;
s->local_redir = 0;
s->xsendfile_allow= 0;
s->xsendfile_docroot = array_init();
cv[0].destination = s->cgi;
cv[1].destination = &(s->execute_x_only);
cv[2].destination = &(s->xsendfile_allow);
cv[3].destination = s->xsendfile_docroot;
cv[4].destination = &(s->local_redir);
p->config_storage[i] = s;
if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
return HANDLER_ERROR;
}
if (!array_is_kvstring(s->cgi)) {
log_error_write(srv, __FILE__, __LINE__, "s",
"unexpected value for cgi.assign; expected list of \"ext\" => \"exepath\"");
return HANDLER_ERROR;
}
if (s->xsendfile_docroot->used) {
size_t j;
for (j = 0; j < s->xsendfile_docroot->used; ++j) {
data_string *ds = (data_string *)s->xsendfile_docroot->data[j];
if (ds->type != TYPE_STRING) {
log_error_write(srv, __FILE__, __LINE__, "s",
"unexpected type for key cgi.x-sendfile-docroot; expected: cgi.x-sendfile-docroot = ( \"/allowed/path\", ... )");
return HANDLER_ERROR;
}
if (ds->value->ptr[0] != '/') {
log_error_write(srv, __FILE__, __LINE__, "SBs",
"cgi.x-sendfile-docroot paths must begin with '/'; invalid: \"", ds->value, "\"");
return HANDLER_ERROR;
}
buffer_path_simplify(ds->value, ds->value);
buffer_append_slash(ds->value);
}
}
}
return HANDLER_GO_ON;
}
static int cgi_pid_add(server *srv, plugin_data *p, pid_t pid) {
int m = -1;
size_t i;
buffer_pid_t *r = &(p->cgi_pid);
UNUSED(srv);
for (i = 0; i < r->used; i++) {
if (r->ptr[i] > m) m = r->ptr[i];
}
if (r->size == 0) {
r->size = 16;
r->ptr = malloc(sizeof(*r->ptr) * r->size);
force_assert(r->ptr);
} else if (r->used == r->size) {
r->size += 16;
r->ptr = realloc(r->ptr, sizeof(*r->ptr) * r->size);
force_assert(r->ptr);
}
r->ptr[r->used++] = pid;
return m;
}
static int cgi_pid_del(server *srv, plugin_data *p, pid_t pid) {
size_t i;
buffer_pid_t *r = &(p->cgi_pid);
UNUSED(srv);
for (i = 0; i < r->used; i++) {
if (r->ptr[i] == pid) break;
}
if (i != r->used) {
/* found */
if (i != r->used - 1) {
r->ptr[i] = r->ptr[r->used - 1];
}
r->used--;
}
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);
fix buffer, chunk and http_chunk API * remove unused structs and functions (buffer_array, read_buffer) * change return type from int to void for many functions, as the return value (indicating error/success) was never checked, and the function would only fail on programming errors and not on invalid input; changed functions to use force_assert instead of returning an error. * all "len" parameters now are the real size of the memory to be read. the length of strings is given always without the terminating 0. * the "buffer" struct still counts the terminating 0 in ->used, provide buffer_string_length() to get the length of a string in a buffer. unset config "strings" have used == 0, which is used in some places to distinguish unset values from "" (empty string) values. * most buffer usages should now use it as string container. * optimise some buffer copying by "moving" data to other buffers * use (u)intmax_t for generic int-to-string functions * remove unused enum values: UNUSED_CHUNK, ENCODING_UNSET * converted BUFFER_APPEND_SLASH to inline function (no macro feature needed) * refactor: create chunkqueue_steal: moving (partial) chunks into another queue * http_chunk: added separate function to terminate chunked body instead of magic handling in http_chunk_append_mem(). http_chunk_append_* now handle empty chunks, and never terminate the chunked body. From: Stefan Bühler <stbuehler@web.de> git-svn-id: svn://svn.lighttpd.net/lighttpd/branches/lighttpd-1.4.x@2975 152afb58-edef-0310-8abb-c4023f1b3aa9
2015-02-08 12:37:10 +00:00
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)*/
[mod_cgi] skip local-redir handling if to self (fixes #2779, #2108) Loosen local redirect handling in mod_cgi to skip handling as local redirect if the Location matches con->uri.path, since if the request is intended to redirect back to the same CGI using the same request method, path info, and query string, the CGI would logically just return the final intended response. Loosening this handling avoids a problem with applications (potentially) accessible through multiple gateways, where the application is not aware of this specific handling of Location in the Common Gateway Interface (CGI/1.1), the application sends abs-path in the Location response header instead of absoluteURI, and the application expects the client to receive this Location response header instead of the server to process as a CGI local redirect. One example of such an application is LuCI, which sends Set-Cookie with Location: /abs-path https://github.com/openwrt/luci (Note that this loose check for matching con->uri.path is not perfect and might not match if the CGI returned a path with a different case and the server is on a case-insensitive filesystem, or if the path returned by the CGI is rewritten elsewhere to a different con->uri.path before getting to mod_cgi.) RFC3875 CGI 1.1 specification section 6.2.2 Local Redirect Response http://www.ietf.org/rfc/rfc3875 x-ref: "CGI local-redir handling conflicts with LuCI redirect w/ Set-Cookie" https://redmine.lighttpd.net/issues/2779 "CGI local redirect not implemented correctly" https://redmine.lighttpd.net/issues/2108
2017-01-06 23:03:02 +00:00
size_t ulen = buffer_string_length(con->uri.path);
data_string *ds;
if (NULL != (ds = (data_string *) array_get_element(con->response.headers, "Location"))
[mod_cgi] skip local-redir handling if to self (fixes #2779, #2108) Loosen local redirect handling in mod_cgi to skip handling as local redirect if the Location matches con->uri.path, since if the request is intended to redirect back to the same CGI using the same request method, path info, and query string, the CGI would logically just return the final intended response. Loosening this handling avoids a problem with applications (potentially) accessible through multiple gateways, where the application is not aware of this specific handling of Location in the Common Gateway Interface (CGI/1.1), the application sends abs-path in the Location response header instead of absoluteURI, and the application expects the client to receive this Location response header instead of the server to process as a CGI local redirect. One example of such an application is LuCI, which sends Set-Cookie with Location: /abs-path https://github.com/openwrt/luci (Note that this loose check for matching con->uri.path is not perfect and might not match if the CGI returned a path with a different case and the server is on a case-insensitive filesystem, or if the path returned by the CGI is rewritten elsewhere to a different con->uri.path before getting to mod_cgi.) RFC3875 CGI 1.1 specification section 6.2.2 Local Redirect Response http://www.ietf.org/rfc/rfc3875 x-ref: "CGI local-redir handling conflicts with LuCI redirect w/ Set-Cookie" https://redmine.lighttpd.net/issues/2779 "CGI local redirect not implemented correctly" https://redmine.lighttpd.net/issues/2108
2017-01-06 23:03:02 +00:00
&& 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)*/
fdevent_event_del(srv->ev, &(hctx->fde_ndx_tocgi), hctx->fdtocgi);
fdevent_unregister(srv->ev, hctx->fdtocgi);
fdevent_sched_close(srv->ev, hctx->fdtocgi, 0);
hctx->fdtocgi = -1;
}
static void cgi_connection_close(server *srv, handler_ctx *hctx) {
int status;
pid_t pid;
plugin_data *p = hctx->plugin_data;
connection *con = hctx->remote_conn;
#ifndef __WIN32