#!mkcmd
# $Id: stamp.m,v 1.37 2012/10/30 23:04:51 ksb Exp $
#@Version $Revision: 1.37 $@#
#
# Some rule (op or not) creates a session process, we allow or forbid
# access via a helmet/jacket call and keep track of a time limit to
# allow new escalations w/o a re-credential operation. --ksb
#
# Suggested use in op's rule-base (all these are removed from user envronment):
#	$STAMP_SPEC=$l:tab=value:tab=..	# name of socket + tableau checks
#	$STAMP_SET=NAME1:NAME2:NAME3	# recovery of tableau values -> env
#	$STAMP_WARN=Message		# tell the user he failed
#	$STAMP_REVEAL=envname		# standard reveal logic
#	helmet=/usr/local/libexec/jacket/timestamp
# See the few examples in the HTML document.
# $Compile(*): ${mkcmd:-mkcmd} -n %F %f && gcc %s -Wall %F.c -o %F
# $Compile: ${mkcmd:-mkcmd} -n %F %f && gcc -Wall %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
#
# We take a slightly different view than sudo (of course), we build
# an active process for each escalated uid.  The socket for each uid
# lives in a directory that we hard code here.
#
# The modes on the directory may allow already credentialed Customers to
# end the session or renew the timeout _without_ running an escalated
# command.  This is a feature as it doesn't clog the log with no-op
# commands just to hold the session open.  I contend that:
#	while sleep 55; do /usr/local/libexec/jacket/timestamp -v; done
# is better than
#	while sleep 55; do sudo date; done
# because we don't fill the auth log with junk.  And in a .logout:
#	/usr/local/libexec/jacket/timestamp -k 2>/dev/null
# is a fine idea.
#
# Note that the spell below about the same as the -v option:
#	echo P | nc -U /var/op/timestamp/$UID
#
# Easter egg: STAMP_PENALTY='2y?' /usr/local/libexec/jacket/timestamp -V
#
# Run via op this could be an anyone rule, mostly.
#	stamp	/usr/local/libexec/jacket/stampctl $1 ;
#		$1=^-k|-K|-v$ uid=...
#		$STAMP_EXPIRY=10m
# That allows "op stamp -k" to kill your auth token, and "op stamp -v" to do
# something like "sudo -v".  Note that -k won't remove a penalty token.
#
from '<errno.h>'
from '<sys/types.h>'
from '<sys/uio.h>'
from '<unistd.h>'
from '<stdlib.h>'
from '<sys/param.h>'
from '<fcntl.h>'
from '<sys/socket.h>'
from '<sys/un.h>'
from '<sys/stat.h>'
from '<sys/select.h>'
from '<sysexits.h>'
from '<sys/wait.h>'
from '<signal.h>'
from '<pwd.h>'
from '<grp.h>'
from '<regex.h>'

require "util_errno.m"
require "api_jacket.m"
require "std_help.m" "std_version.m"
require "mode.m" "stampproto.m"

augment action 'V' {
	user "Version();"
}

action 'H' {
	named "Explain"
	update "%n(stdout);"
	aborts "exit(EX_OK);"
	help "explain the usage of this helmet"
}

# Since the non-root can't _build_ the socket, let them re-auth if there
# is one, or remove auth if there be need (a logout for example).
list {
	hidden named "Stamp"
	help "silent running"
	param ""
}
exit {
	update "exit(EX_UNAVAILABLE);"
}

%c
static const char rcsid[] =
	"$Id: stamp.m,v 1.37 2012/10/30 23:04:51 ksb Exp $";

static char
	acDefCred[] =
#if defined(LOCAL_STAMP_DEFREQ)		/* allow local site policy */
		LOCAL_STAMP_DEFREQ
#else
		STAMP_DEFREQ
#endif
			;	/* must be +write */
static const char
	acDumpCode[] = "%d\n",
	acStampSpec[] = STAMP_SPEC,
	acStampSet[] = STAMP_SET,
	acStampReveal[] = STAMP_REVEAL,
	acDefAuthFacility[] = STAMP_DEFAUTH_FACILITY;

