romele / git-trace public
62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 10m ago
src/httpd.c
#include "httpd.h" #include <arpa/inet.h> #include <ctype.h> #include <errno.h> #include <netdb.h> #include <netinet/in.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <sys/socket.h> #include <sys/time.h> #include <unistd.h> void http_write_all(int fd, const void *p, size_t n) { const char *c = p; while (n > 0) { ssize_t w = write(fd, c, n); if (w < 0) { if (errno == EINTR) continue; return; } c += w; n -= (size_t)w; } } /* ---------------- request parsing ---------------- */ static char *find_eoh(char *buf, size_t len) { for (size_t i = 0; i + 3 < len; i++) if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n') return buf + i; return NULL; } static int read_request(int fd, struct request *r) { char *buf = xmalloc(HDR_MAX + 1); size_t len = 0; char *eoh = NULL; while (!eoh && len < HDR_MAX) { ssize_t n = read(fd, buf + len, HDR_MAX - len); if (n <= 0) { free(buf); return -1; } len += (size_t)n; buf[len] = '\0'; eoh = find_eoh(buf, len); } if (!eoh) { free(buf); return -1; /* header too large */ } size_t hdrlen = (size_t)(eoh - buf) + 4; size_t over = len - hdrlen; /* request line: METHOD SP URI SP HTTP/1.x */ char *sp1 = memchr(buf, ' ', eoh - buf); if (!sp1 || (size_t)(sp1 - buf) >= sizeof r->method) goto bad; memcpy(r->method, buf, (size_t)(sp1 - buf)); r->method[sp1 - buf] = '\0'; char *uri = sp1 + 1; char *sp2 = memchr(uri, ' ', (size_t)(eoh - uri)); if (!sp2) goto bad; size_t urilen = (size_t)(sp2 - uri); if (urilen == 0 || urilen >= URI_MAX) goto bad; char uridec[URI_MAX]; { char *d = url_decode(uri, urilen); snprintf(uridec, sizeof uridec, "%s", d); free(d); } char *q = strchr(uridec, '?'); if (q) { *q = '\0'; snprintf(r->query, sizeof r->query, "%s", q + 1); } else { r->query[0] = '\0'; } if (uridec[0] != '/' || strlen(uridec) >= sizeof r->path) goto bad; strcpy(r->path, uridec); /* header block: copy lines after the first */ char *hline = strchr(buf, '\n'); hline = hline ? hline + 1 : eoh; size_t hsize = (size_t)(eoh - hline); r->hdrs = xmalloc(hsize + 1); memcpy(r->hdrs, hline, hsize); r->hdrs[hsize] = '\0'; /* body overread */ if (over) { r->over = xmalloc(over); memcpy(r->over, eoh + 4, over); r->over_len = over; } free(buf); r->content_length = -1; const char *cl = req_header(r, "content-length"); if (cl) r->content_length = strtol(cl, NULL, 10); r->body_left = r->content_length > 0 ? r->content_length : 0; const char *te = req_header(r, "transfer-encoding"); if (te && strcasestr(te, "chunked")) r->chunked = 1; return 0; bad: free(buf); return -1; } const char *req_header(struct request *r, const char *name) { if (!r->hdrs) return NULL; size_t nl = strlen(name); const char *p = r->hdrs; while (*p) { const char *eol = strchr(p, '\n'); size_t linelen = eol ? (size_t)(eol - p) : strlen(p); if (linelen > nl + 1 && strncasecmp(p, name, nl) == 0 && p[nl] == ':') { const char *v = p + nl + 1; while (*v == ' ' || *v == '\t') v++; return v; /* points into hdrs; value ends at \r or \n */ } p = eol ? eol + 1 : p + linelen; } return NULL; } char *req_header_dup(struct request *r, const char *name) { const char *v = req_header(r, name); if (!v) return NULL; const char *e = v; while (*e && *e != '\r' && *e != '\n') e++; while (e > v && (e[-1] == ' ' || e[-1] == '\t')) e--; size_t n = (size_t)(e - v); char *out = xmalloc(n + 1); memcpy(out, v, n); out[n] = '\0'; return out; } int req_query(struct request *r, const char *key, char *out, size_t outlen) { size_t kl = strlen(key); const char *p = r->query; while (*p) { const char *amp = strchr(p, '&'); size_t plen = amp ? (size_t)(amp - p) : strlen(p); const char *eq = memchr(p, '=', plen); if (eq && (size_t)(eq - p) == kl && strncmp(p, key, kl) == 0) { char *d = url_decode(eq + 1, plen - (size_t)(eq - p) - 1); int rc = snprintf(out, outlen, "%s", d); free(d); return (rc < 0 || (size_t)rc >= outlen) ? -1 : 0; } p = amp ? amp + 1 : p + plen; } return -1; } /* ---------------- chunked request bodies ---------------- */ /* raw stream for the dechunker: buffered overread first, then socket, * unbounded (body_left is meaningless for chunked input) */ static ssize_t raw_read(struct request *r, void *buf, size_t n) { size_t avail = r->over_len - r->over_off; if (avail) { size_t take = avail < n ? avail : n; memcpy(buf, r->over + r->over_off, take); r->over_off += take; return (ssize_t)take; } return read(r->fd, buf, n); } /* one CRLF-terminated line from the raw stream (chunk headers and * trailers are tiny); returns line length without CRLF, -1 on error */ static ssize_t raw_line(struct request *r, char *out, size_t cap) { size_t n = 0; for (;;) { char c; if (raw_read(r, &c, 1) <= 0) return -1; if (c == '\r') { char lf; if (raw_read(r, &lf, 1) <= 0 || lf != '\n') return -1; break; } if (c == '\n') break; if (n + 1 >= cap) return -1; out[n++] = c; } out[n] = '\0'; return (ssize_t)n; } static long body_max(void) { const char *e = getenv("GIT_TRACE_BODY_MAX"); long v = e ? strtol(e, NULL, 10) : 0; return v > 0 ? v : 256L * 1024 * 1024; } /* Decode a chunked body eagerly: raw framing comes from over+socket, * decoded bytes replace r->over and become the plain Content-Length * body every downstream consumer already understands. * Returns 0, -1 bad framing, -2 over GIT_TRACE_BODY_MAX. */ static int body_dechunk(struct request *r) { long max = body_max(); struct buf out; buf_init(&out); char line[128]; for (;;) { ssize_t ln = raw_line(r, line, sizeof line); if (ln <= 0) goto bad; char *end; unsigned long sz = strtoul(line, &end, 16); if (end == line || (*end != '\0' && *end != ';')) goto bad; if (sz == 0) { for (;;) { /* trailers: drain to empty line */ ln = raw_line(r, line, sizeof line); if (ln < 0) goto bad; if (ln == 0) break; } break; } if (sz > (unsigned long)max || out.len > (size_t)max - (size_t)sz) goto big; while (sz > 0) { char tmp[8192]; size_t take = sz < sizeof tmp ? (size_t)sz : sizeof tmp; ssize_t got = raw_read(r, tmp, take); if (got <= 0) goto bad; buf_put(&out, tmp, (size_t)got); sz -= (unsigned long)got; } if (raw_line(r, line, sizeof line) != 0) goto bad; /* chunk data must end with CRLF */ } free(r->over); r->over = out.data ? out.data : xmalloc(1); r->over_len = out.len; r->over_off = 0; r->content_length = (long)out.len; r->body_left = (long)out.len; return 0; big: buf_free(&out); return -2; bad: buf_free(&out); return -1; } long body_read(struct request *r, void *buf, size_t n) { if (r->body_left <= 0) return 0; size_t avail = r->over_len - r->over_off; if (avail) { size_t take = avail < n ? avail : n; if ((long)take > r->body_left) take = (size_t)r->body_left; memcpy(buf, r->over + r->over_off, take); r->over_off += take; r->body_left -= (long)take; return (long)take; } if ((long)n > r->body_left) n = (size_t)r->body_left; ssize_t got = read(r->fd, buf, n); if (got <= 0) return got; r->body_left -= got; return got; } /* ---------------- responses ---------------- */ void http_write_head(int fd, int code) { char head[128]; int n = snprintf(head, sizeof head, "HTTP/1.1 %d %s\r\n", code, status_text(code)); http_write_all(fd, head, (size_t)n); } /* baseline headers on every response: no sniffing, no framing, * no referrer leakage, and a CSP that only allows our inline styles */ #define SEC_HEADERS \ "X-Content-Type-Options: nosniff\r\n" \ "X-Frame-Options: DENY\r\n" \ "Referrer-Policy: no-referrer\r\n" \ "Content-Security-Policy: default-src 'none'; " \ "style-src 'unsafe-inline'\r\n" void http_respond_csp(struct request *r, int code, const char *ctype, const char *csp, const char *body, size_t n) { char head[1024]; int hn = snprintf(head, sizeof head, "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\n" "X-Content-Type-Options: nosniff\r\n" "X-Frame-Options: DENY\r\n" "Referrer-Policy: no-referrer\r\n" "Content-Security-Policy: %s\r\n" "Connection: close\r\n\r\n", code, status_text(code), ctype, n, csp ? csp : "default-src 'none'; style-src 'unsafe-inline'"); http_write_all(r->fd, head, (size_t)hn); if (n) http_write_all(r->fd, body, n); } void http_respond(struct request *r, int code, const char *ctype, const char *body, size_t n) { http_respond_csp(r, code, ctype, NULL, body, n); } void http_error(struct request *r, int code, const char *msg) { char body[1024]; int n = snprintf(body, sizeof body, "%d %s: %s\n", code, status_text(code), msg ? msg : ""); http_respond(r, code, "text/plain; charset=utf-8", body, (size_t)n); } void http_unauthorized(struct request *r, const char *msg) { char body[512]; int bn = snprintf(body, sizeof body, "401 Unauthorized: %s\n", msg); char head[768]; int hn = snprintf(head, sizeof head, "HTTP/1.1 401 Unauthorized\r\n" "WWW-Authenticate: Basic realm=\"git-trace\", charset=\"UTF-8\"\r\n" "Content-Type: text/plain; charset=utf-8\r\n" SEC_HEADERS "Content-Length: %d\r\nConnection: close\r\n\r\n", bn); http_write_all(r->fd, head, (size_t)hn); http_write_all(r->fd, body, (size_t)bn); } void http_redirect(struct request *r, const char *location) { http_redirect_cookie(r, location, NULL); } void http_redirect_cookie(struct request *r, const char *location, const char *set_cookie) { char head[1400]; int n = snprintf(head, sizeof head, "HTTP/1.1 302 Found\r\nLocation: %s\r\n%s%s" SEC_HEADERS "Content-Length: 0\r\nConnection: close\r\n\r\n", location, set_cookie ? "Set-Cookie: " : "", set_cookie ? set_cookie : ""); http_write_all(r->fd, head, (size_t)n); } /* ---------------- accept loop ---------------- */ struct conn_arg { int fd; void *ctx; route_fn route; const char *alt_origin; /* extra allowed Origin host */ char ip[64]; }; /* hard cap on in-flight connections: each is a thread + likely a * forked git child, so unbounded growth is a fork bomb on ourselves */ #define MAX_CONN 128 static _Atomic int active_conns; /* * POST/PUT/DELETE with an Origin header must come from our own * origin — blocks cross-site form posts even if a CSRF token were * ever mishandled (also covers login CSRF). No Origin header means * a non-browser client (curl, git) and is fine. */ static int origin_host_eq(const char *oh, size_t on, const char *h) { return h && strlen(h) == on && strncasecmp(oh, h, on) == 0; } /* is "host[:port]" or "[v6]:port" a loopback address? */ static int host_is_loopback(const char *h, size_t n) { char hb[80]; if (!h || n == 0 || n >= sizeof hb) return 0; memcpy(hb, h, n); hb[n] = '\0'; char *p = hb; if (*p == '[') { char *e = strchr(p, ']'); if (!e) return 0; *e = '\0'; p++; } else if (strchr(p, ':') && strchr(p, ':') == strrchr(p, ':')) { *strchr(p, ':') = '\0'; /* single colon: strip :port */ } /* multiple colons = bare IPv6 literal — compared as-is below */ return !strcasecmp(p, "localhost") || !strcmp(p, "127.0.0.1") || !strcmp(p, "::1"); } static int origin_ok(struct request *r, const char *alt) { if (strcmp(r->method, "GET") == 0 || strcmp(r->method, "HEAD") == 0) return 1; char *origin = req_header_dup(r, "origin"); if (!origin) return 1; char *host = req_header_dup(r, "host"); int ok = 0; const char *oh = strstr(origin, "://"); oh = oh ? oh + 3 : origin; const char *os = strchr(oh, '/'); size_t on = os ? (size_t)(os - oh) : strlen(oh); if (host && origin_host_eq(oh, on, host)) ok = 1; /* behind a reverse proxy Host != the browser's origin; the * configured public host is an equally valid origin */ if (!ok && origin_host_eq(oh, on, alt)) ok = 1; /* loopback-facing daemon (dev box, IDE preview proxies): accept * any loopback origin regardless of port. A remote page cannot * mint a loopback Origin — the name stays the attacker's domain — * so this admits only processes already running locally. */ if (!ok && host && host_is_loopback(host, strlen(host)) && host_is_loopback(oh, on)) ok = 1; /* "Origin: null" is sent by sandboxed embedders, privacy tools * that strip Origin (Tor, Brave), and cross-origin redirects — * our own www -> apex and http -> https Caddy redirects turn a * POST's Origin into "null". Accepting it is safe everywhere: * every session-bound POST still requires the CSRF token, which * a cross-origin attacker cannot read — only the anonymous * routes (register/login) are reachable this way. */ if (!ok && on == 4 && strncmp(oh, "null", 4) == 0) ok = 1; if (!ok) fprintf(stderr, "origin refused: origin='%.*s' host='%s'\n", (int)on, oh, host ? host : "-"); free(origin); free(host); return ok; } static void *conn_thread(void *p) { struct conn_arg *a = p; struct request r; memset(&r, 0, sizeof r); r.fd = a->fd; snprintf(r.remote_ip, sizeof r.remote_ip, "%s", a->ip); if (read_request(a->fd, &r) == 0) { /* git/curl hold big bodies until they see this */ const char *exp = req_header(&r, "expect"); if (exp && strcasestr(exp, "100-continue")) http_write_all(r.fd, "HTTP/1.1 100 Continue\r\n\r\n", 25); int drc = r.chunked ? body_dechunk(&r) : 0; if (drc == -2) http_error(&r, 413, "request body too large"); else if (drc != 0) http_error(&r, 400, "malformed chunked body"); else if (!origin_ok(&r, a->alt_origin)) http_error(&r, 403, "cross-origin request refused"); else a->route(a->ctx, &r); } else { http_error(&r, 400, "malformed request"); } shutdown(a->fd, SHUT_RDWR); close(a->fd); free(r.hdrs); free(r.over); free(a); active_conns--; return NULL; } void httpd_run(int port, void *ctx, route_fn route, const char *alt_origin) { int ls = socket(AF_INET6, SOCK_STREAM, 0); if (ls < 0) die("socket"); int one = 1; setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); int v6only = 0; setsockopt(ls, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof v6only); struct sockaddr_in6 sa; memset(&sa, 0, sizeof sa); sa.sin6_family = AF_INET6; sa.sin6_addr = in6addr_any; sa.sin6_port = htons((uint16_t)port); if (bind(ls, (struct sockaddr *)&sa, sizeof sa) < 0) die("bind"); if (listen(ls, 128) < 0) die("listen"); fprintf(stderr, "git-trace: listening on port %d\n", port); for (;;) { struct sockaddr_storage ss; socklen_t sl = sizeof ss; int fd = accept(ls, (struct sockaddr *)&ss, &sl); if (fd < 0) { if (errno == EINTR) continue; perror("accept"); continue; } if (active_conns >= MAX_CONN) { static const char busy[] = "HTTP/1.1 503 Service Unavailable\r\n" "Content-Length: 9\r\nConnection: close\r\n\r\noverloaded"; http_write_all(fd, busy, sizeof busy - 1); close(fd); continue; } struct timeval rcv = { .tv_sec = 60 }, snd = { .tv_sec = 300 }; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &rcv, sizeof rcv); setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &snd, sizeof snd); struct conn_arg *a = xmalloc(sizeof *a); a->fd = fd; a->ctx = ctx; a->route = route; a->alt_origin = alt_origin; char host[64] = "?"; getnameinfo((struct sockaddr *)&ss, sl, host, sizeof host, NULL, 0, NI_NUMERICHOST); snprintf(a->ip, sizeof a->ip, "%s", host); active_conns++; pthread_t th; if (pthread_create(&th, NULL, conn_thread, a) != 0) { active_conns--; close(fd); free(a); continue; } pthread_detach(th); } }