#!mkcmd
# $Id: stampctl.m,v 1.20 2012/11/02 21:57:58 ksb Exp $
#@Version $Revision: 1.20 $@#
#
# This is the interface to stop/stop an op stamp process.
# See the few examples in the HTML document.
#
# $Compile(*): ${mkcmd:-mkcmd} -n %F %f && gcc %s -Wall %F.c -o %F -lpam
# $Compile: ${mkcmd:-mkcmd} -n %F %f && gcc -Wall %F.c -o %F -lpam
# $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
#
# Run via op this could be an anyone rule, mostly.
# Allow anyone to make a stamp with their tty in the creds:
#	stamp	/usr/local/libexec/jacket/stampctl -M$1/$l -E1h -Dstamp TTY=$y ;
#		$1=^-M$,^make$
#		users=^.*$  uid=stamp euid=root gid=stamp
# This only means we can start a stamp process with our tty (TTY=$y) as
# the only credential.  (We must be on a tty to run this, since we used $y).
# So run "op stamp wheel" to give yourself an hour to run escalataion.
#
#	client	/something/we/run/lots ... ;
#		groups=^wheel$
#		helmet=/usr/local/libexec/jackets/stamp
#		STAMP_CREDS=wheel/$l:TTY=$y
#		uid=source gid=...
# This allows anyone in group wheel with an active stamp _on_the_same_tty_
# to run the "client" escalation.  If you don't have a stamp it fails, if
# you are not in group wheel you can't even try, if you are on the wrong
# tty you loose too.  The tty restriction could be removed or expanded to
# include other attributes about the session (even $Z) which really limits
# you to the same shell (PPID=$Z is the common idiom).
# Note that the directory "wheel" could have any spelling, it is common to
# factor the stamps into purpose-built directories to limit name collisions.
#
# This replaces sudo's timestamp with a more secure and versitile facility.
#
from '<errno.h>'
from '<stdlib.h>'
from '<sys/param.h>'
from '<sys/types.h>'
from '<sys/uio.h>'
from '<unistd.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 '<pwd.h>'
from '<signal.h>'
from '<setjmp.h>'
from '<grp.h>'
from '<security/pam_appl.h>'
from '<netinet/in.h>'
from '<sys/time.h>'
from '<sys/ioctl.h>'
from '<netdb.h>'

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

augment action 'V' {
	user "Version();"
}
# Since the non-root can't _build_ the socket, let them reauthorize if there
# is one, or remove the stamp if there be need (a logout for example).
exclude 'Mvk'
char* 'm' {
	local named "pcMode"
	param "mode"
	help "permissions for the new stamp"
}
char* 'u' {
	local named "pcUser"
	param "user"
	help "owner of the new stamp"
}
char* 'g' {
	local named "pcGroup"
	param "group"
	help "group ownership for the new stamp"
}
list {
	named "InitAllDirs"
	update "exit(%n(%#, %@, pcStampDir, pcMode, pcGroup, pcUser));"
	param "facilities"
	help "list of additional facility directories to create"
}

char* 'M' {
	local named "pcSocket"
	param "name"
	help "create an authorization stamp at name"
	number {
		param "max"
		track 'fGaveAllowed'
		named 'iAllowed'
		help "number of authorizations allowed by this stamp"
	}
	boolean 'C' {
		named "fChRoot"
		hidden help "try to be rediculously secure with chroot"
	}
	boolean 'n' {
		named "fSoMuchHate"
		help "start in penalty mode (never allow any escalations)"
	}
	char* 'R' {
		local named "pcRemote"
		param "roap"
		help "fetch tableau from an authorization service by name"
		boolean 'X' {
			named "fExchange"
			help "reverse the precedence of remote and rule tableaus"
		}
		type "seconds" 'T' {
			named "iRemConn" "pcRemConn"
			init 'DEF_MUX_TIMEOUT'
			param "timeout"
			help "allowed delay for the roap tcpmux connection"
		}
		type "service" 'p' {
			named "pSEConn" "pcRoapSvc"
			init '"tcpmux"'
			help "specify the tcpmux service port on remote"
		}
	}
	char* 'D' {
		local named "pcDrop"
		param "drop"
		help "setuid to user (or user:group) to process requests"
	}
	type "seconds" 'I' {
		named "iIdleSpan"
		init '"1h"'		# 1 hour is liberal, tune it
		keep "pcIdleSpec"
		param "idle"
		help "specify an idle timeout in scaled seconds (default %qi)"
	}
	type "seconds" 'E' {
		named "iEndSpan"
		init '"0"'		# no set termination date -blade runner
		keep "pcEndSpec"
		param "end"
		help "specify a maximum time limit in scaled seconds (default %qi)"
	}
	list {
		named "AuthForce"
		param "name=values"
		update "%n(%#, %@, %rMn, %run, %rgn, %rmn, %rDn, %rRn);/*NOTREACHED*/"
		help "set qualifiers in the attribute tableau"
	}
}

char* 'Q' {
	help "output requested tableau elements"
	param "stamp"
	boolean 'F' {
		local named "fExtLike"
		help "output can be sent to op as external input from a jacket"
	}
	list {
		named "Query"
		update "%n(%#, %@, %rQn, %rFn);/*NOTREACHED*/"
		param "tableaus"
		help "sessions to terminate, by name"
	}
	exclude "mgu"
}
boolean 'v' {
	help "reset the timeout on an existing authorization"
	list {
		hidden named "AuthRenew"
		param "stamps"
		help "the stamp to refresh"
	}
	exclude "mgu"
}
boolean 'kKN' {
	help "terminate an existing authorization"
	update "%n = %w;"
	list {
		hidden named "AuthTerminate"
		update "%n(%#, %@, %rkn);/*NOTREACHED*/"
		param "stamps"
		help "sessions to terminate, by name"
	}
	exclude "mgu"
}

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

exit {
	update "exit(EX_UNAVAILABLE);"
}

%i
static const char *pcStampDir = (char *)0;
extern char **environ;
%%
before "pcStampDir = SetCache();"

%i
static const char rcsid[] =
	"$Id: stampctl.m,v 1.20 2012/11/02 21:57:58 ksb Exp $";

#if !defined(DEF_MUX_TIMEOUT)
#define DEF_MUX_TIMEOUT "19"
#endif
%%
#@Explode NVPReply@
%hi
static void NVPReply(int sClient, char *pcCmd, char *pcEntry);
%%
%c
/* When the network doesn't take the whole write, push again		(ksb)
 */
static int
FlushWrite(int fd, char *pcOut, int iLen)
{
	register int iCc;
	while (iLen > 0) {
		if (-1 == (iCc = write(fd, pcOut, iLen)))
			return iLen;
		pcOut += iCc;
		iLen -= iCc;
	}
	return 0;
}

/* Convert the input NPV into "quoted" form for op, if needed.		(ksb)
 * If you add encoding here (\x) add to stamp's RecoverEntry function.
 */
static void	/*ARGSUSED*/
NVPReply(int sClient, char *pcCmd, char *pcEntry)
{
	register size_t wNeed;
	register char *pcCursor;
	auto struct iovec aIOV[4];
	static char *pcQuote = (char *)0;
	static size_t wMax;

	if ((char *)0 == strchr(pcEntry, '\n') && (char *)0 == strchr(pcEntry, '\"')) {
		wNeed = 0;
		aIOV[wNeed  ].iov_base = "$";
		aIOV[wNeed++].iov_len = 1;
		aIOV[wNeed  ].iov_base = pcCmd;
		aIOV[wNeed++].iov_len = strlen(pcCmd);
		aIOV[wNeed  ].iov_base = pcEntry;
		aIOV[wNeed++].iov_len = strlen(pcEntry);
		aIOV[wNeed  ].iov_base = "\n";
		aIOV[wNeed++].iov_len = 1;
		(void)writev(sClient, aIOV, wNeed);
		return;
	}

	/* We have to quote it, sigh.  Allocate enough to backslash
	 * every character, which is worst-case.  Pad so we might not
	 * have to realloc in future calls.
	 */
	wNeed = (2*strlen(pcEntry)|63)+65;
	if ((char *)0 == pcQuote) {
		wMax = wNeed;
		pcQuote = malloc(wMax);
	} else if (wMax < wNeed) {
		wMax = wNeed;
		pcQuote = realloc((void *)pcQuote, wMax);
	}
	if ((char *)0 == pcQuote) {
		FlushWrite(sClient, "71\n", 3);
		return;
	}
	for (pcCursor = pcQuote; '\000' != (*pcCursor = *pcEntry); ++pcEntry) {
		switch (*pcCursor) {
		case '\\':
			*pcCursor++ = '\\';
			*pcCursor++ = '\\';
			break;
		case '\n':
			*pcCursor++ = '\\';
			*pcCursor++ = 'n';
			break;
		case '\t':
			*pcCursor++ = '\\';
			*pcCursor++ = 't';
			break;
		case '\"':
			*pcCursor++ = '\\';
			*pcCursor++ = 'd';	/* op uses this for " */
			break;
		case '\'':
			*pcCursor++ = '\\';
			*pcCursor++ = 'q';	/* op uses this for ' */
			break;
		case '`':
			*pcCursor++ = '\\';	/* op uses this for ` */
			*pcCursor++ = 'o';
			break;
		default:
			++pcCursor;
			break;
		}
	}
	wNeed = 0;
	aIOV[wNeed  ].iov_base = "$\"";
	aIOV[wNeed++].iov_len = 2;
	aIOV[wNeed  ].iov_base = pcCmd;
	aIOV[wNeed++].iov_len = strlen(pcCmd);
	aIOV[wNeed  ].iov_base = pcQuote;
	aIOV[wNeed++].iov_len = strlen(pcQuote);
	aIOV[wNeed  ].iov_base = "\"\n";
	aIOV[wNeed++].iov_len = 2;
	(void)writev(sClient, aIOV, wNeed);
	return;
}
%%
#@Remove@


