/* * relaynews - relay Usenet news (version C) * See the file COPYRIGHT for the copyright notice. * * relaynews should be setuid-news and setgid-news on systems that lack a * mkdir system call, thus requiring the invocation of a setuid-root mkdir * command. You'll need to install setnewsids setuid-root if * setuid(geteuid()) doesn't work on such systems (e.g. on V7 and possibly * System III). * * Written by Geoff Collyer, 15-20 November 1985 and revised periodically * since. * * relaynews parses article headers, rejects articles by newsgroup & * message-id, files articles, updates the active & history files, * transmits articles, and honours (infrequent) control messages, which do * all sorts of varied and rococo things. Control messages are implemented * by separate programs. relaynews reads a "sys" file to control the * transmission of articles but can function as a promiscuous leaf node * without one. See Internet RFC 1036 for the whole story and RFC 850 * for background. * * A truly radical notion: people may over-ride via environment variables * the compiled-in default directories so IHCC kludges are not needed and * testing is possible (and encouraged) in alternate directories. This * does cause a loss of privilege, to avoid spoofing. * * The disused old unbatched ihave/sendme protocol is gone because it was * too wasteful; use the batched form instead (see the ihave sys flag * ("I") instead). */ #include #include #include #include /* to make locking safe */ #include #include "fixerrno.h" #include #include #include "libc.h" #include "news.h" #include "config.h" #include "fgetmfs.h" #include "active.h" #include "caches.h" #include "fileart.h" #include "headers.h" #include "history.h" #include "transmit.h" /* * setuid-root program to set ids to news/news & re-exec rnews with * NEWSPERMS in the environment to break loops. */ #ifndef SETNEWSIDS #define SETNEWSIDS "setnewsids" #endif /* exports */ char *progname = "relaynews"; boolean okrefusal = YES; /* okay to refuse articles? */ char *exclude = NULL; /* site to exclude, for erik */ boolean histreject = NO; /* keep history of rejects? */ long staledays = 0; /* articles stale after this many days */ boolean genxref = NO; /* iff true, always generate Xref: */ boolean blvxref = NO; /* iff true, believe Xref: contents */ char *blvsite = NULL; /* ... for this site only. */ boolean dupsokay = NO; /* iff true, allow duplicates ... */ char *dupsite = NULL; /* ... from this site only. */ /* internal */ static boolean userealids = NO; static boolean uunlink = NO; static char incsfx[] = "in.coming"; /* need this for -u, alas */ /* imports */ extern int optind; /* set by getopt */ extern char *optarg; extern statust cpinsart(); /* from procart.c */ /* forwards */ extern void prelude(), setids(), procopts(), redirectlogs(), logfile(); extern void getwdandcd(); extern statust procargs(), relnmprocess(), process(), unbatch(); extern boolean batchln(); FORWARD boolean debugon(); FORWARD long maxlong(); /* * main - take setuid precautions, switch to "news" ids, ignore signals, * handle options, lock news system, process files & unlock news system. */ int main(argc, argv) int argc; char *argv[]; { statust status = ST_OKAY; int redirlogs = 0; /* redirect n std output streams to logs */ char *origdir = NULL; /* current directory at start */ if (argc > 0) progname = argv[0]; #ifdef CSRIMALLOC mal_debug(0); /* was 2; 3 is too slow */ mal_leaktrace(0); /* was 1 */ #endif prelude(argv); /* various precautions; switch to "news" */ /* ignore signals (for locking). relaynews runs quickly, so don't worry. */ (void) signal(SIGINT, SIG_IGN); (void) signal(SIGQUIT, SIG_IGN); (void) signal(SIGHUP, SIG_IGN); (void) signal(SIGTERM, SIG_IGN); (void) signal(SIGPIPE, SIG_IGN); /* we check write returns */ procopts(argc, argv, &origdir, &redirlogs, &okrefusal); (void) morefds(); /* ask Unix for more descriptors */ newslock(); /* done here due to dbm internal cacheing */ if (redirlogs > 0) { redirectlogs(redirlogs); /* redirect std output streams to logs */ #ifdef MANYERRORS (void) putc('\n', stderr); /* leave a blank line */ /* prints "Jun 5 12:34:56" */ timestamp(stderr, (time_t *)NULL); (void) putc('\n', stderr); #endif } getwdandcd(argc, argv, &origdir); status |= procargs(argc, argv, &origdir); status |= synccaches(); /* being cautious: write & close caches */ status |= closehist(); (void) fflush(stdout); /* log file */ (void) fflush(stderr); /* errlog file */ #ifdef notdef #ifdef CSRIMALLOC mal_dumpleaktrace(fileno(stderr)); #endif #endif newsunlock(); exit(status); /* NOTREACHED */ } /* * reset various environmental things for safety: umask, alarm, * environment variables (PATH, IFS), standard file descriptors, * user & group ids. */ void prelude(argv) /* setuid daemon prelude */ char **argv; { register char *newpath; (void) umask(newsumask()); (void) alarm(0); /* cancel any pending alarm */ /* TODO: suppress chatter, lock freeing on failure here */ newpath = str3save("PATH=", newspath(), ""); if (newpath == NULL) exit(1); /* no chatter until stdfdopen */ if (putenv(newpath) || putenv("SHELL=/bin/sh") || putenv("IFS= \t\n")) exit(1); /* no chatter until stdfdopen */ closeall(1); /* closes all but std descriptors */ stdfdopen(); /* ensure open standard descriptors */ setids(argv); /* change of real and effective ids */ } /* * change real and effective ids to real ids if unprivileged() is called, * else to effective ("news") ids. ctlfile((char *)0) will trigger a call * to unprivileged() if any environment variables override the default * path names. unprivileged() in turn sets userealids. * * If setuid(geteuid()) fails, try execing a small, setuid-root program * to just do "getpwnam(), getgrnam() (with NEWSPERMS set), setgid(), * setuid()," and exec this program again. If NEWSPERMS is set, * the failure is a fatal error (recursive loop). * This program (relaynews) can be setuid-news. * * The peculiar tests for failure (getuid() != newsuid) are to work * around a Xenix bug which returns 0 from setuid() upon failure. */ void setids(argv) char **argv; { int newsuid, newsgid; (void) ctlfile((char *)NULL); if (userealids) newsuid = getuid(), newsgid = getgid(); else newsuid = geteuid(), newsgid = getegid(); if (setgid(newsgid) < 0 || setuid(newsuid) < 0 || getgid() != newsgid || getuid() != newsuid) { if (getenv("NEWSPERMS") != 0) error("recursive loop setting ids", ""); /* * normally we would use execvp, but for security SETNEWSIDS * had better be an absolute path to a binary. */ /* TODO: ctlfile -> binfile */ execv(ctlfile(SETNEWSIDS), argv); error("can't exec `%s' to set ids", ctlfile(SETNEWSIDS)); /* NOTREACHED */ } /* we are now running as news, so you can all relax */ } /* * parse options and set flags */ void procopts(argc, argv, origdirp, redirlogsp, okrefusalp) int argc; char **argv; char **origdirp; int *redirlogsp; boolean *okrefusalp; { int c, errflg = 0; char *incdir; while ((c = getopt(argc, argv, "a:b:d:gino:rsx:u")) != EOF) switch (c) { case 'a': dupsokay = YES; dupsite = optarg; break; case 'b': blvxref = YES; blvsite = optarg; break; case 'd': /* -d debug-options; thanks, henry */ if (!debugon(optarg)) errflg++; /* debugon has complained */ break; case 'g': genxref = YES; break; case 'i': /* redirect stdout to log (inews) */ *redirlogsp = 1; /* just stdout */ break; case 'n': /* nntp mode: keep history of rejects */ histreject = YES; break; case 'o': /* "oldness": drop articles older than this many days */ staledays = atol(optarg); break; case 'r': /* redirect std. ostreams to logs (rnews) */ *redirlogsp = 2; /* stdout & stderr */ break; case 's': /* dropping input is serious (inews) */ *okrefusalp = NO; break; case 'u': /* unlink good batches when done */ incdir = fullartfile(incsfx); if (cwdis(incdir)) { uunlink = YES; *origdirp = strsave(incdir); } else (void) fprintf(stderr, "%s: -u ignored - current directory is not %s\n", progname, incdir); break; case 'x': /* -x site: don't send to site */ /* you're welcome, erik */ /* erik says he only needs one -x per inews */ if (exclude != NULL) { (void) fprintf(stderr, "%s: more than one -x site (%s)\n", progname, optarg); errflg++; } else exclude = optarg; break; default: errflg++; break; } if (errflg) { (void) fprintf(stderr, "usage: %s [-ginrsu][-d fhlmt][-x site][-o days][-b xrefsite][-a dupsite]\n", progname); exit(1); } } /* * called if NEWSARTS, NEWSCTL, NEWSBIN, etc. are non-standard. * this may be due to legitimate testing, but we can't tell. * the error message will at least be seen by a human trying to * track down a problem, even if stderr isn't normally seen. */ void unprivileged(reason) char *reason; { userealids = YES; (void) fprintf(stderr, "%s: warning: renouncing setuid due to nonstandard `%s' in environment\n", progname, reason); } STATIC boolean debugon(dbopt) register char *dbopt; { statust status = YES; for (; *dbopt != '\0'; dbopt++) switch (*dbopt) { case 'f': filedebug(YES); break; case 'h': hdrdebug(YES); break; case 'l': lockdebug(YES); break; case 'm': matchdebug(YES); break; case 't': transdebug(YES); break; default: status = NO; /* unknown debugging option */ (void) fprintf(stderr, "%s: bad -d %c\n", progname, *dbopt); break; } return status; } /* * Redirect stdout or stderr into log files at known locations. */ void redirectlogs(count) int count; { if (count > 0) logfile(stdout, ctlfile("log")); if (count > 1) logfile(stderr, ctlfile("errlog")); } void logfile(stream, name) /* redirect stream into name */ FILE *stream; char *name; { if (freopen(name, "a", stream) == NULL) errunlock("can't redirect standard stream to `%s'", name); } /* * if argv contains relative file name arguments, save current directory name * in malloced memory, through origdirp. * then change directory to the spool directory ($NEWSARTS). */ void getwdandcd(argc, argv, origdirp) int argc; char **argv; char **origdirp; { register int argind; boolean needpwd = NO; char dirtmp[MAXPATH]; /* much bigger than needed */ if (*origdirp == NULL) { /* not yet set? */ for (argind = optind; argind < argc; argind++) if (argv[argind][0] != FNDELIM) needpwd = YES; *origdirp = "/???"; /* pessimism */ if (needpwd && getcwd(dirtmp, sizeof dirtmp) != 0) *origdirp = dirtmp; *origdirp = strsave(*origdirp); /* save a smaller copy */ } cd(fullartfile((char *)NULL)); /* move to spool directory */ } int /* actually boolean */ cwdis(dir) char *dir; { struct stat dotstat, dirstat; static char dot[] = "."; /* name of current dir. */ if (stat(dir, &dirstat) < 0) { warning("no %s directory", dir); return NO; } if (stat(dot, &dotstat) < 0) { warning("no %s directory", dot); return NO; } return dotstat.st_dev == dirstat.st_dev && dotstat.st_ino == dirstat.st_ino; } /* * process files named as arguments (or implied) */ statust procargs(argc, argv, origdirp) int argc; char **argv; char **origdirp; { register statust status = ST_OKAY; if (optind == argc) status |= process(stdin, "(stdin)"); else for (; optind < argc; optind++) status |= relnmprocess(argv[optind], *origdirp); nnfree(origdirp); return status; } statust relnmprocess(name, origdir) /* process a (relative) file name */ char *name, *origdir; { register statust status = ST_OKAY; register FILE *in; register char *fullname = (name[0] != FNDELIM? str3save(origdir, SFNDELIM, name): strsave(name)); in = fopenwclex(fullname, "r"); if (in != NULL) { status |= process(in, fullname); (void) nfclose(in); /* * in unlink mode, try to unlink good batches in a * known-safe place, but not very hard. * main() ensured that relative names are relative to * $NEWSARTS/in.coming. */ if (uunlink && !(status&(ST_DROPPED|ST_SHORT)) && strchr(name, FNDELIM) == NULL) { char *incname = str3save(incsfx, SFNDELIM, name); (void) unlink(incname); free(incname); } } free(fullname); return status; } /* * process - process input file * If it starts with '#', assume it's a batch and unravel it, * else it's a single article, so just inject it. */ statust process(in, inname) FILE *in; char *inname; { register int c; if ((c = getc(in)) == EOF) return ST_OKAY; /* normal EOF */ (void) ungetc(c, in); if (c == '#') return unbatch(in, inname); else /* -SIZENUL is to avoid overflow later during +SIZENUL */ return cpinsart(in, inname, maxlong() - SIZENUL, NO); } /* * compute the largest number that can be stored in a long. in theory, * #define MAXLONG ((long)(~(unsigned long)0 >> 1)) * will do the job, but old compilers don't have "unsigned long", don't * like casts in initialisers, or otherwise miscompute. */ STATIC long maxlong() { register int bits = 0; register unsigned word = 1; /* "unsigned" avoids overflow */ static long savemaxlong = 0; if (savemaxlong > 0) return savemaxlong; for (bits = 0, word = 1; word != 0; word <<= 1) bits++; /* bits/sizeof word = bits per char; all bits on but the sign bit */ savemaxlong = ~(1L << (bits/sizeof word * sizeof savemaxlong - 1)); if (savemaxlong <= 0) { /* sanity check */ errno = 0; errunlock("maxlong is non-positive; your compiler is broken", ""); } return savemaxlong; } /* * Unwind "in" and insert each article. * For each article, call cpinsart to copy the article from "in" into * a (temporary) file in the news spool directory and rename the temp file * to the correct final name if it isn't right already. * * If the unbatcher gets out of sync with the input batch, the unbatcher * will print and discard each input line until it gets back in sync. */ statust unbatch(in, inname) register FILE *in; char *inname; { register int c; /* register */ char *line; register statust status = ST_OKAY; long charcnt; while (!(status&ST_NEEDATTN) && (c = getc(in)) != EOF) { (void) ungetc(c, in); while ((line = fgetms(in)) != NULL && !batchln(line, &charcnt)) { /* returns charcnt */ status |= ST_DROPPED; (void) fprintf(stderr, "%s: unbatcher out of synch, tossing: ", progname); (void) fputs(line, stderr); free(line); } nnfree(&line); /* free "#! rnews n" */ if (!feof(in) && charcnt > 0) /* anything to do? */ status |= cpinsart(in, inname, charcnt, YES); } if (ferror(in)) errunlock("error reading `%s'", inname); return status; } /* * Is line a batcher-produced line (#! rnews count)? * If so, return the count through charcntp. * This is slightly less convenient than sscanf, but a lot smaller. */ boolean batchln(line, charcntp) register char *line; register long *charcntp; { register char *countp; static char batchtext[] = "#! rnews "; countp = line + STRLEN(batchtext); if (STREQN(line, batchtext, STRLEN(batchtext)) && isascii(*countp) && isdigit(*countp)) { *charcntp = atol(countp); return YES; } else { *charcntp = 0; return NO; } }