/* * 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); } enum cmdline { hg_init, hg_serve, }; /* * attempt to verify that a directory is really a hg repo, by testing * for the existence of a subdirectory. */ static int validate_repo(const char *repo_root, const char *subdir) { char *abs_path; struct stat st; int ret; if (asprintf(&abs_path, "%s.hg/%s", repo_root, subdir) == -1) { ret = -1; goto bail; } /* verify that we really are looking at valid repo. */ if (stat(abs_path, &st) == -1) { ret = 0; } else { ret = 1; } bail: return ret; } /* * paranoid wrapper, runs hg executable in server mode. */ static void serve_data(int argc, char **argv) { char *hg_root = HG_ROOT; char *repo, *repo_root; enum cmdline cmd; char *nargv[6]; 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 init %as", &repo) == 1) { cmd = hg_init; } else if (sscanf(argv[2], "hg -R %as serve --stdio", &repo) == 1) { cmd = hg_serve; } else { goto badargs; } repolen = repo ? strlen(repo) : 0; if (repolen == 0) { goto badargs; } if (hg_root) { if (asprintf(&repo_root, "%s/%s/", 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(repo_root, "/../") != NULL) { goto badargs; } /* only hg init expects no repo. */ if (cmd != hg_init) { int valid; valid = validate_repo(repo_root, "data"); if (valid == -1) { goto badargs; } if (valid == 0) { valid = validate_repo(repo_root, "store"); if (valid == -1) { goto badargs; } } if (valid == 0) { perror(repo); exit(EX_DATAERR); } } if (chdir(hg_root) == -1) { perror(hg_root); exit(EX_SOFTWARE); } } i = 0; switch (cmd) { case hg_serve: nargv[i++] = HG; nargv[i++] = "-R"; nargv[i++] = repo; nargv[i++] = "serve"; nargv[i++] = "--stdio"; break; case hg_init: nargv[i++] = HG; nargv[i++] = "init"; nargv[i++] = repo; break; } 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; }