%c
static const char
	acStampSpec[] = STAMP_SPEC,
	acStampReveal[] = STAMP_REVEAL,
	acStampFacility[] = STAMP_FACILITY,
	acStampQuery[] = STAMP_QUERY,
	acDefAuthFacility[] = STAMP_DEFAUTH_FACILITY,
	acStampWarn[] = STAMP_WARN;

/* Also take -H like any stamp would					(ksb)
 */
static void
Explain(FILE *fp)
{
	fprintf(fp, "%s: build a new authorization stamp\n", progname);
	fprintf(fp, "%s\tauthentication location (default %s)\n", acStampFacility, acDefAuthFacility);
	fprintf(fp, "%s\tstamp specification\n", acStampSpec);
	fprintf(fp, "%s\tstandard reveal logic\n", acStampReveal);
	fprintf(fp, "%s\tescalation denied message\n", acStampWarn);
}

/* output our version information					(ksb)
 */
static void
Version()
{
	auto const char *pcStat;
	auto struct stat stCheck;
	auto char acState[MAXPATHLEN+4];

	if (0 != stat(pcStampDir, &stCheck))
		pcStat = strerror(errno);
	else if (S_ISSOCK(stCheck.st_mode))
		pcStat = "global access socket";
	else if (!S_ISDIR(stCheck.st_mode))
		pcStat = "not a directory";
	else if (0 != (022 & stCheck.st_mode) && 0 == (01000 & stCheck.st_mode))
		pcStat = "writable and not sticky";
	else if (0711 != (0711 & stCheck.st_mode))
		pcStat = "should be at least mode 0711";
	else if (0 != (0004 & stCheck.st_mode))
		pcStat = "directory permissive";
	else
		pcStat = "directory ok";
	printf("%s: cache directory: %s [%s]\n", progname, pcStampDir, pcStat);
	snprintf(acState, sizeof(acState), "%s/%ld", pcStampDir, (long)getuid());
	if (0 != stat(acState, &stCheck) && S_ISSOCK(stCheck.st_mode))
		printf("%s: existing socket for %ld\n", progname, (long)getuid());
	printf("%s: timeout under -R defaults to \"%s\"\n", progname, DEF_MUX_TIMEOUT);

	if ('/' != *pcStampDir) {
		fprintf(stderr, "%s: the compiled top-level directory is not an absolute path!\n", progname);
		exit(EX_NOPERM);
	}
}

/* Build (or update the modes on) a stamp directory			(ksb)
 */
static int
InitSDir(const char *pcDir, const char *pcUid, const char *pcGid, const char *pcMode, int iModeAbove)
{
	auto char *pcEnd, *pc;
	register struct group *grp;
	register struct passwd *pwd;
	auto uid_t wUid;
	auto gid_t wGid;
	auto int wMode;
	auto u_MODE_CVT MCMode;

	if ((char *)0 != pcUid && (char *)0 != (pc = strchr(pcUid, ':'))) {
		*pc++ = '\000';
		if ('\000' != *pc && (char *)0 == pcGid)
			pcGid = pc;
	}
	mode_cvt((char *)pcMode, &MCMode, "mode");
#if defined(DEBUG)
	fprintf(stderr, "%s: FYI build -m%04o/%04o -u%s -g%s %s\n", progname, MCMode.iset, MCMode.ioptional, pcUid, pcGid, pcDir);
#endif
	if ((char *)0 == pcDir || '\000' == *pcDir)
		return EX_OK;
	wMode = MCMode.iset|(iModeAbove&MCMode.ioptional);
	if (-1 == mkdir(pcDir, wMode) && EEXIST != errno) {
		fprintf(stderr, "%s: mkdir: %s: %s\n", progname, pcDir, strerror(errno));
		return EX_OSERR;
	}
	/* mkdir will not set sticky, setuid, setgid, use chmod
	 */
	if ((EEXIST == errno || 0 != (07000&wMode)) && -1 == chmod(pcDir, wMode)) {
		fprintf(stderr, "%s: chmod: %s, %04o: %s\n", progname, pcDir, wMode, strerror(errno));
		return EX_OSERR;
	}
	if (isdigit(pcUid[0])) {
		wUid = strtoul(pcUid, &pcEnd, 0);
		if ('\000' != *pcEnd) {
			fprintf(stderr, "%s: -u: trailing junk on uid (%s)\n", progname, pcEnd);
			exit(EX_DATAERR);
		}
#if defined(DEBUG)
		if ((struct passwd *)0 == (pwd = getpwuid(wUid)))
			fprintf(stderr, "%s: FYI -u uid %ld unmapped for %s\n", progname, (long)wUid, pcDir);
#endif
	} else if ((struct passwd *)0 == (pwd = getpwnam(pcUid))) {
		fprintf(stderr, "%s: -u: unmapped login (%s)\n", progname, pcUid);
		exit(EX_DATAERR);
	} else {
		wUid = pwd->pw_uid;
	}
	if (isdigit(pcGid[0])) {
		wGid = strtoul(pcGid, &pcEnd, 0);
		if ('\000' != *pcEnd) {
			fprintf(stderr, "%s: -g: trailing junk on gid (%s)\n", progname, pcEnd);
			exit(EX_DATAERR);
		}
#if defined(DEBUG)
		if ((struct group *)0 == (grp = getgrgid(wGid)))
			fprintf(stderr, "%s: FYI -g gid %ld unmapped for %s\n", progname, (long)wGid, pcDir);
#endif
	} else if ((struct group *)0 == (grp = getgrnam(pcGid))) {
		fprintf(stderr, "%s: -g: unmapped group (%s)\n", progname, pcGid);
		exit(EX_DATAERR);
	} else {
		wGid = grp->gr_gid;
	}
	if (-1 == chown(pcDir, wUid, wGid)) {
		fprintf(stderr, "%s: chown: %s: %s\n", progname, pcDir, strerror(errno));
		return EX_OSERR;
	}
	return EX_OK;
}

/* Allow for explicit facilities specified by to create/update		(ksb)
 * the cache directory structure.
 * We overload -m mac (-m mode), -g group, -u user to specify the
 * permissions on each directory.  We allow mixing options and arguments.
 * We are a Bad Dog.
 * Remove junk, line any socket w/o a password entry? Let the admin
 * use "find ... \( -nouser -or -nogroup \) -delete"
 */
