(2 months ago)commit 7526fca: fixed bug with pushing to non-existent repository
tree / marrow-shell.c
#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 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 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) {
char full_path[PATH_MAX];
char expected_prefix[PATH_MAX];
/* Reject paths with relative components */
if (contains_relative_components(repo_path)) {
return 0;
}
/* Construct full path: GIT_BASE_PATH/repo_path */
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);
}
/* Build expected prefix based on user */
if (slice_isempty(user)) {
/* Anonymous/read: must be under GIT_BASE_PATH/ */
snprintf(expected_prefix, sizeof(expected_prefix), "%s/", GIT_BASE_PATH);
} else {
/* Authenticated/write: must be under GIT_BASE_PATH/username/ */
snprintf(expected_prefix, sizeof(expected_prefix), "%s/%.*s/", GIT_BASE_PATH, (int32_t)user.size, user.ptr);
}
/* Check if full_path starts with expected_prefix */
return (0 == strncmp(full_path, expected_prefix, strlen(expected_prefix)));
}
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 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) {
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 (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;
}