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