Mon, 10 Jul 2023 18:39:24 +0200
update ucx
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2016 Olaf Wintermann. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "cgi.h" #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <signal.h> #include <sys/wait.h> #include <cx/string.h> #include "../util/util.h" #include "../util/pblock.h" #include "../daemon/netsite.h" #include "../daemon/vfs.h" #include "../util/io.h" #include "../daemon/event.h" #include "cgiutils.h" #define CGI_VARS 32 #define CGI_RESPONSE_PARSER_BUFLEN 2048 #define CGI_RESPONSE_MAX_LINE_LENGTH 512 int send_cgi(pblock *pb, Session *sn, Request *rq) { char *path = pblock_findkeyval(pb_key_path, rq->vars); char *ctlen = pblock_findkeyval(pb_key_content_length, rq->headers); int64_t content_length = 0; log_ereport(LOG_DEBUG, "cgi-send: path: %s content-length: %s", path, ctlen); if(ctlen) { if(!util_strtoint(ctlen, &content_length)) { log_ereport( LOG_FAILURE, "send-cgi: content-length header is not an integer"); protocol_status(sn, rq, 400, NULL); return REQ_ABORTED; } } // using stat, not vfs_stat, because running scripts/executables works only // with the sys fs if(!vfs_is_sys(rq->vfs)) { log_ereport(LOG_WARN, "send-cgi: VFS setting ignored"); } struct stat s; if(stat(path, &s)) { int statuscode = util_errno2status(errno); protocol_status(sn, rq, statuscode, NULL); return REQ_ABORTED; } if(S_ISDIR(s.st_mode)) { protocol_status(sn, rq, 403, NULL); return REQ_ABORTED; } param_free(pblock_remove("content-type", rq->srvhdrs)); const char *args = pblock_findval("query", rq->reqpb); char **argv = cgi_create_argv(path, NULL, args); if(!argv) { return REQ_ABORTED; } char **env = http_hdrs2env(rq->headers); env = cgi_common_vars(sn, rq, env); env = cgi_specific_vars(sn, rq, args, env, 1); // event handler object for non-blocking io event handler CGIHandler *handler = pool_malloc(sn->pool, sizeof(CGIHandler)); if(!handler) { return REQ_ABORTED; } ZERO(handler, sizeof(CGIHandler)); handler->path = path; int ret = cgi_start(&handler->process, path, argv, env); if(ret != REQ_PROCEED) { util_env_free(env); cgi_free_argv(argv); return ret; } util_env_free(env); cgi_free_argv(argv); char buf[4096]; // I/O buffer ssize_t r; if(content_length > 0) { ssize_t n = 0; while(n < content_length) { r = netbuf_getbytes(sn->inbuf, buf, 4096); if(r <= 0) { log_ereport( LOG_FAILURE, "send-cgi: script: %s: cannot read request body", path); kill(handler->process.pid, SIGTERM); cgi_close(&handler->process); return REQ_ABORTED; } ssize_t w = write(handler->process.in[1], buf, r); if(w <= 0) { log_ereport( LOG_FAILURE, "send-cgi: script: %s: cannot send request body to cgi process", path); kill(handler->process.pid, SIGKILL); cgi_close(&handler->process); return REQ_ABORTED; } n += r; } } system_close(handler->process.in[1]); handler->process.in[1] = -1; handler->parser = cgi_parser_new(sn, rq); // set pipes non-blocking int flags; if ((flags = fcntl(handler->process.err[0], F_GETFL, 0)) == -1) { flags = 0; } if (fcntl(handler->process.err[0], F_SETFL, flags | O_NONBLOCK) != 0) { log_ereport(LOG_FAILURE, "cgi-bin: fcntl err[0] failed: %s", strerror(errno)); } if ((flags = fcntl(handler->process.out[0], F_GETFL, 0)) == -1) { flags = 0; } if (fcntl(handler->process.out[0], F_SETFL, flags | O_NONBLOCK) != 0) { log_ereport(LOG_FAILURE, "cgi-bin: fcntl out[0] failed: %s", strerror(errno)); } // create events for reading cgi's stdout/stderr Event *readev = pool_malloc(sn->pool, sizeof(Event)); ZERO(readev, sizeof(Event)); readev->cookie = handler; readev->fn = cgi_stdout_readevent; readev->finish = cgi_event_finish; Event *stderr_readev = pool_malloc(sn->pool, sizeof(Event)); ZERO(stderr_readev, sizeof(Event)); stderr_readev->cookie = handler; stderr_readev->fn = cgi_stderr_readevent; stderr_readev->finish = cgi_event_finish; Event *writeev = pool_malloc(sn->pool, sizeof(Event)); ZERO(writeev, sizeof(Event)); writeev->cookie = handler; writeev->fn = cgi_writeevent; writeev->finish = cgi_event_finish; handler->writeev = writeev; net_setnonblock(sn->csd, 1); // add poll events for cgi stdout/stderr int error = 0; if(ev_pollin(sn->ev, handler->process.err[0], stderr_readev)) { log_ereport(LOG_FAILURE, "send-cgi: stderr ev_pollin failed"); error = 1; } if(ev_pollin(sn->ev, handler->process.out[0], readev)) { log_ereport(LOG_FAILURE, "send-cgi: stdout ev_pollin failed"); error = 1; } handler->wait_read = TRUE; handler->events = 2; // 2 events (stdout, stderr) if(error) { log_ereport(LOG_FAILURE, "cgi-send: kill script: %s", path); kill(handler->process.pid, SIGKILL); cgi_parser_free(handler->parser); cgi_close(&handler->process); return REQ_ABORTED; } return REQ_PROCESSING; } static int cgi_try_write_flush(CGIHandler *handler, Session *sn) { ssize_t wr = 0; while( handler->writebuf_size - handler->writebuf_pos > 0 && (wr = net_write( sn->csd, handler->writebuf + handler->writebuf_pos, handler->writebuf_size - handler->writebuf_pos)) > 0) { handler->writebuf_pos += wr; handler->count_write += wr; } if(handler->writebuf_size - handler->writebuf_pos > 0) { if(net_errno(sn->csd) != EWOULDBLOCK) { handler->result = REQ_ABORTED; } return 1; } return 0; } static int cgi_try_write(CGIHandler *handler, EventHandler *ev, Session *sn, char *buf, size_t size) { size_t pos = 0; ssize_t wr = 0; while(size - pos > 0 && (wr = net_write(sn->csd, buf + pos, size - pos)) > 0) { pos += wr; handler->count_write += wr; } if(pos < size) { if(net_errno(sn->csd) == EWOULDBLOCK) { // copy remaining bytes to the write buffer // we assume there are no remaining bytes in writebuf size_t remaining = size-pos; if(remaining > handler->writebuf_alloc) { handler->writebuf_alloc = size > 4096 ? size : 4096; handler->writebuf = pool_realloc(sn->pool, handler->writebuf, handler->writebuf_alloc); if(!handler->writebuf) { handler->result = REQ_ABORTED; return 1; } } memcpy(handler->writebuf, buf+pos, remaining); handler->writebuf_size = remaining; handler->writebuf_pos = 0; // initialize poll, if it isn't already active if(!handler->poll_out) { if(event_pollout(ev, sn->csd, handler->writeev)) { handler->result = REQ_ABORTED; return 0; } handler->events++; handler->poll_out = TRUE; } } else { handler->result = REQ_ABORTED; log_ereport(LOG_FAILURE, "cgi_try_write: %s", strerror(net_errno(sn->csd))); } return 1; } return 0; } int cgi_stdout_readevent(EventHandler *ev, Event *event) { CGIHandler *handler = event->cookie; int ret = cgi_read_output(handler, ev); if(ret == 0) { handler->wait_read = FALSE; } return ret; } int cgi_writeevent(EventHandler *ev, Event *event) { CGIHandler *handler = event->cookie; // cgi_read_output will try to flush the buffer int ret = cgi_read_output(handler, ev); if(ret == 0) { handler->poll_out = FALSE; } return ret; } int cgi_read_output(CGIHandler *handler, EventHandler *ev) { CGIResponseParser *parser = handler->parser; Session *sn = parser->sn; Request *rq = parser->rq; // try to flush handler->writebuf // if writebuf is empty, this does nothing and returns 0 if(cgi_try_write_flush(handler, sn)) { if(handler->result == REQ_ABORTED) { log_ereport(LOG_DEBUG, "cgi-send: req: %p write failed: %s: abort", handler->parser->rq, strerror(net_errno(sn->csd)), rq); return 0; } else { return 1; } } char buf[4096]; // I/O buffer ssize_t r; handler->result = REQ_PROCEED; while((r = read(handler->process.out[0], buf, 4096)) > 0) { if(parser->cgiheader) { size_t pos; int ret = cgi_parse_response(parser, buf, r, &pos); if(ret == -1) { log_ereport( LOG_FAILURE, "broken cgi script response: path: %s", handler->path); protocol_status(sn, rq, 500, NULL); handler->result = REQ_ABORTED; return 0; } else if(ret == 1) { WS_ASSERT(pos <= r); parser->response_length += r-pos; parser->cgiheader = FALSE; if(parser->status > 0) { protocol_status(sn, rq, parser->status, parser->msg); } handler->response = http_create_response(sn, rq); if(!handler->response) { handler->result = REQ_ABORTED; return 0; } int send_response = http_send_response(handler->response); if(send_response < 0) { handler->result = REQ_ABORTED; break; } else if(send_response == 1) { // EWOULDBLOCK if(!handler->poll_out) { if(event_pollout(ev, sn->csd, handler->writeev)) { handler->result = REQ_ABORTED; return 0; } handler->poll_out = TRUE; return 1; } } else { handler->response = NULL; } if(pos < r) { if(cgi_try_write(handler, ev, sn, &buf[pos], r-pos)) { return handler->result == REQ_ABORTED ? 0 : 1; } } } } else { parser->response_length += r; if(cgi_try_write(handler, ev, sn, buf, r)) { return handler->result == REQ_ABORTED ? 0 : 1; } } } if(r < 0 && errno == EWOULDBLOCK) { return 1; } handler->cgi_eof = TRUE; return 0; } int cgi_stderr_readevent(EventHandler *ev, Event *event) { CGIHandler *handler = event->cookie; pool_handle_t *pool = handler->parser->sn->pool; char buf[4096]; char *line = buf; int line_start; ssize_t r; while((r = read(handler->process.err[0], buf, 4096)) > 0) { line_start = 0; int pos = 0; // log stderr output lines for(int i=0;i<r;i++) { if(buf[i] == '\n') { log_ereport( LOG_INFORM, "cgi pid %d %s stderr: %.*s%.*s", (int)handler->process.pid, handler->path, (int)handler->stderr_tmplen, handler->stderr_tmp, i - line_start, line + line_start); line_start = i+1; pos = i+1; if(handler->stderr_tmp) { handler->stderr_tmplen = 0; } } } // check for incomplete line if(pos < r) { int tmplen = r-pos; if(handler->stderr_tmplen > 0) { // append new text to the temp buffer if(handler->stderr_tmplen + tmplen > handler->stderr_tmpalloc) { handler->stderr_tmpalloc = handler->stderr_tmplen + tmplen; handler->stderr_tmp = pool_realloc(pool, handler->stderr_tmp, handler->stderr_tmpalloc); if(!handler->stderr_tmp) { log_ereport(LOG_FAILURE, "send-cgi: cannot create tmp buffer for parsing stderr"); handler->stderr_tmpalloc = 0; handler->stderr_tmplen = 0; continue; } } memcpy(handler->stderr_tmp + handler->stderr_tmplen, line + line_start, tmplen); handler->stderr_tmplen += tmplen; } else { if(handler->stderr_tmpalloc < tmplen) { // tmp buffer too small or not allocated handler->stderr_tmpalloc = tmplen < 256 ? 256 : tmplen; if(handler->stderr_tmp) { // free old tmp buf // pool_realloc doesn't make sense here, because it // is just free+malloc+memcpy and we don't need the // memcpy part, because we are just reusing the buffer // and the previous content doesn't matter pool_free(pool, handler->stderr_tmp); } handler->stderr_tmp = pool_malloc(pool, handler->stderr_tmpalloc); if(!handler->stderr_tmp) { log_ereport(LOG_FAILURE, "send-cgi: cannot create tmp buffer for parsing stderr"); handler->stderr_tmpalloc = 0; handler->stderr_tmplen = 0; continue; } } memcpy(handler->stderr_tmp, line + line_start, tmplen); handler->stderr_tmplen = tmplen; } } else { handler->stderr_tmplen = 0; } } if(r < 0 && errno == EWOULDBLOCK) { return 1; } if(handler->stderr_tmp) { pool_free(handler->parser->sn->pool, handler->stderr_tmp); } return 0; } int cgi_event_finish(EventHandler *ev, Event *event) { CGIHandler *handler = event->cookie; CGIResponseParser *parser = handler->parser; Session *sn = parser->sn; Request *rq = parser->rq; char *event_fn = ""; if(event->fn == cgi_stdout_readevent) { event_fn = "stdout"; } else if(event->fn == cgi_stderr_readevent) { event_fn = "stderr"; } else if(event->fn == cgi_writeevent) { event_fn = "httpout"; } log_ereport(LOG_DEBUG, "cgi-send: req: %p finish: event: %d pollout: %d cgi_eof: %d fn: %s", rq, handler->events, handler->poll_out, handler->cgi_eof, event_fn); if(--handler->events > 0) { if(handler->events == 1) { if(handler->poll_out) { // write event registered, however it will not be activated anymore // we can safely remove the event log_ereport(LOG_DEBUG, "cgi-send: req: %p finish: event: 1 remove-poll write", rq); if(event_removepoll(ev, sn->csd)) { log_ereport(LOG_FAILURE, "cgi_event_finish: event_removepoll: %s", strerror(errno)); } } else if(handler->cgi_eof && handler->wait_read) { log_ereport(LOG_DEBUG, "cgi-send: req: %p finish: event: 1 remove-poll read", rq); if(ev_remove_poll(ev, handler->process.out[0])) { log_ereport(LOG_FAILURE, "cgi_event_finish: ev_remove_poll: %s", strerror(errno)); } } else { return 0; } } else { return 0; } } if(handler->result == REQ_ABORTED && handler->process.pid != 0) { log_ereport(LOG_FAILURE, "cgi-send: kill script: %s", handler->path); killpg(handler->process.pid, SIGTERM); } log_ereport(LOG_DEBUG, "cgi-send: req: %p cgi_close", rq); int exit_code = cgi_close(&handler->process); if(exit_code != 0) { log_ereport(LOG_FAILURE, "send-cgi: script: %s exited with code %d", handler->path, exit_code); handler->result = REQ_ABORTED; } cgi_parser_free(parser); WSBool response_length_error = FALSE; // check if content-length set by the cgi script matches the number // of writes, that were written to the stream // this ensures, that broken cgi scripts don't break the connection char *ctlen_header = pblock_findkeyval(pb_key_content_length, rq->srvhdrs); if(ctlen_header) { int64_t ctlenhdr; if(util_strtoint(ctlen_header, &ctlenhdr)) { if(ctlenhdr != parser->response_length) { log_ereport( LOG_FAILURE, "cgi-send: script: %s: content length mismatch", handler->path); response_length_error = TRUE; } } } // make sure we haven't lost any bytes // should not happen unless the non-blocking IO code is buggy if(handler->result != REQ_ABORTED && handler->parser->response_length != handler->count_write) { log_ereport( LOG_FAILURE, "cgi-send: script: %s: IO error: cgi response length != http response length", handler->path); response_length_error = TRUE; } // if the response length is broken, we must close the connection if(response_length_error) { rq->rq_attr.keep_alive = 0; handler->result = REQ_ABORTED; } net_setnonblock(sn->csd, 0); // return to nsapi loop log_ereport(LOG_DEBUG, "cgi-send: req: %p event-finish nsapi return", rq); nsapi_function_return(sn, rq, handler->result); return 0; } int cgi_start(CGIProcess *p, char *path, char *const argv[], char *const envp[]) { if(pipe(p->in) || pipe(p->out) || pipe(p->err)) { log_ereport( LOG_FAILURE, "send-cgi: cannot create pipe: %s", strerror(errno)); return REQ_ABORTED; } p->pid = fork(); if(p->pid == 0) { // child // get script directory and script name cxstring script = cx_str(path); cxmutstr parent; int len = strlen(path); for(int i=len-1;i>=0;i--) { if(path[i] == '/') { script = cx_strn(path + i + 1, len - i); parent = cx_strdup(cx_strn(path, i)); if(chdir(parent.ptr)) { perror("cgi_start: chdir"); free(parent.ptr); exit(-1); } free(parent.ptr); break; } } if(dup2(p->in[0], STDIN_FILENO) == -1) { perror("cgi_start: dup2"); exit(EXIT_FAILURE); } if(dup2(p->out[1], STDOUT_FILENO) == -1) { perror("cgi_start: dup2"); exit(EXIT_FAILURE); } if(dup2(p->err[1], STDERR_FILENO) == -1) { perror("cgi_start: dup2"); exit(EXIT_FAILURE); } // we need to close this unused pipe // otherwise stdin cannot reach EOF system_close(p->in[1]); // execute program exit(execve(script.ptr, argv, envp)); } else { // parent system_close(p->out[1]); system_close(p->err[1]); p->out[1] = -1; p->err[1] = -1; } return REQ_PROCEED; } int cgi_close(CGIProcess *p) { int status = -1; waitpid(p->pid, &status, 0); if(p->in[0] != -1) { system_close(p->in[0]); } if(p->in[1] != -1) { system_close(p->in[1]); } if(p->out[0] != -1) { system_close(p->out[0]); } if(p->out[1] != -1) { system_close(p->out[1]); } if(p->err[0] != -1) { system_close(p->err[0]); } if(p->err[1] != -1) { system_close(p->err[1]); } return status; } CGIResponseParser* cgi_parser_new(Session *sn, Request *rq) { CGIResponseParser* parser = pool_malloc(sn->pool, sizeof(CGIResponseParser)); parser->sn = sn; parser->rq = rq; parser->status = 0; parser->msg = NULL; parser->response_length = 0; parser->cgiheader = TRUE; cxBufferInit(&parser->tmp, NULL, 64, pool_allocator(sn->pool), CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); return parser; } void cgi_parser_free(CGIResponseParser *parser) { if(parser->tmp.space) { cxBufferDestroy(&parser->tmp); } pool_free(parser->sn->pool, parser); } /* * parses a cgi response line and adds the response header to rq->srvhdrs * returns 0: incomplete line * 1: successfully parsed lines * 2: cgi response header complete (empty line) * -1: error */ static int parse_lines(CGIResponseParser *parser, char *buf, size_t len, int *pos) { CxAllocator *a = pool_allocator(parser->sn->pool); cxmutstr name; cxmutstr value; WSBool space = TRUE; int i; int line_begin = 0; int value_begin = 0; for(i=0;i<len;i++) { char c = buf[i]; if(value_begin == line_begin && c == ':') { name = cx_mutstrn(buf + line_begin, i - line_begin); value_begin = i + 1; } else if(c == '\n') { if(value_begin == line_begin) { if(space) { *pos = i + 1; return 2; } else { // line ends with content but without ':' -> error return -1; } } value = cx_mutstrn(buf + value_begin, i - value_begin); cx_strlower(name); name = cx_strdup_a(a, cx_strtrim((cxstring){name.ptr, name.length})); value = cx_strtrim_m(value); if(name.length == 0 || value.length == 0) { return -1; } if(!cx_strcmp((cxstring){name.ptr, name.length}, (cxstring)CX_STR("status"))) { cxmutstr codestr = value; int j; for(j=0;j<codestr.length;j++) { if(!isdigit(codestr.ptr[j])) { break; } if(j > 2) { break; } } codestr.ptr[j] = '\0'; int64_t s = 0; util_strtoint(codestr.ptr, &s); parser->status = (int)s; cxmutstr msg = cx_strtrim_m(cx_strsubs_m(value, j + 1)); if(msg.length > 0) { parser->msg = cx_strdup_pool(parser->sn->pool, msg).ptr; } } else { pblock_nvlinsert( name.ptr, name.length, value.ptr, value.length, parser->rq->srvhdrs); } line_begin = i+1; value_begin = line_begin; space = TRUE; } else if(!isspace(c)) { space = FALSE; } } if(i < len) { *pos = i; return 0; } return 1; } /* * returns -1: error * 0: response header incomplete * 1: complete */ int cgi_parse_response(CGIResponseParser *parser, char *buf, size_t len, size_t *bpos) { *bpos = 0; int pos = 0; if(parser->tmp.pos > 0) { // the tmp buffer contains an unfinished line // fill up the buffer until the line is complete WSBool nb = FALSE; for(pos=0;pos<len;pos++) { if(buf[pos] == '\n') { nb = TRUE; break; } } cxBufferWrite(buf, 1, pos, &parser->tmp); if(nb) { // line complete int npos; int r = parse_lines(parser, parser->tmp.space, parser->tmp.pos, &npos); switch(r) { case -1: return -1; case 0: return -1; case 1: break; case 2: { *bpos = pos + 1; return 1; } } // reset tmp buffer parser->tmp.pos = 0; } else { if(parser->tmp.pos > CGI_RESPONSE_MAX_LINE_LENGTH) { return -1; } } } int npos = 0; int r = parse_lines(parser, buf + pos, len - pos, &npos); switch(r) { default: return -1; case 0: case 1: { int newlen = len - npos; if(npos > 0) { cxBufferWrite(buf + npos, 1, newlen, &parser->tmp); } return 0; } case 2: { *bpos = pos + npos; return 1; } } }