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/wcommit.c

/* Web commits — turn a browser form into a real git commit on a bare
 * repository.  Plumbing only: hash-object -> temp GIT_INDEX_FILE ->
 * write-tree -> commit-tree -> compare-and-swap update-ref.  The same
 * branch-check/quota-check subcommands the update hook runs are
 * re-invoked here so web writes obey identical policy and audit. */

#include "wcommit.h"

#include "gitx.h"
#include "httpd.h"
#include "util.h"

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

#define ZERO_SHA "0000000000000000000000000000000000000000"

/* ---------------- bounded multipart/form-data parsing ---------------- */

int wc_mp_attr(const char *hdrs, const char *attr,
               char *out, size_t outlen)
{
    char needle[64];
    snprintf(needle, sizeof needle, "%s=\"", attr);
    const char *p = strstr(hdrs, needle);
    if (!p)
        return -1;
    p += strlen(needle);
    const char *e = strchr(p, '"');
    if (!e || (size_t)(e - p) >= outlen)
        return -1;
    memcpy(out, p, (size_t)(e - p));
    out[e - p] = '\0';
    return 0;
}

int wc_mp_find(char *body, size_t blen, const char *bnd,
               const char *field, const char **data, size_t *dlen,
               char *fname, size_t flen)
{
    if (!bnd || strlen(bnd) > 200)      /* snprintf would truncate,
                                        * ml would lie about needle len */
        return 0;
    char mark[280], nmark[284];
    int ml = snprintf(mark, sizeof mark, "--%s", bnd);
    int nl = snprintf(nmark, sizeof nmark, "\r\n--%s", bnd);
    const char *p = body, *end = body + blen;
    for (;;) {
        const char *m = memmem(p, (size_t)(end - p), mark, (size_t)ml);
        if (!m)
            return 0;
        const char *hp = m + ml;
        if (end - hp >= 2 && hp[0] == '-' && hp[1] == '-')
            return 0;                            /* closing marker */
        if (end - hp >= 2 && hp[0] == '\r' && hp[1] == '\n')
            hp += 2;
        /* part headers: bounded copy so strstr sees a C string */
        size_t hscan = (size_t)(end - hp) < 4096 ? (size_t)(end - hp) : 4096;
        const char *he = memmem(hp, hscan, "\r\n\r\n", 4);
        if (!he)
            return 0;
        size_t hl = (size_t)(he - hp);
        if (hl > 2000)
            hl = 2000;
        char hbuf[2100];
        memcpy(hbuf, hp, hl);
        hbuf[hl] = '\0';
        char nm[128];
        const char *cstart = he + 4;
        const char *ce = memmem(cstart, (size_t)(end - cstart),
                                nmark, (size_t)nl);
        if (!ce)
            ce = end;
        if (wc_mp_attr(hbuf, "name", nm, sizeof nm) == 0 &&
            strcmp(nm, field) == 0) {
            *data = cstart;
            *dlen = (size_t)(ce - cstart);
            if (fname && flen > 0) {
                if (wc_mp_attr(hbuf, "filename", fname, flen) != 0)
                    fname[0] = '\0';
            }
            return 1;
        }
        p = ce;
    }
}

int wc_mp_csrf_ok(struct request *r, char *body, size_t blen,
                  const char *bnd)
{
    if (!r->session_tok[0])
        return r->user_id > 0;
    const char *d;
    size_t dl;
    if (!wc_mp_find(body, blen, bnd, "csrf", &d, &dl, NULL, 0))
        return 0;
    return dl == strlen(r->session_tok) &&
           memcmp(d, r->session_tok, dl) == 0;
}

int wc_mp_boundary(struct request *r, char *out, size_t outlen)
{
    const char *ct = req_header(r, "Content-Type");
    if (!ct)
        return -1;
    const char *b = strstr(ct, "boundary=");
    if (!b)
        return -1;
    b += 9;
    size_t n = 0;
    if (*b == '"')
        b++;
    while (n < outlen - 1 && b[n] && b[n] != '"' && b[n] != ';' &&
           b[n] != '\r' && b[n] != '\n' && b[n] != ' ') {
        out[n] = b[n];
        n++;
    }
    out[n] = '\0';
    return n > 0 && n <= 200 ? 0 : -1;
}

