summaryrefslogtreecommitdiff
path: root/contrib/hgsh/hgsh.c
blob: 544610781bfcb416edfa4fc407d39428c2302185 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
/*
 * 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;
}