Sun, 13 Nov 2022 09:41:07 +0100
parse cgi stderr output for logging and use non-blocking pipes
/* * 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 "../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; 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; } } 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) { // TODO: handle error 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) { // TODO: handle error 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 = NULL; Event *writeev = pool_malloc(sn->pool, sizeof(Event)); ZERO(writeev, sizeof(Event)); writeev->cookie = handler; // TODO: fn handler->writeev = writeev; handler->stderrev = stderr_readev; // 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; } if(error) { log_ereport(LOG_FAILURE, "cgi-send: kill script: %s", path); kill(handler->process.pid, SIGKILL); cgi_parser_free(handler->parser); return REQ_ABORTED; } return REQ_PROCESSING; } int cgi_stdout_readevent(EventHandler *ev, Event *event) { CGIHandler *handler = event->cookie; CGIResponseParser *parser = handler->parser; Session *sn = parser->sn; Request *rq = parser->rq; char buf[4096]; // I/O buffer ssize_t r; ssize_t wr = 0; 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; break; } else if(ret == 1) { parser->cgiheader = FALSE; if(parser->status > 0) { protocol_status(sn, rq, parser->status, parser->msg); } http_start_response(sn, rq); if(pos < r) { parser->response_length += r-pos; wr = net_write(sn->csd, &buf[pos], r-pos); if(wr <= 0) { handler->result = REQ_ABORTED; break; } } } } else { parser->response_length += r; wr = net_write(sn->csd, buf, r); if(wr <= 0) { handler->result = REQ_ABORTED; break; } } } if(r < 0 && errno == EWOULDBLOCK) { return 1; } 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); rq->rq_attr.keep_alive = 0; handler->result = REQ_ABORTED; } } } 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 = 0; ssize_t r; while((r = read(handler->process.err[0], buf, 4096)) > 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) { pool_free(pool, handler->stderr_tmp); handler->stderr_tmp = NULL; handler->stderr_tmplen = 0; } } } // check for incomplete line if(pos < r) { int tmplen = r-pos; if(handler->stderr_tmp) { handler->stderr_tmp = pool_realloc(pool, handler->stderr_tmp, handler->stderr_tmplen + tmplen); memcpy(handler->stderr_tmp + handler->stderr_tmplen, line + line_start, tmplen); handler->stderr_tmplen += tmplen; } else { handler->stderr_tmp = pool_malloc(pool, tmplen); memcpy(handler->stderr_tmp, line + line_start, tmplen); handler->stderr_tmplen = tmplen; } } else { pool_free(pool, handler->stderr_tmp); handler->stderr_tmp = NULL; handler->stderr_tmplen = 0; } } if(r < 0 && errno == EWOULDBLOCK) { return 1; } if(handler->stderr_tmp) { pool_free(handler->parser->sn->pool, handler->stderr_tmp); } handler->stderr_finished = TRUE; 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; if(handler->result == REQ_ABORTED) { log_ereport(LOG_FAILURE, "cgi-send: kill script: %s", handler->path); kill(handler->process.pid, SIGKILL); } if(!handler->stderr_finished) { // stderr handler is still active // set stderr event finish function, to run the finish code later handler->stderrev->finish = cgi_event_finish; return 0; } 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); // return to nsapi loop 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; } } }