git repos / sago_web_git

src/cgi.cpp

browsing at commit = e6fed232320fc1bd4c7f35df8325080183937ba4

raw · blame · history

#ifndef _GNU_SOURCE
#define _GNU_SOURCE // for execvpe
#endif

#include "cgi.h"

#include <cstdlib>
#include <cstring>
#include <stdexcept>

#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>

namespace {
constexpr size_t CHUNK = 64 * 1024;

// Builds a null-terminated char* array from strings for execvp/environ. The
// pointers borrow from `strings`, which must outlive the returned vector.
std::vector<char *> to_c_array(const std::vector<std::string> &strings) {
    std::vector<char *> out;
    out.reserve(strings.size() + 1);
    for (const std::string &s : strings)
        out.push_back(const_cast<char *>(s.c_str()));
    out.push_back(nullptr);
    return out;
}

// Splits one CGI header line "Name: value" into a trimmed pair. Returns false
// for malformed lines (no colon).
bool parse_header_line(const std::string &line, std::string &name,
                       std::string &value) {
    const size_t colon = line.find(':');
    if (colon == std::string::npos)
        return false;
    name = line.substr(0, colon);
    size_t start = colon + 1;
    while (start < line.size() && (line[start] == ' ' || line[start] == '\t'))
        ++start;
    value = line.substr(start);
    return true;
}

// Parses the accumulated CGI header block (already stripped of the trailing
// blank line) into `result`. A "Status: NNN ..." header sets the HTTP status;
// every other header is preserved verbatim.
void parse_headers(const std::string &block, CgiResult &result) {
    size_t pos = 0;
    while (pos < block.size()) {
        size_t end = block.find('\n', pos);
        if (end == std::string::npos)
            end = block.size();
        std::string line = block.substr(pos, end - pos);
        pos = end + 1;
        if (!line.empty() && line.back() == '\r')
            line.pop_back();
        if (line.empty())
            continue;
        std::string name, value;
        if (!parse_header_line(line, name, value))
            continue;
        if (name == "Status")
            result.status = std::atoi(value.c_str());
        else
            result.headers.emplace_back(std::move(name), std::move(value));
    }
}
} // namespace

void run_cgi(const std::vector<std::string> &argv,
             const std::vector<std::string> &env, int input_fd,
             const std::function<std::ostream &(const CgiResult &)> &on_headers) {
    int out_pipe[2];
    if (pipe(out_pipe) != 0)
        throw std::runtime_error("cgi: pipe failed");

    const pid_t pid = fork();
    if (pid < 0) {
        close(out_pipe[0]);
        close(out_pipe[1]);
        throw std::runtime_error("cgi: fork failed");
    }

    if (pid == 0) {
        // Child: wire up stdin/stdout, then exec.
        const int in = input_fd >= 0 ? input_fd : open("/dev/null", O_RDONLY);
        if (in >= 0)
            dup2(in, STDIN_FILENO);
        dup2(out_pipe[1], STDOUT_FILENO);
        close(out_pipe[0]);
        close(out_pipe[1]);

        std::vector<char *> c_argv = to_c_array(argv);
        std::vector<char *> c_env = to_c_array(env);
        execvpe(c_argv[0], c_argv.data(), c_env.data());
        _exit(127); // exec failed
    }

    // Parent: read the child's stdout.
    close(out_pipe[1]);

    std::string header_block;
    std::ostream *body = nullptr; // set once headers are parsed
    char buf[CHUNK];
    ssize_t n;
    while ((n = read(out_pipe[0], buf, sizeof(buf))) > 0) {
        if (body != nullptr) {
            body->write(buf, n);
            continue;
        }
        header_block.append(buf, n);
        const size_t sep_crlf = header_block.find("\r\n\r\n");
        const size_t sep_lf = header_block.find("\n\n");
        size_t sep = std::string::npos;
        size_t skip = 0;
        if (sep_crlf != std::string::npos &&
            (sep_lf == std::string::npos || sep_crlf <= sep_lf)) {
            sep = sep_crlf;
            skip = 4;
        } else if (sep_lf != std::string::npos) {
            sep = sep_lf;
            skip = 2;
        }
        if (sep != std::string::npos) {
            const std::string rest = header_block.substr(sep + skip);
            header_block.resize(sep);
            CgiResult result;
            parse_headers(header_block, result);
            body = &on_headers(result);
            body->write(rest.data(), rest.size());
        }
    }
    close(out_pipe[0]);

    // No blank line ever arrived: still surface whatever headers we have.
    if (body == nullptr) {
        CgiResult result;
        parse_headers(header_block, result);
        on_headers(result);
    }

    int wstatus = 0;
    waitpid(pid, &wstatus, 0);
    if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0)
        throw std::runtime_error("cgi: child exited abnormally");
}