#include #include #include #include #include #include #include #include #include #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 NULL == 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 char * extract_repo_path(const char *cmd) { char *path = NULL; const char *start = strchr(cmd, '\''); if (start) { start++; const char *end = strchr(start, '\''); if (end) { size_t len = end - start; path = malloc(len + 1); if (path) { strncpy(path, start, len); path[len] = '\0'; } } } else { const char *space = strchr(cmd, ' '); if (space) { space++; while (' ' == *space) space++; size_t len = strlen(space); path = malloc(len + 1); if (path) { strcpy(path, space); char *end = strchr(path, ' '); if (end) *end = '\0'; } } } return (path); } static char * normalize_path(const char *path) { char *normalized = malloc(PATH_MAX); if (!normalized) { return NULL; } 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(NULL, "/"); } /* 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 char *repo_path, const char *user) { char real_path[PATH_MAX]; char expected_base[PATH_MAX]; if (0 == strncmp(repo_path, GIT_BASE_PATH, strlen(GIT_BASE_PATH))) { snprintf(real_path, sizeof(real_path), "%s", repo_path); } else { if ('/' == repo_path[0]) { snprintf(real_path, sizeof(real_path), "%s%s", GIT_BASE_PATH, repo_path); } else { snprintf(real_path, sizeof(real_path), "%s/%s", GIT_BASE_PATH, repo_path); } } /* Always normalize the path to resolve .. components */ char *resolved = normalize_path(real_path); if (!resolved) { return 0; } if (!user) { snprintf(expected_base, sizeof(expected_base), "%s/", GIT_BASE_PATH); } else { snprintf(expected_base, sizeof(expected_base), "%s/%s/", GIT_BASE_PATH, user); } int32_t valid = (0 == strncmp(resolved, expected_base, strlen(expected_base))) || (0 == strcmp(resolved, GIT_BASE_PATH) && !user); free(resolved); return valid; } static int32_t check_permissions(const char *cmd) { git_command_t git_cmd = parse_command(cmd); if (CMD_UNKNOWN == git_cmd) { fputs("Error: Unknown command\n", stderr); return 0; } const char *user = get_git_user(); char *repo_path = extract_repo_path(cmd); if (!repo_path) { fputs("Error: Could not extract repository path\n", stderr); return 0; } if (is_anonymous()) { if (is_write_command(git_cmd)) { fputs("Error: Anonymous users cannot perform write operations\n", stderr); free(repo_path); return 0; } } else { if (is_write_command(git_cmd)) { if (!validate_repo_path(repo_path, user)) { fputs("Error: User '", stderr); fputs(user, stderr); fputs("' can only write to repositories under ", stderr); fputs(GIT_BASE_PATH, stderr); fputs("/", stderr); fputs(user, stderr); fputs("/\n", stderr); free(repo_path); return 0; } } } if (is_read_command(git_cmd)) { if (!validate_repo_path(repo_path, NULL)) { fputs("Error: Repository path must be under ", stderr); fputs(GIT_BASE_PATH, stderr); fputs("/\n", stderr); free(repo_path); return 0; } } free(repo_path); 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); char *repo_path = extract_repo_path(cmd); if (!repo_path) { fputs("Error: Invalid command format\n", stderr); exit(1); } char full_path[PATH_MAX]; if (0 == strncmp(repo_path, GIT_BASE_PATH, strlen(GIT_BASE_PATH))) { snprintf(full_path, sizeof(full_path), "%s", repo_path); } else { if ('/' == repo_path[0]) { snprintf(full_path, sizeof(full_path), "%s%s", GIT_BASE_PATH, repo_path); } else { snprintf(full_path, sizeof(full_path), "%s/%s", GIT_BASE_PATH, repo_path); } } /* 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)) { fputs("Error: Failed to create repository\n", stderr); free(repo_path); exit(1); } } else { perror("stat"); free(repo_path); 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: fputs("Error: Unknown git command\n", stderr); free(repo_path); exit(1); } free(repo_path); execl("/bin/sh", "sh", "-c", command, NULL); perror("execl"); exit(1); } int32_t main(int32_t argc, char *argv[]) { const char *username = NULL; int32_t arg_offset = 0; /* Check for -u USER flag for authenticated access */ if (argc > 2 && 0 == strcmp(argv[1], "-u")) { username = argv[2]; arg_offset = 2; /* Set GIT_USER environment variable */ setenv("GIT_USER", username, 1); } /* Check for SSH_ORIGINAL_COMMAND first (SSH forced command) */ const char *ssh_cmd = getenv("SSH_ORIGINAL_COMMAND"); const char *cmd = NULL; 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 { fputs("Usage: ", stderr); fputs(argv[0], stderr); fputs(" [-u USER] -c \n", stderr); fputs("This shell is designed to work with git SSH access\n", stderr); fputs("Usually invoked via SSH with SSH_ORIGINAL_COMMAND set\n", stderr); fputs("Supported commands:\n", stderr); fputs(" git-upload-pack - Clone/fetch repository\n", stderr); fputs(" git-receive-pack - Push to repository\n", stderr); fputs(" git-upload-archive - Archive repository\n", stderr); return (EXIT_FAILURE); } const char *user = get_git_user(); if (user) { fputs("Authenticated as: ", stderr); fputs(user, stderr); fputs("\n", stderr); } else { fputs("Anonymous access\n", stderr); } if (!check_permissions(cmd)) { return 1; } execute_command(cmd); return 0; }