git / brickware / marrow.git - f64bdd6

(2 months ago)commit b8cdced: README / cleanup

tree / marrow-shell.c

marrow-shell.c

raw

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <errno.h>
#include <stdint.h>

#include "./slice.h"

#define MAX_CMD_LEN 4096
#define GIT_BASE_PATH "/srv/git"

typedef enum {
    CMD_UNKNOWN,
    CMD_GIT_UPLOAD_PACK,
    CMD_GIT_RECEIVE_PACK,
    CMD_GIT_UPLOAD_ARCHIVE
} git_command_t;


static git_command_t
parse_command(const char *cmd) {
    if (0 == strncmp(cmd, "git-upload-pack", 15)) {
        return CMD_GIT_UPLOAD_PACK;
    } else if (0 == strncmp(cmd, "git-receive-pack", 16)) {
        return CMD_GIT_RECEIVE_PACK;
    } else if (0 == strncmp(cmd, "git-upload-archive", 18)) {
        return CMD_GIT_UPLOAD_ARCHIVE;
    }
    return CMD_UNKNOWN;
}

static int32_t
is_read_command(git_command_t cmd) {
    return (CMD_GIT_UPLOAD_PACK == cmd) || (CMD_GIT_UPLOAD_ARCHIVE == cmd);
}

static int32_t
is_write_command(git_command_t cmd) {
    return (CMD_GIT_RECEIVE_PACK == cmd);
}

static struct slice
extract_repo_path(const char *cmd) {
    const char *start = strchr(cmd, '\'');
    struct slice result = slice_empty();

    if (start) {
        start++;
        const char *end = strchr(start, '\'');
        if (end) {
            result.ptr = start;
            result.size = end - start;
        }
    } else {
        const char *space = strchr(cmd, ' ');
        if (space) {
            space++;
            while (' ' == *space) space++;

            const char *end = strchr(space, ' ');
            if (end) {
                result.ptr = space;
                result.size = end - space;
            } else {
                result.ptr = space;
                result.size = strlen(space);
            }
        }
    }

    return (result);
}

static int32_t
contains_relative_components(const struct slice path) {
    size_t i = 0;

    while (i < path.size) {
        /* Check for ".." */
        if (i + 2 <= path.size &&
            '.' == path.ptr[i] &&
            '.' == path.ptr[i + 1] &&
            (i + 2 == path.size || '/' == path.ptr[i + 2])) {
            return 1;
        }

        /* Check for "." */
        if ('.' == path.ptr[i] &&
            (i + 1 == path.size || '/' == path.ptr[i + 1]) &&
            (0 == i || '/' == path.ptr[i - 1])) {
            return 1;
        }

        i++;
    }

    return 0;
}

static int32_t
validate_repo_path(const struct slice repo_path, const struct slice user) {
    /* Reject paths with relative components */
    if (contains_relative_components(repo_path)) {
        return 0;
    }

    /* For anonymous users, any path is OK (already under GIT_BASE_PATH) */
    if (slice_isempty(user)) {
        return 1;
    }

    /* For authenticated users, ensure path starts with username/ */
    const char *path_start = repo_path.ptr;
    size_t remaining_size = repo_path.size;

    /* Skip leading slash if present */
    if (remaining_size > 0 && '/' == *path_start) {
        path_start++;
        remaining_size--;
    }

    /* Check if path starts with username followed by '/' */
    if (remaining_size >= user.size + 1 &&
        0 == strncmp(path_start, user.ptr, user.size) &&
        '/' == path_start[user.size]) {
        return 1;
    }

    return 0;
}

static int32_t
check_permissions(const struct slice *user, const char *cmd) {
    git_command_t git_cmd = parse_command(cmd);

    if (CMD_UNKNOWN == git_cmd) {
        dprintf(STDERR_FILENO, "Error: Unknown command\n");
        return 0;
    }

    struct slice repo_path = extract_repo_path(cmd);
    if (slice_isempty(repo_path)) {
        dprintf(STDERR_FILENO, "Error: Could not extract repository path\n");
        return 0;
    }

    if (slice_isempty(*user)) {  /* anonymous */
        if (is_write_command(git_cmd)) {
            dprintf(STDERR_FILENO, "Error: Anonymous users cannot perform write operations\n");
            return 0;
        }
    } else {
        if (is_write_command(git_cmd)) {
            if (!validate_repo_path(repo_path, *user)) {
                dprintf(STDERR_FILENO, "Error: User '%.*s' can only write to repositories under %s/%.*s/\n",
                        (int32_t)user->size, user->ptr,
                        GIT_BASE_PATH,
                        (int32_t)user->size, user->ptr);
                return 0;
            }
        }
    }

    if (is_read_command(git_cmd)) {
        if (!validate_repo_path(repo_path, slice_empty())) {
            dprintf(STDERR_FILENO, "Error: Repository path must be under %s/\n", GIT_BASE_PATH);
            return 0;
        }
    }

    return 1;
}

