[mod_cgi] cgi.limits "read-timeout" "write-timeout" (#3086)

implement write(), read() timeouts for CGI

"write-timeout"
"read-timeout"

x-ref:
  "sockets disabled, out-of-fds with proxy module"
  https://redmine.lighttpd.net/issues/3086
This commit is contained in:
Glenn Strauss 2021-07-25 00:37:15 -04:00
parent da562e3fd6
commit 8c36615f85
1 changed files with 116 additions and 1 deletions

View File

@ -62,8 +62,14 @@ typedef struct {
size_t size;
} buffer_pid_t;
typedef struct {
unix_time64_t read_timeout;
unix_time64_t write_timeout;
} cgi_limits;
typedef struct {
const array *cgi;
const cgi_limits *limits;
unsigned short execute_x_only;
unsigned short local_redir;
unsigned short xsendfile_allow;
@ -91,6 +97,8 @@ typedef struct {
plugin_data *plugin_data; /* dumb pointer */
buffer *response;
unix_time64_t read_ts;
unix_time64_t write_ts;
buffer *cgi_handler; /* dumb pointer */
http_response_opts opts;
plugin_config conf;
@ -148,6 +156,22 @@ FREE_FUNC(mod_cgi_free) {
#ifdef __CYGWIN__
buffer_free(p->env.systemroot);
#endif
if (NULL == p->cvlist) return;
/* (init i to 0 if global context; to 1 to skip empty global context) */
for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
for (; -1 != cpv->k_id; ++cpv) {
if (cpv->vtype != T_CONFIG_LOCAL || NULL == cpv->v.v) continue;
switch (cpv->k_id) {
case 6: /* cgi.limits */
free(cpv->v.v);
break;
default:
break;
}
}
}
}
static void mod_cgi_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
@ -170,6 +194,10 @@ static void mod_cgi_merge_config_cpv(plugin_config * const pconf, const config_p
case 5: /* cgi.upgrade */
pconf->upgrade = (unsigned short)cpv->v.u;
break;
case 6: /* cgi.limits */
if (cpv->vtype != T_CONFIG_LOCAL) break;
pconf->limits = cpv->v.v;
break;
default:/* should not happen */
return;
}
@ -190,6 +218,26 @@ static void mod_cgi_patch_config(request_st * const r, plugin_data * const p) {
}
}
static cgi_limits * mod_cgi_parse_limits(const array * const a, log_error_st * const errh) {
cgi_limits * const limits = calloc(1, sizeof(cgi_limits));
force_assert(limits);
for (uint32_t i = 0; i < a->used; ++i) {
const data_unset * const du = a->data[i];
int32_t v = config_plugin_value_to_int32(du, -1);
if (buffer_eq_icase_slen(&du->key, CONST_STR_LEN("read-timeout"))) {
limits->read_timeout = (unix_time64_t)v;
continue;
}
if (buffer_eq_icase_slen(&du->key, CONST_STR_LEN("write-timeout"))) {
limits->write_timeout = (unix_time64_t)v;
continue;
}
log_error(errh, __FILE__, __LINE__,
"unrecognized cgi.limits param: %s", du->key.ptr);
}
return limits;
}
SETDEFAULTS_FUNC(mod_cgi_set_defaults) {
static const config_plugin_keys_t cpk[] = {
{ CONST_STR_LEN("cgi.assign"),
@ -210,6 +258,9 @@ SETDEFAULTS_FUNC(mod_cgi_set_defaults) {
,{ CONST_STR_LEN("cgi.upgrade"),
T_CONFIG_BOOL,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("cgi.limits"),
T_CONFIG_ARRAY_KVANY,
T_CONFIG_SCOPE_CONNECTION }
,{ NULL, 0,
T_CONFIG_UNSET,
T_CONFIG_SCOPE_UNSET }
@ -222,7 +273,7 @@ SETDEFAULTS_FUNC(mod_cgi_set_defaults) {
/* process and validate config directives
* (init i to 0 if global context; to 1 to skip empty global context) */
for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
const config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
for (; -1 != cpv->k_id; ++cpv) {
switch (cpv->k_id) {
case 0: /* cgi.assign */
@ -245,6 +296,11 @@ SETDEFAULTS_FUNC(mod_cgi_set_defaults) {
case 4: /* cgi.local-redir */
case 5: /* cgi.upgrade */
break;
case 6: /* cgi.limits */
cpv->v.v = mod_cgi_parse_limits(cpv->v.a, srv->errh);
if (NULL == cpv->v.v) return HANDLER_ERROR;
cpv->vtype = T_CONFIG_LOCAL;
break;
default:/* should not happen */
break;
}
@ -431,9 +487,12 @@ static handler_t cgi_response_headers(request_st * const r, struct http_response
static int cgi_recv_response(request_st * const r, handler_ctx * const hctx) {
const off_t bytes_in = r->write_queue.bytes_in;
switch (http_response_read(r, &hctx->opts,
hctx->response, hctx->fdn)) {
default:
if (r->write_queue.bytes_in > bytes_in)
hctx->read_ts = log_monotonic_secs;
return HANDLER_GO_ON;
case HANDLER_ERROR:
http_response_backend_error(r);
@ -543,6 +602,7 @@ static int cgi_write_request(handler_ctx *hctx, int fd) {
for (c = cq->first; c; c = cq->first) {
ssize_t wr = chunkqueue_write_chunk_to_pipe(fd, cq, r->conf.errh);
if (wr > 0) {
hctx->write_ts = log_monotonic_secs;
chunkqueue_mark_written(cq, wr);
/* continue if wrote whole chunk or wrote 16k block
* (see chunkqueue_write_chunk_file_intermed()) */
@ -605,6 +665,7 @@ static int cgi_write_request(handler_ctx *hctx, int fd) {
}
} else {
/* more request body remains to be sent to CGI so register for fdevents */
hctx->write_ts = log_monotonic_secs;
fdevent_fdnode_event_set(ev, hctx->fdntocgi, FDEVENT_OUT);
}
}
@ -734,6 +795,7 @@ static int cgi_create_env(request_st * const r, plugin_data * const p, handler_c
cgi_connection_close(hctx);
return -1;
}
hctx->read_ts = log_monotonic_secs;
fdevent_fdnode_event_set(ev, hctx->fdn, FDEVENT_IN | FDEVENT_RDHUP);
return 0;
@ -825,6 +887,7 @@ SUBREQUEST_FUNC(mod_cgi_handle_subrequest) {
handler_t rc = cgi_recv_response(r, hctx); /*(might invalidate hctx)*/
if (rc == HANDLER_COMEBACK) mod_cgi_local_redir(r);
if (rc != HANDLER_GO_ON) return rc; /*(unless HANDLER_GO_ON)*/
hctx->read_ts = log_monotonic_secs;
fdevent_fdnode_event_add(hctx->ev, hctx->fdn, FDEVENT_IN);
}
}
@ -878,6 +941,57 @@ SUBREQUEST_FUNC(mod_cgi_handle_subrequest) {
}
__attribute_cold__
__attribute_noinline__
static void cgi_trigger_hctx_timeout(handler_ctx * const hctx, const char * const msg) {
request_st * const r = hctx->r;
joblist_append(r->con);
log_error(r->conf.errh, __FILE__, __LINE__,
"%s timeout on CGI: %s (pid: %lld)",
msg, r->physical.path.ptr, (long long)hctx->pid);
if (*msg == 'w') { /* "write" */
/* theoretically, response might be waiting on hctx->fdn pipe
* if it arrived since we last checked for event, and if CGI
* timeout out while reading (or did not read) request body */
handler_t rc = cgi_recv_response(r, hctx); /*(might invalidate hctx)*/
if (rc != HANDLER_GO_ON) return; /*(unless HANDLER_GO_ON)*/
}
if (0 == r->http_status) r->http_status = 504; /* Gateway Timeout */
cgi_connection_close(hctx);
}
static handler_t cgi_trigger_cb(server *srv, void *p_d) {
UNUSED(srv);
const unix_time64_t mono = log_monotonic_secs;
const buffer_pid_t * const cgi_pid = &((plugin_data *)p_d)->cgi_pid;
for (size_t i = 0, used = cgi_pid->used; i < used; ++i) {
/*(hctx stays in cgi_pid list until process pid is reaped,
* so p->cgi_pid[] is not modified during this loop)*/
handler_ctx * const hctx = (handler_ctx *)cgi_pid->ptr[i].ctx;
if (!hctx) continue; /*(already called cgi_pid_kill())*/
const cgi_limits * const limits = hctx->conf.limits;
if (NULL == limits) continue;
if (limits->read_timeout && hctx->fdn
&& (fdevent_fdnode_interest(hctx->fdn) & FDEVENT_IN)
&& mono - hctx->read_ts > limits->read_timeout) {
cgi_trigger_hctx_timeout(hctx, "read");
continue;
}
if (limits->write_timeout && hctx->fdntocgi
&& (fdevent_fdnode_interest(hctx->fdntocgi) & FDEVENT_OUT)
&& mono - hctx->write_ts > limits->write_timeout) {
cgi_trigger_hctx_timeout(hctx, "write");
continue;
}
}
return HANDLER_GO_ON;
}
static handler_t cgi_waitpid_cb(server *srv, void *p_d, pid_t pid, int status) {
plugin_data *p = (plugin_data *)p_d;
for (size_t i = 0; i < p->cgi_pid.used; ++i) {
@ -920,6 +1034,7 @@ int mod_cgi_plugin_init(plugin *p) {
p->handle_request_reset = cgi_connection_close_callback;
p->handle_subrequest_start = cgi_is_handled;
p->handle_subrequest = mod_cgi_handle_subrequest;
p->handle_trigger = cgi_trigger_cb;
p->handle_waitpid = cgi_waitpid_cb;
p->init = mod_cgi_init;
p->cleanup = mod_cgi_free;