src/server/safs/cgi.c

Mon, 06 Mar 2017 17:32:26 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Mon, 06 Mar 2017 17:32:26 +0100
changeset 179
ef6827505bd2
parent 171
af7e2d80dee6
child 254
4784c14aa639
permissions
-rw-r--r--

merge srvctrl into default branch

/*
 * 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 "../util/util.h"
#include "../util/pblock.h"
#include "../../ucx/string.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);
    }
    cgi_close(&cgip); // TODO: check return value
      
    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 0;
}

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

mercurial