static int
InitAllDirs(int argc, char **argv, const char *pcTopDir, char *pcMode, char *pcGroup, char *pcLogin)
{
	register char *pcArg;
	auto int iRet;
	auto int fSawDashDash, iMyMode;
	auto char acCvt[64];	/* %ld */
	auto char acFullPath[MAXPATHLEN+4];
	auto struct stat stTop;

	setpwent();
	setgrent();
	if ('/' != *pcTopDir) {
		fprintf(stderr, "%s: the compiled top-level directory is not an absolute path (%s)\n", progname, pcTopDir);
		exit(EX_NOPERM);
	}

	/* Set any unspecified mode options, and check for no dirs.
	 */
	if (0 == stat(pcTopDir, &stTop)) {
		if ((char *)0 == pcLogin) {
			snprintf(acCvt, sizeof(acCvt), "%ld", (long)stTop.st_uid);
			pcLogin = strdup(acCvt);
		}
		if ((char *)0 == pcGroup) {
			snprintf(acCvt, sizeof(acCvt), "%ld", (long)stTop.st_gid);
			pcGroup = strdup(acCvt);
		}
		iMyMode = 07777&stTop.st_mode;
		if ((char *)0 == pcMode) {
			snprintf(acCvt, sizeof(acCvt), "%04lo", (long)iMyMode);
			pcMode = strdup(acCvt);
		}
		iMyMode |= 01000;	/* always allow sticky if opt'd */
	} else {
		iMyMode = 03775;	/* allow sticky, grp write+iherrit */
		if ((char *)0 == pcLogin)
			pcLogin = "0";
		if ((char *)0 == pcGroup)
			pcGroup = "0";
		if ((char *)0 == pcMode)
			pcMode = "0700";
	}
	iRet = EX_OK;
	fSawDashDash = 0;
	while (EX_OK == iRet && (char *)0 != (pcArg = *argv++)) {
		register char *pcSlash;
		if (fSawDashDash || '-' != pcArg[0]) {
			while ((char *)0 != (pcSlash = strrchr(pcArg, '/')) && '\000' == pcSlash[1] && pcSlash > pcArg)
				*pcSlash = '\000';
			while ('.' == pcArg[0] && '/' == pcArg[1])
				while ('/' == *++pcArg)
					/*nada*/;
			if (('.' == pcArg[0] && '\000' == pcArg[1]) || '\000' == *pcArg)
				pcArg = (char *)pcTopDir;
			if (Climbing(pcArg)) {
				fprintf(stderr, "%s: climbing never allowed (%s)\n", progname, pcArg);
				break;
			}
			if ('/' != *pcArg) {
				snprintf(acFullPath, sizeof(acFullPath), "%s/%s", pcTopDir, pcArg);
				pcArg = acFullPath;
			}
			iRet = InitSDir(pcArg, pcLogin, pcGroup, pcMode, iMyMode);
			continue;
		}
		/* so arg must be a mixed option or -- */
		switch (pcArg[1]) {
		case '-':
			fSawDashDash = 1;
			continue;
		case 'm':
			if ('\000' != pcArg[2]) {
				pcMode = pcArg+2;
			} else if ((char *)0 == (pcMode = *argv++)) {
				fprintf(stderr, "%s: -m missing mode\n", progname);
				break;
			}
			continue;
		case 'g':
			if ('\000' != pcArg[2]) {
				pcGroup = pcArg+2;
			} else if ((char *)0 == (pcGroup = *argv++)) {
				fprintf(stderr, "%s: -g missing group\n", progname);
				break;
			}
			continue;
		case 'u':
			if ('\000' != pcArg[2]) {
				pcLogin = pcArg+2;
			} else if ((char *)0 == (pcLogin = *argv++)) {
				fprintf(stderr, "%s: -u missing uid\n", progname);
				break;
			}
			continue;
		default:
			fprintf(stderr, "%s: options are limited to [-m mode] [-g gid] [-u uid]\n", progname);
			break;
		}
		exit(EX_USAGE);
	}
	endgrent();
	endpwent();
	return iRet;
}

/* We maintain a list of groups for, we know the max number possible	(ksb)
 * in the caller, which is (NGROUPS_MAX + 2) actually.
 */
static void
AddGroup(gid_t wAdd, int *piCur, gid_t *pwList)
{
	register int i;

	for (i = 0; i < *piCur; ++i) {
		if (wAdd == pwList[i])
			break;
	}
	if (i == *piCur) {
		pwList[(*piCur)++] = wAdd;
	}
}

%%
%i
#if !defined(NGROUPS_MAX)
#define NGROUPS_MAX	120		/* usually 8 or 16 */
#endif

#if !defined(MAX_ID_WIDTH)
#define MAX_ID_WIDTH	48		/* %ld */
#endif
%%
#@Explode CredList@
%hi
static char *CredList(char *const pcOut, int iSize, const char *);
%%
%c

/* Build the login:groups:netgroups:domain cred list			(ksb)
 */
static char *
CredList(char *const pcOut, int iSize, const char *pcQuery)
{
	register int i;
	register char *pcUser;
	register struct passwd *pw;
	register struct group *gr;
	register FILE *fpNg;
	auto int iGrps;
	auto gid_t awGids[NGROUPS_MAX+8];
	auto gid_t awGList[NGROUPS_MAX+8];
	auto char acSep[4];		/* like ":" or " " or "," */
	auto char acCvt[MAX_ID_WIDTH];

	/* Make cred list-- nobody said it would be easy.
	 */
	iGrps = 0;
	setgrent();
	setpwent();
	if ((char *)0 == (pcUser = getenv("USER"))) {
		pcUser = getenv("LOGNAME");
	}
	if ((char *)0 != pcUser && (struct passwd *)0 != (pw = getpwnam(pcUser)) && getuid() == pw->pw_uid) {
		AddGroup(pw->pw_gid, &iGrps, awGids);
	} else if ((struct passwd *)0 != (pw = getpwuid(getuid()))) {
		pcUser = pw->pw_name;
		AddGroup(pw->pw_gid, &iGrps, awGids);
	} else {
		snprintf(acCvt, MAX_ID_WIDTH, "%ld", (long)getuid());
		pcUser = acCvt;
	}
	snprintf(pcOut, iSize, "%s", pcUser);
	endpwent();

	/* Mapping gid->group, no common way to disambiguate between dup
	 * groups we could try $GROUP, but that's never going to work.
	 */
	AddGroup(getgid(), &iGrps, awGids);
	if (0 < (i = getgroups(sizeof(awGList)/sizeof(awGList[0]), awGList))) {
		register int j;

		for (j = 0; j < i; ++j) {
			AddGroup(awGList[j], &iGrps, awGids);
		}
	}

	/* Convert the unique gids to group name if we can.
	 */
	acSep[0] = ':';
	acSep[1] = '\000';
	for (i = 0; i < iGrps; ++i) {
		strncat(pcOut, acSep, iSize);
		acSep[0] = ' ';
		if ((struct group *)0 == (gr = getgrgid(awGids[i]))) {
			snprintf(acCvt, MAX_ID_WIDTH, "%ld", (long)awGids[i]);
			strncat(pcOut, acCvt, sizeof(acCvt));
		} else {
			strncat(pcOut, gr->gr_name, iSize);
		}
	}
	endgrent();

	/* Netgroups. A little harder. And we don't look a ypcat without
	 * a "+" in the netgroups file, which might be a bug.
	 */
	strncat(pcOut, ":", iSize);
	if ((FILE *)0 != (fpNg = fopen("/etc/netgroup", "r"))) {
		register int c, fSkip, iLen, fPlus;
		register char *pcBack;
		auto char acMyself[
#if defined(HOST_NAME_MAX)
			(HOST_NAME_MAX|7)+1
#else
#if defined(MAXHOSTNAMELEN)
			MAXHOSTNAMELEN
#else
			512	/* traditionally 256 */
#endif
#endif
		];
		static const char acLocalhost[] = "localhost";

		if (-1 == gethostname(acMyself, sizeof(acMyself))) {
			acMyself[0] = '\000';
		}

		pcBack = pcOut+strlen(pcOut);
		iLen = fSkip = fPlus = 0;
		/* a netgroup must start in col1 and have an isspace, '\\',
		 * or '(' after the name, AFAICT --ksb  
		 */
		while (EOF != (c = getc(fpNg))) {
			if (fSkip) {
				if ('\n' == c) {
					fSkip = 0;
					iLen = 0;
				}
				continue;
			}
			/* groupname1\ or groupname2(rockopera,ab,)
			 */
			if ('('/*)*/ == c || '\\' == c) {
				c = ' ';
			}
			if ('#' == c) {
				iLen = 0;
				fSkip = 1;
				continue;
			}
			if (!isspace(c)) {
				pcBack[iLen++] = c;
				continue;
			}
			if (0 == iLen) {
				fSkip = 1;
				continue;
			}
			pcBack[iLen] = '\000';
			if (0 == strcmp("+", pcBack)) {
				fPlus = 1;
			} else if (('\000' != acMyself[0] && innetgr(pcBack, acMyself, pcUser, (char *)0)) || innetgr(pcBack, acLocalhost, pcUser, (char *)0)) {
				pcBack += strlen(pcBack);
				*pcBack++ = ' ';
			}
			iLen = 0;
			if ('\n' != c) {
				fSkip = 1;
			}
		}
		/* When fPlus is set, we should parse "ypcat -k netgroup"
		 * output as above.  Or send "+" and our NIS domain, which
		 * is kinda a cop-out?  OK for now.
		 */
		if (fPlus) {
			*pcBack++ = '+';
			*pcBack++ = ' ';
		}

		/* Remove trailing space, if we found any netgroups.
		*/
		if (' ' == *--pcBack) {
			*pcBack = '\000';
		}
		fclose(fpNg);
	}

	/* append YP/NIS domain name, just for grins, if we have one.
	 */
	strncat(pcOut, ":", iSize);
	i = strlen(pcOut);
	if (-1 == getdomainname(pcOut+i, iSize-i)) {
		pcOut[i] = '\000';
	}

	strncat(pcOut, ":", iSize);
	if ((char *)0 != pcQuery) {
		strncat(pcOut, pcQuery, iSize);
	}
	return pcOut;
}


/* There is a tcpmux peer that is sending us a stream which represents	(ksb)
 * tableau entries.  They can send 2 forms:
 *	NAME=value[\r]?\n
 * and
 *	NAME="value"[\r]?\n
 * you cannot include a NUL (\000), it is never a valid tableau entry.
 */