static const char *pcStampDir = (char *)0;
static const char acStampWarn[] = STAMP_WARN;

%%
before 7 "pcStampDir = SetCache();"
#@Explode ExtLine@
%i
static char *ExtLine(int fdPeer, size_t *pwLen);
%%
%c
/* grab a complete line from the peer ExtInput stream			(ksb)
 */
static char *
ExtLine(int fdPeer, size_t *pwLen)
{
	register int fQuoted;
	register size_t wLen;
	auto char acReply[8192];	/* part of the value */
	static char *pcMem = (char *)0;
	static size_t wMaxMem = 0;

	if ((size_t *)0 != pwLen) {
		*pwLen = -1;
	}
	if (-1 == fdPeer) {		/* std reset logic */
		if ((char *)0 != pcMem) {
			free((void *)pcMem);
			pcMem = (char *)0;
		}
		/* Do not vetch: API spec says with is not a failure.
		 */
		return (char *)0;
	}

	acReply[0] = '\000';
	if (0 == wMaxMem) {
		wMaxMem = 16*1024;
		while (wMaxMem > 0 && (char *)0 == (pcMem = malloc(wMaxMem)))
			wMaxMem /= 2;
		if (0 == wMaxMem) {
			return (char *)0;
		}
	}

	/* Reading  "FOO=C-string"\n  or  FOO=literal\n  or  &1/dev/null\n
	 * or any other op jacket external command.
	 */
	wLen = 0;
	if (1 > read(fdPeer, pcMem, 1)) {
		return (char *)0;
	}
	fQuoted = '\"' == acReply[wLen++];

	/* Yeah, we beat the machine to death with 1 character reads. Sorry.
	 * we know we need 2 chacters ("\n) to end a string and 1 for a
	 * non-encoded command (\n) -- but that's error-prone code.  We
	 * don't send too many commands this way in the Real World(tm).
	 */
	for (/*nada*/; 1 == read(fdPeer, pcMem+wLen, 1); ++wLen) {
		if (fQuoted) {
			if (wLen > 1 && '\n' == pcMem[wLen] && '\"' == pcMem[wLen-1]) {
				break;
			}
		} else if ('\n' == pcMem[wLen]) {
			break;
		}
		if (wLen+2 >= wMaxMem) {
			wMaxMem += 1024;
			if ((char *)0 != (pcMem = realloc((void *)pcMem, wMaxMem))) {
				return (char *)0;
			}
		}
	}
	pcMem[wLen] = '\000';
	if ((size_t *)0 != pwLen)
		*pwLen = wLen;
	return pcMem;
}
%%
#@Remove@
#@Explode ExtDecode@
%i
static size_t ExtDecode(char *pcLine);
%%
%c
/* Given a "$VAR=value" (or really "anything legit") decode it		(ksb)
 * in-place returning the new length.  Before you call this please
 * strip off any terminating '\n' (replace it with a '\000').
 * The inverse of the this is NVPReply in stampctl.m
 */
static size_t
ExtDecode(char *pcLine)
{
	register char *pcSrc, *pcCursor;

	pcSrc = pcCursor = pcLine;
	while (isspace(*pcSrc)) {
		++pcSrc;
	}
	if ('\"' != *pcSrc++) {	/* not really quoted */
		return strlen(pcLine);
	}
	for (/*above*/; '\"' != *pcSrc && '\000' != *pcSrc; ++pcSrc) {
		if ('\\' != *pcSrc) {
			*pcCursor++ = *pcSrc;
			continue;
		}
		switch (*++pcSrc) {
		case '\\':
			*pcCursor++ = '\\';
			break;
		case 'n':
			*pcCursor++ = '\n';
			break;
		case 't':
			*pcCursor++ = '\t';
			break;
		case 'd':
			*pcCursor++ = '\"';
			break;
		case 'q':
			*pcCursor++ = '\'';
			break;
		case 'o':
			*pcCursor++ = '`';
			break;
		default:
			*pcCursor++ = pcSrc[-1];
			break;
		}
	}
	*pcCursor = '\000';

	return pcCursor-pcLine;
}
%%
#@Remove@
%c

