1 /* $Id: rnews.c 7424 2005-10-09 05:04:12Z eagle $
3 ** A front-end for InterNetNews.
5 ** Read UUCP batches and offer them up NNTP-style. Because we may end
6 ** up sending our input down a pipe to uncompress, we have to be careful
7 ** to do unbuffered reads.
12 #include "portable/wait.h"
21 #include "inn/innconf.h"
22 #include "inn/messages.h"
30 typedef struct _HEADER {
37 static const char *InputFile = "stdin";
38 static char *UUCPHost;
39 static char *PathBadNews = NULL;
40 static char *remoteServer;
41 static FILE *FromServer;
42 static FILE *ToServer;
43 static char UNPACK[] = "gzip";
44 static HEADER RequiredHeaders[] = {
58 #define IS_MESGID(hp) ((hp) == &RequiredHeaders[_messageid])
59 #define IS_PATH(hp) ((hp) == &RequiredHeaders[_path])
64 ** Open up a pipe to a process with fd tied to its stdin. Return a
65 ** descriptor tied to its stdout or -1 on error.
68 StartChild(int fd, const char *path, const char *argv[])
76 sysdie("cannot pipe for %s", path);
79 for (i = 0; (pid = fork()) < 0; i++) {
80 if (i == innconf->maxforks) {
81 syswarn("cannot fork %s, spooling", path);
84 notice("cannot fork %s, waiting", path);
88 /* Run the child, with redirection. */
90 close(pan[PIPE_READ]);
92 /* Stdin comes from our old input. */
93 if (fd != STDIN_FILENO) {
94 if ((i = dup2(fd, STDIN_FILENO)) != STDIN_FILENO) {
95 syswarn("cannot dup2 %d to 0, got %d", fd, i);
101 /* Stdout goes down the pipe. */
102 if (pan[PIPE_WRITE] != STDOUT_FILENO) {
103 if ((i = dup2(pan[PIPE_WRITE], STDOUT_FILENO)) != STDOUT_FILENO) {
104 syswarn("cannot dup2 %d to 1, got %d", pan[PIPE_WRITE], i);
107 close(pan[PIPE_WRITE]);
110 execv(path, (char * const *)argv);
111 syswarn("cannot execv %s", path);
115 close(pan[PIPE_WRITE]);
117 return pan[PIPE_READ];
122 ** Wait for the specified number of children.
125 WaitForChildren(int n)
130 pid = waitpid(-1, NULL, WNOHANG);
131 if (pid == (pid_t) -1 && errno != EINTR) {
133 syswarn("cannot wait");
143 ** Clean up the NNTP escapes from a line.
145 static char *REMclean(char *buff)
149 if ((p = strchr(buff, '\r')) != NULL)
151 if ((p = strchr(buff, '\n')) != NULL)
154 /* The dot-escape is only in text, not command responses. */
160 ** Write an article to the rejected directory.
163 Reject(const char *article, size_t length UNUSED, const char *reason,
166 #if defined(DO_RNEWS_SAVE_BAD)
170 #endif /* defined(DO_RNEWS_SAVE_BAD) */
174 fprintf(stderr, "%s: ", InputFile);
175 fprintf(stderr, reason, arg);
176 fprintf(stderr, " [%.40s...]\n", article);
179 #if defined(DO_RNEWS_SAVE_BAD)
180 filename = concat(PathBadNews, "/XXXXXX", (char *) 0);
181 fd = mkstemp(filename);
183 warn("cannot create temporary file");
188 warn("cannot fdopen %s", filename);
191 if (fwrite(article, 1, length, F) != length)
192 warn("cannot fwrite to %s", filename);
193 if (fclose(F) == EOF)
194 warn("cannot close %s", filename);
196 #endif /* defined(DO_RNEWS_SAVE_BAD) */
201 ** Process one article. Return true if the article was okay; false if the
202 ** whole batch needs to be saved (such as when the server goes down or if
203 ** the file is corrupted).
206 Process(char *article, size_t artlen)
212 const char *id = NULL;
215 #if defined(FILE_RNEWS_LOG_DUPS)
217 #endif /* defined(FILE_RNEWS_LOG_DUPS) */
218 #if !defined(DONT_RNEWS_LOG_DUPS)
220 #endif /* !defined(DONT_RNEWS_LOG_DUPS) */
223 if (*article == '\0')
226 /* Convert the article to wire format. */
227 wirefmt = ToWireFmt(article, artlen, &length);
229 /* Make sure that all the headers are there, note the ID. */
230 for (hp = RequiredHeaders; hp < ARRAY_END(RequiredHeaders); hp++) {
231 p = wire_findheader(wirefmt, length, hp->Name);
234 Reject(article, artlen, "bad_article missing %s", hp->Name);
241 #if !defined(DONT_RNEWS_LOG_DUPS)
243 strlcpy(path, p, sizeof(path));
244 if ((q = strchr(path, '\r')) != NULL)
247 #endif /* !defined(DONT_RNEWS_LOG_DUPS) */
250 /* Send the NNTP "ihave" message. */
251 if ((p = strchr(id, '\r')) == NULL) {
253 Reject(article, artlen, "bad_article unterminated %s header",
257 msgid = xstrndup(id, p - id);
258 fprintf(ToServer, "ihave %s\r\n", msgid);
261 notice("offered %s %s", msgid, UUCPHost);
264 /* Get a reply, see if they want the article. */
265 if (fgets(buff, sizeof buff, FromServer) == NULL) {
267 if (ferror(FromServer))
268 syswarn("cannot fgets after ihave");
270 warn("unexpected EOF from server after ihave");
274 if (!CTYPE(isdigit, buff[0])) {
276 notice("bad_reply after ihave %s", buff);
279 switch (atoi(buff)) {
282 notice("unknown_reply after ihave %s", buff);
284 case NNTP_RESENDIT_VAL:
287 case NNTP_SENDIT_VAL:
289 case NNTP_HAVEIT_VAL:
290 #if defined(SYSLOG_RNEWS_LOG_DUPS)
292 notice("duplicate %s %s", id, path);
293 #endif /* defined(SYSLOG_RNEWS_LOG_DUPS) */
294 #if defined(FILE_RNEWS_LOG_DUPS)
295 if ((F = fopen(_PATH_RNEWS_DUP_LOG, "a")) != NULL) {
297 fprintf(F, "duplicate %s %s\n", id, path);
300 #endif /* defined(FILE_RNEWS_LOG_DUPS) */
305 /* Send the article to the server. */
306 if (fwrite(wirefmt, length, 1, ToServer) != 1) {
308 sysnotice("cant sendarticle");
313 /* Flush the server buffer. */
314 if (fflush(ToServer) == EOF) {
315 syswarn("cant fflush after article");
319 /* Process server reply code. */
320 if (fgets(buff, sizeof buff, FromServer) == NULL) {
321 if (ferror(FromServer))
322 syswarn("cannot fgets after article");
324 warn("unexpected EOF from server after article");
328 if (!CTYPE(isdigit, buff[0])) {
329 notice("bad_reply after article %s", buff);
332 switch (atoi(buff)) {
334 notice("unknown_reply after article %s", buff);
336 case NNTP_RESENDIT_VAL:
338 case NNTP_TOOKIT_VAL:
340 case NNTP_REJECTIT_VAL:
341 Reject(article, artlen, "rejected %s", buff);
349 ** Read the rest of the input as an article.
352 ReadRemainder(int fd, char first, char second)
364 /* Get an initial allocation, leaving space for the \0. */
366 article = xmalloc(size + 2);
369 used = second ? 2 : 1;
373 /* Read the input, coverting line ends as we go if necessary. */
374 while ((n = read(fd, buf, sizeof(buf))) > 0) {
376 for (i = 0; i < n; i++) {
379 if (buf[i] == '\n') continue;
381 if (buf[i] == '\r') {
391 article = xrealloc(article, size);
397 sysdie("cannot read after %d bytes", used);
399 if (article[used - 1] != '\n')
400 article[used++] = '\n';
401 article[used] = '\0';
403 ok = Process(article, used);
410 ** Read an article from the input stream that is artsize bytes long.
413 ReadBytecount(int fd, int artsize)
415 static char *article;
421 /* If we haven't gotten any memory before, or we didn't get enough,
423 if (article == NULL) {
425 article = xmalloc(oldsize + 1 + 1);
427 else if (artsize > oldsize) {
429 article = xrealloc(article, oldsize + 1 + 1);
432 /* Read in the article. */
433 for (p = article, left = artsize; left; p += i, left -= i)
434 if ((i = read(fd, p, left)) <= 0) {
436 warn("cannot read, wanted %d got %d", artsize, artsize - left);
438 /* Don't do this -- if the article gets re-processed we
439 * will end up accepting the truncated version. */
440 artsize = p - article;
441 article[artsize] = '\0';
442 Reject(article, "short read (%s?)", strerror(i));
452 return Process(article, artsize);
458 ** Read a single text line; not unlike fgets(). Just more inefficient.
461 ReadLine(char *p, int size, int fd)
465 /* Fill the buffer, a byte at a time. */
466 for (save = p; size > 0; p++, size--) {
467 if (read(fd, p, 1) != 1) {
469 sysdie("cannot read first line, got %s", save);
477 warn("bad_line too long %s", save);
483 ** Unpack a single batch.
486 UnpackOne(int *fdp, size_t *countp)
488 #if defined(DO_RNEWSPROGS)
489 char path[(SMBUF * 2) + 1];
491 #endif /* defined(DO_RNEWSPROGS) */
493 const char *cargv[4];
502 for (SawCunbatch = false, HadCount = false; ; ) {
503 /* Get the first character. */
504 if ((i = read(*fdp, &buff[0], 1)) < 0) {
505 syswarn("cannot read first character");
513 else if (buff[0] != '#')
514 /* Not a batch file. If we already got one count, the batch
515 * is corrupted, else read rest of input as an article. */
516 return HadCount ? false : ReadRemainder(*fdp, buff[0], '\0');
518 /* Get the second character. */
519 if ((i = read(*fdp, &buff[1], 1)) < 0) {
520 syswarn("cannot read second character");
524 /* A one-byte batch? */
527 /* Check second magic character. */
528 /* gzipped ($1f$8b) or compressed ($1f$9d) */
529 if (gzip && ((buff[1] == (char)0x8b) || (buff[1] == (char)0x9d))) {
533 lseek(*fdp, 0, 0); /* Back to the beginning */
534 *fdp = StartChild(*fdp, _PATH_GZIP, cargv);
542 return HadCount ? false : ReadRemainder(*fdp, buff[0], buff[1]);
544 /* Some kind of batch -- get the command. */
545 if (!ReadLine(&buff[2], (int)(sizeof buff - 3), *fdp))
548 if (strncmp(buff, "#! rnews ", 9) == 0) {
549 artsize = atoi(&buff[9]);
551 syswarn("bad_line bad count %s", buff);
555 if (ReadBytecount(*fdp, artsize))
561 /* Already saw a bytecount -- probably corrupted. */
564 if (strcmp(buff, "#! cunbatch") == 0) {
566 syswarn("nested_cunbatch");
572 *fdp = StartChild(*fdp, _PATH_GZIP, cargv);
580 #if defined(DO_RNEWSPROGS)
583 /* Ignore any possible leading pathnames, to avoid trouble. */
584 if ((p = strrchr(&buff[3], '/')) != NULL)
588 if (strchr(_PATH_RNEWSPROGS, '/') == NULL) {
589 snprintf(path, sizeof(path), "%s/%s/%s", innconf->pathbin,
590 _PATH_RNEWSPROGS, p);
591 len = strlen(innconf->pathbin) + 1 + sizeof _PATH_RNEWSPROGS;
593 snprintf(path, sizeof(path), "%s/%s", _PATH_RNEWSPROGS, p);
594 len = sizeof _PATH_RNEWSPROGS;
596 for (p = &path[len]; *p; p++)
601 *fdp = StartChild(*fdp, path, cargv);
607 warn("bad_format unknown command %s", buff);
609 #endif /* defined(DO_RNEWSPROGS) */
616 ** Read all articles in the spool directory and unpack them. Print all
617 ** errors with xperror as well as syslog, since we're probably being run
632 message_handlers_die(2, message_log_stderr, message_log_syslog_err);
633 message_handlers_warn(2, message_log_stderr, message_log_syslog_err);
635 /* Go to the spool directory, get ready to scan it. */
636 if (chdir(innconf->pathincoming) < 0)
637 sysdie("cannot chdir to %s", innconf->pathincoming);
638 if ((dp = opendir(".")) == NULL)
639 sysdie("cannot open spool directory");
641 /* Loop over all files, and parse them. */
642 while ((ep = readdir(dp)) != NULL) {
643 InputFile = ep->d_name;
644 if (InputFile[0] == '.')
646 if (stat(InputFile, &Sb) < 0 && errno != ENOENT) {
647 syswarn("cannot stat %s", InputFile);
651 if (!S_ISREG(Sb.st_mode))
654 if ((fd = open(InputFile, O_RDWR)) < 0) {
656 syswarn("cannot open %s", InputFile);
660 /* Make sure multiple Unspools don't stomp on eachother. */
661 if (!inn_lock_file(fd, INN_LOCK_WRITE, 0)) {
666 /* Get UUCP host from spool file, deleting the mktemp XXXXXX suffix. */
669 if ((i = strlen(InputFile)) > 6) {
671 if (i > sizeof hostname - 1)
672 /* Just in case someone wrote their own spooled file. */
673 i = sizeof hostname - 1;
674 strlcpy(hostname, InputFile, i + 1);
677 ok = UnpackOne(&fd, &i);
681 /* If UnpackOne returned true, the article has been dealt with one way
682 or the other, so remove it. Otherwise, leave it in place; either
683 we got an unknown error from the server or we got a deferral, and
684 for both we want to try later. */
686 if (unlink(InputFile) < 0)
687 syswarn("cannot remove %s", InputFile);
694 message_handlers_die(1, message_log_syslog_err);
695 message_handlers_warn(1, message_log_syslog_err);
701 ** Can't connect to the server, so spool our input. There isn't much
702 ** we can do if this routine fails, unfortunately. Perhaps try to use
703 ** an alternate filesystem?
706 Spool(int fd, int mode)
711 char *tmpspool, *spoolfile, *p;
718 tmpspool = concat(innconf->pathincoming, "/.",
719 UUCPHost ? UUCPHost : "", "XXXXXX", (char *)0);
720 spfd = mkstemp(tmpspool);
722 sysdie("cannot create temporary batch file %s", tmpspool);
723 if (fchmod(spfd, BATCHFILE_MODE) < 0)
724 sysdie("cannot chmod temporary batch file %s", tmpspool);
726 /* Read until we there is nothing left. */
727 for (status = 0, count = 0; (i = read(fd, buff, sizeof buff)) != 0; ) {
728 /* Break out on error. */
730 syswarn("cannot read after %d", count);
734 /* Write out what we read. */
735 for (count += i, p = buff; i; p += j, i -= j)
736 if ((j = write(spfd, p, i)) <= 0) {
737 syswarn("cannot write around %d", count);
743 /* Close the file. */
744 if (close(spfd) < 0) {
745 syswarn("cannot close spooled article %s", tmpspool);
749 /* Move temp file into the spool area, and exit appropriately. */
750 spoolfile = concat(innconf->pathincoming, "/",
751 UUCPHost ? UUCPHost : "", "XXXXXX", (char *)0);
752 spfd = mkstemp(spoolfile);
754 syswarn("cannot create spool file %s", spoolfile);
758 if (rename(tmpspool, spoolfile) < 0) {
759 syswarn("cannot rename %s to %s", tmpspool, spoolfile);
771 ** Try to read the password file and open a connection to a remote
774 static bool OpenRemote(char *server, int port, char *buff)
778 /* Open the remote connection. */
780 i = NNTPconnect(server, port, &FromServer, &ToServer, buff);
782 i = NNTPremoteopen(port, &FromServer, &ToServer, buff);
787 if (NNTPsendpassword(server, FromServer, ToServer) < 0) {
799 ** Can't connect to server; print message and spool if necessary.
802 CantConnect(char *buff, int mode, int fd)
805 notice("rejected connection %s", REMclean(buff));
807 syswarn("cant open_remote");
814 int main(int ac, char *av[])
821 int port = NNTP_PORT;
823 /* First thing, set up logging and our identity. */
824 openlog("rnews", L_OPENLOG_FLAGS, LOG_INN_PROG);
825 message_program_name = "rnews";
826 message_handlers_notice(1, message_log_syslog_notice);
827 message_handlers_warn(1, message_log_syslog_err);
828 message_handlers_die(1, message_log_syslog_err);
830 /* The reason for the following is somewhat obscure and is done only
831 because rnews is sometimes installed setuid.
833 The stderr stream used by message_log_syslog_err is associated with
834 file descriptor 2, generally even if that file descriptor is closed.
835 Someone running rnews may close all of the standard file descriptors
836 before running it, in which case, later in its operations, one of the
837 article files or network connections it has open could be file
838 descriptor 2. If an error occurs at that point, the error message may
839 be written to that file or network connection instead of to stderr,
840 with unpredictable results.
842 We avoid this by burning three file descriptors if the real and
843 effective user IDs don't match, or if we're running as root. (If they
844 do match, there is no escalation of privileges and at worst the user is
845 just managing to produce a strange bug.) */
846 if (getuid() != geteuid() || geteuid() == 0) {
847 if (open("/dev/null", O_RDONLY) < 0)
848 sysdie("cannot open /dev/null");
849 if (open("/dev/null", O_RDONLY) < 0)
850 sysdie("cannot open /dev/null");
851 if (open("/dev/null", O_RDONLY) < 0)
852 sysdie("cannot open /dev/null");
855 /* Make sure that we switch to the news user if we're running as root,
856 since we may spool files and don't want those files owned by root.
857 Don't require that we be running as the news user, though; there are
858 other setups where rnews might be setuid news or be run by other
859 processes in the news group. */
860 if (getuid() == 0 || geteuid() == 0) {
863 pwd = getpwnam(NEWSUSER);
865 die("can't resolve %s to a UID (account doesn't exist?)",
870 if (!innconf_read(NULL))
872 UUCPHost = getenv(_ENV_UUCPHOST);
873 PathBadNews = concatpath(innconf->pathincoming, _PATH_BADNEWS);
874 port = innconf->nnrpdpostport;
881 while ((i = getopt(ac, av, "h:P:NUvr:S:")) != EOF)
887 UUCPHost = *optarg ? optarg : NULL;
901 remoteServer = optarg;
907 /* Parse arguments. At most one, the input file. */
917 if (freopen(av[0], "r", stdin) == NULL)
918 sysdie("cannot freopen %s", av[0]);
924 /* Open the link to the server. */
925 if (remoteServer != NULL) {
926 if (!OpenRemote(remoteServer,port,buff))
927 CantConnect(buff,mode,fd);
928 } else if (innconf->nnrpdposthost != NULL) {
929 if (!OpenRemote(innconf->nnrpdposthost,
930 (port != NNTP_PORT) ? port : innconf->nnrpdpostport, buff))
931 CantConnect(buff, mode, fd);
934 #if defined(DO_RNEWSLOCALCONNECT)
935 if (NNTPlocalopen(&FromServer, &ToServer, buff) < 0) {
936 /* If server rejected us, no point in continuing. */
938 CantConnect(buff, mode, fd);
939 if (!OpenRemote((char *)NULL,
940 (port != NNTP_PORT) ? port : innconf->port, buff))
941 CantConnect(buff, mode, fd);
944 if (!OpenRemote((char *)NULL,
945 (port != NNTP_PORT) ? port : innconf->port, buff))
946 CantConnect(buff, mode, fd);
947 #endif /* defined(DO_RNEWSLOCALCONNECT) */
949 close_on_exec(fileno(FromServer), true);
950 close_on_exec(fileno(ToServer), true);
952 /* Execute the command. */
956 if (!UnpackOne(&fd, &count)) {
961 WaitForChildren(count);
964 /* Tell the server we're quitting, get his okay message. */
965 fprintf(ToServer, "quit\r\n");
967 fgets(buff, sizeof buff, FromServer);
969 /* Return the appropriate status. */