1 /*
2 **  Send an article (prepared by someone on the local site) to the
3 **  master news server.
4 */
5 
6 #include "portable/system.h"
7 
8 #include <ctype.h>
9 #include <errno.h>
10 #include <fcntl.h>
11 #include <pwd.h>
12 #include <sys/stat.h>
13 
14 #ifdef HAVE_SYS_TIME_H
15 #    include <sys/time.h>
16 #endif
17 #include <time.h>
18 
19 #include "inn/innconf.h"
20 #include "inn/libinn.h"
21 #include "inn/messages.h"
22 #include "inn/newsuser.h"
23 #include "inn/nntp.h"
24 #include "inn/paths.h"
25 
26 /* Signature handling.  The separator will be appended before the signature,
27    and at most SIG_MAXLINES will be appended. */
28 #define SIG_MAXLINES  4
29 #define SIG_SEPARATOR "-- \n"
30 
31 #define FLUSH_ERROR(F) (fflush((F)) == EOF || ferror((F)))
32 #define LPAREN         '(' /* For vi :-) */
33 #define HEADER_DELTA   20
34 #define GECOSTERM(c)   ((c) == ',' || (c) == ';' || (c) == ':' || (c) == LPAREN)
35 
36 typedef enum _HEADERTYPE
37 {
38     HTobs,
39     HTreq,
40     HTstd
41 } HEADERTYPE;
42 
43 typedef struct _HEADER {
44     const char *Name;
45     bool CanSet;
46     HEADERTYPE Type;
47     int Size;
48     char *Value;
49 } HEADER;
50 
51 static bool Dump;
52 static bool Revoked;
53 static bool Spooling;
54 static char **OtherHeaders;
55 static char SIGSEP[] = SIG_SEPARATOR;
56 static FILE *FromServer;
57 static FILE *ToServer;
58 static int OtherCount;
59 static int OtherSize;
60 static const char *Exclusions = "";
61 static const char *const BadDistribs[] = {BAD_DISTRIBS};
62 
63 static void Usage(void) __attribute__((__noreturn__));
64 
65 /* clang-format off */
66 static HEADER Table[] = {
67     /* Name,           CanSet,  Type, Size, Value */
68     {"Path",             true,   HTstd, 0, NULL},
69 #define _path          0
70     {"From",             true,   HTstd, 0, NULL},
71 #define _from          1
72     {"Newsgroups",       true,   HTreq, 0, NULL},
73 #define _newsgroups    2
74     {"Subject",          true,   HTreq, 0, NULL},
75 #define _subject       3
76     {"Control",          true,   HTstd, 0, NULL},
77 #define _control       4
78     {"Supersedes",       true,   HTstd, 0, NULL},
79 #define _supersedes    5
80     {"Followup-To",      true,   HTstd, 0, NULL},
81 #define _followupto    6
82     {"Date",             true,   HTstd, 0, NULL},
83 #define _date          7
84     {"Organization",     true,   HTstd, 0, NULL},
85 #define _organization  8
86     {"Lines",            true,   HTstd, 0, NULL},
87 #define _lines         9
88     {"Sender",           true,   HTstd, 0, NULL},
89 #define _sender       10
90     {"Approved",         true,   HTstd, 0, NULL},
91 #define _approved     11
92     {"Distribution",     true,   HTstd, 0, NULL},
93 #define _distribution 12
94     {"Expires",          true,   HTstd, 0, NULL},
95 #define _expires      13
96     {"Message-ID",       true,   HTstd, 0, NULL},
97 #define _messageid    14
98     {"References",       true,   HTstd, 0, NULL},
99 #define _references   15
100     {"Reply-To",         true,   HTstd, 0, NULL},
101 #define _replyto      16
102     {"Also-Control",     true,   HTstd, 0, NULL},
103 #define _alsocontrol  17
104     {"Xref",             false,  HTstd, 0, NULL},
105     {"Summary",          true,   HTstd, 0, NULL},
106     {"Keywords",         true,   HTstd, 0, NULL},
107     {"Date-Received",    false,  HTobs, 0, NULL},
108     {"Received",         false,  HTobs, 0, NULL},
109     {"Posted",           false,  HTobs, 0, NULL},
110     {"Posting-Version",  false,  HTobs, 0, NULL},
111     {"Relay-Version",    false,  HTobs, 0, NULL},
112 };
113 /* clang-format on */
114 
115 #define HDR(_x) (Table[(_x)].Value)
116 
117 
118 /*
119 **  Send the server a quit message, wait for a reply.
120 */
121 __attribute__((__noreturn__)) static void
QuitServer(int x)122 QuitServer(int x)
123 {
124     char buff[MED_BUFFER];
125     char *p;
126 
127     if (Spooling)
128         exit(x);
129     if (x)
130         warn("article not posted");
131     fprintf(ToServer, "quit\r\n");
132     if (FLUSH_ERROR(ToServer))
133         sysdie("cannot send quit to server");
134     if (fgets(buff, sizeof buff, FromServer) == NULL)
135         sysdie("warning: server did not reply to quit");
136     if ((p = strchr(buff, '\r')) != NULL)
137         *p = '\0';
138     if ((p = strchr(buff, '\n')) != NULL)
139         *p = '\0';
140     if (atoi(buff) != NNTP_OK_QUIT)
141         die("server did not reply to quit properly: %s", buff);
142     fclose(FromServer);
143     fclose(ToServer);
144     exit(x);
145 }
146 
147 
148 /*
149 **  Failure handler, called by die.  Calls QuitServer to cleanly shut down the
150 **  connection with the remote server before exiting.
151 */
152 static int
fatal_cleanup(void)153 fatal_cleanup(void)
154 {
155     /* Don't recurse. */
156     message_fatal_cleanup = NULL;
157 
158     /* QuitServer does all the work. */
159     QuitServer(1);
160     /* NOTREACHED */
161     return 1;
162 }
163 
164 
165 /*
166 **  Flush a stdio FILE; exit if there are any errors.
167 */
168 static void
SafeFlush(FILE * F)169 SafeFlush(FILE *F)
170 {
171     if (FLUSH_ERROR(F))
172         sysdie("cannot send text to server");
173 }
174 
175 
176 /*
177 **  Trim trailing spaces, return pointer to first non-space char.
178 */
179 static char *
TrimSpaces(char * p)180 TrimSpaces(char *p)
181 {
182     char *start;
183 
184     for (start = p; ISWHITE(*start); start++)
185         continue;
186     for (p = start + strlen(start);
187          p > start && isspace((unsigned char) p[-1]);)
188         *--p = '\0';
189     return start;
190 }
191 
192 
193 /*
194 **  Mark the end of the header field starting at p, and return a pointer
195 **  to the start of the next one.  Handles continuations.
196 */
197 static char *
NextHeader(char * p)198 NextHeader(char *p)
199 {
200     char *q;
201     for (q = p;; p++) {
202         if ((p = strchr(p, '\n')) == NULL) {
203             die("article is all headers");
204         }
205         /* Check the maximum length of a single line. */
206         if (p - q + 1 > MAXARTLINELENGTH) {
207             die("header line too long");
208         }
209         /* Check if there is a continuation line for the header field. */
210         if (ISWHITE(p[1])) {
211             q = p + 1;
212             continue;
213         }
214         *p = '\0';
215         return p + 1;
216     }
217 }
218 
219 
220 /*
221 **  Strip any header fields off the article and dump them into the table.
222 */
223 static char *
StripOffHeaders(char * article)224 StripOffHeaders(char *article)
225 {
226     char *p;
227     char *q;
228     HEADER *hp;
229     char c;
230 
231     /* Set up the other header fields list. */
232     OtherSize = HEADER_DELTA;
233     OtherHeaders = xmalloc(OtherSize * sizeof(char *));
234     OtherCount = 0;
235 
236     /* Scan through buffer, a header field at a time. */
237     for (p = article;;) {
238 
239         if ((q = strchr(p, ':')) == NULL)
240             die("no colon in header field \"%.30s...\"", p);
241         if (q[1] == '\n' && !ISWHITE(q[2])) {
242             /* Empty header field body; ignore this one, get next line. */
243             p = NextHeader(p);
244             if (*p == '\n')
245                 break;
246         }
247 
248         if (q[1] != '\0' && !ISWHITE(q[1])) {
249             if ((q = strchr(q, '\n')) != NULL)
250                 *q = '\0';
251             die("no space after colon in \"%.30s...\"", p);
252         }
253 
254         /* See if it's a known header field name. */
255         c = islower((unsigned char) *p) ? toupper((unsigned char) *p) : *p;
256         for (hp = Table; hp < ARRAY_END(Table); hp++)
257             if (c == hp->Name[0] && p[hp->Size] == ':'
258                 && ISWHITE(p[hp->Size + 1])
259                 && strncasecmp(p, hp->Name, hp->Size) == 0) {
260                 if (hp->Type == HTobs)
261                     die("obsolete header field: %s", hp->Name);
262                 if (hp->Value)
263                     die("duplicate header field: %s", hp->Name);
264                 for (q = &p[hp->Size + 1]; ISWHITE(*q); q++)
265                     continue;
266                 hp->Value = q;
267                 break;
268             }
269 
270         /* No; add it to the set of other header fields. */
271         if (hp == ARRAY_END(Table)) {
272             if (OtherCount >= OtherSize - 1) {
273                 OtherSize += HEADER_DELTA;
274                 OtherHeaders =
275                     xrealloc(OtherHeaders, OtherSize * sizeof(char *));
276             }
277             OtherHeaders[OtherCount++] = p;
278         }
279 
280         /* Get start of next header field; if it's a blank line, we hit
281          * the end. */
282         p = NextHeader(p);
283         if (*p == '\n')
284             break;
285     }
286 
287     return p + 1;
288 }
289 
290 
291 /*
292 **  See if the user is allowed to cancel the indicated message.  Assumes
293 **  that the Sender or From line has already been filled in.
294 */
295 static void
CheckCancel(char * msgid,bool JustReturn)296 CheckCancel(char *msgid, bool JustReturn)
297 {
298     char localfrom[SMBUF];
299     char *p;
300     char buff[BUFSIZ];
301     char remotefrom[SMBUF];
302 
303     /* Ask the server for the article. */
304     fprintf(ToServer, "head %s\r\n", msgid);
305     SafeFlush(ToServer);
306     if (fgets(buff, sizeof buff, FromServer) == NULL
307         || atoi(buff) != NNTP_OK_HEAD) {
308         if (JustReturn)
309             return;
310         die("server has no such article");
311     }
312 
313     /* Read the header fields, looking for the From or Sender. */
314     remotefrom[0] = '\0';
315     while (fgets(buff, sizeof buff, FromServer) != NULL) {
316         if ((p = strchr(buff, '\r')) != NULL)
317             *p = '\0';
318         if ((p = strchr(buff, '\n')) != NULL)
319             *p = '\0';
320         if (buff[0] == '.' && buff[1] == '\0')
321             break;
322         if (strncasecmp(buff, "Sender:", 7) == 0)
323             strlcpy(remotefrom, TrimSpaces(&buff[7]), SMBUF);
324         else if (remotefrom[0] == '\0' && strncasecmp(buff, "From:", 5) == 0)
325             strlcpy(remotefrom, TrimSpaces(&buff[5]), SMBUF);
326     }
327     if (remotefrom[0] == '\0') {
328         if (JustReturn)
329             return;
330         die("article is garbled");
331     }
332     HeaderCleanFrom(remotefrom);
333 
334     /* Get the local user. */
335     strlcpy(localfrom, HDR(_sender) ? HDR(_sender) : HDR(_from), SMBUF);
336     HeaderCleanFrom(localfrom);
337 
338     /* Is the right person cancelling? */
339     if (strcasecmp(localfrom, remotefrom) != 0)
340         die("article was posted by \"%s\" and you are \"%s\"", remotefrom,
341             localfrom);
342 }
343 
344 
345 /*
346 **  See if the user is the news administrator.
347 */
348 static bool
AnAdministrator(void)349 AnAdministrator(void)
350 {
351     uid_t news_uid;
352     gid_t news_gid;
353 
354     if (Revoked)
355         return false;
356 
357     /* Find out who we are. */
358     if (get_news_uid_gid(&news_uid, &news_gid, false) != 0) {
359         /* Silent failure; clients might not have the group. */
360         return false;
361     }
362     if (getuid() == news_uid)
363         return true;
364 
365         /* See if we are in the right group and examine process
366          * supplementary groups, rather than the group(5) file entry.
367          */
368 #ifdef HAVE_GETGROUPS
369     {
370         int ngroups = getgroups(0, 0);
371         GETGROUPS_T *groups, *gp;
372         int rv;
373         int rest;
374 
375         groups = (GETGROUPS_T *) xmalloc(ngroups * sizeof(GETGROUPS_T));
376         if ((rv = getgroups(ngroups, groups)) < 0) {
377             /* Silent failure; client doesn't have the group. */
378             return false;
379         }
380         for (rest = ngroups, gp = groups; rest > 0; rest--, gp++) {
381             if (*gp == (GETGROUPS_T) news_gid)
382                 return true;
383         }
384     }
385 #endif
386 
387     return false;
388 }
389 
390 
391 /*
392 **  Check the control message, and see if it's legit.
393 */
394 static void
CheckControl(char * ctrl)395 CheckControl(char *ctrl)
396 {
397     char *p;
398     char *q;
399     char save;
400 
401     /* Snip off the first word. */
402     for (p = ctrl; ISWHITE(*p); p++)
403         continue;
404     for (ctrl = p; *p && !ISWHITE(*p); p++)
405         continue;
406     if (p == ctrl)
407         die("empty control message");
408     save = *p;
409     *p = '\0';
410 
411     if (strcasecmp(ctrl, "cancel") == 0) {
412         for (q = p + 1; ISWHITE(*q); q++)
413             continue;
414         if (*q == '\0')
415             die("message ID missing in cancel");
416         if (!Spooling)
417             CheckCancel(q, false);
418     } else if (strcasecmp(ctrl, "checkgroups") == 0
419                || strcasecmp(ctrl, "ihave") == 0
420                || strcasecmp(ctrl, "sendme") == 0
421                || strcasecmp(ctrl, "newgroup") == 0
422                || strcasecmp(ctrl, "rmgroup") == 0) {
423         if (!AnAdministrator())
424             die("ask your news administrator to do the %s for you", ctrl);
425     } else {
426         die("%s is not a valid control message", ctrl);
427     }
428     *p = save;
429 }
430 
431 
432 /*
433 **  Parse the GECOS field to get the user's full name.  This comes Sendmail's
434 **  buildfname routine.  Ignore leading stuff like "23-" "stuff]-" or
435 **  "stuff -" as well as trailing whitespace, or anything that comes after
436 **  a comma, semicolon, or in parentheses.  This seems to strip off most of
437 **  the UCB or ATT stuff people fill out the entries with.  Also, turn &
438 **  into the login name, with perhaps an initial capital.  (Everyone seems
439 **  to hate that, but everyone also supports it.)
440 */
441 static char *
FormatUserName(struct passwd * pwp,char * node)442 FormatUserName(struct passwd *pwp, char *node)
443 {
444     char outbuff[SMBUF];
445     char *buff;
446     char *out;
447     char *p;
448 #ifdef DO_MUNGE_GECOS
449     int left = SMBUF - 1;
450 #endif
451 
452 #if !defined(DONT_MUNGE_GETENV)
453     memset(outbuff, 0, SMBUF);
454     if ((p = getenv("NAME")) != NULL)
455         strlcpy(outbuff, p, SMBUF);
456     if (strlen(outbuff) == 0) {
457 #endif /* !defined(DONT_MUNGE_GETENV) */
458 
459 
460 #ifndef DO_MUNGE_GECOS
461         strlcpy(outbuff, pwp->pw_gecos, SMBUF);
462 #else
463     /* Be very careful here.  If we're not, we can potentially overflow our
464      * buffer.  Remember that on some Unix systems, the content of the GECOS
465      * field is under (untrusted) user control and we could be setgid. */
466     p = pwp->pw_gecos;
467     if (*p == '*')
468         p++;
469     for (out = outbuff; *p && !GECOSTERM(*p) && left; p++) {
470         if (*p == '&') {
471             strncpy(out, pwp->pw_name, left);
472             if (islower((unsigned char) *out)
473                 && (out == outbuff || !isalpha((unsigned char) out[-1])))
474                 *out = toupper((unsigned char) *out);
475             while (*out) {
476                 out++;
477                 left--;
478             }
479         } else if (*p == '-' && p > pwp->pw_gecos
480                    && (isdigit((unsigned char) p[-1])
481                        || isspace((unsigned char) p[-1]) || p[-1] == ']')) {
482             out = outbuff;
483             left = SMBUF - 1;
484         } else {
485             *out++ = *p;
486             left--;
487         }
488     }
489     *out = '\0';
490 #endif /* DO_MUNGE_GECOS */
491 
492 #if !defined(DONT_MUNGE_GETENV)
493     }
494 #endif /* !defined(DONT_MUNGE_GETENV) */
495 
496     out = TrimSpaces(outbuff);
497     if (out[0])
498         buff = concat(pwp->pw_name, "@", node, " (", out, ")", (char *) 0);
499     else
500         buff = concat(pwp->pw_name, "@", node, (char *) 0);
501     return buff;
502 }
503 
504 
505 /*
506 **  Check the Distribution header field, and exit on error.
507 */
508 static void
CheckDistribution(char * p)509 CheckDistribution(char *p)
510 {
511     static char SEPS[] = " \t,";
512     const char *const *dp;
513 
514     if ((p = strtok(p, SEPS)) == NULL)
515         die("cannot parse Distribution header field");
516     do {
517         for (dp = BadDistribs; *dp; dp++)
518             if (uwildmat(p, *dp))
519                 die("illegal distribution %s", p);
520     } while ((p = strtok((char *) NULL, SEPS)) != NULL);
521 }
522 
523 
524 /*
525 **  Process all the headers.  FYI, they're done in RFC-order.
526 */
527 static void
ProcessHeaders(bool AddOrg,int linecount,struct passwd * pwp)528 ProcessHeaders(bool AddOrg, int linecount, struct passwd *pwp)
529 {
530     static char PATHFLUFF[] = PATHMASTER;
531     HEADER *hp;
532     char *p;
533     char buff[SMBUF];
534     char from[SMBUF];
535 
536     /* Do some preliminary fix-ups. */
537     for (hp = Table; hp < ARRAY_END(Table); hp++) {
538         if (!hp->CanSet && hp->Value)
539             die("cannot set system header field %s", hp->Name);
540         if (hp->Value) {
541             hp->Value = TrimSpaces(hp->Value);
542             if (*hp->Value == '\0')
543                 hp->Value = NULL;
544         }
545     }
546 
547     /* Set From or Sender. */
548     if ((p = innconf->fromhost) == NULL)
549         sysdie("cannot get hostname");
550     if (HDR(_from) == NULL)
551         HDR(_from) = FormatUserName(pwp, p);
552     else {
553         if (strlen(pwp->pw_name) + strlen(p) + 2 > sizeof(buff))
554             die("username and host are too long");
555         sprintf(buff, "%s@%s", pwp->pw_name, p);
556         strlcpy(from, HDR(_from), SMBUF);
557         HeaderCleanFrom(from);
558         if (strcmp(from, buff) != 0)
559             HDR(_sender) = xstrdup(buff);
560     }
561 
562     if (HDR(_date) == NULL) {
563         /* Set Date. */
564         if (!makedate(-1, true, buff, sizeof(buff)))
565             die("cannot generate Date header field body");
566         HDR(_date) = xstrdup(buff);
567     }
568 
569     /* Newsgroups are checked later. */
570 
571     /* Set Subject; Control overrides the subject. */
572     if (HDR(_control)) {
573         CheckControl(HDR(_control));
574     } else {
575         p = HDR(_subject);
576         if (p == NULL)
577             die("required Subject header field is missing or empty");
578         else if (HDR(_alsocontrol))
579             CheckControl(HDR(_alsocontrol));
580     }
581 
582     /* Set Message-ID */
583     if (HDR(_messageid) == NULL) {
584         if ((p = GenerateMessageID(innconf->domain)) == NULL)
585             die("cannot generate Message-ID header field body");
586         HDR(_messageid) = xstrdup(p);
587     } else if ((p = strchr(HDR(_messageid), '@')) == NULL
588                || strchr(++p, '@') != NULL) {
589         die("message ID must have exactly one @");
590     }
591 
592     /* Set Path */
593     if (HDR(_path) == NULL) {
594 #if defined(DO_INEWS_PATH)
595         if ((p = innconf->pathhost) != NULL) {
596             if (*p)
597                 HDR(_path) = concat(Exclusions, p, "!", PATHFLUFF, (char *) 0);
598             else
599                 HDR(_path) = concat(Exclusions, PATHFLUFF, (char *) 0);
600         } else if (innconf->server != NULL) {
601             p = inn_getfqdn(innconf->domain);
602             if (p == NULL)
603                 sysdie("cannot get hostname");
604             HDR(_path) = concat(Exclusions, p, "!", PATHFLUFF, (char *) 0);
605             free(p);
606         } else {
607             HDR(_path) = concat(Exclusions, PATHFLUFF, (char *) 0);
608         }
609 #else
610         HDR(_path) = concat(Exclusions, PATHFLUFF, (char *) 0);
611 #endif /* defined(DO_INEWS_PATH) */
612     }
613 
614     /* Reply-To; left alone. */
615     /* Sender; set above. */
616     /* Followup-To; checked with Newsgroups. */
617 
618     /* Check Expires. */
619     if (HDR(_expires) && parsedate_rfc5322_lax(HDR(_expires)) == -1)
620         die("cannot parse \"%s\" as an expiration date", HDR(_expires));
621 
622     /* References; left alone. */
623     /* Control; checked above. */
624 
625     /* Distribution. */
626     if ((p = HDR(_distribution)) != NULL) {
627         p = xstrdup(p);
628         CheckDistribution(p);
629         free(p);
630     }
631 
632     /* Set Organization. */
633     if (AddOrg && HDR(_organization) == NULL
634         && (p = innconf->organization) != NULL) {
635         HDR(_organization) = xstrdup(p);
636     }
637 
638     /* Keywords; left alone. */
639     /* Summary; left alone. */
640     /* Approved; left alone. */
641 
642     /* Set Lines */
643     sprintf(buff, "%d", linecount);
644     HDR(_lines) = xstrdup(buff);
645 
646     /* Check Supersedes. */
647     if (HDR(_supersedes))
648         CheckCancel(HDR(_supersedes), true);
649 
650     /* Now make sure everything is there. */
651     for (hp = Table; hp < ARRAY_END(Table); hp++)
652         if (hp->Type == HTreq && hp->Value == NULL)
653             die("required header field %s is missing or empty", hp->Name);
654 }
655 
656 
657 /*
658 **  Try to append $HOME/.signature to the article.  When in doubt, exit
659 **  out in order to avoid postings like "Sorry, I forgot my .signature
660 **  -- here's the article again."
661 */
662 static char *
AppendSignature(bool UseMalloc,char * article,char * homedir,int * linesp)663 AppendSignature(bool UseMalloc, char *article, char *homedir, int *linesp)
664 {
665     int i;
666     int length;
667     size_t artsize;
668     char *p;
669     char buff[BUFSIZ];
670     FILE *F;
671 
672     /* Open the file. */
673     *linesp = 0;
674     if (strlen(homedir) > sizeof(buff) - 14)
675         die("home directory path too long");
676     snprintf(buff, sizeof(buff), "%s/.signature", homedir);
677     if ((F = fopen(buff, "r")) == NULL) {
678         if (errno == ENOENT)
679             return article;
680         fprintf(stderr, "Can't add your .signature (%s), article not posted",
681                 strerror(errno));
682         QuitServer(1);
683     }
684 
685     /* Read it in. */
686     length = fread(buff, 1, sizeof buff - 2, F);
687     i = feof(F);
688     fclose(F);
689     if (length == 0)
690         die("signature file is empty");
691     if (length < 0)
692         sysdie("cannot read signature file");
693     if (length == sizeof buff - 2 && !i)
694         die("signature is too large");
695 
696     /* Make sure the buffer ends with \n\0. */
697     if (buff[length - 1] != '\n')
698         buff[length++] = '\n';
699     buff[length] = '\0';
700 
701     /* Count the lines. */
702     for (i = 0, p = buff; (p = strchr(p, '\n')) != NULL; p++)
703         if (++i > SIG_MAXLINES)
704             die("signature has too many lines");
705     *linesp = 1 + i;
706 
707     /* Grow the article to have the signature. */
708     i = strlen(article);
709     artsize = i + sizeof(SIGSEP) - 1 + length + 1;
710     if (UseMalloc) {
711         p = xmalloc(artsize);
712         strlcpy(p, article, artsize);
713         article = p;
714     } else
715         article = xrealloc(article, artsize);
716     strlcat(article, SIGSEP, artsize);
717     strlcat(article, buff, artsize);
718     return article;
719 }
720 
721 
722 /*
723 **  See if the user has more included text than new text.  Simple-minded, but
724 **  reasonably effective for catching neophyte's mistakes.  A line starting
725 **  with > is included text.  Decrement the count on lines starting with <
726 **  so that we don't reject diff(1) output.
727 */
728 static void
CheckIncludedText(char * p,int lines)729 CheckIncludedText(char *p, int lines)
730 {
731     int i;
732 
733     for (i = 0;; p++) {
734         switch (*p) {
735         case '>':
736             i++;
737             break;
738         case '|':
739             i++;
740             break;
741         case ':':
742             i++;
743             break;
744         case '<':
745             i--;
746             break;
747         }
748         if ((p = strchr(p, '\n')) == NULL)
749             break;
750     }
751     if ((i * 2 > lines) && (lines > 40))
752         die("more included text than new text");
753 }
754 
755 
756 /*
757 **  Read stdin into a string and return it.  Can't use ReadInDescriptor
758 **  since that will fail if stdin is a tty.
759 */
760 static char *
ReadStdin(void)761 ReadStdin(void)
762 {
763     int size;
764     char *p;
765     char *article;
766     char *end;
767     int i;
768 
769     size = BUFSIZ;
770     article = xmalloc(size);
771     end = &article[size - 3];
772     for (p = article; (i = getchar()) != EOF; *p++ = (char) i)
773         if (p == end) {
774             article = xrealloc(article, size + BUFSIZ);
775             p = &article[size - 3];
776             size += BUFSIZ;
777             end = &article[size - 3];
778         }
779 
780     /* Force a \n terminator. */
781     if (p > article && p[-1] != '\n')
782         *p++ = '\n';
783     *p = '\0';
784     return article;
785 }
786 
787 
788 /*
789 **  Offer the article to the server, return its reply.
790 */
791 static int
OfferArticle(char * buff,bool Authorized)792 OfferArticle(char *buff, bool Authorized)
793 {
794     fprintf(ToServer, "post\r\n");
795     SafeFlush(ToServer);
796     if (fgets(buff, MED_BUFFER, FromServer) == NULL)
797         sysdie(Authorized ? "Can't offer article to server (authorized)"
798                           : "Can't offer article to server");
799     return atoi(buff);
800 }
801 
802 
803 /*
804 **  Spool article to temp file.
805 */
806 static void
Spoolit(char * article,size_t Length,char * deadfile)807 Spoolit(char *article, size_t Length, char *deadfile)
808 {
809     HEADER *hp;
810     FILE *F;
811     int i;
812 
813     /* Try to write to the deadfile. */
814     if (deadfile == NULL)
815         return;
816     F = xfopena(deadfile);
817     if (F == NULL)
818         sysdie("cannot create spool file");
819 
820     /* Write the headers and a blank line. */
821     for (hp = Table; hp < ARRAY_END(Table); hp++)
822         if (hp->Value)
823             fprintf(F, "%s: %s\n", hp->Name, hp->Value);
824     for (i = 0; i < OtherCount; i++)
825         fprintf(F, "%s\n", OtherHeaders[i]);
826     fprintf(F, "\n");
827     if (FLUSH_ERROR(F))
828         sysdie("cannot write headers");
829 
830     /* Write the article and exit. */
831     if (fwrite(article, 1, Length, F) != Length)
832         sysdie("cannot write article");
833     if (FLUSH_ERROR(F))
834         sysdie("cannot write article");
835     if (fclose(F) == EOF)
836         sysdie("cannot close spool file");
837 }
838 
839 
840 /*
841 **  Print usage message and exit.
842 */
843 static void
Usage(void)844 Usage(void)
845 {
846     fprintf(stderr, "Usage: inews [-D] [-h] [header_flags] [article]\n");
847     /* Don't call QuitServer here -- connection isn't open yet. */
848     exit(1);
849 }
850 
851 
852 int
main(int ac,char * av[])853 main(int ac, char *av[])
854 {
855     static char NOCONNECT[] = "cannot connect to server";
856     int i;
857     char *p;
858     HEADER *hp;
859     int j;
860     int port;
861     int Mode;
862     int SigLines;
863     struct passwd *pwp;
864     char *article;
865     char *deadfile;
866     char buff[MED_BUFFER];
867     char SpoolMessage[MED_BUFFER];
868     bool DoSignature;
869     bool AddOrg;
870     size_t Length;
871     uid_t uid;
872 
873     /* First thing, set up logging and our identity. */
874     message_program_name = "inews";
875 
876     /* Find out who we are. */
877     uid = geteuid();
878     if (uid == (uid_t) -1)
879         sysdie("cannot get your user ID");
880     if ((pwp = getpwuid(uid)) == NULL)
881         sysdie("cannot get your passwd entry");
882 
883     /* Set defaults. */
884     Mode = '\0';
885     Dump = false;
886     DoSignature = true;
887     AddOrg = true;
888     port = 0;
889 
890     if (!innconf_read(NULL))
891         exit(1);
892 
893     umask(NEWSUMASK);
894 
895     /* Parse JCL. */
896     while ((i = getopt(ac, av, "DNAVWORShx:a:c:d:e:f:n:p:r:t:F:o:w:"))
897            != EOF) {
898         switch (i) {
899         default:
900             Usage();
901             /* NOTREACHED */
902         case 'D':
903         case 'N':
904             Dump = true;
905             break;
906         case 'A':
907         case 'V':
908         case 'W':
909             /* Ignore C News options. */
910             break;
911         case 'O':
912             AddOrg = false;
913             break;
914         case 'R':
915             Revoked = true;
916             break;
917         case 'S':
918             DoSignature = false;
919             break;
920         case 'h':
921             Mode = i;
922             break;
923         case 'x':
924             Exclusions = concat(optarg, "!", (char *) 0);
925             break;
926         case 'p':
927             port = atoi(optarg);
928             break;
929         /* Header fields that can be specified on the command line. */
930         /* clang-format off */
931         case 'a':   HDR(_approved) = optarg;       break;
932         case 'c':   HDR(_control) = optarg;        break;
933         case 'd':   HDR(_distribution) = optarg;   break;
934         case 'e':   HDR(_expires) = optarg;        break;
935         case 'f':   HDR(_from) = optarg;           break;
936         case 'n':   HDR(_newsgroups) = optarg;     break;
937         case 'r':   HDR(_replyto) = optarg;        break;
938         case 't':   HDR(_subject) = optarg;        break;
939         case 'F':   HDR(_references) = optarg;     break;
940         case 'o':   HDR(_organization) = optarg;   break;
941         case 'w':   HDR(_followupto) = optarg;     break;
942         }
943         /* clang-format on */
944     }
945     ac -= optind;
946     av += optind;
947 
948     /* Parse positional arguments; at most one, the input file. */
949     switch (ac) {
950     default:
951         Usage();
952         /* NOTREACHED */
953     case 0:
954         /* Read stdin. */
955         article = ReadStdin();
956         break;
957     case 1:
958         /* Read named file. */
959         article = ReadInFile(av[0], (struct stat *) NULL);
960         if (article == NULL)
961             sysdie("cannot read input file");
962         break;
963     }
964 
965     if (port == 0)
966         port = NNTP_PORT;
967 
968     /* Try to open a connection to the server. */
969     if (NNTPremoteopen(port, &FromServer, &ToServer, buff, sizeof(buff)) < 0) {
970         Spooling = true;
971         if ((p = strchr(buff, '\n')) != NULL)
972             *p = '\0';
973         if ((p = strchr(buff, '\r')) != NULL)
974             *p = '\0';
975         strlcpy(SpoolMessage, buff[0] ? buff : NOCONNECT,
976                 sizeof(SpoolMessage));
977         deadfile = concatpath(pwp->pw_dir, "dead.article");
978     } else {
979         /* We now have an open server connection, so close it on failure. */
980         message_fatal_cleanup = fatal_cleanup;
981 
982         /* See if we can post. */
983         i = atoi(buff);
984 
985         /* Tell the server we're posting. */
986         setbuf(FromServer, xmalloc(BUFSIZ));
987         setbuf(ToServer, xmalloc(BUFSIZ));
988         fprintf(ToServer, "mode reader\r\n");
989         SafeFlush(ToServer);
990         if (fgets(buff, MED_BUFFER, FromServer) == NULL)
991             sysdie("cannot tell server we're reading");
992         if ((j = atoi(buff)) != NNTP_ERR_COMMAND)
993             i = j;
994 
995         if (i != NNTP_OK_BANNER_POST) {
996             /* We try to authenticate in case it is all the same possible
997              * to post. */
998             if (NNTPsendpassword((char *) NULL, FromServer, ToServer) < 0)
999                 die("you do not have permission to post");
1000         }
1001         deadfile = NULL;
1002     }
1003 
1004     /* Basic processing. */
1005     for (hp = Table; hp < ARRAY_END(Table); hp++)
1006         hp->Size = strlen(hp->Name);
1007     if (Mode == 'h')
1008         article = StripOffHeaders(article);
1009     for (i = 0, p = article; (p = strchr(p, '\n')) != NULL; i++, p++)
1010         continue;
1011     if (innconf->checkincludedtext)
1012         CheckIncludedText(article, i);
1013     if (DoSignature)
1014         article =
1015             AppendSignature(Mode == 'h', article, pwp->pw_dir, &SigLines);
1016     else
1017         SigLines = 0;
1018     ProcessHeaders(AddOrg, i + SigLines, pwp);
1019     Length = strlen(article);
1020     if ((innconf->localmaxartsize != 0) && (Length > innconf->localmaxartsize))
1021         die("article is larger than local limit of %lu bytes",
1022             innconf->localmaxartsize);
1023 
1024     /* Do final checks. */
1025     if (i == 0 && HDR(_control) == NULL)
1026         die("article is empty");
1027 
1028     if (Dump) {
1029         /* Write the headers and a blank line. */
1030         for (hp = Table; hp < ARRAY_END(Table); hp++)
1031             if (hp->Value)
1032                 printf("%s: %s\n", hp->Name, hp->Value);
1033         for (i = 0; i < OtherCount; i++)
1034             printf("%s\n", OtherHeaders[i]);
1035         printf("\n");
1036         if (FLUSH_ERROR(stdout))
1037             sysdie("cannot write headers");
1038 
1039         /* Write the article and exit. */
1040         if (fwrite(article, 1, Length, stdout) != Length)
1041             sysdie("cannot write article");
1042         SafeFlush(stdout);
1043         QuitServer(0);
1044     }
1045 
1046     if (Spooling) {
1047         warn("warning: %s", SpoolMessage);
1048         warn("article will be spooled");
1049         Spoolit(article, Length, deadfile);
1050         exit(0);
1051     }
1052 
1053     /* Article is prepared, offer it to the server. */
1054     i = OfferArticle(buff, false);
1055     if (i == NNTP_FAIL_AUTH_NEEDED) {
1056         /* Posting not allowed, try to authorize. */
1057         if (NNTPsendpassword((char *) NULL, FromServer, ToServer) < 0)
1058             sysdie("authorization error");
1059         i = OfferArticle(buff, true);
1060     }
1061     if (i != NNTP_CONT_POST)
1062         die("server doesn't want the article: %s", buff);
1063 
1064     /* Write the headers, a blank line, then the article. */
1065     for (hp = Table; hp < ARRAY_END(Table); hp++)
1066         if (hp->Value)
1067             fprintf(ToServer, "%s: %s\r\n", hp->Name, hp->Value);
1068     for (i = 0; i < OtherCount; i++)
1069         fprintf(ToServer, "%s\r\n", OtherHeaders[i]);
1070     fprintf(ToServer, "\r\n");
1071     if (NNTPsendarticle(article, ToServer, true) < 0)
1072         sysdie("cannot send article to server");
1073     SafeFlush(ToServer);
1074 
1075     if (fgets(buff, sizeof buff, FromServer) == NULL)
1076         sysdie("no reply from server after sending the article");
1077     if ((p = strchr(buff, '\r')) != NULL)
1078         *p = '\0';
1079     if ((p = strchr(buff, '\n')) != NULL)
1080         *p = '\0';
1081     if (atoi(buff) != NNTP_OK_POST)
1082         die("cannot send article to server: %s", buff);
1083 
1084     /* Close up. */
1085     QuitServer(0);
1086     /* NOTREACHED */
1087     return 1;
1088 }
1089