/* How do we use this mess?						(ksb)
 */
static void
Explain(FILE *fp)
{
	register int iWidth, i;
	static int aiLen[] = {
		sizeof(acStampSpec), sizeof(acStampSet),
		sizeof(acStampReveal), sizeof(acStampWarn)
	};
	iWidth = strlen(pcStampFacility);
	for (i = 0; i < sizeof(aiLen)/sizeof(aiLen[0]); ++i)
		if (aiLen[i] > iWidth)
			iWidth = aiLen[i];
	iWidth *= -1;
	fprintf(fp, "%s: check stamp authorization criteria\n", progname);
	fprintf(fp, "%*s the default facility\n", iWidth, pcStampFacility);
	fprintf(fp, "%*s stamp-path:npv-checks\n%*sfor string compares use name{!,=}value\n%*sfor RE matches use name{=~,!~}RE\n%*sfor integer compares use name{<,<=,>,>=,==,!=}value\n%*s(string suffixes are also compared after the integers)\n", iWidth, acStampSpec, iWidth-1, "", iWidth-1, "", iWidth-1, "", iWidth-2, "");
	fprintf(fp, "%*s copy listed tableau entries to the environment\n", iWidth, acStampSet);
	fprintf(fp, "%*s standard reveal logic\n", iWidth, acStampReveal);
	fprintf(fp, "%*s authorization failure message\n", iWidth, acStampWarn);
	exit(EX_OK);
}

/* output our version information					(ksb)
 */
static void
Version()
{
	register char *pcCheck;

	pcCheck = strstr(pcStampDir, acDefAuthFacility);
	if ((char *)0 != pcCheck && '\000' != pcCheck[sizeof(acDefAuthFacility)-1])
		pcCheck = (char *)0;
	printf("%s: cache directory: %s\n", progname, pcStampDir);
	printf("%s: default facility: %s [%sactive]\n", progname, acDefAuthFacility, (char *)0 == pcCheck ? "not " : "");
	printf("%s: environment: $%s, $%s, $%s, $%s \n", progname, pcStampFacility, acStampSpec, acStampReveal, acStampWarn);
}

/* The op admin can leave a kill file in place of the top level		(ksb)
 * directory, a login or a login/group to kill access for that combination
 * (all, user, user requesting group).  We copy the message to stderr.
 */
static void
ErrCat(const char *pcFile)
{
	register FILE *fpShow;
	register char *pc;
	auto char acCopy[8192];

	if ((char *)0 == pcFile) {
		fprintf(stderr, "No reason given.\n");
	} else if ((FILE *)0 != (fpShow = fopen(pcFile, "r"))) {
		while ((char *)0 != (pc = fgets(acCopy, sizeof(acCopy), fpShow)))
			fputs(acCopy, stderr);
		fclose(fpShow);
	}
}

/* We asked for a name=value, and the stamp is sending it back		(ksb)
 * convert it into a string again (might be quoted).  This allows
 * us to emulate op version 3 checks (=~, !~, <=, etc.) below.
 * If you want to save the value, copy it.  We use the same memory
 * next time.  We don't use this to just copy data back to op.
 * When we fail we must output a suggested exit code from sysexits.h.
 */
char *
RecoverEntry(int iStamp)
{
	register char *pcRet;
	auto size_t sNull;

	if (-1 == iStamp) {		/* std reset logic */
		return ExtLine(-1, (size_t *)0);
	}
	if ((char *)0 == (pcRet = ExtLine(iStamp, &sNull))) {
		printf(acDumpCode, EX_PROTOCOL);
		return (char *)0;
	}
	ExtDecode(pcRet);
	return pcRet;
}

static char acChatEnd[] = "\000\000\n";

/* As the stamp at Stamp about the value to $Name			(ksb)
 * wire format is <cmd><q>name<q>value<q>, where
 *	cmd	is "=" for is equal to, and "!" for is not equal to
 *		We emulate RE "=~", "!~" and numeric in ops "<", ">",
 *		"<=", ">=", "==", "!=", in versions 2.x.
 *	q	any character not in name or value (\000 a winner) and
 *	name	the tableau name
 *	value	the value we want to match (or exclude)
 * Return Boolean success for matched (or not).
 *
 * When the value might start with a = or ~ you could be hosed, match it
 * with an RE, since it must be a string and not a number.  If we return
 * failure we must output a suggested exit from sysexits.h.
 */
