/* request-key.c: hand a key request off to the appropriate process
 *
 * Copyright (C) 2005 Red Hat, Inc. All Rights Reserved.
 * Written by David Howells (dhowells@redhat.com)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 *
 * /sbin/request-key <op> <key> <uid> <gid> <threadring> <processring> <sessionring> [<info>]
 *
 * Searches the specified session ring for a key indicating the command to run:
 *	type:	"user"
 *	desc:	"request-key:<op>"
 *	data:	command name, eg: "/home/dhowells/request-key-create.sh"
 */

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <syslog.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <ctype.h>
#include <sys/select.h>
#include <sys/wait.h>
#include "keyutils.h"


static int xdebug;
static int xnolog;
static char *xkey;
static char *xuid;
static char *xgid;
static char *xthread_keyring;
static char *xprocess_keyring;
static char *xsession_keyring;
static int confline;
static int norecurse;

static void lookup_action(char *op,
			  key_serial_t key,
			  char *ktype,
			  char *kdesc,
			  char *callout_info)
	__attribute__((noreturn));

static void execute_program(char *op,
			    key_serial_t key,
			    char *ktype,
			    char *kdesc,
			    char *callout_info,
			    char *cmdline)
	__attribute__((noreturn));

static void pipe_to_program(char *op,
			    key_serial_t key,
			    char *ktype,
			    char *kdesc,
			    char *callout_info,
			    char *prog,
			    char **argv)
	__attribute__((noreturn));

static int match(const char *pattern, int plen, const char *datum, int dlen);

static void debug(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static void debug(const char *fmt, ...)
{
	va_list va;

	if (xdebug) {
		va_start(va, fmt);
		vfprintf(stderr, fmt, va);
		va_end(va);

		if (!xnolog) {
			openlog("request-key", 0, LOG_AUTHPRIV);

			va_start(va, fmt);
			vsyslog(LOG_DEBUG, fmt, va);
			va_end(va);

			closelog();
		}
	}
}

static void error(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2)));
static void error(const char *fmt, ...)
{
	va_list va;

	if (xdebug) {
		va_start(va, fmt);
		vfprintf(stderr, fmt, va);
		va_end(va);
	}

	if (!xnolog) {
		openlog("request-key", 0, LOG_AUTHPRIV);

		va_start(va, fmt);
		vsyslog(LOG_ERR, fmt, va);
		va_end(va);

		closelog();
	}

	exit(1);
}

static void oops(int x)
{
	error("Died on signal %d", x);
}

/*****************************************************************************/
/*
 *
 */
int main(int argc, char *argv[])
{
	key_serial_t key;
	char *ktype, *kdesc, *buf, *callout_info;
	int ret, ntype, dpos, n, fd;

	signal(SIGSEGV, oops);
	signal(SIGBUS, oops);
	signal(SIGPIPE, SIG_IGN);

	for (;;) {
		if (argc > 1 && strcmp(argv[1], "-d") == 0) {
			xdebug++;
			argv++;
			argc--;
		}
		else if (argc > 1 && strcmp(argv[1], "-n") == 0) {
			xnolog = 1;
			argv++;
			argc--;
		}
		else
			break;
	}

	if (argc != 8 && argc != 9)
		error("Unexpected argument count: %d\n", argc);

	fd = open("/dev/null", O_RDWR);
	if (fd < 0)
		error("open");
	if (fd > 2) {
		close(fd);
	}
	else if (fd < 2) {
		ret = dup(fd);
		if (ret < 0)
			error("dup failed: %m\n");

		if (ret < 2 && dup(fd) < 0)
			error("dup failed: %m\n");
	}

	xkey = argv[2];
	xuid = argv[3];
	xgid = argv[4];
	xthread_keyring = argv[5];
	xprocess_keyring = argv[6];
	xsession_keyring = argv[7];

	key = atoi(xkey);

	/* assume authority over the key
	 * - older kernel doesn't support this function
	 */
	ret = keyctl_assume_authority(key);
	if (ret < 0 && !(argc == 9 || errno == EOPNOTSUPP))
		error("Failed to assume authority over key %d (%m)\n", key);

	/* ask the kernel to describe the key to us */
	if (xdebug < 2) {
		ret = keyctl_describe_alloc(key, &buf);
		if (ret < 0)
			goto inaccessible;
	}
	else {
		buf = strdup("user;0;0;1f0000;debug:1234");
	}

	/* extract the type and description from the key */
	debug("Key descriptor: \"%s\"\n", buf);
	ntype = -1;
	dpos = -1;

	n = sscanf(buf, "%*[^;]%n;%*d;%*d;%x;%n", &ntype, &n, &dpos);
	if (n != 1)
		error("Failed to parse key description\n");

	ktype = buf;
	ktype[ntype] = 0;
	kdesc = buf + dpos;

	debug("Key type: %s\n", ktype);
	debug("Key desc: %s\n", kdesc);

	/* get hold of the callout info */
	callout_info = argv[8];

	if (!callout_info) {
		void *tmp;

		if (keyctl_read_alloc(KEY_SPEC_REQKEY_AUTH_KEY, &tmp) < 0)
			error("Failed to retrieve callout info (%m)\n");

		callout_info = tmp;
	}

	debug("CALLOUT: '%s'\n", callout_info);

	/* determine the action to perform */
	lookup_action(argv[1],		/* op */
		      key,		/* ID of key under construction */
		      ktype,		/* key type */
		      kdesc,		/* key description */
		      callout_info	/* call out information */
		      );

inaccessible:
	error("Key %d is inaccessible (%m)\n", key);

} /* end main() */

