hgsh.c
372 lines
| 8.4 KiB
| text/x-c
|
CLexer
Vadim Gelfer
|
r2341 | /* | ||
* 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; | ||||
} | ||||