static char **
ReadEnv(FILE *fpPeer)
{
	register char **ppcCur, *pcRaw, *pcResize, *pcEnd, *pcCursor;
	register int c, i, iCount, iMax;

	iMax = 5906;
	while (iMax > 2 && (char *)0 == (pcRaw = calloc(iMax, sizeof(char)))) {
		iMax /= 2;
	}
	if (iMax < 3) {
		return (char **)0;
	}
	i = iCount = 0;
	while (EOF != (c = fgetc(fpPeer))) {
		/* Out of space -> realloc if we can, like I would on a PDP11
		 * Seq. 9557, 15465, 25026, 40497, 65532...
		 */
		if (i+1 >= iMax) {
			iMax *= 89;
			iMax /= 55;
			++iMax;
			while (iMax > i && (char *)0 == (pcResize = realloc((void *)pcRaw, iMax))) {
				iMax -= 1024;
			}
			if ((char *)0 == pcResize || i+1 >= iMax)
				return (char **)0;
			pcRaw = pcResize;
		}
		pcRaw[i++] = c;
		iCount += '\n' == c;
	}
	pcEnd = pcRaw+i;
	if ((char **)0 == (ppcCur = calloc(iCount+1, sizeof(char *)))) {
		return (char **)0;
	}
	pcCursor = pcRaw;
	for (i = 0; pcRaw < pcEnd && i < iCount; ++i) {
		register int fQuote;

		if (isspace(*pcRaw)) {
			++pcRaw;
			continue;
		}
		ppcCur[i] = pcCursor;
		fQuote = 0;
		while ('\n' != (c = *pcRaw++) || fQuote) {
			if (!fQuote) {
				if ('\"' == c) {
					fQuote = 1;
					continue;
				}
				if ('\r' == c && pcRaw != pcEnd && '\n' == pcRaw[1]) {
					++pcRaw;
					break;
				}
				if ('\n' == c) {
					break;
				}
				*pcCursor++ = c;
				continue;
			}
			if ('\"' == c) {
				fQuote = 0;
			} else if ('\\' != c) {
				*pcCursor++ = c;
			} else if (pcRaw == pcEnd) {
				*pcCursor++ = '\\';
			} else switch (c = *pcRaw++) {
			case 'e': break;	/* empty */
			case 's': *pcCursor++ = ' '; break;
			case 'n': *pcCursor++ = '\n'; break;
			case 't': *pcCursor++ = '\t'; break;
			case 'b': *pcCursor++ = '\b'; break;
			case 'r': *pcCursor++ = '\r'; break;
			case 'f': *pcCursor++ = '\f'; break;
			case 'v': *pcCursor++ = '\013'; break;
			case '\"': *pcCursor++ = '\"'; break;
			case '\\': *pcCursor++ = '\\'; break;
			default: *pcCursor++ = c; break;
			/* we do not do octal, you really need that? */
			}
		}
		if (ppcCur[i] != pcCursor) {
			*pcCursor++ = '\000';
		}
	}
	*pcCursor = '\000';
	ppcCur[i] = (char *)0;
	return ppcCur;
}

/* Just to keep lint happier						(ksb)
 */
static int
StrSort(const void *pvL, const void *pvR)
{
	return strcmp((const char *)pvL, (const char *)pvR);
}

/* Return '=' when the left env is the same as the right		(ksb)
 * return '<' when left strcmp's less then right, else '>'.
 * the first check is just SEGV or snark avoidance, mayhap.
 */
static int
MergeOrder(register char *pcLeft, register char *pcRight)
{
	register char *pcEq;
	register int iL, iR;

	if ((char *)0 == pcLeft || (char *)0 == pcRight)
		return (char *)0 == pcLeft ? '>' : '<';
	iL = ((char *)0 == (pcEq = strchr(pcLeft, '='))) ? strlen(pcLeft) : pcEq-pcLeft;
	iR = ((char *)0 == (pcEq = strchr(pcRight, '='))) ? strlen(pcRight) : pcEq-pcRight;
	if (iL == iR && 0 == strncmp(pcLeft, pcRight, iL))
		return '=';
	if (iL <= iR && 0 < strncmp(pcLeft, pcRight, iL))
		return '>';
	if (iL > iR && 0 < strncmp(pcLeft, pcRight, iR))
		return '>';
	return '<';
}

/* The tableau entries on the left are more profound than the ones	(ksb)
 * on the right.  But an empty left allows the right through.
 */
static char **
EnvMerge(char **ppcLeft, char **ppcRight)
{
	register char **ppcRet, *pcLeft, *pcRight;
	register int iLeft, iRight, iMerge;
	static char *pcEmpty = (char *)0;

	if ((char **)0 == ppcRight || (char *)0 == *ppcRight) {
		return (char **)0 == ppcLeft ? &pcEmpty : ppcLeft;
	}
	if ((char **)0 == ppcLeft || (char *)0 == *ppcLeft) {
		return ppcRight;
	}
	for (iLeft = 0; (char *)0 != ppcLeft[iLeft]; ++iLeft)
		/* count'm */;
	for (iRight = 0; (char *)0 != ppcRight[iRight]; ++iRight)
		/* count'm */;
	iMerge = ((iLeft+iRight)|1)+1;
	if ((char **)0 == (ppcRet = calloc(iMerge, sizeof(char *)))) {
		fprintf(stderr, "%s: calloc: %ld,%ld: %s\n", progname, (long)iMerge, (long)sizeof(char *), strerror(errno));
		exit(EX_OSERR);
	}
	qsort((void *)ppcLeft, iLeft, sizeof(char *), StrSort);
	qsort((void *)ppcRight, iRight, sizeof(char *), StrSort);
	iRight = iLeft = 0;
	for (iMerge = 0; (char *)0 != (pcLeft = ppcLeft[iLeft]) && (char *)0 != (pcRight = ppcRight[iRight]); ++iMerge) {
		switch (MergeOrder(pcLeft, pcRight)) {
		case '=':
			++iRight;
			/*FALLTHROUGH*/
		case '<':
			++iLeft, ppcRet[iMerge] = pcLeft;
			break;
		case '>':
			++iRight, ppcRet[iMerge] = pcRight;
			break;
		}
	}
	if ((char *)0 == ppcLeft[iLeft]) {
		ppcLeft = ppcRight, iLeft = iRight;
	}
	while ((char *)0 != (pcLeft = ppcLeft[iLeft++])) {
		ppcRet[iMerge++] = pcLeft;
	}

	ppcRet[iMerge] = (char *)0;
	return ppcRet;
}

/* To support ksb's @ extension to RCS1918 we need loop detection code.
 * A reply "@host:port\r\n" retrys the same service as the new host:port.
 */
typedef struct RCnode {
	struct sockaddr_in sain;
	struct RCnode *pRClast;
} ReCurTrap;

/* Connect to roapmux running under TCPMUX on Remote to fetch a list	(ksb)
 * of authorization tokens.  These overwrite any from argv with the
 * same name.  Remote may be host:muxname as well, default "roap:roapmux"
 */
