view contrib/hgsh/hgsh.c @ 27142:060f83d219b9

extensions: refuse to load extensions if minimum hg version not met As the author of several 3rd party extensions, I frequently see bug reports from users attempting to run my extension with an old version of Mercurial that I no longer support in my extension. Oftentimes, the extension will import just fine. But as soon as we run extsetup(), reposetup(), or get into the guts of a wrapped function, we encounter an exception and abort. Today, Mercurial will print a message about extensions that don't have a "testedwith" declaring explicit compatibility with the current version. The existing mechanism is a good start. But it isn't as robust as I would like. Specifically, Mercurial assumes compatibility by default. This means extension authors must perform compatibility checking in their extsetup() or we wait and see if we encounter an abort at runtime. And, compatibility checking can involve a lot of code and lots of error checking. It's a lot of effort for extension authors. Oftentimes, extension authors know which versions of Mercurial there extension works on and more importantly where it is broken. This patch introduces a magic "minimumhgversion" attribute in extensions. When found, the extension loading mechanism will compare the declared version against the current Mercurial version. If the extension explicitly states we require a newer Mercurial version, a warning is printed and the extension isn't loaded beyond importing the Python module. This causes a graceful failure while alerting the user of the compatibility issue. I would be receptive to the idea of making the failure more fatal. However, care would need to be taken to not criple every hg command. e.g. the user may use `hg config` to fix the hgrc and if we aborted trying to run that, the user would effectively be locked out of `hg`! A potential future improvement to this functionality would be to catch ImportError for the extension/module and parse the source code for "minimumhgversion = 'XXX'" and do similar checking. This way we could give more information about why the extension failed to load.
author Gregory Szorc <gregory.szorc@gmail.com>
date Tue, 24 Nov 2015 15:16:25 -0800
parents a4e0908ce35b
children 580f7b1b88c7
line wrap: on
line source

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