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