/*****************************************************************************/
/*
 * determine the action to perform
 */
static void lookup_action(char *op,
			  key_serial_t key,
			  char *ktype,
			  char *kdesc,
			  char *callout_info)
{
	char buf[4096 + 2], *p, *q;
	FILE *conf;
	int len, oplen, ktlen, kdlen, cilen;

	oplen = strlen(op);
	ktlen = strlen(ktype);
	kdlen = strlen(kdesc);
	cilen = strlen(callout_info);

	/* search the config file for a command to run */
	conf = fopen(xdebug < 2 ? "/etc/request-key.conf" : "request-key.conf", "r");
	if (!conf)
		error("Cannot open /etc/request-key.conf: %m\n");

	for (confline = 1;; confline++) {
		/* read the file line-by-line */
		if (!fgets(buf, sizeof(buf), conf)) {
			if (feof(conf))
				error("Cannot find command to construct key %d\n", key);
			error("Error reading /etc/request-key.conf\n");
		}

		len = strlen(buf);
		if (len >= sizeof(buf) - 2)
			error("/etc/request-key.conf:%d: Line too long\n", confline);

		/* ignore blank lines and comments */
		if (len == 1 || buf[0] == '#' || isspace(buf[0]))
			continue;

		buf[--len] = 0;
		p = buf;

		/* attempt to match the op */
		q = p;
		while (*p && !isspace(*p)) p++;
		if (!*p)
			goto syntax_error;
		*p = 0;

		if (!match(q, p - q, op, oplen))
			continue;

		p++;

		/* attempt to match the type */
		while (isspace(*p)) p++;
		if (!*p)
			goto syntax_error;

		q = p;
		while (*p && !isspace(*p)) p++;
		if (!*p)
			goto syntax_error;
		*p = 0;

		if (!match(q, p - q, ktype, ktlen))
			continue;

		p++;

		/* attempt to match the description */
		while (isspace(*p)) p++;
		if (!*p)
			goto syntax_error;

		q = p;
		while (*p && !isspace(*p)) p++;
		if (!*p)
			goto syntax_error;
		*p = 0;

		if (!match(q, p - q, kdesc, kdlen))
			continue;

		p++;

		/* attempt to match the callout info */
		while (isspace(*p)) p++;
		if (!*p)
			goto syntax_error;

		q = p;
		while (*p && !isspace(*p)) p++;
		if (!*p)
			goto syntax_error;
		*p = 0;

		if (!match(q, p - q, callout_info, cilen))
			continue;

		p++;

		debug("Line %d matches\n", confline);

		/* we've got an action */
		while (isspace(*p)) p++;
		if (!*p)
			goto syntax_error;

		fclose(conf);

		execute_program(op, key, ktype, kdesc, callout_info, p);
	}

	error("/etc/request-key.conf: No matching action\n");

syntax_error:
	error("/etc/request-key.conf:%d: Syntax error\n", confline);

} /* end lookup_action() */

/*****************************************************************************/
/*
 * attempt to match a datum to a pattern
 * - one asterisk is allowed anywhere in the pattern to indicate a wildcard
 * - returns true if matched, false if not
 */