static int32_t
create_bare_repository(const char *path) {
    char command[MAX_CMD_LEN];
    char parent_dir[PATH_MAX];

    /* Extract parent directory */
    strncpy(parent_dir, path, sizeof(parent_dir) - 1);
    parent_dir[sizeof(parent_dir) - 1] = '\0';
    char *last_slash = strrchr(parent_dir, '/');
    if (last_slash) {
        *last_slash = '\0';

        /* Create parent directory if it doesn't exist */
        snprintf(command, sizeof(command), "mkdir -p '%s' >/dev/null 2>&1", parent_dir);
        if (0 != system(command)) {
            return 0;
        }
    }

    /* Create bare git repository - redirect output to avoid interfering with git protocol */
    snprintf(command, sizeof(command), "git init --bare '%s' >/dev/null 2>&1", path);
    return (0 == system(command));
}

static void
execute_command(const struct slice *user, const char *cmd) {
    char command[MAX_CMD_LEN];
    git_command_t git_cmd = parse_command(cmd);
    struct slice repo_path = extract_repo_path(cmd);

    if (slice_isempty(repo_path)) {
        dprintf(STDERR_FILENO, "Error: Invalid command format\n");
        exit(1);
    }

    char full_path[PATH_MAX];
    if (repo_path.size >= strlen(GIT_BASE_PATH) &&
        0 == strncmp(repo_path.ptr, GIT_BASE_PATH, strlen(GIT_BASE_PATH))) {
        snprintf(full_path, sizeof(full_path), "%.*s", (int32_t)repo_path.size, repo_path.ptr);
    } else {
        if (repo_path.size > 0 && '/' == repo_path.ptr[0]) {
            snprintf(full_path, sizeof(full_path), "%s%.*s", GIT_BASE_PATH, (int32_t)repo_path.size, repo_path.ptr);
        } else {
            snprintf(full_path, sizeof(full_path), "%s/%.*s", GIT_BASE_PATH, (int32_t)repo_path.size, repo_path.ptr);
        }
    }

    /* Auto-create repository for authenticated write operations */
    if (is_write_command(git_cmd) && !slice_isempty(*user)) {
        struct stat st;
        if (0 != stat(full_path, &st)) {
            if (ENOENT == errno) {
                dprintf(STDERR_FILENO, "the repository does not exist. created.\n");
                if (!create_bare_repository(full_path)) {
                    dprintf(STDERR_FILENO, "Error: Failed to create repository\n");
                    exit(1);
                }
            } else {
                perror("stat");
                exit(1);
            }
        }
    }

    switch (git_cmd) {
        case CMD_GIT_UPLOAD_PACK:
            snprintf(command, sizeof(command), "git-upload-pack '%s'", full_path);
            break;
        case CMD_GIT_RECEIVE_PACK:
            snprintf(command, sizeof(command), "git-receive-pack '%s'", full_path);
            break;
        case CMD_GIT_UPLOAD_ARCHIVE:
            snprintf(command, sizeof(command), "git-upload-archive '%s'", full_path);
            break;
        default:
            dprintf(STDERR_FILENO, "Error: Unknown git command\n");
            exit(1);
    }

    execl("/bin/sh", "sh", "-c", command, (char *)0x00);
    perror("execl");
    exit(1);
}

int32_t
main(int32_t argc, char *argv[]) {
    struct slice user = slice_empty();
    int32_t arg_offset = 0;
    
    /* Check for -u USER flag for authenticated access */
    if (argc > 2 && 0 == strcmp(argv[1], "-u")) {
        user = slice_fromcstr(argv[2]);
        arg_offset = 2;
    }

    /* Check for SSH_ORIGINAL_COMMAND first (SSH forced command) */
    const char *ssh_cmd = getenv("SSH_ORIGINAL_COMMAND");
    const char *cmd = 0x00;
    
    if (ssh_cmd) {
        cmd = ssh_cmd;
    } else if (argc >= (3 + arg_offset) && 0 == strcmp(argv[1 + arg_offset], "-c")) {
        /* Fallback to command line argument */
        cmd = argv[2 + arg_offset];
    } else {
        dprintf(STDERR_FILENO, "Usage: %s", argv[0]);
        dprintf(STDERR_FILENO, " [-u USER] -c <command>\n");
        dprintf(STDERR_FILENO, "This shell is designed to work with git SSH access\n");
        dprintf(STDERR_FILENO, "Usually invoked via SSH with SSH_ORIGINAL_COMMAND set\n");
        dprintf(STDERR_FILENO, "Supported commands:\n");
        dprintf(STDERR_FILENO, "  git-upload-pack <repo>   - Clone/fetch repository\n");
        dprintf(STDERR_FILENO, "  git-receive-pack <repo>  - Push to repository\n");
        dprintf(STDERR_FILENO, "  git-upload-archive <repo> - Archive repository\n");
        return (EXIT_FAILURE);
    }

    if (!check_permissions(&user, cmd)) {
        return 1;
    }
    
    execute_command(&user, cmd);
    
    return 0;
}