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