romele / git-trace public
62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 10m ago
src/ssh.c
#include "auth.h" #include "repo.h" #include "ssh.h" #include "util.h" #include <ctype.h> #include <limits.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <unistd.h> /* * SSH_ORIGINAL_COMMAND forms produced by git clients: * git-upload-pack '/path' git-receive-pack '/path' * git upload-pack '/path' git receive-pack '/path' * (path may also be unquoted) * Returns 0 on success; *is_receive set; path unquoted (leading '/' * stripped). */ static int parse_ssh_git_cmd(const char *oc, int *is_receive, char *path, size_t pn) { char tok1[64] = "", tok2[64] = ""; int n = sscanf(oc, " %63s %63s", tok1, tok2); if (n < 2) return -1; const char *verb; const char *rest; if (strcmp(tok1, "git") == 0) { verb = tok2; rest = oc; rest = strchr(rest, ' '); /* skip "git" */ rest = rest ? strchr(rest + 1, ' ') : NULL; /* skip verb */ if (rest) rest++; /* skip the space */ } else { const char *sp = strchr(oc, ' '); if (!sp) return -1; if (strncmp(tok1, "git-", 4) != 0) return -1; verb = tok1 + 4; rest = sp + 1; } if (strcmp(verb, "upload-pack") == 0) *is_receive = 0; else if (strcmp(verb, "receive-pack") == 0) *is_receive = 1; else return -1; /* tok2 may actually be the verb when tok1="git"; locate path after * verb regardless of quoting */ if (!rest) return -1; while (*rest == ' ') rest++; if (*rest == '\'') { const char *end = strrchr(rest + 1, '\''); if (!end) return -1; size_t l = (size_t)(end - (rest + 1)); if (l == 0 || l >= pn) return -1; memcpy(path, rest + 1, l); path[l] = '\0'; } else { size_t l = strcspn(rest, " \t\r\n"); if (l == 0 || l >= pn) return -1; memcpy(path, rest, l); path[l] = '\0'; } while (*path == '/') memmove(path, path + 1, strlen(path)); return path[0] ? 0 : -1; } /* split "owner/name[.git]" and resolve to an on-disk bare repo */ static int resolve_repo(struct app_ctx *c, const char *path, char *owner, size_t on, char *name, size_t nn, char *rdir, size_t rn) { const char *slash = strchr(path, '/'); if (!slash) return -1; size_t ol = (size_t)(slash - path); if (ol == 0 || ol >= on) return -1; memcpy(owner, path, ol); owner[ol] = '\0'; const char *np = slash + 1; size_t nl = strlen(np); if (nl > 4 && strcmp(np + nl - 4, ".git") == 0) nl -= 4; if (nl == 0 || nl >= nn) return -1; memcpy(name, np, nl); name[nl] = '\0'; if (!owner_ok(owner) || !name_ok(name)) return -1; return repo_path(rdir, rn, c->repos_root, owner, name); } static void list_accessible(struct app_ctx *c, int uid, const char *uname) { printf("git-trace: interactive shell disabled — hi %s.\n\n", uname); printf("you can access:\n"); sqlite3_stmt *st; if (sqlite3_prepare_v2(c->db, "SELECT r.name, COALESCE(u.name,'_') FROM repos r" " LEFT JOIN users u ON u.id=r.owner_id ORDER BY 2,1", -1, &st, NULL) != SQLITE_OK) return; int any = 0; while (sqlite3_step(st) == SQLITE_ROW) { const char *rn = (const char *)sqlite3_column_text(st, 0); const char *ow = (const char *)sqlite3_column_text(st, 1); int rk = perm_rank(repo_perm(c->db, ow, rn, uid)); if (rk > 0) { printf(" %c %s/%s\n", rk >= 2 ? 'W' : 'R', ow, rn); any = 1; } } sqlite3_finalize(st); if (!any) printf(" (none — all public repos are still clonable)\n"); } int cmd_shell(struct app_ctx *c, const char *keyid_str) { long keyid = strtol(keyid_str, NULL, 10); char uname[64] = ""; int uid = sshkey_uid(c->db, keyid, uname, sizeof uname); if (uid < 0) { fprintf(stderr, "git-trace: unknown key\n"); return 1; } const char *oc = getenv("SSH_ORIGINAL_COMMAND"); if (!oc || !*oc) { list_accessible(c, uid, uname); return 0; } int is_receive; char path[PATH_MAX], owner[64], name[64], rdir[PATH_MAX]; if (parse_ssh_git_cmd(oc, &is_receive, path, sizeof path) != 0 || resolve_repo(c, path, owner, sizeof owner, name, sizeof name, rdir, sizeof rdir) != 0) { fprintf(stderr, "git-trace: bad command or repository\n"); return 1; } int rk = perm_rank(repo_perm(c->db, owner, name, uid)); int need = is_receive ? 2 : 1; if (rk < need) { fprintf(stderr, "git-trace: permission denied for %s on %s/%s\n", uname, owner, name); return 1; } setenv("REMOTE_USER", uname, 1); const char *verb = is_receive ? "receive-pack" : "upload-pack"; execlp("git", "git", verb, rdir, (char *)NULL); fprintf(stderr, "git-trace: exec failed\n"); return 127; } /* ---------------- authorized_keys generation ---------------- */ static void emit_keyfile(struct app_ctx *c, FILE *out) { char bin[PATH_MAX]; if (!realpath("/proc/self/exe", bin)) snprintf(bin, sizeof bin, "git-trace"); sqlite3_stmt *st; if (sqlite3_prepare_v2(c->db, "SELECT id,pubkey FROM ssh_keys ORDER BY id", -1, &st, NULL) != SQLITE_OK) return; while (sqlite3_step(st) == SQLITE_ROW) { fprintf(out, "command=\"GIT_TRACE_REPOS=%s GIT_TRACE_DB=%s %s shell %lld\"" ",no-pty,no-agent-forwarding,no-port-forwarding," "no-X11-forwarding %s\n", c->repos_root, c->db_path, bin, (long long)sqlite3_column_int64(st, 0), (const char *)sqlite3_column_text(st, 1)); } sqlite3_finalize(st); } /* regenerate <dbdir>/authorized_keys; quiet — used by web key add/rm */ int ssh_keyfile_write(struct app_ctx *c) { char fp[PATH_MAX]; snprintf(fp, sizeof fp, "%s", c->db_path); char *slash = strrchr(fp, '/'); if (slash) strcpy(slash + 1, "authorized_keys"); else snprintf(fp, sizeof fp, "authorized_keys"); FILE *f = fopen(fp, "w"); if (!f) return -1; emit_keyfile(c, f); fclose(f); chmod(fp, 0644); return 0; } int cmd_keyfile(struct app_ctx *c, int write_file) { if (!write_file) { emit_keyfile(c, stdout); return 0; } if (ssh_keyfile_write(c) != 0) { perror("authorized_keys"); return 1; } fprintf(stderr, "wrote authorized_keys\n"); fprintf(stderr, "sshd: AuthorizedKeysFile <dbdir>/authorized_keys\n"); return 0; } /* ---------------- sshkey CLI ---------------- */ int cmd_sshkey(struct app_ctx *c, int argc, char **argv) { if (argc < 1) { fprintf(stderr, "usage: sshkey add|list|rm ...\n"); return 2; } if (strcmp(argv[0], "list") == 0) { sqlite3_stmt *st; const char *sql = "SELECT k.id,u.name,k.name,k.fingerprint FROM ssh_keys k" " JOIN users u ON u.id=k.user_id" " WHERE (? IS NULL OR u.name=?) ORDER BY k.id"; if (sqlite3_prepare_v2(c->db, sql, -1, &st, NULL) != SQLITE_OK) return 1; if (argc > 1) { sqlite3_bind_text(st, 1, argv[1], -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, argv[1], -1, SQLITE_TRANSIENT); } else { sqlite3_bind_null(st, 1); sqlite3_bind_null(st, 2); } while (sqlite3_step(st) == SQLITE_ROW) printf("%4lld %-16s %-16s %s\n", (long long)sqlite3_column_int64(st, 0), (const char *)sqlite3_column_text(st, 1), (const char *)sqlite3_column_text(st, 2), (const char *)sqlite3_column_text(st, 3)); sqlite3_finalize(st); return 0; } if (strcmp(argv[0], "rm") == 0 && argc > 1) { if (sshkey_rm(c->db, strtol(argv[1], NULL, 10)) != 0) { fprintf(stderr, "git-trace: no such key id\n"); return 1; } cmd_keyfile(c, 1); return 0; } if (strcmp(argv[0], "add") == 0 && argc > 1) { const char *user = argv[1]; const char *label = NULL; char keybuf[2048]; const char *key = NULL; for (int i = 2; i < argc; i++) { if (starts_with(argv[i], "--name=")) label = argv[i] + 7; else if (starts_with(argv[i], "ssh-") || starts_with(argv[i], "ecdsa-")) key = argv[i]; } if (!key) { if (!fgets(keybuf, sizeof keybuf, stdin)) { fprintf(stderr, "git-trace: pass the public key on stdin" " or as an argument\n"); return 1; } keybuf[strcspn(keybuf, "\r\n")] = '\0'; key = keybuf; } int id = sshkey_add(c->db, user, key, label); if (id < 0) { fprintf(stderr, "git-trace: cannot add key " "(bad key? unknown user? duplicate?)\n"); return 1; } printf("key %d added\n", id); cmd_keyfile(c, 1); return 0; } fprintf(stderr, "usage: sshkey add <user> [key|--name=L] | " "list [user] | rm <id>\n"); return 2; }