git-trace

romele / git-trace public

62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 10m ago

src/cgi.c

#include "cgi.h"

#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>

#define BODY_CHUNK (64 * 1024)

int git_rest_ok(const char *rest)
{
    if (!rest || rest[0] != '/')
        return 0;
    const char *p = rest + 1;
    while (*p) {
        const char *slash = strchr(p, '/');
        size_t n = slash ? (size_t)(slash - p) : strlen(p);
        if (n == 0)
            return 0;                    /* "//" or trailing "/" */
        if (n > 128)
            return 0;
        for (size_t i = 0; i < n; i++) {
            unsigned char c = (unsigned char)p[i];
            if (!(isalnum(c) || c == '.' || c == '_' || c == '-'))
                return 0;
        }
        if (n == 1 && p[0] == '.')
            return 0;
        if (n == 2 && p[0] == '.' && p[1] == '.')
            return 0;
        p = slash ? slash + 1 : p + n;
    }
    return 1;
}

/* forward a request body of body_left bytes into the child's stdin */
static void pump_body(struct request *r, int wfd)
{
    char chunk[BODY_CHUNK];
    for (;;) {
        long n = body_read(r, chunk, sizeof chunk);
        if (n <= 0)
            break;
        const char *p = chunk;
        long left = n;
        while (left > 0) {
            ssize_t w = write(wfd, p, (size_t)left);
            if (w < 0) {
                if (errno == EINTR)
                    continue;
                return; /* child closed stdin early */
            }
            p += w;
            left -= w;
        }
    }
}

/*
 * Read CGI output from child stdout, parse the header block, emit the
 * HTTP response, then stream the remainder verbatim.  The connection is
 * closed at end of body (no keep-alive), which doubles as the
 * Content-Length delimiter when the CGI omits it.
 */
static int send_cgi_response(struct request *r, int rfd)
{
    struct buf hb;
    buf_init(&hb);
    char *eoh = NULL;
    size_t eohlen = 0;
    char tmp[BODY_CHUNK];

    while (!eoh && hb.len < 65536) {
        ssize_t n = read(rfd, tmp, sizeof tmp);
        if (n <= 0) {
            buf_free(&hb);
            return -1;
        }
        buf_put(&hb, tmp, (size_t)n);
        for (size_t i = 0; i + 3 < hb.len; i++)
            if (hb.data[i] == '\r' && hb.data[i+1] == '\n' &&
                hb.data[i+2] == '\r' && hb.data[i+3] == '\n') {
                eoh = hb.data + i;
                eohlen = 4;
                break;
            }
        if (!eoh)
            for (size_t i = 0; i + 1 < hb.len; i++)
                if (hb.data[i] == '\n' && hb.data[i+1] == '\n') {
                    eoh = hb.data + i;
                    eohlen = 2;
                    break;
                }
    }
    if (!eoh) {
        buf_free(&hb);
        return -1;
    }

    /* parse CGI headers, emit HTTP status + forwarded headers */
    struct buf out;
    buf_init(&out);
    int status = 200;

    char *p = hb.data;
    while (p < eoh) {
        char *eol = memchr(p, '\n', (size_t)(eoh - p));
        size_t linelen = eol ? (size_t)(eol - p) : (size_t)(eoh - p);
        /* strip trailing \r */
        if (linelen && p[linelen - 1] == '\r')
            linelen--;
        if (linelen) {
            if (linelen > 7 && strncasecmp(p, "Status:", 7) == 0) {
                const char *v = p + 7;
                while (*v == ' ')
                    v++;
                status = atoi(v);
            } else {
                buf_put(&out, p, linelen);
                buf_puts(&out, "\r\n");
            }
        }
        p = eol ? eol + 1 : eoh;
    }

    char head[128];
    int hn = snprintf(head, sizeof head, "HTTP/1.1 %d %s\r\n",
                      status, status_text(status));
    http_write_all(r->fd, head, (size_t)hn);
    http_write_all(r->fd, out.data, out.len);
    http_write_all(r->fd, "Connection: close\r\n\r\n", 21);

    /* flush any body bytes already read past the CGI header */
    size_t body_off = (size_t)(eoh - hb.data) + eohlen;
    if (hb.len > body_off)
        http_write_all(r->fd, hb.data + body_off, hb.len - body_off);

    ssize_t n;
    while ((n = read(rfd, tmp, sizeof tmp)) > 0)
        http_write_all(r->fd, tmp, (size_t)n);

    buf_free(&hb);
    buf_free(&out);
    return 0;
}

