--- /dev/null
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <string.h>
+
+/* 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));
+ }
+}