src/server/safs/cgi.c

Sun, 20 Nov 2022 11:39:46 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 20 Nov 2022 11:39:46 +0100
changeset 435
713ec3da79ec
parent 433
39fe86ae4db0
child 450
d7b276de183b
permissions
-rw-r--r--

expression parser: add support for functions

/*
 * 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;
    
    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;
    }
    
    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;
}

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;
    }
    if(wr < 0) {
        if(errno != 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;
    }
    if(wr < 0) {
        if(errno == 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) {
                memcpy(handler->writebuf, buf+pos, remaining);
            } else {
                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;
                }
            }
            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 1;
                }
                handler->poll_out = TRUE;
            }
            return 1;
        }
        handler->result = REQ_ABORTED;
        return 1;
    }
    
    return 0;
}

int cgi_stdout_readevent(EventHandler *ev, Event *event) {
    CGIHandler *handler = event->cookie;
    
    return cgi_read_output(handler, ev);
}

int cgi_writeevent(EventHandler *ev, Event *event) {
    CGIHandler *handler = event->cookie;
    
    // cgi_read_output will try to flush the buffer
    return cgi_read_output(handler, ev);
}



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)) {
        return handler->result == REQ_ABORTED ? 0 : 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->read_output_finished = 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 = 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;
    }
    if(handler->poll_out && !handler->send_response_finished) {
        // send response is still active
        handler->writeev->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);
    
    // 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);
                rq->rq_attr.keep_alive = 0;
                handler->result = REQ_ABORTED;
            }
        }
    }
    
    net_setnonblock(sn->csd, 0);
    
    // 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;
        }
    }
}

mercurial