#!/usr/bin/env mkcmd
# We layer other jackets one-on the next to make a new jacket.		(ksb)
# Accept a list of jackets in $COAT as
#	COAT=/path/to/fist:second:/new/dir/third:...
# Each jacket wraps those after it, the first one must pass before the
# second one starts.  If all the jackets/helmet pass we show them
# each the exit code of the access (or jacket to the right) to get
# them to map the exit code as they see fit.  (For example the exit
# code 69 might map to 77, if there were a jacket to do that mapping.)
#
# The common usage might be to lock 2 resources with a locking jacket.
# Another might allow both a ttyowner check with a timestamp check.
#
# $Compile(*): ${mkcmd:-mkcmd} -n %F %f && ${cc:-gcc} -Wall %s -g %F.c -o %[F.-$]
# $Compile: ${mkcmd:-mkcmd} -n %F %f && ${cc:-gcc} -Wall -g %F.c -o %[F.-$]
# $InstallDir: ${install:-install} -dr ${DESTDIR}/usr/local/libexec/jacket
# $Install: ( [ -f %F ] || %b -mCompile %f ) && ${install:-install} -c -m 0711 %F ${DESTDIR}/usr/local/libexec/jacket/%F
#
from '<sys/types.h>'
from '<sys/wait.h>'
from '<stdlib.h>'
from '<stdio.h>'
from '<unistd.h>'
from '<limits.h>'
from '<sysexits.h>'
from '<fcntl.h>'
from '<errno.h>'
from '<sys/uio.h>'
from '<signal.h>'

require "std_help.m" "std_version.m"
require "api_jacket.m"
require "util_ppm.m"
require "ext.m"

%i
#if !defined(COAT_LIST)
#define COAT_LIST	"COAT"
#endif
#if !defined(COAT_REVEAL)
#define COAT_REVEAL	"COAT_REVEAL"
#endif

#if !defined(HAVE_VOID_UNSETENV)
#define HAVE_VOID_UNSETENV	1
#endif

#if !defined(HAVE_SETPROCTITLE)
#define HAVE_SETPROCTITLE (defined __NetBSD__ || defined __FreeBSD__ || defined __OpenBSD__)
#endif


static const char rcsid[] =
	"$Id: coat.m,v 1.22 2012/10/19 20:15:04 ksb Exp $",
	acCoatList[] = COAT_LIST,
	acReveal[] = COAT_REVEAL,
	acDevNull[] = "/dev/null";
%%

augment action 'V' {
	user "CoatVersion();"
}
action 'H' {
	named "Explain"
	help "explain the usage of the environment configuration"
	update "%n(stdout);"
	aborts "exit(EX_OK);"
}

list {
	hidden named "Coat"
	help "silent running"
	param ""
	update "%n(argc, argv);"
}
exit {
	update "exit(EX_UNAVAILABLE);"
}

%c
/* mkcmd hook for detailed version information				(ksb)
 */
static void
CoatVersion()
{
	printf("%s: list: %s\n", progname, acCoatList);
	printf("%s: reveal: %s\n", progname, acReveal);
}

/* We could do better here						(ksb)
 */
static void
Explain(FILE *fp)
{
	fprintf(fp, "%s: nest multiple jackets in layers\n", progname);
	fprintf(fp, "%-13s  a colon separate list of helmets or jackets\n", acCoatList);
	fprintf(fp, "%-13s  the standard reveal prefix hook\n", acReveal);
}

/* Try to reproduce the same exit code we reaped from wait		(ksb)
 */
static void
DupExit(int iCode)
{
	/* Try to exit with a signal, if we saw one
	 */
	if (iCode < 127 && 0 != iCode)
		kill(getpid(), iCode);
	else
		iCode >>= 8;
	exit(iCode);
	abort();
}

/* The shim waits for an exit code on stdin, then returns that		(ksb)
 */
static int
Shim()
{
	auto int iCode;

#if HAVE_SETPROCTITLE
	(void)setproctitle("shim for pid %ld", (long)getppid());
#endif
	if (1 != scanf(" %d", & iCode))
		return EX_PROTOCOL;

	DupExit(iCode);
	return EX_OSERR;
}

