/*
 * 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);
}

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;
}