static int match(const char *pattern, int plen, const char *datum, int dlen)
{
	const char *asterisk;
	int n;

	debug("match(%*.*s,%*.*s)\n", plen, plen, pattern, dlen, dlen, datum);

	asterisk = memchr(pattern, '*', plen);
	if (!asterisk) {
		/* exact match only if no wildcard */
		if (plen == dlen && memcmp(pattern, datum, dlen) == 0)
			goto yes;
		goto no;
	}

	/* the datum mustn't be shorter than the pattern without the asterisk */
	if (dlen < plen - 1)
		goto no;

	n = asterisk - pattern;
	if (n == 0) {
		/* wildcard at beginning of pattern */
		pattern++;
		if (!*pattern)
			goto yes; /* "*" matches everything */

		/* match the end of the datum */
		plen--;
		if (memcmp(pattern, datum + (dlen - plen), plen) == 0)
			goto yes;
		goto no;
	}

	/* need to match beginning of datum for "abc*" and "abc*def" */
	if (memcmp(pattern, datum, n) != 0)
		goto no;

	if (!asterisk[1])
		goto yes; /* "abc*" matches */

	/* match the end of the datum */
	asterisk++;
	n = plen - n - 1;
	if (memcmp(pattern, datum + (dlen - n), n) == 0)
		goto yes;

no:
	debug(" = no\n");
	return 0;

yes:
	debug(" = yes\n");
	return 1;

} /* end match() */

/*****************************************************************************/
/*
 * execute a program to deal with a key
 */
static void execute_program(char *op,
			    key_serial_t key,
			    char *ktype,
			    char *kdesc,
			    char *callout_info,
			    char *cmdline)
{
	char *argv[256];
	char *prog, *p, *q;
	int argc, pipeit;

	debug("execute_program('%s','%s')\n", callout_info, cmdline);

	/* if the commandline begins with a bar, then we pipe the callout data into it and read
	 * back the payload data
	 */
	pipeit = 0;

	if (cmdline[0] == '|') {
		pipeit = 1;
		cmdline++;
	}

	/* extract the path to the program to run */
	prog = p = cmdline;
	while (*p && !isspace(*p)) p++;
//	if (!*p)
//		error("/etc/request-key.conf:%d: No command path\n", confline);
//	*p++ = 0;
	if (*p)
		*p++ = 0;

	argv[0] = strrchr(prog, '/') + 1;

	/* extract the arguments */
	for (argc = 1; p; argc++) {
		while (isspace(*p)) p++;
		if (!*p)
			break;

		if (argc >= 254)
			error("/etc/request-key.conf:%d: Too many arguments\n", confline);
		argv[argc] = q = p;

		while (*p && !isspace(*p)) p++;

		if (*p)
			*p++ = 0;
		else
			p = NULL;

		debug("argv[%d]: '%s'\n", argc, argv[argc]);

		if (*q != '%')
			continue;

		/* it's a macro */
		q++;
		if (!*q)
			error("/etc/request-key.conf:%d: Missing macro name\n", confline);

		if (*q == '%') {
			/* it's actually an anti-macro escape "%%..." -> "%..." */
			argv[argc]++;
			continue;
		}

		/* single character macros */
		if (!q[1]) {
			switch (*q) {
			case 'o': argv[argc] = op;			continue;
			case 'k': argv[argc] = xkey;			continue;
			case 't': argv[argc] = ktype;			continue;
			case 'd': argv[argc] = kdesc;			continue;
			case 'c': argv[argc] = callout_info;		continue;
			case 'u': argv[argc] = xuid;			continue;
			case 'g': argv[argc] = xgid;			continue;
			case 'T': argv[argc] = xthread_keyring;		continue;
			case 'P': argv[argc] = xprocess_keyring;	continue;
			case 'S': argv[argc] = xsession_keyring;	continue;
			default:
				error("/etc/request-key.conf:%d: Unsupported macro\n", confline);
			}
		}

		/* keysub macro */
		if (*q == '{') {
			key_serial_t keysub;
			void *tmp;
			char *ksdesc, *end, *subdata;
			int ret, loop;

			/* extract type and description */
			q++;
			ksdesc = strchr(q, ':');
			if (!ksdesc)
				error("/etc/request-key.conf:%d: Keysub macro lacks ':'\n",
				      confline);
			*ksdesc++ = 0;
			end = strchr(ksdesc, '}');
			if (!end)
				error("/etc/request-key.conf:%d: Unterminated keysub macro\n",
				      confline);

			*end++ = 0;
			if (*end)
				error("/etc/request-key.conf:%d:"
				      " Keysub macro has trailing rubbish\n",
				      confline);

			debug("Keysub: %s key \"%s\"\n", q, ksdesc);

			if (!q[0])
				error("/etc/request-key.conf:%d: Keysub type empty\n", confline);

			if (!ksdesc[0])
				error("/etc/request-key.conf:%d: Keysub description empty\n",
				      confline);

			/* look up the key in the requestor's keyrings, but fail immediately if the
			 * key is not found rather than invoking /sbin/request-key again
			 */
			keysub = request_key(q, ksdesc, NULL, 0);
			if (keysub < 0)
				error("/etc/request-key.conf:%d:"
				      " Keysub key not found: %m\n",
				      confline);

			ret = keyctl_read_alloc(keysub, &tmp);
			if (ret < 0)
				error("/etc/request-key.conf:%d:"
				      " Can't read keysub %d data: %m\n",
				      confline, keysub);
			subdata = tmp;

			for (loop = 0; loop < ret; loop++)
				if (!isprint(subdata[loop]))
					error("/etc/request-key.conf:%d:"
					      " keysub %d data not printable ('%02hhx')\n",
					      confline, keysub, subdata[loop]);

			argv[argc] = subdata;
			continue;
		}
	}

	if (argc == 0)
		error("/etc/request-key.conf:%d: No arguments\n", confline);

	argv[argc] = NULL;

	if (xdebug) {
		char **ap;

		debug("%s %s\n", pipeit ? "PipeThru" : "Run", prog);
		for (ap = argv; *ap; ap++)
			debug("- argv[%zd] = \"%s\"\n", ap - argv, *ap);
	}

	/* become the same UID/GID as the key requesting process */
	//setgid(atoi(xuid));
	//setuid(atoi(xgid));

	/* if the last argument is a single bar, we spawn off the program dangling on the end of
	 * three pipes and read the key material from the program, otherwise we just exec
	 */
	if (pipeit)
		pipe_to_program(op, key, ktype, kdesc, callout_info, prog, argv);

	/* attempt to execute the command */
	execv(prog, argv);

	error("/etc/request-key.conf:%d: Failed to execute '%s': %m\n", confline, prog);

} /* end execute_program() */

