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