static int
CheckEnv(int iStamp, char *pcName)
{
	register int c, iOK, iSend, iRet;
	register char *pcOp, *pcEq, *pcBang;
	auto struct iovec aIOV[8];
	auto char acReply[64];		/* %c\n */

	iOK = iSend = 0;
	pcEq = pcName;
	while ('\000' != (c = *pcEq) && '=' != c && '!' != c && '<' != c && '>' != c && '~' != c)
		++pcEq;
	/* We found a 2 character op (RE or number), act like V3 op
	 */
	if ('<' == *pcEq || '>' == *pcEq || '=' == pcEq[1] || '~' == pcEq[1]) {
		auto regex_t reg;
		auto char *pcEndLeft, *pcEndRight, *pcValue;
		auto int fLt, fGt, fEq;
		auto regmatch_t aRM[1];
		auto long int lLeft, lRight;

		/* Check exists, sending "$\000name\000\000\n"
		 */
		aIOV[iSend  ].iov_base = "$\000";;
		aIOV[iSend++].iov_len  = 2;	/* yes we need the \000 */
		aIOV[iSend  ].iov_base = pcName;
		aIOV[iSend++].iov_len  = pcEq-pcName;
		aIOV[iSend  ].iov_base = acChatEnd;
		aIOV[iSend++].iov_len  = 3;	/* \000\000\n */
		if (-1 == writev(iStamp, aIOV, iSend)) {
			printf(acDumpCode, EX_OSERR);
			return 0;
		}
		if ((char *)0 == (pcValue = RecoverEntry(iStamp))) {
			/* function set error code */
			return 0;
		}
		++pcValue;	/* skip the '$' prefix */
		if (0 != strncmp(pcValue, pcName, pcEq-pcName) || '=' != pcValue[pcEq-pcName]) {
			printf("# stamp returned a different macro?\n");
			printf(acDumpCode, EX_PROTOCOL);
			return 0;
		}
		pcValue += pcEq-pcName + 1;
		/* handle the RE matches first */
		if ('~' == pcEq[1]) {		/* !~ or =~ */
			if (0 != regcomp(&reg, pcEq+2, REG_EXTENDED|REG_NOSUB)) {
				/* If this has a \n in it we should
				 * not print it, as op may read the part
				 * after the \n as an ExtCmd.
				 */
				if ((char *)0 == strchr(pcEq, '\n'))
					printf("# RE failed to compile: %s\n", pcEq+2);
				else
					printf("# RE with a newline in it failed to compile for %.*s\n", (int)(pcEq-pcName), pcName);
				printf(acDumpCode, EX_CONFIG);
				return 0;
			}
			aRM[0].rm_so = 0;
			aRM[0].rm_eo = strlen(pcValue);
			switch (regexec(&reg, pcValue, 1, aRM, 0)) {
			case 0:
				iRet = ('=' == pcEq[0]);
				break;
			case REG_NOMATCH:
				iRet = ('!' == pcEq[0]);
				break;
			default:
				printf("# regexec failed for %s\n", pcEq+2);
				printf(acDumpCode, EX_CONFIG);
				return 0;
			}
			/* match operation failed
			 */
			if (0 == iRet)
				printf(acDumpCode, EX_DATAERR);
			return iRet;
		}

		/* Convert the value to a number+suffix
		 */
		lLeft = strtol(pcValue, &pcEndLeft, 0);
		lRight = strtol(pcEq+2, &pcEndRight, 0);
		fEq = lLeft == lRight && 0 == strcmp(pcEndLeft, pcEndRight);
		fGt = lLeft > lRight || (lLeft == lRight && 0 > strcmp(pcEndLeft, pcEndRight));
		fLt= lLeft < lRight || (lLeft == lRight && 0 < strcmp(pcEndLeft, pcEndRight));
		if ('<' == pcEq[0] && '=' == pcEq[1]) {			/*<=*/
			iRet = fLt || fEq;
		} else if ('<' == pcEq[0]) {				/*< */
			iRet = fLt;
		} else if ('>' == pcEq[0] && '=' == pcEq[1]) {		/*>=*/
			iRet = fGt || fEq;
		} else if ('>' == pcEq[0]) {				/*> */
			iRet = fGt;
		} else if ('=' == pcEq[0] && '=' == pcEq[1]) {		/*==*/
			iRet = fEq;
		} else if ('!' == pcEq[0] && '=' == pcEq[1]) {		/*!=*/
			iRet = !fEq;
		} else { /* wow! >~ or <~ or ~= or ~~ */
			printf("# unknown operator %.2s\n", pcEq);
			printf(acDumpCode, EX_CONFIG);
			return 0;
		}
		if (0 == iRet)
			printf(acDumpCode, EX_DATAERR);
		return iRet;
	}

	/* Must be a string compare, let the stamp do it so we
	 * don't have to parse it.
	 */
	pcEq = strchr(pcName, '='), pcBang = strchr(pcName, '!');
	if ((char *)0 == pcEq && (char *)0 == pcBang) {
		/* Check exists, sending "+\000name\000\000\n"
		 */
		aIOV[iSend  ].iov_base = "+\000";;
		aIOV[iSend++].iov_len  = 2;	/* yes we need the \000 */
		aIOV[iSend  ].iov_base = pcName;
		aIOV[iSend++].iov_len  = strlen(pcName);
		aIOV[iSend  ].iov_base = acChatEnd;
		aIOV[iSend++].iov_len  = 3;	/* \000\000\n */
	} else {
		/* Check equal, sending "=\000name\000value\000\n"
		 */
		if (((char *)0 == pcBang) || ((char *)0 != pcEq && pcEq < pcBang))
			pcOp = pcEq;
		else
			pcOp = pcBang;
		aIOV[iSend  ].iov_base = (pcOp == pcEq) ? "=\000" : "!\000";
		aIOV[iSend++].iov_len  = 2;		/* =\000 or !\000 */
		aIOV[iSend  ].iov_base = pcName;
		aIOV[iSend++].iov_len  = pcOp-pcName;	/* name */
		aIOV[iSend  ].iov_base = acChatEnd;
		aIOV[iSend++].iov_len  = 1;		/* \000 */
		aIOV[iSend  ].iov_base = ++pcOp;
		aIOV[iSend++].iov_len  = strlen(pcOp);	/* value */
		aIOV[iSend  ].iov_base = acChatEnd+1;
		aIOV[iSend++].iov_len  = 2;		/* \000\n */
	}
	if (-1 == writev(iStamp, aIOV, iSend) || 2 != read(iStamp, acReply, 2)) {
		printf(acDumpCode, EX_OSERR);
		return 0;
	}
	/* The only OK reply is exactly "y\n" 'p' is penalty, and
	 * 'n' is not, others are errors we don't care which, like a
	 * dropped connection is just as bad.
	 */
	iRet = 'y' == acReply[0] && '\n' == acReply[1];
	if (0 == iRet)
		printf(acDumpCode, EX_DATAERR);
	return iRet;
}

