contrib: add restricted shell.
authorVadim Gelfer <vadim.gelfer@gmail.com>
Tue, 23 May 2006 09:33:09 -0700
changeset 2341 dbbe7f72d15a
parent 2338 391c5d0f9ef3
child 2342 c6391adc356a
contrib: add restricted shell.
.hgignore
contrib/hgsh/Makefile
contrib/hgsh/hgsh.c
--- a/.hgignore	Mon May 22 12:17:44 2006 -0400
+++ b/.hgignore	Tue May 23 09:33:09 2006 -0700
@@ -4,6 +4,7 @@
 *.orig
 *.rej
 *~
+*.o
 *.so
 *.pyc
 *.swp
@@ -12,6 +13,7 @@
 tests/annotated
 tests/*.err
 build
+contrib/hgsh/hgsh
 dist
 doc/*.[0-9]
 doc/*.[0-9].gendoc.txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/hgsh/Makefile	Tue May 23 09:33:09 2006 -0700
@@ -0,0 +1,13 @@
+CC := gcc
+CFLAGS := -g -O2 -Wall -Werror
+
+prefix ?= /usr/bin
+
+hgsh: hgsh.o
+	$(CC) -o $@ $<
+
+install: hgsh
+	install -m755 hgsh $(prefix)
+
+clean:
+	rm -f *.o hgsh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/hgsh/hgsh.c	Tue May 23 09:33:09 2006 -0700
@@ -0,0 +1,372 @@
+/*
+ * hgsh.c - restricted login shell for mercurial
+ *
+ * Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
+ *
+ * This software may be used and distributed according to the terms of the
+ * GNU General Public License, incorporated herein by reference.
+ *
+ * this program is login shell for dedicated mercurial user account. it
+ * only allows few actions:
+ *
+ * 1. run hg in server mode on specific repository. no other hg commands
+ * are allowed. we try to verify that repo to be accessed exists under
+ * given top-level directory.
+ *
+ * 2. (optional) forward ssh connection from firewall/gateway machine to
+ * "real" mercurial host, to let users outside intranet pull and push
+ * changes through firewall.
+ *
+ * 3. (optional) run normal shell, to allow to "su" to mercurial user, use
+ * "sudo" to run programs as that user, or run cron jobs as that user.
+ *
+ * only tested on linux yet. patches for non-linux systems welcome.
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* for asprintf */
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+/*
+ * user config.
+ *
+ * if you see a hostname below, just use first part of hostname. example,
+ * if you have host named foo.bar.com, use "foo".
+ */
+
+/*
+ * HG_GATEWAY: hostname of gateway/firewall machine that people outside your
+ * intranet ssh into if they need to ssh to other machines. if you do not
+ * have such machine, set to NULL.
+ */
+#ifndef HG_GATEWAY
+#define HG_GATEWAY     "gateway"
+#endif
+
+/*
+ * HG_HOST: hostname of mercurial server. if any machine is allowed, set to
+ * NULL.
+ */
+#ifndef HG_HOST
+#define HG_HOST         "mercurial"
+#endif
+
+/*
+ * HG_USER: username to log in from HG_GATEWAY to HG_HOST. if gateway and
+ * host username are same, set to NULL.
+ */
+#ifndef HG_USER
+#define HG_USER         "hg"
+#endif
+
+/*
+ * HG_ROOT: root of tree full of mercurial repos. if you do not want to
+ * validate location of repo when someone is try to access, set to NULL.
+ */
+#ifndef HG_ROOT
+#define HG_ROOT         "/home/hg/repos"
+#endif
+
+/*
+ * HG: path to the mercurial executable to run.
+ */
+#ifndef HG
+#define HG              "/home/hg/bin/hg"
+#endif
+
+/*
+ * HG_SHELL: shell to use for actions like "sudo" and "su" access to
+ * mercurial user, and cron jobs. if you want to make these things
+ * impossible, set to NULL.
+ */
+#ifndef HG_SHELL
+#define HG_SHELL        NULL
+// #define HG_SHELL        "/bin/bash"
+#endif
+
+/*
+ * HG_HELP: some way for users to get support if they have problem. if they
+ * should not get helpful message, set to NULL.
+ */
+#ifndef HG_HELP
+#define HG_HELP         "please contact support@example.com for help."
+#endif
+
+/*
+ * SSH: path to ssh executable to run, if forwarding from HG_GATEWAY to
+ * HG_HOST. if you want to use rsh instead (why?), you need to modify
+ * arguments it is called with. see forward_through_gateway.
+ */
+#ifndef SSH
+#define SSH             "/usr/bin/ssh"
+#endif
+
+/*
+ * tell whether to print command that is to be executed. useful for
+ * debugging. should not interfere with mercurial operation, since
+ * mercurial only cares about stdin and stdout, and this prints to stderr.
+ */
+static const int debug = 0;
+
+static void print_cmdline(int argc, char **argv)
+{
+    FILE *fp = stderr;
+    int i;
+
+    fputs("command: ", fp);
+
+    for (i = 0; i < argc; i++) {
+        char *spc = strpbrk(argv[i], " \t\r\n");
+        if (spc) {
+            fputc('\'', fp);
+        }
+        fputs(argv[i], fp);
+        if (spc) {
+            fputc('\'', fp);
+        }
+        if (i < argc - 1) {
+            fputc(' ', fp);
+        }
+    }
+    fputc('\n', fp);
+    fflush(fp);
+}
+
+static void usage(const char *reason, int exitcode)
+{
+    char *hg_help = HG_HELP;
+
+    if (reason) {
+        fprintf(stderr, "*** Error: %s.\n", reason);
+    }
+    fprintf(stderr, "*** This program has been invoked incorrectly.\n");
+    if (hg_help) {
+        fprintf(stderr, "*** %s\n", hg_help);
+    }
+    exit(exitcode ? exitcode : EX_USAGE);
+}
+
+/*
+ * run on gateway host to make another ssh connection, to "real" mercurial
+ * server. it sends its command line unmodified to far end.
+ *
+ * never called if HG_GATEWAY is NULL.
+ */
+static void forward_through_gateway(int argc, char **argv)
+{
+    char *ssh = SSH;
+    char *hg_host = HG_HOST;
+    char *hg_user = HG_USER;
+    char **nargv = alloca((10 + argc) * sizeof(char *));
+    int i = 0, j;
+
+    nargv[i++] = ssh;
+    nargv[i++] = "-q";
+    nargv[i++] = "-T";
+    nargv[i++] = "-x";
+    if (hg_user) {
+        nargv[i++] = "-l";
+        nargv[i++] = hg_user;
+    }
+    nargv[i++] = hg_host;
+
+    /*
+     * sshd called us with added "-c", because it thinks we are a shell.
+     * drop it if we find it.
+     */
+    j = 1;
+    if (j < argc && strcmp(argv[j], "-c") == 0) {
+        j++;
+    }
+
+    for (; j < argc; i++, j++) {
+        nargv[i] = argv[j];
+    }
+    nargv[i] = NULL;
+
+    if (debug) {
+        print_cmdline(i, nargv);
+    }
+
+    execv(ssh, nargv);
+    perror(ssh);
+    exit(EX_UNAVAILABLE);
+}
+
+/*
+ * run shell. let administrator "su" to mercurial user's account to do
+ * administrative works.
+ *
+ * never called if HG_SHELL is NULL.
+ */
+static void run_shell(int argc, char **argv)
+{
+    char *hg_shell = HG_SHELL;
+    char **nargv;
+    char *c;
+    int i;
+
+    nargv = alloca((argc + 3) * sizeof(char *));
+    c = strrchr(hg_shell, '/');
+
+    /* tell "real" shell it is login shell, if needed. */
+
+    if (argv[0][0] == '-' && c) {
+        nargv[0] = strdup(c);
+        if (nargv[0] == NULL) {
+            perror("malloc");
+            exit(EX_OSERR);
+        }
+        nargv[0][0] = '-';
+    } else {
+        nargv[0] = hg_shell;
+    }
+
+    for (i = 1; i < argc; i++) {
+        nargv[i] = argv[i];
+    }
+    nargv[i] = NULL;
+
+    if (debug) {
+        print_cmdline(i, nargv);
+    }
+
+    execv(hg_shell, nargv);
+    perror(hg_shell);
+    exit(EX_OSFILE);
+}
+
+/*
+ * paranoid wrapper, runs hg executable in server mode.
+ */
+static void serve_data(int argc, char **argv)
+{
+    char *hg_root = HG_ROOT;
+    char *repo, *abspath;
+    char *nargv[6];
+    struct stat st;
+    size_t repolen;
+    int i;
+
+    /*
+     * check argv for looking okay. we should be invoked with argv
+     * resembling like this:
+     *
+     *   hgsh
+     *   -c
+     *   hg -R some/path serve --stdio
+     *
+     * the "-c" is added by sshd, because it thinks we are login shell.
+     */
+
+    if (argc != 3) {
+        goto badargs;
+    }
+
+    if (strcmp(argv[1], "-c") != 0) {
+        goto badargs;
+    }
+
+    if (sscanf(argv[2], "hg -R %as serve --stdio", &repo) != 1) {
+        goto badargs;
+    }
+
+    repolen = repo ? strlen(repo) : 0;
+
+    if (repolen == 0) {
+        goto badargs;
+    }
+
+    if (hg_root) {
+        if (asprintf(&abspath, "%s/%s/.hg/data", hg_root, repo) == -1) {
+            goto badargs;
+        }
+
+        /*
+         * attempt to stop break out from inside the repository tree. could
+         * do something more clever here, because e.g. we could traverse a
+         * symlink that looks safe, but really breaks us out of tree.
+         */
+
+        if (strstr(abspath, "/../") != NULL) {
+            goto badargs;
+        }
+
+        /* verify that we really are looking at valid repo. */
+
+        if (stat(abspath, &st) == -1) {
+            perror(repo);
+            exit(EX_DATAERR);
+        }
+
+        if (chdir(hg_root) == -1) {
+            perror(hg_root);
+            exit(EX_SOFTWARE);
+        }
+    }
+
+    i = 0;
+    nargv[i++] = HG;
+    nargv[i++] = "-R";
+    nargv[i++] = repo;
+    nargv[i++] = "serve";
+    nargv[i++] = "--stdio";
+    nargv[i] = NULL;
+
+    if (debug) {
+        print_cmdline(i, nargv);
+    }
+
+    execv(HG, nargv);
+    perror(HG);
+    exit(EX_UNAVAILABLE);
+
+badargs:
+    /* print useless error message. */
+
+    usage("invalid arguments", EX_DATAERR);
+}
+
+int main(int argc, char **argv)
+{
+    char host[1024];
+    char *c;
+
+    if (gethostname(host, sizeof(host)) == -1) {
+        perror("gethostname");
+        exit(EX_OSERR);
+    }
+
+    if ((c = strchr(host, '.')) != NULL) {
+        *c = '\0';
+    }
+
+    if (getenv("SSH_CLIENT")) {
+        char *hg_gateway = HG_GATEWAY;
+        char *hg_host = HG_HOST;
+
+        if (hg_gateway && strcmp(host, hg_gateway) == 0) {
+            forward_through_gateway(argc, argv);
+        }
+
+        if (hg_host && strcmp(host, hg_host) != 0) {
+            usage("invoked on unexpected host", EX_USAGE);
+        }
+
+        serve_data(argc, argv);
+    } else if (HG_SHELL) {
+        run_shell(argc, argv);
+    } else {
+        usage("invalid arguments", EX_DATAERR);
+    }
+
+    return 0;
+}