extern char **environ;

/* Fake op's reveal feature in our current environment			(ksb)
 * Return 1 if we updated our list variable, we we don't also remove it.
 */
static int
InternalReveal(char *pcPrefix, size_t wLen)
{
	register int j, iRet;
	register unsigned int iKeep;
	register char *pcEq;
	register char **ppcList;

	iRet = 0;
#if DEBUG
	printf("# %s internal reveal `%.*s'\n", progname, (int)wLen, pcPrefix);
	fflush(stdout);
#endif
	for (iKeep = 0; (char *)0 != environ[iKeep]; ++iKeep) {
		/* count them all */
	}
	if ((char **)0 == (ppcList = calloc((iKeep|7)+1, sizeof(char *)))) {
		if (fJacket) printf("%d\n", EX_OSERR);
		exit(EX_OSERR);
	}
	iKeep = 0;
	for (j = 0; (char *)0 != environ[j]; ++j) {
		if (0 != strncmp(environ[j], pcPrefix, wLen))
			continue;
		ppcList[iKeep++] = strdup(environ[j]);
	}
	for (j = 0; j < iKeep; ++j) {
		if ((char *)0 == (pcEq = strchr(ppcList[j], '='))) {
			unsetenv(ppcList[j]);
			printf("-%s\n", ppcList[j]);
			fflush(stdout);
			continue;
		}
		*pcEq = '\000';
		if (0 == strcmp(acCoatList, ppcList[j]+wLen))
			iRet = 1;
		printf("-%s\n", ppcList[j]);
		unsetenv(ppcList[j]);
		if (0 != setenv(ppcList[j]+wLen, pcEq+1, 1))
			printf("# setenv failed!\n");
		printf("\"$%s", ppcList[j]+wLen);
		*pcEq = '=';
		do switch (*pcEq) {
		case '\"':
			printf("\\d");
			continue;
		case '\'':
			printf("\\q");
			continue;
		case '`':
			printf("\\o");
			continue;
		case '\n':
			printf("\\n");
			continue;
		case '\t':
			printf("\\t");
			continue;
		default:
			printf("%c", *pcEq);
			continue;
		} while ('\000' != *++pcEq);
		printf("\"\n");
		fflush(stdout);
	}
	return iRet;
}

/* We apply the normal ksb path compression here			(ksb)
 * that is to say "/usr/local/libexec/jacket/a:b:c" uses the directory
 * from the first component to find the others (/usr/local/libexec/jacket/b,
 * and .../jacket/c are implied).
 */
