Sun, 15 May 2022 08:56:00 +0200
make sure the http stream is finished if headers are sent
/* * 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 <ucx/string.h> #include "../util/util.h" #include "../util/pblock.h" #include "../daemon/netsite.h" #include "../util/io.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); char **env = http_hdrs2env(rq->headers); env = cgi_common_vars(sn, rq, env); env = cgi_specific_vars(sn, rq, args, env, 1); CGIProcess cgip; int ret = cgi_start(&cgip, 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(cgip.pid, SIGTERM); cgi_close(&cgip); return REQ_ABORTED; } ssize_t w = write(cgip.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(cgip.pid, SIGKILL); cgi_close(&cgip); return REQ_ABORTED; } n += r; } } system_close(cgip.in[1]); cgip.in[1] = -1; // read from child CGIResponseParser *parser = cgi_parser_new(sn, rq); WSBool cgiheader = TRUE; ssize_t wr = 0; int result = REQ_PROCEED; size_t response_length = 0; while((r = read(cgip.out[0], buf, 4096)) > 0) { if(cgiheader) { size_t pos; ret = cgi_parse_response(parser, buf, r, &pos); if(ret == -1) { log_ereport( LOG_FAILURE, "broken cgi script response: path: %s", path); protocol_status(sn, rq, 500, NULL); result = REQ_ABORTED; break; } else if(ret == 1) { cgiheader = FALSE; if(parser->status > 0) { protocol_status(sn, rq, parser->status, parser->msg); } http_start_response(sn, rq); if(pos < r) { response_length += r-pos; wr = net_write(sn->csd, &buf[pos], r-pos); if(wr <= 0) { result = REQ_ABORTED; break; } } } } else { response_length += r; wr = net_write(sn->csd, buf, r); if(wr <= 0) { result = REQ_ABORTED; break; } } } 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 != response_length) { log_ereport( LOG_FAILURE, "cgi-send: script: %s: content length mismatch", path); rq->rq_attr.keep_alive = 0; result = REQ_ABORTED; } } } if(result == REQ_ABORTED) { log_ereport(LOG_FAILURE, "cgi-send: kill script: %s", path); kill(cgip.pid, SIGKILL); } int exit_code = cgi_close(&cgip); if(exit_code != 0) { log_ereport(LOG_FAILURE, "send-cgi: script: %s exited with code %d", path, exit_code); ret = REQ_ABORTED; } cgi_parser_free(parser); return result; } int cgi_start(CGIProcess *p, char *path, char *const argv[], char *const envp[]) { if(pipe(p->in) || pipe(p->out)) { 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 sstr_t script = sstr(path); sstr_t parent; int len = strlen(path); for(int i=len-1;i>=0;i--) { if(path[i] == '/') { script = sstrn(path + i + 1, len - i); parent = sstrdup(sstrn(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); } // 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]); p->out[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]); } 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->tmp = ucx_buffer_new(NULL, 64, UCX_BUFFER_AUTOEXTEND); parser->status = 0; parser->msg = NULL; return parser; } void cgi_parser_free(CGIResponseParser *parser) { if(parser->tmp) { ucx_buffer_free(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) { UcxAllocator a = util_pool_allocator(parser->sn->pool); sstr_t name; sstr_t 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 = sstrn(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 = sstrn(buf + value_begin, i - value_begin); name = sstrlower_a(&a, sstrtrim(name)); value = sstrtrim(value); if(name.length == 0 || value.length == 0) { return -1; } if(!sstrcmp(name, S("status"))) { sstr_t 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; sstr_t msg = sstrtrim(sstrsubs(value, j + 1)); if(msg.length > 0) { parser->msg = sstrdup_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; } } ucx_buffer_write(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) { ucx_buffer_write(buf + npos, 1, newlen, parser->tmp); } return 0; } case 2: { *bpos = pos + npos; return 1; } } }