/*****************************************************************************/
/*
 * pipe the callout information to the specified program and retrieve the payload data over another
 * pipe
 */
static void pipe_to_program(char *op,
			    key_serial_t key,
			    char *ktype,
			    char *kdesc,
			    char *callout_info,
			    char *prog,
			    char **argv)
{
	char errbuf[512], payload[32768 + 1], *pp, *pc, *pe;
	int ipi[2], opi[2], epi[2], childpid;
	int ifl, ofl, efl, npay, ninfo, espace, tmp;

	debug("pipe_to_program(%s -> %s)", callout_info, prog);

	if (pipe(ipi) < 0 || pipe(opi) < 0 || pipe(epi) < 0)
		error("pipe failed: %m");

	childpid = fork();
	if (childpid == -1)
		error("fork failed: %m");

	if (childpid == 0) {
		/* child process */
		if (dup2(ipi[0], 0) < 0 ||
		    dup2(opi[1], 1) < 0 ||
		    dup2(epi[1], 2) < 0)
			error("dup2 failed: %m");
		close(ipi[0]);
		close(ipi[1]);
		close(opi[0]);
		close(opi[1]);
		close(epi[0]);
		close(epi[1]);

		execv(prog, argv);
		error("/etc/request-key.conf:%d: Failed to execute '%s': %m\n", confline, prog);
	}

	/* parent process */
	close(ipi[0]);
	close(opi[1]);
	close(epi[1]);

#define TOSTDIN ipi[1]
#define FROMSTDOUT opi[0]
#define FROMSTDERR epi[0]

	ifl = fcntl(TOSTDIN, F_GETFL);
	ofl = fcntl(FROMSTDOUT, F_GETFL);
	efl = fcntl(FROMSTDERR, F_GETFL);
	if (ifl < 0 || ofl < 0 || efl < 0)
		error("fcntl/F_GETFL failed: %m");

	ifl |= O_NONBLOCK;
	ofl |= O_NONBLOCK;
	efl |= O_NONBLOCK;

	if (fcntl(TOSTDIN, F_SETFL, ifl) < 0 ||
	    fcntl(FROMSTDOUT, F_SETFL, ofl) < 0 ||
	    fcntl(FROMSTDERR, F_SETFL, efl) < 0)
		error("fcntl/F_SETFL failed: %m");

	ninfo = strlen(callout_info);
	pc = callout_info;

	npay = sizeof(payload);
	pp = payload;

	espace = sizeof(errbuf);
	pe = errbuf;

	do {
		fd_set rfds, wfds;

		FD_ZERO(&rfds);
		FD_ZERO(&wfds);

		if (TOSTDIN != -1) {
			if (ninfo > 0) {
				FD_SET(TOSTDIN, &wfds);
			}
			else {
				close(TOSTDIN);
				TOSTDIN = -1;
				continue;
			}
		}

		if (FROMSTDOUT != -1)
			FD_SET(FROMSTDOUT, &rfds);

		if (FROMSTDERR != -1)
			FD_SET(FROMSTDERR, &rfds);

		tmp = TOSTDIN > FROMSTDOUT ? TOSTDIN : FROMSTDOUT;
		tmp = tmp > FROMSTDERR ? tmp : FROMSTDERR;
		tmp++;

		debug("select r=%d,%d w=%d m=%d\n", FROMSTDOUT, FROMSTDERR, TOSTDIN, tmp);

		tmp = select(tmp, &rfds, &wfds, NULL, NULL);
		if (tmp < 0)
			error("select failed: %m\n");

		debug("select -> %d r=%x w=%x\n",
		      tmp,
		      *(unsigned *) (void *) &rfds,
		      *(unsigned *) (void *) &wfds);

		if (TOSTDIN != -1 && FD_ISSET(TOSTDIN, &wfds)) {
			tmp = write(TOSTDIN, pc, ninfo);
			if (tmp < 0) {
				if (errno != EPIPE)
					error("write failed: %m\n");

				debug("EPIPE");
				ninfo = 0;
			}
			else {
				debug("wrote %d\n", tmp);

				pc += tmp;
				ninfo -= tmp;
			}
		}

		if (FROMSTDOUT != -1 && FD_ISSET(FROMSTDOUT, &rfds)) {
			tmp = read(FROMSTDOUT, pp, npay);
			if (tmp < 0)
				error("read failed: %m\n");

			debug("read %d\n", tmp);

			if (tmp == 0) {
				close(FROMSTDOUT);
				FROMSTDOUT = -1;
			}
			else {
				pp += tmp;
				npay -= tmp;

				if (npay == 0)
					error("Too much data read from query program\n");
			}
		}

		if (FROMSTDERR != -1 && FD_ISSET(FROMSTDERR, &rfds)) {
			char *nl;

			tmp = read(FROMSTDERR, pe, espace);
			if (tmp < 0)
				error("read failed: %m\n");

			debug("read err %d\n", tmp);

			if (tmp == 0) {
				close(FROMSTDERR);
				FROMSTDERR = -1;
				continue;
			}

			pe += tmp;
			espace -= tmp;

			while ((nl = memchr(errbuf, '\n', pe - errbuf))) {
				int n, rest;

				nl++;
				n = nl - errbuf;

				if (xdebug)
					fprintf(stderr, "Child: %*.*s", n, n, errbuf);

				if (!xnolog) {
					openlog("request-key", 0, LOG_AUTHPRIV);
					syslog(LOG_ERR, "Child: %*.*s", n, n, errbuf);
					closelog();
				}

				rest = pe - nl;
				if (rest > 0) {
					memmove(errbuf, nl, rest);
					pe -= n;
					espace += n;
				}
				else {
					pe = errbuf;
					espace = sizeof(errbuf);
				}
			}

			if (espace == 0) {
				int n = sizeof(errbuf);

				if (xdebug)
					fprintf(stderr, "Child: %*.*s", n, n, errbuf);

				if (!xnolog) {
					openlog("request-key", 0, LOG_AUTHPRIV);
					syslog(LOG_ERR, "Child: %*.*s", n, n, errbuf);
					closelog();
				}

				pe = errbuf;
				espace = sizeof(errbuf);
			}
		}

	} while (TOSTDIN != -1 || FROMSTDOUT != -1 || FROMSTDERR != -1);

	/* wait for the program to exit */
	if (waitpid(childpid, &tmp, 0) != childpid)
		error("wait for child failed: %m\n");

	/* if the process exited non-zero or died on a signal, then we call back in to ourself to
	 * decide on negation
	 * - this is not exactly beautiful but the quickest way of having configurable negation
	 *   settings
	 */
	if (WIFEXITED(tmp) && WEXITSTATUS(tmp) != 0) {
		if (norecurse)
			error("child exited %d\n", WEXITSTATUS(tmp));

		norecurse = 1;
		debug("child exited %d\n", WEXITSTATUS(tmp));
		lookup_action("negate", key, ktype, kdesc, callout_info);
	}

	if (WIFSIGNALED(tmp)) {
		if (norecurse)
			error("child died on signal %d\n", WTERMSIG(tmp));

		norecurse = 1;
		debug("child died on signal %d\n", WTERMSIG(tmp));
		lookup_action("negate", key, ktype, kdesc, callout_info);
	}

	/* attempt to instantiate the key */
	debug("instantiate with %zd bytes\n", pp - payload);

	if (keyctl_instantiate(key, payload, pp - payload, 0) < 0)
		error("instantiate key failed: %m\n");

	debug("instantiation successful\n");
	exit(0);

} /* end pipe_to_program() */
