git-trace

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