int wc_basename(const char *fname, char *out, size_t outlen)
{
    const char *base = strrchr(fname, '/');
    const char *base2 = strrchr(fname, '\\');
    if (base2 && (!base || base2 > base))
        base = base2;
    base = base ? base + 1 : fname;
    if (strlen(base) >= outlen || !*base || !git_path_ok(base) ||
        strchr(base, '/'))
        return -1;
    snprintf(out, outlen, "%s", base);
    return 0;
}


/* run `git-trace <argv...>` with cwd=repo_dir and REMOTE_USER=user —
 * the exact invocation hooks/update performs */
static int run_self(struct app_ctx *c, const char *repo_dir,
                    const char *user, const char *const argv[])
{
    pid_t pid = fork();
    if (pid < 0)
        return -1;
    if (pid == 0) {
        int devnull = open("/dev/null", O_RDWR);
        if (devnull >= 0) {
            dup2(devnull, 0);
            dup2(devnull, 2);
        }
        if (chdir(repo_dir) != 0)
            _exit(127);
        char e1[PATH_MAX + 32], e2[PATH_MAX + 32], eu[128];
        snprintf(e1, sizeof e1, "GIT_TRACE_REPOS=%s", c->repos_root);
        snprintf(e2, sizeof e2, "GIT_TRACE_DB=%s", c->db_path);
        snprintf(eu, sizeof eu, "REMOTE_USER=%s", user);
        putenv(e1);
        putenv(e2);
        putenv(eu);
        execv("/proc/self/exe", (char *const *)argv);
        _exit(127);
    }
    int st = 0;
    if (waitpid(pid, &st, 0) < 0)
        return -1;
    return WIFEXITED(st) ? WEXITSTATUS(st) : -1;
}

/* first line of capture, NUL-terminated; "" on failure */
static void first_line(struct buf *b, char *out, size_t outlen)
{
    size_t n = b->len < outlen - 1 ? b->len : outlen - 1;
    memcpy(out, b->data, n);
    out[n] = '\0';
    char *e = strpbrk(out, "\r\n ");
    if (e)
        *e = '\0';
}

