# HG changeset patch # User Vadim Gelfer # Date 2006-05-23 16:33:09 # Node ID dbbe7f72d15a752a235d49e00f29949c4c1d7c41 # Parent 391c5d0f9ef3e0768295de492a588ef997223fa3 contrib: add restricted shell. diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -4,6 +4,7 @@ syntax: glob *.orig *.rej *~ +*.o *.so *.pyc *.swp @@ -12,6 +13,7 @@ tests/.coverage* tests/annotated tests/*.err build +contrib/hgsh/hgsh dist doc/*.[0-9] doc/*.[0-9].gendoc.txt diff --git a/contrib/hgsh/Makefile b/contrib/hgsh/Makefile new file mode 100644 --- /dev/null +++ b/contrib/hgsh/Makefile @@ -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 diff --git a/contrib/hgsh/hgsh.c b/contrib/hgsh/hgsh.c new file mode 100644 --- /dev/null +++ b/contrib/hgsh/hgsh.c @@ -0,0 +1,372 @@ +/* + * hgsh.c - restricted login shell for mercurial + * + * Copyright 2006 Vadim Gelfer + * + * 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 +#include +#include +#include +#include +#include +#include + +/* + * 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; +}