#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 const char * get_git_user(void) { return getenv("GIT_USER"); } static int32_t is_anonymous(void) { return 0x00 == get_git_user(); } 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 = { .ptr = 0x00, .size = 0, }; 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 char * normalize_path(const char *path) { char *normalized = malloc(PATH_MAX); if (!normalized) { return 0x00; } char temp[PATH_MAX]; strncpy(temp, path, sizeof(temp) - 1); temp[sizeof(temp) - 1] = '\0'; char *components[PATH_MAX / 2]; /* Max possible components */ int32_t comp_count = 0; /* Split path into components */ char *token = strtok(temp, "/"); while (token && comp_count < (PATH_MAX / 2 - 1)) { if (0 == strcmp(token, ".")) { /* Skip current directory */ continue; } else if (0 == strcmp(token, "..")) { /* Go up one directory */ if (comp_count > 0) { comp_count--; } } else { /* Normal component */ components[comp_count++] = token; } token = strtok(0x00, "/"); } /* Rebuild the path */ normalized[0] = '\0'; for (int32_t i = 0; i < comp_count; i++) { strcat(normalized, "/"); strcat(normalized, components[i]); } /* Handle root case */ if (0 == strlen(normalized)) { strcpy(normalized, "/"); } return normalized; } static int32_t validate_repo_path(const struct slice repo_path, const struct slice user) { char real_path[PATH_MAX]; char expected_base[PATH_MAX]; if (repo_path.size >= strlen(GIT_BASE_PATH) && 0 == strncmp(repo_path.ptr, GIT_BASE_PATH, strlen(GIT_BASE_PATH))) { snprintf(real_path, sizeof(real_path), "%.*s", (int32_t)repo_path.size, repo_path.ptr); } else { if (repo_path.size > 0 && '/' == repo_path.ptr[0]) { snprintf(real_path, sizeof(real_path), "%s%.*s", GIT_BASE_PATH, (int32_t)repo_path.size, repo_path.ptr); } else { snprintf(real_path, sizeof(real_path), "%s/%.*s", GIT_BASE_PATH, (int32_t)repo_path.size, repo_path.ptr); } } /* Always normalize the path to resolve .. components */ char *resolved = normalize_path(real_path); if (!resolved) { return 0; } if (slice_isempty(user)) { snprintf(expected_base, sizeof(expected_base), "%s/", GIT_BASE_PATH); } else { snprintf(expected_base, sizeof(expected_base), "%s/%.*s/", GIT_BASE_PATH, (int32_t)user.size, user.ptr); } int32_t valid = (0 == strncmp(resolved, expected_base, strlen(expected_base))) || (0 == strcmp(resolved, GIT_BASE_PATH) && slice_isempty(user)); free(resolved); return valid; } 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'", parent_dir); if (0 != system(command)) { return 0; } } /* Create bare git repository */ snprintf(command, sizeof(command), "git init --bare '%s'", path); return (0 == system(command)); } static void execute_command(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) && !is_anonymous()) { struct stat st; if (0 != stat(full_path, &st)) { if (ENOENT == errno) { 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 (slice_isempty(user)) { dprintf(STDERR_FILENO, "Authenticated as: %.*s\n", (int32_t)user.size, user.ptr); } else { dprintf(STDERR_FILENO, "Anonymous access\n"); } if (!check_permissions(&user, cmd)) { return 1; } execute_command(cmd); return 0; }