/* $Id: rfc1078.c,v 1.15 2007/06/18 13:59:56 ksb Exp $ * We implement the RRC 1078 (tcpmux) service for inetd's that don't. * We should be installed in inted.conf as a nowait stream service: * tcpmux stream tcp nowait root /usr/libexec/tcpmux * * Just take the inetd.conf line for each muxed service and put it in * our config file. -- ksb * tcpmux/+date stream tcp nowait nobody /bin/date date * * See FreeBSD's inetd.conf manual page, as well as the RFC at * http://www.ietf.org/rfc/rfc1078.txt */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "machine.h" /* Figure a reasonable max for a muxed service name; * "tcpmux/+name" is 2 service names + "/" and "+" and "\000", so * 2 * NI_MAXSERV + 4 is a fine default. */ #if !defined(MAX_MUX_NAME_LEN) #define MAX_MUX_NAME_LEN (256+4) /* common + \r\n + \000 */ #endif #if !defined(TCPMUX_CONFIG) #define TCPMUX_CONFIG "/etc/tcpmux.conf" #endif static char *progname = "tcpmux"; static char rcsid[] = "$Id: rfc1078.c,v 1.15 2007/06/18 13:59:56 ksb Exp $"; static char acConfig[] = TCPMUX_CONFIG; static char *pcConfig = acConfig; static char *apcConfOpts[] = { #if RFC1078_REDIR "redirection", #endif #if RFC1078_RECURSE "recursive calls", #endif (char *)0 }; /* We are trying to read a token for the service we are de-muxing: (ksb) * we know we have to read at least 3 characters initially and at least * 2 characters if the last character was on a '\r' or '\n'. But we are * more liberal that that, we'll take just '\n' on the end so we drop to * 2 characters initally and 1 for each subsequent read. The called set * an alarm to kill us if we never get any data. * t o k e n [\r] \n */ static void FetchService(fd, pcOut, iLen) int fd, iLen; char *pcOut; { register int cc, iTake; iTake = 2; do { cc = read(fd, pcOut, iTake); if (1 > cc) { exit(1); } while (cc-- > 0) { if ('\n' == *pcOut) { *pcOut = '\000'; return; } else if (isupper(*pcOut)) { *pcOut = tolower(*pcOut); } if ('\r' == *pcOut) { if (0 != cc) *pcOut = pcOut[1]; } else { pcOut++, --iLen; } } iTake = 1; } while (iLen > 0); exit(2); } #if NEED_FGETLN /* return a pointer to the next line in the stream (ksb) * set *piRet = strlen(the_line); but NO '\000' is appended * this is really cheap if implemented in stdio.h, but not so * cool when we have to emulate it. Sigh. * * N.B. We grab RAM in 128 byte increments, because we never free it. * [the stdio implementation frees it when you fclose() the fp, of course] */ char * fgetln(fp, piRet) FILE *fp; size_t *piRet; { static char *pcFGBuf = (char *)0; static size_t iSize = 0; register size_t iRead; register int c; register char *pc; /* realloc test */ if (0 == iSize) { iSize = 256; pcFGBuf = calloc(iSize, sizeof(char)); } /* inv. the buffer has room for the next character at least */ for (iRead = 0; EOF != (c = getc(fp)); /* nothing */) { if ('\n' == (pcFGBuf[iRead++] = c)) break; if (iRead < iSize) continue; iSize += 128; pc = realloc(pcFGBuf, iSize * sizeof(char)); if ((char *)0 == pc) return (char *)0; pcFGBuf = pc; } pcFGBuf[iRead] = '\000'; if ((size_t *)0 != piRet) *piRet = iRead; return (0 == iRead) ? (char *)0 : pcFGBuf; } #endif /* emulation for fgetln from mkcmd's lib -- ksb */ /* Read lines from an inetd.confg style configuration looking for (ksb) * the one we were asked to chain to, e.g.: * tcpmux/+date stream tcp nowait nobody /bin/date date * we have to match "tcp" and "stream", we ignore the wait param. * we ignore the prefix "tcpmux/" on all lines, and take "+" as FreeBSD's * inetd internal MUX does. */ typedef struct TXnode { char cack; /* + or - for auto-ack feature */ char *pccred; /* user[:group][/login-class] */ char *pcexec; /* binary to run */ char *pcargv; /* arguments */ } TCPMUX_PARAM; static int SearchConfig(pcReply, pcService, fpConfig, pTX) char *pcReply, *pcService; FILE *fpConfig; TCPMUX_PARAM *pTX; { register char *pcLine; register int iServLen; auto int cAck, iHelp; auto size_t iLen; static char acVulgar[] = "tcpmux/"; pTX->cack = cAck = '\000'; pTX->pccred = pTX->pcexec = pTX->pcargv = (char *)0; iServLen = strlen(pcService); iHelp = (0 == strcmp(pcService, "help")); while ((char *)0 != (pcLine = fgetln(fpConfig, & iLen))) { if ('\n' == pcLine[iLen-1]) { pcLine[iLen-1] = '\000'; } while (isspace(*pcLine)) { ++pcLine; } if ('\000' == *pcLine || '#' == *pcLine) { continue; } if (0 == strncasecmp(acVulgar, pcLine, sizeof(acVulgar)-1)) { pcLine += sizeof(acVulgar)-1; } #if RFC1078_REDIR if ('@' == *pcLine) cAck = *pcLine++; else #endif #if RFC1078_RECURSE if ('<' == *pcLine) cAck = *pcLine++; else #endif if ('+' == *pcLine || '-' == *pcLine) cAck = *pcLine++; else cAck = '\000'; if (iHelp) { register char *pcEnd; for (pcEnd = pcLine; '\000' != *pcEnd; ++pcEnd) { if (!isspace(*pcEnd)) continue; *pcEnd++ = '\r'; *pcEnd++ = '\n'; break; } write(1, pcLine, pcEnd-pcLine); continue; } if (0 != strncasecmp(pcLine, pcService, iServLen) || !isspace(pcLine[iServLen])) { continue; } pcLine += iServLen; break; } if (iHelp) { exit(0); } if ((char *)0 == pcLine) { sprintf(pcReply, "-%s not implemented\r\n", pcService); return 1; } /* We found the line, break it up from the name on * skip \s stream \s tcp \s nowait \s */ while (isspace(*pcLine)) ++pcLine; #if RFC1078_REDIR /* We are trusting our config file to have no hostnames + ports * longer than MAXPATHLEN+change (ksb) */ if ('@' == cAck) { sprintf(pcReply, "%c%s\r\n", cAck, pcLine); return 2; } #endif for (iHelp = 0; iHelp < 3; ++iHelp) { while ('\000' != *pcLine && !isspace(*pcLine)) ++pcLine; while (isspace(*pcLine)) ++pcLine; } pTX->cack = cAck; pTX->pccred = pcLine; while ('\000' != *pcLine && !isspace(*pcLine)) ++pcLine; while (isspace(*pcLine)) *pcLine++ = '\000'; pTX->pcexec = pcLine; while ('\000' != *pcLine && !isspace(*pcLine)) ++pcLine; while (isspace(*pcLine)) *pcLine++ = '\000'; /* avoid the free when we fclose this file --ksb */ pTX->pcargv = strdup(pcLine); pTX->pcexec = strdup(pTX->pcexec); pTX->pccred = strdup(pTX->pccred); return 0; } /* Split the args given into an argv (white space sep). (ksb) * Count as if every space character made a new argument (adjacents do not); * the fill in the pointers to the first character in each parameter. */ static char ** BreakArgs(pcArgs) char *pcArgs; { register int i, iMax; register char **ppcArgs, *pcCursor; iMax = 1; for (pcCursor = pcArgs; '\000' != *pcCursor; ++pcCursor) { if (isspace(*pcCursor)) ++iMax; } iMax |= 3; if ((char **)0 == (ppcArgs = calloc(iMax+1, sizeof(char *)))) { return (char **)0; } i = 0; ppcArgs[0] = (char *)0; for (pcCursor = pcArgs; '\000' != *pcCursor; ++pcCursor) { if (isspace(*pcCursor)) { *pcCursor = '\000'; if ((char *)0 != ppcArgs[i]) { ppcArgs[++i] = (char *)0; } continue; } if ((char *)0 == ppcArgs[i]) { ppcArgs[i] = pcCursor; } } if ((char *)0 != ppcArgs[i]) ++i; ppcArgs[i] = (char *)0; return ppcArgs; } /* drop to the creds given (ksb) * login [:group][/login-class] */ static void DropCreds(pcReply, pcLogin, ppcHome) char *pcReply, *pcLogin, **ppcHome; { register char *pcGroup, *pcClass; register struct passwd *pwd; register struct group *grp; auto uid_t wUid; auto gid_t wGid; if ((char *)0 != (pcClass = strchr(pcLogin, '/'))) { *pcClass++ = '\000'; } if ((char *)0 != (pcGroup = strchr(pcLogin, ':')) || (char *)0 != (pcGroup = strchr(pcLogin, '.'))) { *pcGroup++ = '\000'; } setgrent(); setpwent(); if ('*' == pcLogin[0] && '\000' == pcLogin[1]) { wUid = getuid(); wGid = getgid(); } else if ((struct passwd *)0 == (pwd = getpwnam(pcLogin))) { sprintf(pcReply, "-%s no such login\r\n", pcLogin); return; } else { wUid = pwd->pw_uid; wGid = pwd->pw_gid; if ((char **)0 != ppcHome) { *ppcHome = strdup(pwd->pw_dir); } } endpwent(); if ((char *)0 == pcGroup || ('*' == pcGroup[0] && '\000' == pcGroup[1])) { /* don't change it */ } else if ((struct group *)0 == (grp = getgrnam(pcGroup))) { sprintf(pcReply, "-%s no such group\r\n", pcGroup); return; } else { wGid = grp->gr_gid; } endgrent(); /* XXX login class doesn't exist on most systems */ if (-1 == setgid(wGid)) { sprintf(pcReply, "-setgid(%d) failed\r\n", (int)wGid); return; } if (-1 == setuid(wUid)) { sprintf(pcReply, "-setuid(%d) failed\r\n", (int)wUid); return; } } /* Find the new config file for a pw_dir; } if ((char *)0 != pcHome) { #if HAVE_SNPRINTF snprintf(pcReply, MAXPATHLEN, "%s/%s", pcHome, pcNamed); #else sprintf(pcReply, "%s/%s", pcHome, pcNamed); #endif } endpwent(); return 0; } /* look before we leap, because the error we get back is not so good (ksb) */ static void StatProgram(pcReply, pcExec) char *pcReply, *pcExec; { auto struct stat stThis; if (-1 == stat(pcExec, & stThis)) { sprintf(pcReply, "-%s doesn't exist\r\n", pcExec); } else if (! S_ISREG(stThis.st_mode)) { sprintf(pcReply, "-%s not a plain file\r\n", pcExec); } else if (0 == (stThis.st_mode & 00111)) { sprintf(pcReply, "-%s not executable\r\n", pcExec); } } /* Tell the client if the entty is marked with a "+" or "-" prefix (ksb) * strangely this allows a named service to always fail (viz. -false). */ static void NotifyClient(pcReply, cAck) char *pcReply, cAck; { if ('\000' == cAck) return; sprintf(pcReply, "%c%s\r\n", cAck, '+' == cAck ? "Go" : "Sorry"); } /* We were just run from inetd (more than likely) so stdin/stdout (ksb) * are attached to the network peer [client] and we need to find a * service to render to them. * * We know the service name is at least 3 charcters in the input, since * empty service names are not mentioned as valid. */ int main(argc, argv, envp) int argc; char **argv, **envp; { auto char acReply[1024+MAXPATHLEN], **ppcNewArgs, *pcHome; auto char acService[MAX_MUX_NAME_LEN]; auto TCPMUX_PARAM TXFound; auto FILE *fpConfig; if ((char *)0 == argv[0]) { /* leave progname */; } else if ((char *)0 == (progname = strrchr(argv[0], '/'))) { progname = argv[0]; } else { ++progname; } if (argc > 1 && (char *)0 != argv[1]) { auto struct stat stConf; auto char *pcNote; auto char acMode[(sizeof("[mode ____]")|15)+1]; if ('-' != argv[1][0] || 'V' != argv[1][1] || '\000' != argv[1][2]) { fprintf(stderr, "%s: usage [-h|-V]\n", progname); exit('h' != argv[1][1]); } pcNote = ""; if (-1 == stat(pcConfig, &stConf)) { pcNote = strerror(errno); } else if (!S_ISREG(stConf.st_mode)) { pcNote = "not a plain file"; } else { #if HAVE_SNPRINTF snprintf(acMode, sizeof(acMode), "mode %04o", stConf.st_mode & 07777); #else sprintf(acMode, "mode %04o", stConf.st_mode & 07777); #endif pcNote = acMode; } printf("%s: %s\n", progname, rcsid); printf("%s: configfile: `%s' [%s]\n", progname, pcConfig, pcNote); if ((char *)0 != apcConfOpts[0]) { register int i; pcNote = ": "; printf("%s: compile options", progname); for (i = 0; (char *)0 != apcConfOpts[i]; ++i) { printf("%s%s", pcNote, apcConfOpts[i]); pcNote = ", "; } printf("\n"); } exit(0); } alarm(20); /* we won't wait for more than 20 seconds for this */ FetchService(0, acService, sizeof(acService)); alarm(0); /* read service entry from our config file */ if ((FILE *)0 == (fpConfig = fopen(pcConfig, "r"))) { sprintf(acReply, "-No config file \"%s\"\r\n", pcConfig); write(1, acReply, strlen(acReply)); exit(0); } acReply[0] = '\000'; if (0 != SearchConfig(acReply, acService, fpConfig, & TXFound)) { write(1, acReply, strlen(acReply)); exit(0); } (void)fclose(fpConfig); /* We know what to do now * drop creds (if we can) * [recurse if we've been dropped to another user] * stat the program we are about to run (carp if -exist or -x) * notify the client * exec the program with the args set. */ acReply[0] = '\000'; if ((char **)0 == (ppcNewArgs = BreakArgs(TXFound.pcargv))) { sprintf(acReply, "-Unable to make new argv\r\n"); } pcHome = (char *)0; if ('\000' == acReply[0]) { DropCreds(acReply, TXFound.pccred, & pcHome); } #if RFC1078_RECURSE /* We accept the redirection to the mortal user's command set *