(6 hours ago)commit f79468c: added hook install
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 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 <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;
}