/* Ask the stamp for $\000<Name>\000$\000\n, send what ever it		(ksb)
 * sends back to use (that starts with a '$') to iOut.
 * Return Boolean success, when we return failure we must output a
 * suggested exit code from sysexits.h.
 */
static int
PushEnv(char *pcName, int iStamp, int iOut)
{
	register int iCc, iSend, iRet;
	auto struct iovec aIOV[8];
	auto char acReply[8192];	/* "$ENV=value" */

	iSend = 0;
	aIOV[iSend  ].iov_base = "$\000";;
	aIOV[iSend++].iov_len  = 2;	/* yes we need the \000 */
	aIOV[iSend  ].iov_base = pcName;
	aIOV[iSend++].iov_len  = strlen(pcName);
	aIOV[iSend  ].iov_base = acChatEnd;
	aIOV[iSend++].iov_len  = 3;	/* \000\000\n */
	if (-1 == writev(iStamp, aIOV, iSend)) {
		printf(acDumpCode, EX_OSERR);
		return 0;
	}

	/* Expect "$.*"\n or $.*\n, which we can just sent to op
	 * text like "#no name\n78\n" is the alternative
	 */
	iRet = EX_OK;
	acReply[0] = '\000';
	if (2 != (iCc = read(iStamp, acReply, 2)) || ('$' != acReply[0] && '\"' != acReply[0])) {
		printf(acDumpCode, EX_CANTCREAT);
		return 0;
	}
	if (iCc > 0 && iCc != write(iOut, acReply, iCc)) {
		printf(acDumpCode, EX_OSERR);
		return 0;
	}

	while (1) {
		if (0 >= (iCc = read(iStamp, acReply, sizeof(acReply))))
			break;
		if (iCc != write(iOut, acReply, iCc)) {
			printf(acDumpCode, EX_OSERR);
			return 0;
		}
		if ('\n' == acReply[iCc-1])
			break;
	}

	return 1;
}

