#include #include #include #include #include #include #include #include #include #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); if (0 != system(command)) { return 0; } /* Install post-receive hook */ char hook_path[PATH_MAX]; snprintf(hook_path, sizeof(hook_path), "%s/hooks/post-receive", path); FILE *hook_file = fopen(hook_path, "w"); if (!hook_file) { return 0; } fprintf(hook_file, "#!/bin/sh\n" "REPO_PATH=\"$(pwd)\"\n" "GIT_BASE_PATH=\"/srv/git\"\n" "OUTPUT_DIR=\"/var/www/git\"\n" "\n" "REPO_NAME=\"${REPO_PATH#$GIT_BASE_PATH/}\"\n" "\n" "/usr/local/bin/marrow-static -D \"$GIT_BASE_PATH\" \"$OUTPUT_DIR\" \"$REPO_NAME\"\n"); fclose(hook_file); /* Make hook executable */ snprintf(command, sizeof(command), "chmod +x '%s' >/dev/null 2>&1", hook_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 \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 - Clone/fetch repository\n"); dprintf(STDERR_FILENO, " git-receive-pack - Push to repository\n"); dprintf(STDERR_FILENO, " git-upload-archive - Archive repository\n"); return (EXIT_FAILURE); } if (!check_permissions(&user, cmd)) { return 1; } execute_command(&user, cmd); return 0; }