From: R. Steve McKown Date: Sun, 23 Sep 2012 17:44:27 +0000 (-0600) Subject: Initial commit X-Git-Tag: 0.1~6 X-Git-Url: https://oss.titaniummirror.com/gitweb?p=repo_shell.git;a=commitdiff_plain;h=3d9b0d26525b04fb22e608d3679b073d0096d8d6 Initial commit --- 3d9b0d26525b04fb22e608d3679b073d0096d8d6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5dba6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +repo-shell +*.swp diff --git a/repo-shell.c b/repo-shell.c new file mode 100644 index 0000000..6962d35 --- /dev/null +++ b/repo-shell.c @@ -0,0 +1,332 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* TODO: these should come from a config file */ +static char *svn_repo_root = "/var/lib/svn/"; +static char *git_repo_root = "/var/lib/svn/"; +//static char *repo_user = "repo"; +static char *repo_user = "smckown"; + +#define alloc_nr(x) (((x)+16)*3/2) + +/* + * Realloc the buffer pointed at by variable 'x' so that it can hold + * at least 'nr' entries; the number of entries currently allocated + * is 'alloc', using the standard growing factor alloc_nr() macro. + * + * DO NOT USE any expression with side-effect for 'x', 'nr', or 'alloc'. + */ +#define ALLOC_GROW(x, nr, alloc) \ + do { \ + if ((nr) > alloc) { \ + if (alloc_nr(alloc) < (nr)) \ + alloc = (nr); \ + else \ + alloc = alloc_nr(alloc); \ + x = xrealloc((x), alloc * sizeof(*(x))); \ + } \ + } while (0) + +static inline void die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + fprintf(stderr, "error: "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n" ); + va_end(ap); + exit(1); +} + +char *xstrdup(const char *str) +{ + char *ret = strdup(str); + if (!ret) + die("out of memory"); + return ret; +} + +void *xmalloc(size_t size) +{ + void *ret; + + ret = malloc(size); + if (!ret && !size) + ret = malloc(1); + if (!ret) + die("out of memory"); + return ret; +} + +void *xrealloc(void *ptr, size_t size) +{ + void *ret; + + ret = realloc(ptr, size); + if (!ret && !size) + ret = realloc(ptr, 1); + if (!ret) + die("Out of memory, realloc failed"); + return ret; +} + +static uid_t user_uid(char *user) +{ + struct passwd *pw = getpwnam(user); + + if (!pw) + die("invalid user %s", user); + return pw->pw_uid; +} + +static void change_user(char *user) +{ + /* This is the function for which setuid is required, as root */ + setuid(user_uid(user)); +} + +static char *dequote(char *arg) +{ + char* narg = NULL; + + if (arg && *arg == '\'') { + char* end = arg + strlen(arg) - 1; + + if (end != arg && *end == '\'') { + narg = arg + 1; + *end = '\0'; + } + } + return narg; +} + +static char *add_prefix(char *prefix, char* arg) +{ + int size; + + if (arg && prefix && strlen(prefix)) { + char *n = xmalloc(sizeof(char *) * + (strlen(prefix) + strlen(arg) + 2)); + strcpy(n, prefix); + strcat(n, "/"); + strcat(n, arg); + arg = n; + } + return arg; +} + +static int check_ssh_interactive(uid_t uid) +{ + /* TODO: Check the config file for the user owning uid to see if that + * user should be able to execute any commands other than those required + * to support repository access. Return a boolean true/false. + */ + return 1; /* for now */ +} + +static int git_check_access(const char *cmd, const char *arg, const char *user) +{ + /* TODO: Read some configuration file which maps users and access + * to a boolean true/false value. + * + * The git command can support read and write. + * git-receive-pack is ok for readers and writers + * git-upload-pack is ok only for writers + * git-upload-archive is ok only for writers + */ + return 1; /* assume OK for now */ +} + +static int do_git_cmd(const char *cmd, char *arg, char *user) +{ + const char *nargv[4]; + char* narg; + int ret; + + if (!(arg = dequote(arg))) + die("bad argument"); + if (strncmp(cmd, "git-", 4)) + die("bad command"); + + change_user(repo_user); + if (!git_check_access(cmd, arg, user)) + die("permission denied"); + + nargv[0] = cmd; + nargv[1] = arg; + nargv[2] = NULL; + + ret = execvp(nargv[0], (char *const *) nargv); + /* Code unreached if execv successful */ + free(narg); + return ret; +} + +static int do_svnserve_cmd(const char *cmd, char *arg, char *user) +{ + const char *svnserve_argv[7] = { + cmd, "-t", "--root", svn_repo_root, "--tunnel-user", user, NULL + }; + int ret; + + change_user(repo_user); + return execvp(svnserve_argv[0], (char *const *) svnserve_argv); +} + +#define SPLIT_CMDLINE_BAD_ENDING 1 +#define SPLIT_CMDLINE_UNCLOSED_QUOTE 2 +static const char *split_cmdline_errors[] = { + "cmdline ends with \\", + "unclosed quote" +}; + +int split_cmdline(char *cmdline, const char ***argv) +{ + int src, dst, count = 0, size = 16; + char quoted = 0; + + *argv = xmalloc(sizeof(char *) * size); + + /* split alias_string */ + (*argv)[count++] = cmdline; + for (src = dst = 0; cmdline[src];) { + char c = cmdline[src]; + if (!quoted && isspace(c)) { + cmdline[dst++] = 0; + while (cmdline[++src] + && isspace(cmdline[src])) + ; /* skip */ + ALLOC_GROW(*argv, count+1, size); + (*argv)[count++] = cmdline + dst; + } else if (!quoted && (c == '\'' || c == '"')) { + quoted = c; + src++; + } else if (c == quoted) { + quoted = 0; + src++; + } else { + if (c == '\\' && quoted != '\'') { + src++; + c = cmdline[src]; + if (!c) { + free(*argv); + *argv = NULL; + return -SPLIT_CMDLINE_BAD_ENDING; + } + } + cmdline[dst++] = c; + src++; + } + } + + cmdline[dst] = 0; + + if (quoted) { + free(*argv); + *argv = NULL; + return -SPLIT_CMDLINE_UNCLOSED_QUOTE; + } + + ALLOC_GROW(*argv, count+1, size); + (*argv)[count] = NULL; + + return count; +} + +const char *split_cmdline_strerror(int split_cmdline_errno) { + return split_cmdline_errors[-split_cmdline_errno-1]; +} + +static void cd_to_homedir(void) +{ + const char *home = getenv("HOME"); + if (!home) + die("could not determine user's home directory; HOME is unset"); + if (chdir(home) == -1) + die("could not chdir to user's home directory"); +} + +static struct commands { + const char *name; + int (*exec)(const char *cmd, char *arg, char *user); +} cmd_list[] = { + { "git-receive-pack", do_git_cmd }, + { "git-upload-pack", do_git_cmd }, + { "git-upload-archive", do_git_cmd }, + { "svnserve", do_svnserve_cmd }, + { NULL }, +}; + +int main(int argc, char **argv) +{ + char *prog; + const char **user_argv; + struct commands *cmd; + int devnull_fd; + int count; + + /* + * Always open file descriptors 0/1/2 to avoid clobbering files + * in die(). It also avoids not messing up when the pipes are + * dup'ed onto stdin/stdout/stderr in the child processes we spawn. + */ + devnull_fd = open("/dev/null", O_RDWR); + while (devnull_fd >= 0 && devnull_fd <= 2) + devnull_fd = dup(devnull_fd); + if (devnull_fd == -1) + die("opening /dev/null failed"); + close (devnull_fd); + + if (argc < 3) + die("invalid arguments"); + fprintf(stderr, "prog |%s|\n", argv[2]); + prog = xstrdup(argv[2]); + if (!strncmp(prog, "git", 3) && isspace(prog[3])) + /* Accept "git foo" as if the caller said "git-foo". */ + prog[3] = '-'; + + for (cmd = cmd_list ; cmd->name ; cmd++) { + int len = strlen(cmd->name); + char *arg; + struct passwd *pw; + if (strncmp(cmd->name, prog, len)) + continue; + arg = NULL; + switch (prog[len]) { + case '\0': + arg = NULL; + break; + case ' ': + arg = prog + len + 1; + break; + default: + continue; + } + + pw = getpwuid(getuid()); + exit(cmd->exec(cmd->name, arg, pw->pw_name)); + } + + if (!check_ssh_interactive(getuid())) + die("only repository access is allowed"); + + cd_to_homedir(); + count = split_cmdline(prog, &user_argv); + if (count >= 0) { + execv(user_argv[0], (char *const *) user_argv); + free(user_argv); + die("unrecognized command '%s'", argv[2]); + } else { + free(prog); + die("invalid command format '%s': %s", argv[2], + split_cmdline_strerror(count)); + } +}