static char **
FetchRoap(const char *pcRemote, int wPort, char **ppcArgv, struct RCnode *pRCUp)
{
	register int s;
	register char *pcRoap;
	register FILE *fpMux;
	register struct hostent *pHE;
	struct RCnode RCLevel;
	auto char **ppcRemote = (char **)0;
	auto char acReply[8192];	/* max hostname + extra */
	static char
		acDefHost[] = "roap",
		acDefMux[] = "roapmux";

	if ((char *)0 == (pcRoap = strchr(pcRemote, ':'))) {
		pcRoap = acDefMux;
	} else {
		*pcRoap++ = '\000';
		if ('\000' == *pcRoap)
			pcRoap = acDefMux;
	}
	if ('\000' == pcRoap[1]) {
		pcRoap = acDefHost;
	}
	/* connect to pcRoap on our tcpmux port
	 */
	RCLevel.pRClast = pRCUp;
	memset((void *)&RCLevel.sain, '\000', sizeof(RCLevel.sain));
	if ((struct hostent *)0 == (pHE = gethostbyname(pcRemote))) {
		fprintf(stderr, "%s: gethostbyname: %s: %s\n", progname, pcRemote, strerror(errno));
		exit(EX_NOHOST);
	}
	memcpy((char *)&RCLevel.sain.sin_addr, (char *)pHE->h_addr, pHE->h_length);
	RCLevel.sain.sin_port = htons(wPort);
	RCLevel.sain.sin_family = AF_INET;
	for (; (struct RCnode *)0 != pRCUp; pRCUp = pRCUp->pRClast) {
		if (0 != memcmp((void *)&pRCUp->sain, &RCLevel.sain, sizeof(RCLevel.sain)))
			continue;
		fprintf(stderr, "%s: %s:%d detected tcmpux service %s loops to itself\n", progname, pcRemote, wPort, pcRoap);
		exit(EX_CONFIG);
	}

	if (-1 == (s = socket(AF_INET, SOCK_STREAM, 0))) {
		fprintf(stderr, "%s: socket: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	}
	/* Go to the trouble to set a timeout for the connection, because
	 * we don't want a stampctl setup to take too long.		(ksb)
	 */
	{
	auto int iFlags;
	auto socklen_t wRead;
	auto fd_set fdsAccept;
	auto struct timeval tvSlow;

	if (0 != iRemConn && (-1 == (iFlags = fcntl(s, F_GETFL, 0)) || -1 == fcntl(s, F_SETFL, iFlags|O_NDELAY))) {
		fprintf(stderr, "%s: fcntl: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	}
	iFlags = 0;
	if (-1 != connect(s, (struct sockaddr *)&RCLevel.sain, sizeof(RCLevel.sain))) {
		/* OK */
	} else if (EINPROGRESS != errno) {
		iFlags = 1;
	} else {
		FD_ZERO(&fdsAccept);
		FD_SET(s, &fdsAccept);
		tvSlow.tv_sec = iRemConn;
		tvSlow.tv_usec = 121393;	/* fib25 about 0.12s grace */
		if (1 != select(s+1, (fd_set *)0, &fdsAccept, (fd_set *)0, &tvSlow)) {
			fprintf(stderr, "%s: select: %s\n", progname, strerror(errno));
			exit(EX_UNAVAILABLE);
		}
		wRead = sizeof(iFlags);
		if (-1 == getsockopt(s, SOL_SOCKET, SO_ERROR, &iFlags, &wRead)) {
			fprintf(stderr, "%s: getsockopt: %d: %s\n", progname, s, strerror(errno));
			exit(EX_OSERR);
		}
		errno = ECONNREFUSED;
	}
	if (0 != iFlags) {
		fprintf(stderr, "%s: connect: %s:%d: %s\n", progname, pcRemote, ntohs(RCLevel.sain.sin_port), strerror(errno));
		exit(EX_UNAVAILABLE);
	}
	if (0 != iRemConn && -1 == fcntl(s, F_SETFL, iFlags)) {
		fprintf(stderr, "%s: fcntl: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	}
	if ((FILE *)0 == (fpMux = fdopen(s, "a+"))) {
		fprintf(stderr, "%s: fdopen: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	}
	}

	/* send roapmux\r\n or get to our service, I so love tcpmux.
	 */
	if (3 > fprintf(fpMux, "%s\r\n", pcRoap)) {
		fprintf(stderr, "%s: write: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	}
	/* expect "+$hostname\r\n", "+Go\r\n", or "-failure message\r\n"
	 * shortest valid reply "+\r\n" which returns "+" to us.
	 */
	if ((char *)0 == fgets(acReply, sizeof(acReply), fpMux)) {
		fprintf(stderr, "%s: %s: %s: short reply\n", progname, pcRemote, pcRoap);
		exit(EX_CONFIG);
	}
	/* ksb's redirection extension to RFC1918, retry at new address.
	 * Like "@nexthost.domain:port\r\n", where :port is optional.
	 */
	if ('@' == acReply[0]) {
		register char *pcClean;
		register int iNew;

		fclose(fpMux);
		if ((char *)0 != (pcClean = strchr(acReply, '\n'))) {
			*pcClean-- = '\000';
			if (pcClean > acReply && '\r' == *pcClean)
				*pcClean-- = '\000';
		}
		if ((char *)0 == (pcClean = strchr(acReply, ':'))) {
			iNew = 1;
		} else {
			*pcClean++ = '\000';
			iNew = strtol(pcClean, (char **)0, 10);
		}
		wPort = (0 == iNew) ? 1 : iNew;
		return FetchRoap(acReply+1, wPort, ppcArgv, &RCLevel);
	}
	if ('+' != acReply[0]) {
		fprintf(stderr, "%s: %s: %s: %s\n", progname, pcRemote, pcRoap, &acReply['-' == acReply[0]]);
		exit(EX_NOINPUT);
	}

	if ((char *)0 == CredList(acReply, sizeof(acReply), getenv(acStampQuery))) {
		exit(EX_OSFILE);
	}

	/* Tell the roapmux who we think we are (login:groups:netgroups:domain).
	 */
	if (5 > fprintf(fpMux, "%s\r\n", acReply)) {
		fprintf(stderr, "%s: fprintf: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	}
	/* Read reply header
	 */
	if ((char *)0 == fgets(acReply, sizeof(acReply), fpMux)) {
		fprintf(stderr, "%s: fgets: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	}
	if ('+' != acReply[0]) {
		fprintf(stderr, "%s: %s\n", progname, ('-' == acReply[0])+acReply);
		exit(EX_NOPERM);
	}

	{
	/* When roapmux sent max,timeout,idle set those to be our min.
	 */
	auto int iNewMax, iNewIdle, iNewEnd;

	if (3 == sscanf(acReply, "+%d,%d,%d", &iNewMax, &iNewIdle, &iNewEnd)) {
		if (!fGaveAllowed) {
			iAllowed = iNewMax;
			fGaveAllowed = 1;
		} else if (0 != iNewMax && iNewMax < iAllowed) {
			iAllowed = iNewMax;
		}
		if (0 != iNewIdle && iNewIdle < iIdleSpan) {
			iIdleSpan = iNewIdle;
			pcIdleSpec = "from network";
		}
		if (0 != iNewEnd && iNewEnd < iEndSpan) {
			iEndSpan = iNewEnd;
			pcEndSpec = "from network";
		}
		if (!fSoMuchHate && -1 == iAllowed) {
			fprintf(stderr, "%s: %s: penalty mode activated (for %ld seconds)\n", progname, pcRemote, (long)iEndSpan);
		}
		fSoMuchHate = fSoMuchHate || -1 == iAllowed;
	}
	}

	/* read tableau list, merge with argv
	 */
	ppcRemote = ReadEnv(fpMux);
	fclose(fpMux);
	return fExchange ? EnvMerge(ppcArgv, ppcRemote) : EnvMerge(ppcRemote, ppcArgv);
}

/* The customer needs to reset the session, we don't care why.		(ksb)
 * Mocks sudo -k and sudo -K mostly.
 */
static void
AuthTerminate(int argc, char **argv, int cWhich)
{
	register char *pcArg, *pcSocket;
	auto char acState[MAXPATHLEN+4];
	auto int i, s, iCc = 0;
	auto char *apcFake[2];
	auto int iRet;

	if ((char *)0 == argv[0]) {
		apcFake[0] = "";
		apcFake[1] = (char *)0;
		argv = apcFake;
	}
	iRet = EX_OK;
	for (i = 0; (char *)0 != (pcArg = argv[i]); ++i) {
		if ('\000' == pcArg[0]) {
			pcArg = SockSpec(pcArg, getuid());
		}
		pcSocket = StampPath(pcStampDir, pcArg);
		if (-1 == (s = Client(pcSocket))) {
			fprintf(stderr, "%s: no session at %s\n", progname, pcArg);
			iRet = EX_NOUSER;
			continue;
		}
		switch (cWhich) {
		case 'k':
			write(s, "Q\n", 2);
			/* expect reply 'q' or 'p' or 'error text'
			 */
			if (2 != read(s, acState, sizeof(acState)) || 'q' != acState[0]) {
				fprintf(stderr, "%s: %s: termination failed (%.*s)\n", progname, pcArg, iCc-1, acState);
				iRet = 1;
			}
			break;
		case 'K':
			write(s, "J\n", 2);
			/* expect reply 'pid\n', so "9\n" is shortest reply
			 */
			if (2 > read(s, acState, sizeof(acState)) || !isdigit(acState[0])) {
				fprintf(stderr, "%s: %s: cannot recover pid (%.*s)\n", progname, pcArg, iCc-1, acState);
				iRet = 1;
			}
			if (0 != kill(atoi(acState), SIGTERM)) {
				fprintf(stderr, "%s: kill: %ld: %s\n", progname, (long)atoi(acState), strerror(errno));
				iRet = EX_NOPERM;
			}
			break;
		case 'N':
			write(s, "n\n", 2);
			if (2 != (iCc = read(s, acState, sizeof(acState))) || 'p' != acState[0]) {
				fprintf(stderr, "%s: %s: failed to set penalty mode\n", progname, pcArg);
				iRet = EX_NOPERM;
			}
			break;
		default:
			fprintf(stderr, "%s: %c: unknown termination protocol\n", progname, cWhich);
			iRet = EX_SOFTWARE;
		}
		close(s);
	}
	exit(iRet);
}

/* Like sudo -v: ping the stamp to keep idle from killing it		(ksb)
 */
static void
AuthRenew(int argc, char **argv)
{
	register char *pcArg, *pcSocket;
	auto char acState[MAXPATHLEN+4];
	auto int i, s, iCc;
	auto char *apcFake[2];

	if ((char *)0 == argv[0]) {
		apcFake[0] = "";
		apcFake[1] = (char *)0;
		argv = apcFake;
	}
	for (i = 0; (char *)0 != (pcArg = argv[i]); ++i) {
		if ('\000' == pcArg[0]) {
			pcArg = SockSpec(pcArg, getuid());
		}
		pcSocket = StampPath(pcStampDir, pcArg);
		if (-1 == (s = Client(pcSocket))) {
			fprintf(stderr, "%s: no session at %s\n", progname, pcArg);
			exit(EX_NOUSER);
		}
		write(s, "P\n", 2);
		while (0 != (iCc = read(s, acState, sizeof(acState)))) {
			if ('y' != acState[0])
				fprintf(stderr, "%s: %s: %.*s\n", progname, pcArg, iCc-1, acState);
		}
		close(s);
	}
	exit(EX_OK);
}

/* allow sig alarm to cleanly jump to cleanup code			(ksb)
 */
static jmp_buf jbQuit;
void /*ARGSUSED*/
Destruct(int iSig)
{
	(void)longjmp(jbQuit, 1);
}
static const char acNotThere[] = "#no ";

/* Run the authorization service for a session				(ksb)
 * We are fork'd into the bg, hide from the world.  Never exit from here.
 * mode, q=quit, y=authorized, n|p=penalty
 */
static void
Service(int s, int iMode, int *piRemain, char **ppcCred)
{
	register int c, i;
	register char **ppcScan, *pcNVP;
	auto char *pcBuffer;
	auto size_t wMaxNVP, w;
	auto fd_set fdsTemp, fdsReading;
	auto struct timeval tvLease, tvCmd;
	auto char acCmd[8], acReply[8];	 /* 2 characters e.g ?\n or =" */
	auto struct sigaction SAAlarm;

	for (wMaxNVP = 2, ppcScan = ppcCred; (char *)0 != (pcNVP = *ppcScan); ++ppcScan) {
		if (wMaxNVP < (w = strlen(pcNVP)))
			wMaxNVP = w;
	}
	wMaxNVP = (wMaxNVP|15)+65;
	if ((char *)0 == (pcBuffer = calloc(wMaxNVP, sizeof(char)))) {
		fprintf(stderr, "%s: calloc: %ld,%ld: %s\n", progname, (long)wMaxNVP, (long)sizeof(char), strerror(errno));
		exit(EX_OSERR);
	}
	FD_ZERO(&fdsReading);
	FD_SET(s, &fdsReading);

	/* Arange to jump out after iEndSpan second, if EndSpan doesn't
	 * allow at least 1 idle timeout, move it up to 1 idle period.
	 */
	if (0 != iEndSpan) {
		SAAlarm.sa_handler = Destruct;
		SAAlarm.sa_sigaction = (void *)0;
		SAAlarm.sa_flags = 0;
		memset((void *)&SAAlarm.sa_mask, 0, sizeof(SAAlarm.sa_mask));
		sigaction(SIGALRM, &SAAlarm, (struct sigaction *)0);
		alarm((unsigned)iEndSpan);
	}
	if (iEndSpan < iIdleSpan) {
		iEndSpan = iIdleSpan;
	}
	tvLease.tv_sec = iIdleSpan;
	tvLease.tv_usec = 317811;	/* fib27 about 0.3s grace */

	/* Process commands for clients
	 */
	acReply[0] = 'E';
	acReply[1] = '\n';
	while ('q' != iMode) {
		fdsTemp = fdsReading;
		if (-1 == (i = select(s+1, &fdsTemp, (fd_set *)0, (fd_set *)0, &tvLease))) {
			if (errno == EINTR) {
				tvLease.tv_sec = iIdleSpan;
				continue;
			}
			break;
		}
		if (i == 0 || !FD_ISSET(s, &fdsTemp)) {
			/* session timeout */
			break;
		}
		if (-1 == (c = accept(s, (struct sockaddr *)0, (socklen_t *)0))) {
			/* client went away? */
			continue;
		}

		/* We have client, they better be quick, the whole
		 * session with them is limited to the 2.2s.
		 * 2 character cmds: Q\n, P\n, N\n, V\n ?\n, =\n \!
		 */
		tvCmd.tv_sec = 2;
		tvCmd.tv_usec = 196418;
		FD_ZERO(&fdsTemp);
		FD_SET(c, &fdsTemp);
		while (1 != (i = select(c+1, &fdsTemp, (fd_set *)0, (fd_set *)0, &tvLease))) {
			if (-1 == i && errno == EINTR) {
				continue;
			}
			iMode = 'q';
			break;
		}
		if (0 == i) {
			write(c, "q\n", 2);
			break;
		}
		while (1 == read(c, acCmd, 1)) {
			if ('\n' == acCmd[0])
				continue;
			if (1 != read(c, acCmd+1, 1)) {
				write(c, "q\n", 2);
				break;
			}
			switch (acCmd[0]) {
			case 'Q': case 'q':	/* quit is stopped by penalty */
				if ('y' == iMode) {
					acReply[0] = iMode = 'q';
				} else {
					acReply[0] = 'p';
				}
				write(c, acReply, 2);
				break;
			case 'P': case 'p':	/* reset timer with a ping */
				write(c, "y\n", 2);
				break;
			case 'N': case 'n':	/* drop to deny mode */
				acReply[0] = iMode = 'p';
				write(c, acReply, 2);
				break;
			case 'V': case 'v':	/* output protocol version */
				write(c, acProtoVer, sizeof(acProtoVer)-1);
				break;
			case '?':		/* are we allowed? */
				acReply[0] = iMode;
				write(c, acReply, 2);
				break;
			case 'X': case 'x':	/* silent exit (no reply) */
				break;

			/* commands that allow follow-up requests
			 */
			case 'I': case 'i':	/* disaplay idle timeout */
				snprintf(pcBuffer, 64, "%ld\n", (long)iIdleSpan);
				write(c, pcBuffer, strlen(pcBuffer));
				continue;
			case 'J': case 'j':	/* display process-id */
				snprintf(pcBuffer, 64, "%ld\n", (long)getpid());
				write(c, pcBuffer, strlen(pcBuffer));
				continue;
			case '=': /* "NAME"value" NAME==value		*/
			case '!': /* "NAME"value" check for NAME!=value	*/
			case '+': /* "NAME"ignore" check NAME exists	*/
			case '$': /* "NAME"prefix" output "prefixNAME=value" */
				/* the double quote can be any character
				 * not in NAME or value (like \000).
				 *
				 * = $name == value iMode in list & have
				 * ! $name != value iMode in list & not value
				 * + $name	    check for NAME at all
				 *
				 * 'E' for missing separator
				 * 'y' is yes and not in penalty
				 * 'p' for penalty active
				 * 'n' otherwise
				 *
				 * These commands do NOT drop the client
				 * connection as you may want to check
				 * another tableau entry (unless E sent).
				 */
				{
				register char *pcQuest, *pcIn;
				register size_t wL, wScan, iCc;
				register int cSep;
				auto char acNull[2];

				/* After "=_" in "=_name_value_" (make cSep '_')
				 * pcBuffer will get "name" and pcQuest gets
				 * the "value".
				 */
				cSep = acCmd[1];	/* usually " or \n */
				pcQuest = (char *)0;
				wScan = 0;
				for (pcIn = pcBuffer; 1 == (iCc = read(c, pcIn, 1)); ++pcIn) {
					if (++wScan > wMaxNVP) {
						acNull[0] = *pcIn;
						pcIn = acNull;
					}
					if (cSep != *pcIn) {
						continue;
					}
					*pcIn = '\000';
					if ((char *)0 == pcQuest)
						pcQuest = pcIn+1;
					else
						break;
				}
				/* If you asked for a missing entry say no
				 */
				if (pcQuest == acNull+1) {
					acReply[0] = 'n';
					write(c, acReply, 2);
					continue;
				}
				/* Command not finished is an error, close
				 */
				if (1 != iCc) {
					acReply[0] = 'E';
					write(c, acReply, 2);
					break;
				}
				wL = strlen(pcBuffer);
				for (ppcScan = ppcCred; (char *)0 != (pcNVP = *ppcScan); ++ppcScan) {
					if ('=' == MergeOrder(pcNVP, pcBuffer))
						break;
				}
				/* When we got the whole name, we may ignore
				 * the too long value for '+' command.
				 */
				if (wScan > wMaxNVP) {
					acReply[0] = 'p' == iMode ? 'p' : (char *)0 == pcNVP || acCmd[0] != '+' ? 'n' : iMode;
					write(c, acReply, 2);
					continue;
				}
				acReply[0] = iMode;
				/* Not found or penalty, else try to compare
				 */
				if ('y' != iMode) {
					acReply[0] = 'p'; /* not active */
				} else if ((char *)0 == pcNVP) {
					if ('$' == acCmd[0]) {
						auto struct iovec aIOV[4];

						aIOV[0].iov_base = (char *)acNotThere;
						aIOV[0].iov_len = sizeof(acNotThere)-1;
						aIOV[1].iov_base = pcBuffer;
						aIOV[1].iov_len = wL;
						aIOV[2].iov_base = "\n78\n";
						aIOV[2].iov_len = 4;
						(void)writev(c, aIOV, 3);
						continue;
					}
					acReply[0] = 'n';
				} else switch (acCmd[0]) {
				case '=':
					if ('\000' == pcNVP[wL] || 0 != strcmp(pcQuest, pcNVP+wL+1))
						acReply[0] = 'n';
					break;
				case '!':
					/* Humm no value is not any value? */
					if ('\000' == pcNVP[wL] || 0 == strcmp(pcQuest, pcNVP+wL+1))
						acReply[0] = 'n';
					break;
				case '+':
					/* found it, ignore value */
					break;
				case '$': /* "prefixNAME=value" */
					NVPReply(c, pcQuest, pcNVP);
					continue;
				default:
					acReply[0] = 'E';	/* snark! */
					break;
				}
				write(c, acReply, 2);
				}
				continue;
			/* any unknown command reports "U\n"
			 */
			default:
				write(c, "U\n", 2);
				break;
			}
			break;
		}
		close(c);
		if ((int *)0 != piRemain && 0 >= --*piRemain) {
			break;
		}
	}

	/* We are done, one way or the other
	 */
	close(s);
}

/* Allow the stamp owner (or sentinel login) to create any stamp by	(ksb)
 * name (maybe more than one, in fact).  This is how we "login" a Customer.
 */
static void
AuthForce(int argc, char **argv, char *pcName, char *pcUid, char *pcGid, char *pcMode, char *pcDrop, char *pcRemote)
{
	register char *pc;
	register int s, c, i;
	auto char *pcDropGid, *pcSocket;
	auto int aiPipe[2];
	auto int wMode;
	auto int wOwner, wRunUid;
	auto int wGroup, wRunGid;
	auto u_MODE_CVT MCPerms;
	auto struct sockaddr_un run;
	auto struct stat stSockDir;
	auto char acReply[8192];	/* may contain an error message */

	/* Try to hide the argv NVP list from ps. --ksb
	 */
#if HAVE_SETPROCTITLE
	(void)setproctitle("stamp");
#endif

	if ((char *)0 != pcUid && (char *)0 != (pc = strchr(pcUid, ':'))) {
		*pc++ = '\000';
		if ('\000' != *pc && (char *)0 == pcGid)
			pcGid = pc;
	}

	/* If passed NAME=value we keep it, just NAME looks for NAME=value in
	 * the environment which we'll take, else we delete it.
	 */
	for (s = i = 0; (char *)0 != (pc = argv[i]); ++i) {
		register char *pcEnv;
		register size_t w;

		if ((char *)0 != strchr(pc, '=')) {
			argv[s++] = pc;
			continue;
		}
		w = strlen(pc);

		for (c = 0; (char *)0 != (pcEnv = environ[c]); ++c) {
			if (0 != strncmp(pcEnv, pc, w) || '=' != pcEnv[w])
				continue;
			break;
		}
		if ((char *)0 != pcEnv) {
			argv[s++] = pcEnv;
		}
	}
	argv[s] = (char *)0;
	argc = s;		/* dead assignment for sanity */

	if ('\000' == pcName[0] || (('-' == pcName[0] || '.' == pcName[0]) && '\000' == pcName[1])) {
		pcName = SockSpec(getenv(acStampFacility), getuid());
	}
	if ((char *)0 == pcName || '\000' == *pcName) {
		fprintf(stderr, "%s: missing stamp name\n", progname);
		exit(EX_USAGE);
	}
	pcSocket = StampPath(pcStampDir, pcName);

	/* When stat fails, set group access when we specified a group
	 * (I think this never happens.)
	 */
	stSockDir.st_mode = (char *)0 != pcGid ? 0660 : 0600;
	if ((char *)0 != (pc = strrchr(pcSocket, '/'))) {
		*pc = '\000';
		(void)stat(pcSocket, &stSockDir);
		*pc = '/';
	}
	wMode = stSockDir.st_mode & 0666;
	if ((char *)0 != pcMode) {
		mode_cvt(pcMode, &MCPerms, "-m");
		wMode = MCPerms.iset|((0777&stSockDir.st_mode)&MCPerms.ioptional);
	}

	/* -D ksb:staff or -D :madness -g madness -m 0060
	 * When you set a Drop uid then that login may kill stamp process.
	 */
	wRunUid = -1;
	wRunGid = -1;
	if ((char *)0 == pcDrop) {
		pcDropGid = (char *)0;
	} else if ((char *)0 != (pcDropGid = strchr(pcDrop, ':'))) {
		*pcDropGid++ = '\000';
	}

	/* Find the owner of the socket (real uid), effective just allows
	 * the creation of the socket.  If the effective it root we
	 * can drop to anyone specifed in pcDrop.
	 */
	wOwner = getuid();
	wGroup = getgid();
	setpwent();
	if ((char *)0 != (pc = pcUid) && '\000' != *pc) {
		register struct passwd *pw;

		if ((struct passwd *)0 != (pw = getpwnam(pc))) {
			wOwner = pw->pw_uid;
		} else if ((struct passwd *)0 != (pw = getpwuid(atoi(pc)))) {
			wOwner = pw->pw_uid;
		} else {
			fprintf(stderr, "%s: getpwent: %s: not found\n", progname, pc);
			exit(EX_DATAERR);
		}
	}
	if ((char *)0 != (pc = pcDrop) && '\000' != *pc) {
		register struct passwd *pw;

		if ((struct passwd *)0 != (pw = getpwnam(pc))) {
			wRunUid = pw->pw_uid;
		} else if ((struct passwd *)0 != (pw = getpwuid(atoi(pc)))) {
			wRunUid = pw->pw_uid;
		} else {
			fprintf(stderr, "%s: getpwent: %s: not found\n", progname, pc);
			exit(EX_DATAERR);
		}
	}
	endpwent();

	getgrent();
	if ((char *)0 != (pc = pcGid) && '\000' != *pc) {
		register struct group *grp;

		if ((struct group *)0 != (grp = getgrnam(pc))) {
			wGroup = grp->gr_gid;
		} else if ((struct group *)0 != (grp = getgrgid(atoi(pc)))) {
			wGroup = grp->gr_gid;
		} else {
			fprintf(stderr, "%s: getgrent: %s: not found\n", progname, pc);
			exit(EX_DATAERR);
		}
	}
	if ((char *)0 != (pc = pcDropGid) && '\000' != *pc) {
		register struct group *grp;

		if ((struct group *)0 != (grp = getgrnam(pc))) {
			wRunGid = grp->gr_gid;
		} else if ((struct group *)0 != (grp = getgrgid(atoi(pc)))) {
			wRunGid = grp->gr_gid;
		} else {
			fprintf(stderr, "%s: getgrent: %s: not found\n", progname, pc);
			exit(EX_DATAERR);
		}
	}
	endgrent();

	if ((char *)0 != pcRemote && (char **)0 == (argv = FetchRoap(pcRemote, (struct servent *)0 == pSEConn ? 1 : htons(pSEConn->s_port), argv, (void *)0))) {
		fprintf(stderr, "%s: %s: %s\n", progname, pcRemote, strerror(errno));
		exit(EX_PROTOCOL);
	}

	/* Is there a service up? */
	if (-1 != (c = Client(pcSocket))) {
		if (0 != FlushWrite(c, "Q\n", 2)) {
			(void)unlink(pcSocket);
			/* when this fails bind might still work */
		} else
		/* service exists get back "q\n" or "p\n" */
		if (1 >= (i = read(c, acReply, sizeof(acReply)))) {
			fprintf(stderr, "%s: %s: dead socket\n", progname, pcSocket);
		} else if ('q' == acReply[0] || 'Q' == acReply[0]) {
			(void)unlink(pcSocket);
		} else {
			fprintf(stderr, "%s: %s: existing service refused to quit (%.*s)\n", progname, pcSocket, i-1, acReply);
			exit(EX_NOPERM);
		}
		close(c);
	}

	/* Make a clean service because we can't trust any old bits.
	 */
	if (-1 == (s = socket(PF_LOCAL, SOCK_STREAM, 0))) {
		fprintf(stderr, "%s: socket: %s\n", progname, strerror(errno));
		exit(EX_TEMPFAIL);
	}
	run.sun_family = AF_UNIX;
	(void)strcpy(run.sun_path, pcSocket);
	if (0 == bind(s, (struct sockaddr *)&run, sizeof(run.sun_family)+strlen(run.sun_path)+2)) {
		/* happy */
	} else if (-1 == unlink(pcSocket)) {
		fprintf(stderr, "%s: unlink: %s: %s\n", progname, pcSocket, strerror(errno));
		exit(EX_OSERR);
	} else if (-1 == bind(s, (struct sockaddr *)&run, sizeof(run.sun_family)+strlen(run.sun_path)+2)) {
		fprintf(stderr, "%s: bind: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	}
	if (-1 == listen(s, STAMP_LISTENQ)) {
		fprintf(stderr, "%s: listen: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	}
	if (-1 == chown(pcSocket, wOwner, wGroup)) {
		fprintf(stderr, "%s: chown: %s: %s\n", progname, pcSocket, strerror(errno));
		exit(EX_DATAERR);
	}
	if (-1 == chmod(pcSocket, wMode)) {
		fprintf(stderr, "%s: chmod: %s,%04o: %s\n", progname, pcSocket, wMode, strerror(errno));
		exit(EX_DATAERR);
	}
	if (-1 == pipe(aiPipe)) {
		aiPipe[0] = aiPipe[1] = -1;
	}
	fflush(stdout);

	/* make the manager process
	 */
	switch (fork()) {
	case -1:
		fprintf(stderr, "%s: fork: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
		/*NOTREACHED*/
	case 0:
		close(aiPipe[0]);
		if (-1 == chdir(pcStampDir) && -1 == chdir("/")) {
			write(aiPipe[1], "chdir failed!\n", 14);
			exit(EX_UNAVAILABLE);
		}
		if (fChRoot)
			(void)chroot(".");
		setsid();

		if (s != 0)
			close(0);
		if (s != 1)
			close(1);
		if (-1 != wRunGid)
			setgid(wRunGid);
		if (-1 != wRunUid)
			setuid(wRunUid);
		if (s != 2)
			close(2);
		close(aiPipe[1]);
		/* When max spec -0 or --100 we just deny 1, not any more;
		 * if -n10, then we deny 10, so pain intensify set to max.
		 * (The idea: a negative max might be an error, -n is not.)
		 */
		if (fGaveAllowed && iAllowed < 1) {
			fSoMuchHate = 1;
			iAllowed = -1;
		}
		if (0 == setjmp(jbQuit)) {
			Service(s, fSoMuchHate ? 'n' : 'y', fGaveAllowed ? &iAllowed : (int *)0, argv);
		}
		unlink(pcSocket);
		exit(EX_OK);
		/*NOTREACHED*/
	default:
		close(aiPipe[1]);
		if (0 < read(aiPipe[0], acReply, sizeof(acReply))) {
			fprintf(stderr, "%s: %s\n", progname, acReply);
			fflush(stderr);
			exit(EX_UNAVAILABLE);
		}
		break;
	}
	exit(EX_OK);
	/*NOTEACHED*/
}

/* We really want to buffer the whole tableau output, so we output	(ksb)
 * nothing if any of the entries do not exist.
 * start *piMaxOut=0, we'll do the rest.  Reset with (int *)0 == piMaxOut.
 */
static void
AddCh(char **ppcOut, int *piOut, int *piMaxOut, int c, int fQuote)
{
	register int iMaxOut, iOut;
	register char *pcOut;
	static int fBackSlash = 0;

	if ((int *)0 == piMaxOut) {	/* reset logic */
		fBackSlash = 0;
		if ((char **)0 != ppcOut) {
			free((void *)*ppcOut);
			*ppcOut = (char *)0;
		}
		if ((int *)0 != piOut) {
			*piOut = 0;
		}
		return;
	}
	if ((char **)0 == ppcOut || (int *)0 == piOut) {
		fprintf(stderr, "%s: pointer snark\n", progname);
		exit(EX_SOFTWARE);
	}
	if (0 >= (iMaxOut = *piMaxOut)) {
		iMaxOut = 2584;
		while (iMaxOut > 2 && (char *)0 == (*ppcOut = calloc(iMaxOut, sizeof(char)))) {
			iMaxOut /= 2;
		}
		if (iMaxOut < 3) {
			fprintf(stderr, "%s: calloc: %ld,%ld: %s\n", progname, (long)iMaxOut, (long)sizeof(char), strerror(errno));
			exit(EX_OSERR);
		}
		*piMaxOut = iMaxOut;
		*piOut = 0;
	}
	iOut = *piOut;

	if (iMaxOut <= iOut) {
		register char *pcResize;

		iMaxOut *= 89;
		iMaxOut /= 55;
		++iMaxOut;
		while (iMaxOut > iOut && (char *)0 == (pcResize = realloc((void *)*ppcOut, iMaxOut))) {
			iMaxOut -= 1024;
		}
		if ((char *)0 == pcResize || iOut+1 >= iMaxOut) {
			fprintf(stderr, "%s: realloc: %ld: %s\n", progname, (long int)iMaxOut, strerror(ENOMEM));
			exit(EX_OSERR);
		}
		*ppcOut = pcResize;
	}
	pcOut = *ppcOut;
	if (!fQuote) {
		pcOut[iOut++] = c;
	} else
	/* in quotes, look for blackslashes, cribbed from op.m ExtInput
	 */
	if (fBackSlash) {
		switch (c) {	/* \n, \", \^M \o \\ */
		case '\n': /* \^M is the empty string in sh and C */
			break;
		case 'a':
			pcOut[iOut++] = '\007'; break;
		case 'b':
			pcOut[iOut++] = '\b'; break;
		case 'd':
			pcOut[iOut++] = '"'; break;	/* op special */
		case 'f':
			pcOut[iOut++] = '\f'; break;
		case 'n':
			pcOut[iOut++] = '\n'; break;
		case 'o':
			pcOut[iOut++] = '`'; break;	/* op special */
		case 'q':
			pcOut[iOut++] = '\''; break;	/* op special */
		case 'r':
			pcOut[iOut++] = '\r'; break;
		case 's':
			pcOut[iOut++] = ' '; break;
		case 't':
			pcOut[iOut++] = '\t'; break;
		case 'v':
			pcOut[iOut++] = '\v'; break;
		default: /* includes \\ */
			pcOut[iOut++] = c; break;
		}
		fBackSlash = 0;
	} else if ('\\' == c) {
		fBackSlash = 1;
	} else {
		pcOut[iOut++] = c;
	}
	*piOut = iOut;
}

/* Find the requested tableau entries, output them as shell "words"	(ksb)
 * so   Dude="`stampctl -Q /tmp/s0 ROAP_LOGIN`"
 * does something useful.
 */
static void /*ARGSUSED*/
Query(int argc, char **argv, char *pcStamp, int fExtLike)
{
	register int s, i, iCc;
	register char *pc;
	auto char cOK, *pcOut;
	auto int fdOut, fQuote, fSep, iOut, iMaxOut;
	auto char *pcSocket;
	auto struct iovec aIOV[4];
	auto char acBlock[8192];

	if ((char *)0 == pcStamp || '\000' == pcStamp[0]) {
		pcStamp = SockSpec(pcStamp, getuid());
	}
	pcSocket = StampPath(pcStampDir, pcStamp);
	if (-1 == (s = Client(pcSocket))) {
		fprintf(stderr, "%s: no session at %s\n", progname, pcStamp);
		exit(EX_NOUSER);
	}
	pcOut = (char *)0;
	iMaxOut = iOut = fSep = 0;
	while ((char *)0 != (pc = *argv++)) {
		aIOV[0].iov_base = "$\000";
		aIOV[0].iov_len = 2;
		aIOV[1].iov_base = pc;
		aIOV[1].iov_len = strlen(pc);
		aIOV[2].iov_base = "\000\000";
		aIOV[2].iov_len = 2;
		if (aIOV[0].iov_len+aIOV[1].iov_len +aIOV[2].iov_len != writev(s, aIOV, 3) || 1 != read(s, &cOK, 1)) {
			fprintf(stderr, "%s: %s: lost connection\n", progname, pcSocket);
			exit(EX_OSERR);
		}

		/* 3 cases
		 * failed "#no NVP X\n78\n" look for /acNotThere/ above
		 * or $"VAR=value"\n (NVPReply encoded)
		 * or $VAR=value\n (NVPReply)
		 */
		fQuote = 0, fdOut = 1;
		if (acNotThere[0] == cOK) {
			fdOut = 2;
			fprintf(stderr, "%s: %s: %s: no such tableau entry\n", progname, pcStamp, pc);
			exit(78);
		}
		if (fExtLike) {
			AddCh(&pcOut, &iOut, &iMaxOut, cOK, 0);
		}
		if (1 != read(s, &cOK, 1)) {
 read_fail:
			fprintf(stderr, "%s: read: %s: %s\n", progname, pcStamp, strerror(errno));
			exit(EX_OSERR);
		}
		switch (cOK) {
		case '\"':
			if (fExtLike)
				AddCh(&pcOut, &iOut, &iMaxOut, cOK, 0);
			fQuote = !fExtLike;
			/*FALLTHROUGH*/
			do {
				if (1 != read(s, &cOK, 1)) {
					goto read_fail;
				}
		default:
				if (cOK != *pc) {
					fprintf(stderr, "%s: %s: %s: reply garbled\n", progname, pcStamp, pc);
					exit(EX_SOFTWARE);
				}
				if (fExtLike) {
					AddCh(&pcOut, &iOut, &iMaxOut, cOK, fQuote);
				}
			} while ('\000' != *++pc);
			break;
		}
		if (!fExtLike) {
			if (1 != read(s, &cOK, 1) || '=' != cOK)
				goto read_fail;
			if (fSep)
				AddCh(&pcOut, &iOut, &iMaxOut, ' ', fQuote);
			fSep = 1;
		}
		while (0 < (iCc = read(s, acBlock, sizeof(acBlock)))) {
			register int fQuit;

			/* And of message, if we are in quotes we should
			 * also remove the ending quote, it may have been
			 * in the last read
			 */
			if (0 != (fQuit = '\n' == acBlock[iCc-1])) {
				--iCc;
				if (!fQuote || fExtLike)
					/*none to remove */;
				else if (0 == iCc)
					--iOut;
				else
					--iCc;
			}
			for (i = 0; i < iCc; ++i) {
				AddCh(&pcOut, &iOut, &iMaxOut, acBlock[i], fQuote);
			}
			if (fQuit)
				break;
		}
		if (fExtLike) {
			AddCh(&pcOut, &iOut, &iMaxOut, '\n', 0);
		}
	}
	close(s);
	if (!fExtLike) {
		AddCh(&pcOut, &iOut, &iMaxOut, '\n', 0);
	}
	(void)FlushWrite(1, pcOut, iOut);
	exit(EX_OK);
	/*NOTREACHED*/
}
