romele / git-trace public
62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 10m ago
src/webhook.c
#include "webhook.h" #include "auth.h" #include "crypto.h" #include "web.h" #include <errno.h> #include <fcntl.h> #include <limits.h> #include <netdb.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> /* ---- payload helpers --------------------------------------------------- */ void json_put_esc(struct buf *b, const char *s, size_t n) { for (size_t i = 0; i < n; i++) { unsigned char c = (unsigned char)s[i]; switch (c) { case '"': buf_puts(b, "\\\""); break; case '\\': buf_puts(b, "\\\\"); break; case '\n': buf_puts(b, "\\n"); break; case '\r': buf_puts(b, "\\r"); break; case '\t': buf_puts(b, "\\t"); break; default: if (c < 0x20) buf_printf(b, "\\u%04x", c); else buf_put(b, &c, 1); /* UTF-8 passes through */ } } } /* ---- enqueue ------------------------------------------------------------ */ /* comma-separated event list membership ("push,mr") — whole tokens */ static int ev_match(const char *list, const char *event) { size_t el = strlen(event); const char *p = list; while (*p) { const char *cm = strchr(p, ','); size_t tl = cm ? (size_t)(cm - p) : strlen(p); if (tl == el && memcmp(p, event, el) == 0) return 1; if (!cm) break; p = cm + 1; } return 0; } int wh_enqueue(sqlite3 *db, long repo_id, const char *event, const char *payload) { sqlite3_stmt *st; if (sqlite3_prepare_v2(db, "SELECT id, events FROM repo_webhooks" " WHERE repo_id=? AND active=1", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, repo_id); /* collect ids first: inserting while stepping the same table on * one connection is legal but fragile */ long ids[64]; int ni = 0; while (sqlite3_step(st) == SQLITE_ROW && ni < 64) { const char *ev = (const char *)sqlite3_column_text(st, 1); if (ev && ev_match(ev, event)) ids[ni++] = sqlite3_column_int64(st, 0); } sqlite3_finalize(st); sqlite3_stmt *ins; if (ni > 0 && sqlite3_prepare_v2(db, "INSERT INTO webhook_deliveries(hook_id,event,payload)" " VALUES(?,?,?)", -1, &ins, NULL) != SQLITE_OK) return -1; int n = 0; for (int i = 0; i < ni; i++) { sqlite3_reset(ins); sqlite3_bind_int64(ins, 1, ids[i]); sqlite3_bind_text(ins, 2, event, -1, SQLITE_TRANSIENT); sqlite3_bind_text(ins, 3, payload, -1, SQLITE_TRANSIENT); if (sqlite3_step(ins) == SQLITE_DONE) n++; } if (ni > 0) sqlite3_finalize(ins); return n; } void wh_notify(struct app_ctx *c, long repo_id, const char *event, const char *payload) { if (wh_enqueue(c->db, repo_id, event, payload) > 0) wh_dispatch_async(c); } /* ---- detached dispatcher ------------------------------------------------ */ void wh_dispatch_async(struct app_ctx *c) { (void)c; gt_spawn_task("webhook-run"); } /* ---- transport: in-tree HTTP ------------------------------------------- */ /* connect with a hard deadline — a hanging endpoint must never stall * the dispatcher (or, through it, a push) */ static int wh_connect(const char *host, int port, char *err, size_t esz) { char ps[8]; snprintf(ps, sizeof ps, "%d", port); struct addrinfo hints; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; struct addrinfo *ai = NULL; int g = getaddrinfo(host, ps, &hints, &ai); if (g != 0 || !ai) { snprintf(err, esz, "dns: %s", gai_strerror(g)); return -1; } int fd = -1; for (struct addrinfo *a = ai; a && fd < 0; a = a->ai_next) { fd = socket(a->ai_family, a->ai_socktype, a->ai_protocol); if (fd < 0) continue; fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); if (connect(fd, a->ai_addr, a->ai_addrlen) == 0) break; if (errno != EINPROGRESS) { close(fd); fd = -1; continue; } fd_set ws; FD_ZERO(&ws); FD_SET(fd, &ws); struct timeval tv = { .tv_sec = 3, .tv_usec = 0 }; if (select(fd + 1, NULL, &ws, NULL, &tv) <= 0) { close(fd); fd = -1; continue; } int so = 0; socklen_t sl = sizeof so; if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &so, &sl) != 0 || so != 0) { close(fd); fd = -1; continue; } break; } freeaddrinfo(ai); if (fd < 0) { snprintf(err, esz, "connect failed"); return -1; } fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK); struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); return fd; } /* parse http://host[:port][/path] — https is handed to curl */ static int wh_post_http(const char *url, const char *event, const char *sig, const char *body, size_t blen, int *code, char *err, size_t esz) { const char *p = url + 7; /* skip http:// */ const char *sep = p + strcspn(p, "/:"); if (sep == p || sep - p > 250) { snprintf(err, esz, "bad url"); return -1; } char host[256]; memcpy(host, p, (size_t)(sep - p)); host[sep - p] = '\0'; int port = 80; const char *path = "/"; if (*sep == ':') { port = atoi(sep + 1); if (port <= 0 || port > 65535) { snprintf(err, esz, "bad port"); return -1; } const char *sl = strchr(sep + 1, '/'); if (sl) path = sl; } else if (*sep == '/') path = sep; int fd = wh_connect(host, port, err, esz); if (fd < 0) return -1; struct buf req; buf_init(&req); buf_printf(&req, "POST %s HTTP/1.1\r\n" "Host: %s\r\n" "User-Agent: git-trace\r\n" "Content-Type: application/json\r\n" "Content-Length: %zu\r\n" "X-Git-Trace-Event: %s\r\n", path, host, blen, event); if (sig) buf_printf(&req, "X-Git-Trace-Signature: sha256=%s\r\n", sig); buf_puts(&req, "Connection: close\r\n\r\n"); int rc = -1; for (size_t w = 0; w < req.len; ) { ssize_t n = send(fd, req.data + w, req.len - w, 0); if (n <= 0) goto out; w += (size_t)n; } for (size_t w = 0; w < blen; ) { ssize_t n = send(fd, body + w, blen - w, 0); if (n <= 0) goto out; w += (size_t)n; } { char resp[512]; ssize_t r = recv(fd, resp, sizeof resp - 1, 0); if (r > 0) { resp[r] = '\0'; if (strncmp(resp, "HTTP/", 5) == 0) { const char *sp = strchr(resp, ' '); *code = sp ? atoi(sp + 1) : 0; if (*code > 0) rc = 0; } } } if (rc != 0) snprintf(err, esz, "no http response"); out: buf_free(&req); close(fd); return rc; } /* https:// or exotic schemes: delegate to curl as a process — * payload on stdin (@-), nothing sensitive on argv */ static int wh_post_curl(const char *url, const char *event, const char *sig, const char *body, size_t blen, int *code, char *err, size_t esz) { int pin[2], pout[2]; if (pipe(pin) != 0 || pipe(pout) != 0) { snprintf(err, esz, "pipe failed"); return -1; } char evh[128], sih[128]; snprintf(evh, sizeof evh, "X-Git-Trace-Event: %s", event); sih[0] = '\0'; if (sig) snprintf(sih, sizeof sih, "X-Git-Trace-Signature: sha256=%s", sig); pid_t pid = fork(); if (pid == 0) { dup2(pin[0], 0); dup2(pout[1], 1); int devnull = open("/dev/null", O_WRONLY); if (devnull >= 0) dup2(devnull, 2); close(pin[0]); close(pin[1]); close(pout[0]); close(pout[1]); const char *argv[24]; int i = 0; argv[i++] = "curl"; argv[i++] = "-sS"; argv[i++] = "-m"; argv[i++] = "8"; argv[i++] = "-o"; argv[i++] = "/dev/null"; argv[i++] = "-w"; argv[i++] = "%{http_code}"; argv[i++] = "-X"; argv[i++] = "POST"; argv[i++] = "-H"; argv[i++] = "Content-Type: application/json"; argv[i++] = "-H"; argv[i++] = evh; if (sih[0]) { argv[i++] = "-H"; argv[i++] = sih; } argv[i++] = "--data-binary"; argv[i++] = "@-"; argv[i++] = url; argv[i] = NULL; execvp("curl", (char *const *)argv); _exit(127); } close(pin[0]); close(pout[1]); if (pid < 0) { close(pin[1]); close(pout[0]); snprintf(err, esz, "fork failed"); return -1; } /* payloads are capped small; a single write loop is enough — * curl drains stdin while it runs */ for (size_t w = 0; w < blen; ) { ssize_t n = write(pin[1], body + w, blen - w); if (n <= 0) break; w += (size_t)n; } close(pin[1]); char codebuf[64]; size_t cl = 0; while (cl < sizeof codebuf - 1) { ssize_t n = read(pout[0], codebuf + cl, sizeof codebuf - 1 - cl); if (n <= 0) break; cl += (size_t)n; } codebuf[cl] = '\0'; close(pout[0]); int st = 0, done = 0; for (int i = 0; i < 240 && !done; i++) { /* ~12s budget */ done = waitpid(pid, &st, WNOHANG) == pid; if (!done) usleep(50000); } if (!done) { kill(pid, SIGKILL); waitpid(pid, &st, 0); snprintf(err, esz, "curl timeout"); return -1; } if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) { snprintf(err, esz, "curl exit %d", WIFEXITED(st) ? WEXITSTATUS(st) : -1); return -1; } *code = atoi(codebuf); if (*code <= 0) { snprintf(err, esz, "no http response"); return -1; } return 0; } /* ---- one delivery ------------------------------------------------------- */ static int wh_deliver(const char *url, const char *secret, const char *event, const char *payload, int *code, char *err, size_t esz) { char sighex[80]; const char *sig = NULL; if (secret && *secret) { uint8_t mac[32]; gt_hmac_sha256((const uint8_t *)secret, strlen(secret), (const uint8_t *)payload, strlen(payload), mac); char hex[65]; to_hex(mac, 32, hex); snprintf(sighex, sizeof sighex, "%s", hex); sig = sighex; } if (strncmp(url, "http://", 7) == 0) return wh_post_http(url, event, sig, payload, strlen(payload), code, err, esz); if (strncmp(url, "https://", 8) == 0) return wh_post_curl(url, event, sig, payload, strlen(payload), code, err, esz); snprintf(err, esz, "unsupported scheme"); return -1; } int wh_run(struct app_ctx *c) { sqlite3_stmt *st; if (sqlite3_prepare_v2(c->db, "SELECT id FROM webhook_deliveries" " WHERE status='pending' AND attempts<5" " ORDER BY id LIMIT 50", -1, &st, NULL) != SQLITE_OK) return -1; long ids[50]; int n = 0; while (sqlite3_step(st) == SQLITE_ROW && n < 50) ids[n++] = sqlite3_column_int64(st, 0); sqlite3_finalize(st); int done = 0; for (int i = 0; i < n; i++) { if (sqlite3_prepare_v2(c->db, "SELECT h.url, h.secret, d.event, d.payload" " FROM webhook_deliveries d" " JOIN repo_webhooks h ON h.id=d.hook_id" " WHERE d.id=?", -1, &st, NULL) != SQLITE_OK) continue; sqlite3_bind_int64(st, 1, ids[i]); if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); continue; } const char *url = (const char *)sqlite3_column_text(st, 0); const char *sec = (const char *)sqlite3_column_text(st, 1); const char *ev = (const char *)sqlite3_column_text(st, 2); const char *pay = (const char *)sqlite3_column_text(st, 3); int code = 0; char err[120] = ""; int rc = wh_deliver(url ? url : "", sec ? sec : "", ev ? ev : "push", pay ? pay : "{}", &code, err, sizeof err); sqlite3_finalize(st); int ok = (rc == 0 && code >= 200 && code < 300); if (!ok && rc == 0) snprintf(err, sizeof err, "http %d", code); if (sqlite3_prepare_v2(c->db, "UPDATE webhook_deliveries SET" " attempts=attempts+1, status=?, code=?, error=?," " done_at=strftime('%s','now') WHERE id=?", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_text(st, 1, ok ? "ok" : "fail", -1, SQLITE_STATIC); sqlite3_bind_int(st, 2, code); sqlite3_bind_text(st, 3, err, -1, SQLITE_TRANSIENT); sqlite3_bind_int64(st, 4, ids[i]); sqlite3_step(st); sqlite3_finalize(st); } done++; } return done; }