int cgi_git_backend(struct request *req, const char *repos_root,
                    const char *path_info, const char *remote_user)
{
    int pin[2], pout[2];
    if (pipe(pin) < 0 || pipe(pout) < 0)
        return -1;

    pid_t pid = fork();
    if (pid < 0)
        return -1;

    if (pid == 0) {
        /* ---- child ---- */
        dup2(pin[0], 0);
        dup2(pout[1], 1);
        /* stderr inherited: errors land in the server log */
        close(pin[0]); close(pin[1]);
        close(pout[0]); close(pout[1]);

        struct rlimit rl;
        rl.rlim_cur = rl.rlim_max = 600;
        setrlimit(RLIMIT_CPU, &rl);
        rl.rlim_cur = rl.rlim_max = 1024;
        setrlimit(RLIMIT_NOFILE, &rl);
        rl.rlim_cur = rl.rlim_max = 256;
        setrlimit(RLIMIT_NPROC, &rl);

        char env[16][2048];
        char *envp[20];
        int e = 0;
#define SETENV(kv) do { \
        snprintf(env[e], sizeof env[e], "%s", kv); \
        envp[e] = env[e]; e++; \
    } while (0)

        SETENV("PATH=/usr/local/bin:/usr/bin:/bin");
        SETENV("GIT_HTTP_EXPORT_ALL=1");
        snprintf(env[e], sizeof env[e], "GIT_PROJECT_ROOT=%s", repos_root);
        envp[e] = env[e]; e++;
        snprintf(env[e], sizeof env[e], "PATH_INFO=%s", path_info);
        envp[e] = env[e]; e++;
        snprintf(env[e], sizeof env[e], "REQUEST_METHOD=%s", req->method);
        envp[e] = env[e]; e++;
        snprintf(env[e], sizeof env[e], "QUERY_STRING=%s", req->query);
        envp[e] = env[e]; e++;
        SETENV("SERVER_PROTOCOL=HTTP/1.1");

        if (remote_user && remote_user[0]) {
            snprintf(env[e], sizeof env[e], "REMOTE_USER=%s", remote_user);
            envp[e] = env[e]; e++;
        }
        char *ct = req_header_dup(req, "content-type");
        if (ct) {
            snprintf(env[e], sizeof env[e], "CONTENT_TYPE=%s", ct);
            envp[e] = env[e]; e++;
            free(ct);
        }
        /* git deflates RPC request bodies >1KB; http-backend needs the
         * encoding to inflate stdin (shallow pulls carry many haves) */
        char *ce = req_header_dup(req, "content-encoding");
        if (ce) {
            snprintf(env[e], sizeof env[e], "HTTP_CONTENT_ENCODING=%s", ce);
            envp[e] = env[e]; e++;
            free(ce);
        }
        char *gp = req_header_dup(req, "git-protocol");
        if (gp) {
            snprintf(env[e], sizeof env[e], "GIT_PROTOCOL=%s", gp);
            envp[e] = env[e]; e++;
            free(gp);
        }
        if (req->content_length >= 0) {
            snprintf(env[e], sizeof env[e], "CONTENT_LENGTH=%ld",
                     req->content_length);
            envp[e] = env[e]; e++;
        }
        envp[e] = NULL;

        execvpe("git", (char *const[]){ "git", "http-backend", NULL }, envp);
        _exit(127);
    }

    /* ---- parent ---- */
    close(pin[0]);
    close(pout[1]);

    pump_body(req, pin[1]);
    close(pin[1]);

    int rc = send_cgi_response(req, pout[0]);
    close(pout[0]);

    int st = 0;
    waitpid(pid, &st, 0);
    if (rc != 0)
        http_error(req, 502, "git http-backend produced no valid CGI output");
    return rc;
}