int wc_commit_file(struct app_ctx *c, const char *repo_dir,
                   const char *branch, const char *path,
                   const void *data, size_t len,
                   const char *msg, const char *author, const char *amail,
                   char out_sha[41], char *err, size_t errlen)
{
    char ref[320], idx[PATH_MAX], blobf[PATH_MAX];
    snprintf(ref, sizeof ref, "refs/heads/%s", branch);
    /* temp names must be unique per REQUEST, not per process: the
     * threaded server shares getpid() across worker threads */
    snprintf(idx, sizeof idx, "/tmp/gt-wc-XXXXXX");
    int ifd = mkstemp(idx);                  /* reserve the name */
    if (ifd < 0) {
        snprintf(err, errlen, "cannot stage upload");
        return -1;
    }
    close(ifd);
    unlink(idx);                             /* git wants it absent */

    /* payload -> temp file -> blob object (no stdin plumbing) */
    snprintf(blobf, sizeof blobf, "/tmp/gt-wb-XXXXXX");
    int fd = mkstemp(blobf);
    if (fd < 0) {
        snprintf(err, errlen, "cannot stage upload");
        return -1;
    }
    size_t off = 0;
    while (off < len) {
        ssize_t w = write(fd, (const char *)data + off, len - off);
        if (w <= 0) {
            close(fd);
            unlink(blobf);
            snprintf(err, errlen, "cannot stage upload");
            return -1;
        }
        off += (size_t)w;
    }
    close(fd);

    struct buf b;
    buf_init(&b);
    char blob[41];
    if (git_capture(repo_dir, (const char *const[]){
            "git", "hash-object", "-w", "--", blobf, NULL}, &b) != 0) {
        unlink(blobf);
        snprintf(err, errlen, "hash-object failed");
        goto fail;
    }
    unlink(blobf);
    first_line(&b, blob, sizeof blob);
    if (strlen(blob) != 40) {
        snprintf(err, errlen, "hash-object failed");
        goto fail;
    }

    /* current branch tip ("" when unborn) */
    char old[41] = "";
    b.len = 0;
    if (git_capture(repo_dir, (const char *const[]){
            "git", "rev-parse", "--verify", "-q", ref, NULL}, &b) == 0)
        first_line(&b, old, sizeof old);

    /* private index: read the tip tree (or empty), swap in the blob */
    char idxenv[PATH_MAX + 32];
    snprintf(idxenv, sizeof idxenv, "GIT_INDEX_FILE=%s", idx);
    const char *ienv[] = { idxenv, NULL };
    unlink(idx);
    b.len = 0;
    int rc;
    if (old[0])
        rc = git_capture_env(repo_dir, ienv, (const char *const[]){
                "git", "read-tree", old, NULL}, &b);
    else
        rc = git_capture_env(repo_dir, ienv, (const char *const[]){
                "git", "read-tree", "--empty", NULL}, &b);
    if (rc != 0) {
        snprintf(err, errlen, "read-tree failed");
        goto fail;
    }
    b.len = 0;
    if (git_capture_env(repo_dir, ienv, (const char *const[]){
            "git", "update-index", "--add", "--cacheinfo",
            "100644", blob, path, NULL}, &b) != 0) {
        snprintf(err, errlen, "update-index failed");
        goto fail;
    }
    b.len = 0;
    char tree[41];
    if (git_capture_env(repo_dir, ienv, (const char *const[]){
            "git", "write-tree", NULL}, &b) != 0) {
        snprintf(err, errlen, "write-tree failed");
        goto fail;
    }
    first_line(&b, tree, sizeof tree);

    /* commit with the forge user as author and committer */
    char an[128], ae[200];
    snprintf(an, sizeof an, "GIT_AUTHOR_NAME=%s", author);
    snprintf(ae, sizeof ae, "GIT_AUTHOR_EMAIL=%s",
             amail && *amail ? amail : "forge@git-trace");
    char cn[128], ce[200];
    snprintf(cn, sizeof cn, "GIT_COMMITTER_NAME=%s", author);
    snprintf(ce, sizeof ce, "GIT_COMMITTER_EMAIL=%s",
             amail && *amail ? amail : "forge@git-trace");
    const char *cenv[] = { idxenv, an, ae, cn, ce, NULL };
    b.len = 0;
    char commit[41];
    const char *cargv[8];
    int ci = 0;
    cargv[ci++] = "git";
    cargv[ci++] = "commit-tree";
    cargv[ci++] = tree;
    if (old[0]) {
        cargv[ci++] = "-p";
        cargv[ci++] = old;
    }
    cargv[ci++] = "-m";
    cargv[ci++] = msg;
    cargv[ci] = NULL;
    if (git_capture_env(repo_dir, cenv, cargv, &b) != 0) {
        snprintf(err, errlen, "commit-tree failed");
        goto fail;
    }
    first_line(&b, commit, sizeof commit);
    if (strlen(commit) != 40) {
        snprintf(err, errlen, "commit-tree failed");
        goto fail;
    }

    /* identical policy as a git push, before the ref moves */
    const char *bargv[] = { "git-trace", "branch-check",
                            ref, old[0] ? old : ZERO_SHA, commit, NULL };
    if (run_self(c, repo_dir, author, bargv) != 0) {
        snprintf(err, errlen, "protected branch: commit denied");
        goto fail;
    }
    const char *qargv[] = { "git-trace", "quota-check",
                            ref, commit, NULL };
    if (run_self(c, repo_dir, author, qargv) != 0) {
        snprintf(err, errlen, "storage quota exceeded");
        goto fail;
    }

    /* CAS: refuse to clobber a branch that moved underneath us */
    b.len = 0;
    int urc;
    if (old[0])
        urc = git_capture(repo_dir, (const char *const[]){
                "git", "update-ref", ref, commit, old, NULL}, &b);
    else
        urc = git_capture(repo_dir, (const char *const[]){
                "git", "update-ref", ref, commit, ZERO_SHA, NULL}, &b);
    if (urc != 0) {
        snprintf(err, errlen, "branch moved while editing — retry");
        goto fail;
    }

    unlink(idx);
    buf_free(&b);
    memcpy(out_sha, commit, 41);
    return 0;

fail:
    unlink(idx);
    unlink(blobf);
    buf_free(&b);
    return -1;
}