/* Check the stamp expression for authorization				(ksb)
 * return 1 for failure, and dump a non-zero code on stdout as well.
 */
static int
CheckSpec(char *pcStamp, char *pcSet, char **ppcConnect, unsigned int *puTimeout)
{
	register char *pcColon, *pcScan;
	register int s;
	auto char *pcSocket, *pcText, *pcMessage;
	auto StampInfo SI;
	auto char acReady[64];		/* %c\n */

	if ((char **)0 != ppcConnect) {
		*ppcConnect = (char *)0;
	}
	if ((char *)0 != (pcColon = strchr(pcStamp, ':'))) {
		*pcColon++ = '\000';
	}

	/* Convert shorthand name into a socket path (viz. "" -> uid)
	 */
	pcStamp = SockSpec(pcStamp, getuid());
	pcSocket = StampPath(pcStampDir, pcStamp);
	switch (SysStatus(pcSocket, &pcText, &pcMessage, &SI)) {
	case TBS_SEEN:
		if (-1 == (s = Client(pcSocket))) {
	case TBS_AVAIL:
#if defined(DEBUG)
			printf("# %s didn't exist, or failed to connect\n", pcSocket);
#endif
			printf(acDumpCode, EX_NOUSER);
			return 1;
		}
		break;
	case TBS_DIR:
		fprintf(stderr, "%s: misconfigured stamp: \"%s\" is a directory\n", progname, pcSocket);
		printf(acDumpCode, EX_CANTCREAT);
		return 1;
	default:
		if ((char *)0 != pcText)
			fprintf(stderr, "%s\n", '\000' == *pcText ? "stamp unaliailabel" : pcText);
		if ((char *)0 != pcMessage)
			ErrCat(pcMessage);
		fflush(stderr);
		printf(acDumpCode, EX_UNAVAILABLE);
		return 1;
	}

	/* Chat with stamp, verify tableau values, cannot check for
	 * a value with a colon in it, you'll live (since you put the
	 * values in the tableau just don't put a colon in any).
	 */
	if ((char *)0 != (pcScan = pcColon)) {
		while (-1 != s && '\000' != *pcScan) {
			if ((char *)0 != (pcColon = strchr(pcScan, ':'))) {
				*pcColon = '\000';
				if (!CheckEnv(s, pcScan)) {
					close(s);
					s = -1;
					/* CheckEnv dumped a code */
					break;
				}
				*pcColon++ = ':';
				pcScan = pcColon;
				continue;
			}
			if (!CheckEnv(s, pcScan)) {
				close(s);
				s = -1;
				/* CheckEnv dumped a code */
			}
			break;
		}
	}

	/* Fetch and set requested $ENVs, which also can't have a colon in
	 * the name.  That shouldn't be an issue.  If any of these fail
	 * the running stamp outputs "\n78\n" after an error comment, so
	 * as long as we don't output any other exit codes the escalation
	 * fails with EX_CONFIG.
	 */
	if (-1 != s && (char *)0 != (pcScan = getenv(pcSet))) {
		printf("-%s\n", pcSet);
		while (-1 != s && '\000' != *pcScan) {
			if ((char *)0 != (pcColon = strchr(pcScan, ':'))) {
				*pcColon = '\000';
				if (!PushEnv(pcScan, s, 1)) {
					close(s);
					s = -1;
					/* PushEnv dumped a code */
					break;
				}
				*pcColon++ = ':';
				pcScan = pcColon;
				continue;
			}
			if (!PushEnv(pcScan, s, 1)) {
				close(s);
				s = -1;
				/* PushEnv dumped a code */
			}
			break;
		}
	}

	/* If we are a jacket we need to know the idle timeout.
	 */
	if (-1 != s && fJacket) {
		auto char acTimeout[64];	/* "%ld\n" */
		auto char *pcEnd = acChatEnd;

		if (2 != write(s, "I\n", 2) || 0 >= read(s, acTimeout, sizeof(acTimeout))) {
			s = -1;
			printf(acDumpCode, EX_OSERR);
		} else {
			*puTimeout = (unsigned int)strtoul(acTimeout, &pcEnd, 0);
		}
		if ('\000' != *pcEnd && '\n' != *pcEnd) {
			printf("# %s: timeout format error: %s\n", progname, acTimeout);
			*puTimeout = 0U;
		}
	}


	/* Last request to the stamp session, are we allowing any access?
	 * If we are not any of the above MAY have already stopped us.
	 */
	if (-1 != s && (2 != write(s, "?\n", 2) || 2 != read(s, acReady, sizeof(acReady)) || 'y' != acReady[0] || '\n' != acReady[1])) {
		close(s);
		s = -1;
		printf(acDumpCode, EX_NOPERM);
	}

	if (-1 == s) {
		return 1;
	}
	close(s);
	if ((char **)0 != ppcConnect) {
		*ppcConnect = pcSocket;
	} else {
		free((void *)pcSocket);
	}
	return 0;
}