static void	/*ARGSUSED1*/
Coat(int argc, char **argv)
{
	auto char acLayer[PATH_MAX+4];
	auto char *pcLayers, *pcNext;
	register char *pcEnd, *pcScan;
	typedef struct LAnode {
		char *pcpath;		/* path to jacket/helmet */
		int fdshim;		/* write to shim */
		int fdenv;		/* read from jacket */
		int icode;		/* exit code returned (debug) */
		pid_t wpid;		/* pid of the jacket process */
	} LAYER;
	register LAYER *pLA;
	register int i;
	auto char acCvt[64];		/* %ld l(2^128)/l(10) = 38 */
	auto int iRet, aiShim[2], aiEnv[2];
	auto unsigned iPly;
	aiShim[0] = aiShim[1] = -1;

	/* Because we're not going to check this everyplace, do it here.
	 * op does this too, but we may not be called from op.
	 */
	if (-1 == fcntl(0, F_GETFD, 1) && 0 != open(acDevNull, O_RDONLY, 0666)) {
		if (fJacket) printf("%d\n", EX_OSERR);
		exit(EX_OSERR);
	}
	if (-1 == fcntl(1, F_GETFD, 1) && 1 != open(acDevNull, O_WRONLY, 0666)) {
		if (fJacket) printf("%d\n", EX_OSERR);
		exit(EX_OSERR);
	}
	if (-1 == fcntl(2, F_GETFD, 1) && 2 != open(acDevNull, O_RDWR, 0666)) {
		if (fJacket) printf("%d\n", EX_OSERR);
		exit(EX_OSERR);
	}

	/* Recover and remove our data from op's environment
	 */
	if ((char *)0 == (pcLayers = getenv(acCoatList))) {
		if (fJacket) printf("%ld\n", (long int)EX_DATAERR);
		exit(EX_DATAERR);
	}

	/* We'll reveal the next level of magic before we go on.
	 * That way we can call ourself.  Most jackets reveal later.
	 * If you want to reveal later put sheval as the first jacket
	 * and use that to reveal after we fork.
	 */
	{
	register char *pcPrefix;

	if ((char *)0 != (pcPrefix = getenv(acReveal))) {
		(void)unsetenv(acReveal);
		printf("-%s\n", acReveal);
		if (!InternalReveal(pcPrefix, strlen(pcPrefix))) {
			(void)unsetenv(acCoatList);
			printf("-%s\n", acCoatList);
		}
	} else {
		printf("-%s\n", acCoatList);
	}
#if DEBUG
	if ((char *)0 == (pcPrefix = getenv(acCoatList))) {
		printf("# no %s in child environment\n", acCoatList);
	}
#endif
	fflush(stdout);
	}

	/* Build the books to keep track of the layers, if you have
	 * only 1 layer we _could_ just run that jacket/helmet.
	 */
	for (iPly = 1, pcScan = pcLayers; '\000' != *pcScan; /* below */) {
		if (':' == *pcScan++)
			++iPly;
	}
	if ((void *)0 == (pLA = calloc((iPly|3)+1, sizeof(LAYER)))) {
		fprintf(stderr, "%s: calloc: %d plys of size %lu: %s\n", progname, (iPly|3)+1, (unsigned long)sizeof(LAYER), strerror(errno));
		if (fJacket) printf("%d\n", EX_OSERR);
		exit(EX_OSERR);
	}
	pLA[0].fdshim = open(acDevNull, O_RDWR, 0666);
	iRet = EX_OK;
	strncpy(acLayer, argv[0], sizeof(acLayer));
	for (i = 0, pcScan = pcLayers; (char *)0 != pcScan; pcScan = pcNext, ++i) {
		if ((char *)0 != (pcNext = strchr(pcScan, ':'))) {
			/* We could take empty layers to be a copy of
			 * ourself (coat), but that is error-prone.
			 */
			/* skiping m/:+/ here */
			while (':' == *pcNext)
				*pcNext++ = '\000';
			if ('\000' == *pcNext)
				pcNext = (char *)0;
		}
		if ('/' == *pcScan) {
			strncpy(acLayer, pcScan, PATH_MAX);
		} else if ((char *)0 != (pcEnd = strrchr(acLayer, '/'))) {
			*++pcEnd = '\000';
			strncat(acLayer, pcScan, PATH_MAX);
		} else {
			strncpy(acLayer, pcScan, PATH_MAX);
		}
		pLA[i].pcpath = strdup(acLayer);
#if DEBUG
		printf("# ply %d runs %s\n", i, acLayer);
		fflush(stdout);
#endif
	}

	/* Fire up each layer and read+react to the env mod stream.
	 */
	for (i = 0; i < iPly; ++i) {
		auto size_t wLen;
		register pid_t wShim;
		register char *pc;

		if (fJacket && -1 == pipe(aiShim)) {
			fprintf(stderr, "%s: shim pipe: %s\n", progname, strerror(errno));
			iRet = EX_OSERR;
			break;
		}
		if (-1 == pipe(aiEnv)) {
			fprintf(stderr, "%s: env pipe: %s\n", progname, strerror(errno));
			iRet = EX_OSERR;
			break;
		}
		switch (pLA[i].wpid = fork()) {
		case -1:
			fprintf(stderr, "%s: fork: %s\n", progname, strerror(errno));
			iRet = EX_OSERR;
			break;
		case 0:	/* jacket/helmet process */
			/* clean up the already open shim fds */
			{
			register int j;

			for (j = 0; j < i; ++j)
				close(pLA[j].fdshim);
			}

			close(1);
			dup(aiEnv[1]);
			close(aiEnv[1]);
			close(aiEnv[0]);
			pcScan = pLA[i].pcpath;
			if ((char *)0 != (pcScan = strrchr(pcScan, '/')))
				argv[0] = pcScan+1;
			else
				argv[0] = pLA[i].pcpath;
			if (fJacket) {
				register char **ppc;

				(void)fcntl(aiShim[1], F_SETFD, 1);
				switch (wShim = fork()) {
				case -1:
					/* Oh look out */
					exit(EX_OSERR);
					/*NOTREACHED*/
				case 0:
					close(0);
					dup(aiShim[0]);
					close(aiShim[0]);
					/* unblock the esclation */
					close(aiShim[1]);
					close(1);
					dup(2);
					exit(Shim());
					/*NOTREACHED*/
				default:
					close(aiShim[0]);
					break;
				}

				/* Find -P pid and replace it, op always (ksb)
				 * puts it first, but may not in future.
				 * This doesn't parse options perfectly,
				 * the -P must be the first in a bundle, and
				 * a parameter -P might fool it.
				 */
				snprintf(acCvt, sizeof(acCvt), "-P%ld", (long int)wShim);
				for (ppc = argv; (char *)0 != *ppc; ++ppc) {
					if ('-' != ppc[0][0]) {
						continue;
					}
					/* EoO failed to find -Ppid (XXX)
					 */
					if ('-' == ppc[0][1] && '\000' == ppc[0][2]) {
						break;
					}
					if ('P' != ppc[0][1]) {
						continue;
					}
					/* bundled together
					 */
					if ('\000' != ppc[0][2]) {
						ppc[0] = acCvt;
						break;
					}
					/* missing on end of argv (XXX)
					 */
					if ((char *)0 == ppc[1]) {
						break;
					}
					/* separated from option
					 */
					ppc[1] = acCvt+2;
					break;
				}
			}
			execve(pLA[i].pcpath, argv, environ);
#if defined(COAT_SEARCH)
			execvp(pLA[i].pcpath, argv);
			fprintf(stderr, "%s: execvp: %s\n", progname, strerror(errno));
#else
			fprintf(stderr, "%s: execve: %s\n", progname, strerror(errno));
#endif
			if (fJacket) {
				/* we set close-on-exec so we still have it */
				write(aiShim[1], "69\n", 3);
			}
			exit(EX_UNAVAILABLE);
			/*NOTREACHED*/
		default:
			/* parent */
			close(aiEnv[1]);
			if (fJacket) {
				close(aiShim[0]);
			}
			pLA[i+1].fdshim = aiShim[1];
			pLA[i].fdenv = aiEnv[0];
			pLA[i].icode = -1;
			break;
		}
		if (EX_OK != iRet) {
			break;
		}

		/* Fake ExtInput function from op.m about line 6975,
		 * but we also echo the pseudo-script to op (or ourself).
		 * We trust that the input lines are mostly \n terminated,
		 * which op.m doesn't really trust.
		 */
		while ((char *)0 != (pc = ExtLine(pLA[i].fdenv, &wLen))) {
			auto char *pcEol;

			if ((int)wLen != write(1, pc, wLen)) {
				break;
			}
			/* The ext Line took off the \n or \r\n, put it back
			 */
			write(1, "\n", 1);
			if (wLen > 0 && '\n' == pc[wLen]) {
				pc[wLen--] = '\000';
			}
			/* op does NOT put the \r, but some network may have
			 */
			if (wLen > 0 && '\r' == pc[wLen]) {
				pc[wLen--] = '\000';
			}

			/* If you like add, && '\"' == pc[wLen], but I don't.
			 */
			if ('\"' == pc[0]) {
				wLen = ExtDecode(pc);
			}
			switch (pc[0]) {
			case '#':
#if DEBUG
				fprintf(stderr, "%s\n", pc);
				break;
#endif
			case '\r':
			case '\n': /* being liberal with the op spec */
			case '\000':
				break;
			case '$': /* set by name or name=value */
				/* XXX We don't have the "old environment"
				 * to consult for the $env case, set to empty.
				 * It should be set properly in the op process
				 * so it will be fine in the escalated cmd.
				 * But any nested EnvAuth check will fail :-(.
				 */
				if ((char *)0 != (pcEol = strchr(pc+1, '=')))
					*pcEol++ = '\000';
				else
					pcEol = "";
				if (0 != setenv(pc+1, pcEol, 1))
					fprintf(stderr, "setenv failed!\n");
				break;
			case '-': /* delete by name */
				unsetenv(pc+1);
				break;
			case '~': /* reveal names */
				--wLen, ++pc;
				(void)InternalReveal(pc, wLen);
				break;
			case '&':
				/* We can't really do I/O redirections
				 */
				break;
			case '0': case '1': case '2': case '3': case '4':
			case '5': case '6': case '7': case '8': case '9':
				iRet = strtol(pc, &pcEol, 0);
				if ('\000' == *pcEol)
					break;
				/*FALLTHROUGH*/
			default:
				/* not sure this is the correct spelling LLL */
				fprintf(stderr, "%s: %s: %s%s", progname, pLA->pcpath, pc, strchr(pc, '\n') ? "" : "\n");
				iRet = EX_PROTOCOL;
				break;
			}
		}
		close(pLA[i].fdenv);	/* aka close(aiEnv[0]); */
		pLA[i].fdenv = -1;
		if (fJacket) {
			continue;
		}

		/* Helmet checks for exit code
		 */
		if (-1 == waitpid(pLA[i].wpid, &pLA[i].icode, 0)) {
			pLA[i].icode = EX_OSERR;
			break;
		}
		pLA[i].wpid = -1;
		if (EX_OK != iRet || EX_OK != (iRet = pLA[i].icode)) {
			break;
		}
	}
	if (EX_OK != iRet) {
		if (fJacket) printf("%d\n", iRet);
		fflush(stdout);
		/* XXX signal ourself? */
		exit(iRet < 256 ? iRet : iRet >> 8);
	}

	/* Add the escalated process layer, note that shim is already set
	 */
	i = iPly++;
	pLA[i].pcpath = "escalated";
	/* pLA[i].fdshim set in loop above */
	pLA[i].fdenv = -1;
	pLA[i].icode = -1;
	pLA[i].wpid = iPid;

	/* Tell op (or us) to release the actual escalation process.
	 */
	close(1);
	(void)open(acDevNull, O_RDWR, 0666);

	/* If we are a Helmet we are done. Reaped others and good to go.
	 */
	if (!fJacket) {
		exit(iRet);
	}

	/* We are a jacket so we must proxy the exit codes from the
	 * escalated process back though the jackets, and any of the
	 * jackets could exit in any order.  Usually you'd expet the
	 * escalated process to exit first, then the deepest jacket,
	 * outwards to us, but that doesn't always happen.  So Look Out.
	 * Oh, and we might get stray child pids, just like make(1) does.
	 */
	{
	register pid_t wReap;
	register int iScan, iRemain;
	auto int iStatus;

	/* Wait for the deepest ply in the stack, send that status
	 * to the shim above and repeat until no more plys.  Note that
	 * a jacket might interpret an exit code (viz. invert it).
	 */
	iRemain = iPly;
	while (0 < (wReap = wait(& iStatus))) {
		snprintf(acCvt, sizeof(acCvt), "%ld", (long int)iStatus);
		for (iScan = 0; iScan < iPly; ++iScan) {
			if (wReap == pLA[iScan].wpid)
				break;
		}

		/* found some stray process we do not love
		 */
		if (iScan == iPly) {
			continue;
		}
		/* Ask the next plys to map the exit code for us.
		 */
#if DEBUG
		printf("# coat%d: sending status %s from process %ld to next ply via fd %d\n", iScan, acCvt, (long int)pLA[iScan].wpid, pLA[iScan].fdshim);
		fflush(stdout);
#endif
		write(pLA[iScan].fdshim, acCvt, strlen(acCvt));
		close(pLA[iScan].fdshim);
		pLA[iScan].fdshim = -1;
		pLA[iScan].icode = iStatus;
		pLA[iScan].wpid = -1;
		while (iScan > 0 && -1 == pLA[iScan].fdshim)
			--iScan;
		iPid = pLA[iScan].wpid;
		--iRemain;
	}

	iStatus = pLA[0].icode;
	exit(iStatus < 256 ? iStatus : iStatus >> 8);
	}
}