/* Allow sig alarm to cleanly jump to cleanup code			(ksb)
 */
static unsigned int uTimeout = 0L;
void
ChildReady(int iSig)
{
	/* we do not set SA_RESTART which is all we want */
	uTimeout = 0U;
}

/* The helmet/jacket entry point					(ksb)
 * you knew I'd get around to this someday.
 * LLL a timeout here would be nifty (alarm + signal handler)
 */
int
Stamp(int argc, char **argv)
{
	extern char **environ;
	register char **ppc, *pcScan, *pcEq;
	register pid_t wReap;
	register int s;
	auto char *pcWarn, *pcSocket, *pcWorked, *pcLast, *pcLastEq, *pcSet;
	auto int wStatus, iSetLen;
	auto struct sigaction SAChild;

	iSetLen = 1024;
	pcSet = malloc(iSetLen);

	/* Fetch $STAMP_WARN just to be nice
	 */
	if ((char *)0 == (pcWarn = getenv(acStampWarn))) {
		pcWarn = "Sorry";	/* so common, no radiation here */
	} else {
		printf("-%s\n", acStampWarn);
	}

	/* Remove radiation of stamp configuration details
	 */
	if ((char *)0 != getenv(pcStampFacility)) {
		printf("-%s\n", pcStampFacility);
		unsetenv(pcStampFacility);
	}

	/* $STAMP_SPEC.*=[stamp-name][:tableau[=!]value]*
	 * yes, you can check multiple stamps, always an "and" operation.
	 * The socket a jacket is going to refresh is the last one in
	 * sorted order base on the SPEC_tag, so put the one you want
	 * last in the list (helmets do not refresh, of course).
	 * $STAMP_SET.* is the parallel tableau request, and
	 * $STAMP_WARN.* is specific the parallel rejection notification
	 */
	pcLastEq = pcLast = (char *)0;
	for (ppc = environ; (char *)0 != (pcScan = *ppc); ++ppc) {
		register char *pcThisWarn;
		auto unsigned int uSaw;

		if ((char *)0 == (pcEq = strchr(pcScan, '=')))
			continue;
		if (0 != strncmp(pcScan, acStampSpec, sizeof(acStampSpec)-1))
			continue;
		if (iSetLen < pcEq-pcScan) {	/* almost never */
			iSetLen = (63|(pcEq-pcScan))+65;
			pcSet = realloc((void *)pcSet, iSetLen);
			if ((char *)0 == pcSet)
				exit(EX_OSERR);
		}
		snprintf(pcSet, iSetLen, "%s%.*s", acStampSet, (int)(pcEq-pcScan-(sizeof(acStampSpec)-1)), pcScan+(sizeof(acStampSpec)-1));
		if ('\000' == pcEq[1]) {
			s = CheckSpec(acDefCred, pcSet, &pcWorked, &uSaw);
		} else {
			s = CheckSpec(pcEq+1, pcSet, &pcWorked, &uSaw);
		}
		if (s) {
			snprintf(pcSet, iSetLen, "%s%.*s", acStampWarn, (int)(pcEq-pcScan-(sizeof(acStampSpec)-1)), pcScan+(sizeof(acStampSpec)-1));
			if ((char *)0 != (pcThisWarn = getenv(pcSet)))
				pcWarn = pcThisWarn;
			fprintf(stderr, "%s\n", pcWarn);
			exit(EX_OK);	/* CheckSpec set exit code */
		}
		if ((char *)0 != pcLast) {
			*pcEq = *pcLastEq = '\000';
			s = 0 < strcmp(pcLast, pcScan);
			*pcEq = *pcLastEq = '=';
			if (!s)
				continue;
			free((void *)pcSocket);
			/* pcSocket = (char *)0; */
		}
		pcLast = pcScan;
		pcLastEq = pcEq;
		pcSocket = pcWorked;
		uTimeout = uSaw;
	}

	/* Allow a reveal spec.
	 */
	if ((char *)0 != (pcScan = getenv(acStampReveal))) {
		printf("-%s\n~%s\n", acStampReveal, pcScan);
	}

	/* Checking for helmet case here
	 */
	if (!fJacket) {
		exit(EX_OK);
	}

	/* We are a jacket, we need to release the process.  Then we
	 * want to keep the stamp open by pinging it before it
	 * times out until the process finishes.  After all the stamp
	 * is not idle as long as the process is running, right?
	 * So we set child handler to wake up our sleep call
	 */
	memset((void *)&SAChild, 0, sizeof(SAChild));
	SAChild.sa_sigaction = (void *)0;
	SAChild.sa_flags = 0;		/* note: not SA_RESTART */
	SAChild.sa_handler = ChildReady;
	if (0U != uTimeout && -1 != sigaction(SIGCHLD, &SAChild, (struct sigaction *)0) && -1 != sigaction(SIGHUP, &SAChild, (struct sigaction *)0)) {
		auto char acPing[8];	/* "y\n" or "p\n" or "n\n" */

		/* We don't want to  sleep for the whole idle time,
		 * because we might race with the timer.  So we back off
		 * by 39% or so.  Either HUP or CHLD stop this loop.
		 */
		uTimeout = (unsigned int)(((unsigned long)uTimeout * 286UL)/463UL)+1;
		/* go, go go!
		 */
		close(1);
		/* We'll catch a signal for the child, which will
		 * make sleep return non-zero, we hope
		 */
		do {
			if (-1 == (s = Client(pcSocket)))
				break;
			if (2 != write(s, "?\n", 2) || 2 != read(s, acPing, sizeof(acPing))) {
				close(s);
				break;
			}
			close(s);
			if ('y' != acPing[0] || '\n' != acPing[1])
				break;
		} while (0 != uTimeout && 0 == sleep(uTimeout));
	} else {
		/* Just go on, stamp doesn't have a timeout.
		 */
		close(1);
	}

	/* 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).
	 */
	while (iPid != (wReap = wait(& wStatus))) {
		if (WIFSTOPPED(wStatus)) {
			(void)kill(wReap, SIGCONT);
			continue;
		}
		if (-1 == wReap) {
			break;
		}
	}
	exit(WIFEXITED(wStatus) ? WEXITSTATUS(wStatus) : 255);
}
