1 /*
2 **  Check article, send it to the local server.
3 */
4 
5 #include "portable/system.h"
6 
7 #include "inn/innconf.h"
8 #include "inn/ov.h"
9 #include "nnrpd.h"
10 #include "post.h"
11 
12 #define FLUSH_ERROR(F) (fflush((F)) == EOF || ferror((F)))
13 #define HEADER_DELTA   20
14 
15 static char *tmpPtr;
16 static char Error[SMBUF];
17 static char NGSEPS[] = NG_SEPARATOR;
18 char **OtherHeaders;
19 size_t OtherCount;
20 static size_t OtherSize;
21 static const char *const BadDistribs[] = {BAD_DISTRIBS};
22 
23 /*
24 **  Do not modify the table without also looking at post.h for potential
25 **  changes in the order of the fields.
26 **
27 **  The table should reflect the status of the fields in the "Permanent
28 **  Message Header Field Names" registry:
29 **    http://www.iana.org/assignments/message-headers/
30 */
31 /* clang-format off */
32 HEADER Table[] = {
33     /*  Name                    CanSet  Type    Size  Value    Body  Len */
34     {   "Path",                 true,   HTstd,  0,    NULL,    NULL, 0 },
35     {   "From",                 true,   HTreq,  0,    NULL,    NULL, 0 },
36     {   "Newsgroups",           true,   HTreq,  0,    NULL,    NULL, 0 },
37     {   "Subject",              true,   HTreq,  0,    NULL,    NULL, 0 },
38     {   "Control",              true,   HTstd,  0,    NULL,    NULL, 0 },
39     {   "Supersedes",           true,   HTstd,  0,    NULL,    NULL, 0 },
40     {   "Followup-To",          true,   HTstd,  0,    NULL,    NULL, 0 },
41     {   "Date",                 true,   HTstd,  0,    NULL,    NULL, 0 },
42     {   "Organization",         true,   HTstd,  0,    NULL,    NULL, 0 },
43     {   "Lines",                true,   HTstd,  0,    NULL,    NULL, 0 },
44     {   "Sender",               true,   HTstd,  0,    NULL,    NULL, 0 },
45     {   "Approved",             true,   HTstd,  0,    NULL,    NULL, 0 },
46     {   "Archive",              true,   HTstd,  0,    NULL,    NULL, 0 },
47     {   "Distribution",         true,   HTstd,  0,    NULL,    NULL, 0 },
48     {   "Expires",              true,   HTstd,  0,    NULL,    NULL, 0 },
49     {   "Message-ID",           true,   HTstd,  0,    NULL,    NULL, 0 },
50     {   "References",           true,   HTstd,  0,    NULL,    NULL, 0 },
51     {   "Reply-To",             true,   HTstd,  0,    NULL,    NULL, 0 },
52     {   "NNTP-Posting-Host",    false,  HTobs,  0,    NULL,    NULL, 0 },
53     {   "Mime-Version",         true,   HTstd,  0,    NULL,    NULL, 0 },
54     {   "Content-Type",         true,   HTstd,  0,    NULL,    NULL, 0 },
55     {   "Content-Transfer-Encoding", true, HTstd, 0,  NULL,    NULL, 0 },
56     {   "X-Trace",              false,  HTobs,  0,    NULL,    NULL, 0 },
57     {   "X-Complaints-To",      false,  HTobs,  0,    NULL,    NULL, 0 },
58     {   "NNTP-Posting-Date",    false,  HTobs,  0,    NULL,    NULL, 0 },
59     {   "Xref",                 false,  HTstd,  0,    NULL,    NULL, 0 },
60     {   "Injection-Date",       true,   HTstd,  0,    NULL,    NULL, 0 },
61     {   "Injection-Info",       false,  HTstd,  0,    NULL,    NULL, 0 },
62     {   "Summary",              true,   HTstd,  0,    NULL,    NULL, 0 },
63     {   "Keywords",             true,   HTstd,  0,    NULL,    NULL, 0 },
64     {   "User-Agent",           true,   HTstd,  0,    NULL,    NULL, 0 },
65     {   "Date-Received",        false,  HTobs,  0,    NULL,    NULL, 0 },
66     {   "Posting-Version",      false,  HTobs,  0,    NULL,    NULL, 0 },
67     {   "Relay-Version",        false,  HTobs,  0,    NULL,    NULL, 0 },
68     {   "Cc",                   true,   HTstd,  0,    NULL,    NULL, 0 },
69     {   "Bcc",                  true,   HTstd,  0,    NULL,    NULL, 0 },
70     {   "To",                   true,   HTstd,  0,    NULL,    NULL, 0 },
71     {   "Archived-At",          true,   HTstd,  0,    NULL,    NULL, 0 },
72     {   "Also-Control",         false,  HTobs,  0,    NULL,    NULL, 0 },
73     {   "Article-Names",        false,  HTobs,  0,    NULL,    NULL, 0 },
74     {   "Article-Updates",      false,  HTobs,  0,    NULL,    NULL, 0 },
75     {   "See-Also",             false,  HTobs,  0,    NULL,    NULL, 0 },
76     {   "Cancel-Key",           true,   HTstd,  0,    NULL,    NULL, 0 },
77     {   "Cancel-Lock",          true,   HTstd,  0,    NULL,    NULL, 0 },
78 /* The Comments and Original-Sender header fields can appear more than once
79  * in the headers of an article.  Consequently, we MUST NOT put them here. */
80 };
81 /* clang-format on */
82 
83 HEADER *EndOfTable = ARRAY_END(Table);
84 
85 
86 /*
87 **  Turn any \r or \n in text into spaces.  Used to splice back multi-line
88 **  header field bodies into a single line.
89 **  Taken from innd.c.
90 */
91 static char *
Join(char * text)92 Join(char *text)
93 {
94     char *p;
95 
96     for (p = text; *p; p++)
97         if (*p == '\n' || *p == '\r')
98             *p = ' ';
99     return text;
100 }
101 
102 
103 /*
104 **  Return a short name that won't overrun our buffer or syslog's buffer.
105 **  q should either be p, or point into p where the "interesting" part is.
106 **  Taken from innd.c.
107 */
108 static char *
MaxLength(char * p,char * q)109 MaxLength(char *p, char *q)
110 {
111     static char buff[80];
112     unsigned int i;
113 
114 
115     /* Return an empty string when p is NULL. */
116     if (p == NULL) {
117         *buff = '\0';
118         return buff;
119     }
120 
121     /* Already short enough? */
122     i = strlen(p);
123     if (i < sizeof buff - 1)
124         return Join(p);
125 
126     /* Don't want casts to unsigned to go horribly wrong. */
127     if (q < p || q > p + i)
128         q = p;
129 
130     /* Simple case of just want the beginning? */
131     if (q == NULL || (size_t)(q - p) < sizeof(buff) - 4) {
132         strlcpy(buff, p, sizeof(buff) - 3);
133         strlcat(buff, "...", sizeof(buff));
134     } else if ((p + i) - q < 10) {
135         /* Is getting last 10 characters good enough? */
136         strlcpy(buff, p, sizeof(buff) - 13);
137         strlcat(buff, "...", sizeof(buff) - 10);
138         strlcat(buff, &p[i - 10], sizeof(buff));
139     } else {
140         /* Not in last 10 bytes, so use double ellipses. */
141         strlcpy(buff, p, sizeof(buff) - 16);
142         strlcat(buff, "...", sizeof(buff) - 13);
143         strlcat(buff, &q[-5], sizeof(buff) - 3);
144         strlcat(buff, "...", sizeof(buff));
145     }
146     return Join(buff);
147 }
148 
149 
150 /*
151 **  Trim leading and trailing spaces, return the length of the result.
152 */
153 int
TrimSpaces(char * p)154 TrimSpaces(char *p)
155 {
156     char *start;
157 
158     for (start = p; ISWHITE(*start) || *start == '\n'; start++)
159         continue;
160     for (p = start + strlen(start);
161          p > start && isspace((unsigned char) p[-1]); p--)
162         continue;
163     return (int) (p - start);
164 }
165 
166 
167 /*
168 **  Mark the end of the header field starting at p, and return a pointer
169 **  to the start of the next one or NULL.  Handles continuations.
170 */
171 static char *
NextHeader(char * p)172 NextHeader(char *p)
173 {
174     char *q;
175     for (q = p; (p = strchr(p, '\n')) != NULL; p++) {
176         /* Note that '\r\n' has temporarily been internally replaced by '\n'.
177          * Therefore, the count takes it into account (+1, besides the
178          * length (p-q+1) of the string). */
179         if (p - q + 2 > MAXARTLINELENGTH) {
180             strlcpy(Error, "Header line too long", sizeof(Error));
181             return NULL;
182         }
183         /* Check if there is a continuation line for the header field body. */
184         if (ISWHITE(p[1])) {
185             q = p + 1;
186             continue;
187         }
188         *p = '\0';
189         return p + 1;
190     }
191     strlcpy(Error, "Article has no body -- just headers", sizeof(Error));
192     return NULL;
193 }
194 
195 
196 /*
197 **  Strip any header fields off the article and dump them into the table.
198 **  On error, return NULL and fill in Error.
199 */
200 static char *
StripOffHeaders(char * article)201 StripOffHeaders(char *article)
202 {
203     char *p;
204     char *q;
205     HEADER *hp;
206     char c;
207 
208     /* Scan through buffer, a header field at a time. */
209     for (p = article;;) {
210 
211         /* See if it's a known header field name. */
212         c = islower((unsigned char) *p) ? toupper((unsigned char) *p) : *p;
213         for (hp = Table; hp < ARRAY_END(Table); hp++) {
214             if (c == hp->Name[0] && p[hp->Size] == ':'
215                 && strncasecmp(p, hp->Name, hp->Size) == 0) {
216                 if (hp->Type == HTobs) {
217                     snprintf(Error, sizeof(Error), "Obsolete %s header field",
218                              hp->Name);
219                     return NULL;
220                 }
221                 if (hp->Value) {
222                     snprintf(Error, sizeof(Error), "Duplicate %s header field",
223                              hp->Name);
224                     return NULL;
225                 }
226                 hp->Value = &p[hp->Size + 1];
227                 /* '\r\n' is replaced with '\n', and unnecessary to consider
228                  * '\r'. */
229                 for (q = &p[hp->Size + 1];
230                      ISWHITE(*q) || (*q == '\n' && ISWHITE(q[1])); q++) {
231                     continue;
232                 }
233                 hp->Body = q;
234                 break;
235             }
236         }
237 
238         /* No; add it to the set of other header fields. */
239         if (hp == ARRAY_END(Table)) {
240             if (OtherCount + 1 >= OtherSize) {
241                 OtherSize += HEADER_DELTA;
242                 OtherHeaders =
243                     xrealloc(OtherHeaders, OtherSize * sizeof(char *));
244             }
245             OtherHeaders[OtherCount++] = p;
246         }
247 
248         /* Get start of next header field; if it's a blank line, we hit
249          * the end. */
250         if ((p = NextHeader(p)) == NULL) {
251             /* Error set in NextHeader(). */
252             return NULL;
253         }
254         if (*p == '\n')
255             break;
256     }
257 
258     return p + 1;
259 }
260 
261 
262 /*
263 **  Check the control message, and see if it's legit.  Return pointer to
264 **  error message if not.
265 */
266 static const char *
CheckControl(char * ctrl)267 CheckControl(char *ctrl)
268 {
269     char *p;
270     char *q;
271     char save;
272 
273     /* Snip off the first word. */
274     for (p = ctrl; ISWHITE(*p); p++)
275         continue;
276     for (ctrl = p; *p && !ISWHITE(*p); p++)
277         continue;
278     if (p == ctrl)
279         return "Empty control message";
280     save = *p;
281     *p = '\0';
282 
283     if (strcasecmp(ctrl, "cancel") == 0) {
284         for (q = p + 1; ISWHITE(*q); q++)
285             continue;
286         if (*q == '\0')
287             return "Message-ID missing in cancel";
288     } else if (strcasecmp(ctrl, "checkgroups") == 0
289                || strcasecmp(ctrl, "ihave") == 0
290                || strcasecmp(ctrl, "sendme") == 0
291                || strcasecmp(ctrl, "newgroup") == 0
292                || strcasecmp(ctrl, "rmgroup") == 0)
293         ;
294     else {
295         snprintf(Error, sizeof(Error), "\"%s\" is not a valid control message",
296                  MaxLength(ctrl, ctrl));
297         return Error;
298     }
299     *p = save;
300     return NULL;
301 }
302 
303 
304 /*
305 **  Check the Distribution header field, and exit on error.
306 */
307 static const char *
CheckDistribution(char * p)308 CheckDistribution(char *p)
309 {
310     static char SEPS[] = " \t,";
311     const char *const *dp;
312 
313     if ((p = strtok(p, SEPS)) == NULL)
314         return "Can't parse Distribution header field";
315     do {
316         for (dp = BadDistribs; *dp; dp++)
317             if (uwildmat(p, *dp)) {
318                 snprintf(Error, sizeof(Error), "Illegal distribution \"%s\"",
319                          MaxLength(p, p));
320                 return Error;
321             }
322     } while ((p = strtok((char *) NULL, SEPS)) != NULL);
323     return NULL;
324 }
325 
326 
327 /*
328 **  Process all the headers.
329 **  Return NULL if okay, or an error message.
330 */
331 static const char *
ProcessHeaders(char * idbuff,bool needmoderation)332 ProcessHeaders(char *idbuff, bool needmoderation)
333 {
334     static char datebuff[40];
335     static char localdatebuff[40];
336     static char orgbuff[SMBUF];
337     static char pathidentitybuff[SMBUF];
338     static char complaintsbuff[SMBUF];
339     static char postingaccountbuff[SMBUF * 2]; /* Allocate enough room. */
340     static char postinghostbuff[SMBUF * 2];
341     static char sendbuff[SMBUF * 2];
342     static char injectioninfobuff[SMBUF * 7];
343     static char *newpath = NULL;
344     HEADER *hp;
345     char *p;
346     char *bad_header;
347     char *fqdn = NULL;
348     time_t t, now;
349     const char *error;
350     pid_t pid;
351     bool addvirtual = false;
352     size_t i;
353 
354     /* Get the current time, used for creating and checking dates. */
355     now = time(NULL);
356 
357     /* datebuff is used for both Injection-Date and Date header fields
358      * so we have to set it now, and it has to be the UTC date (unless
359      * for the Date header field if localtime is set to true
360      * in readers.conf). */
361     if (!makedate(-1, false, datebuff, sizeof(datebuff)))
362         return "Can't generate Date header field body";
363 
364     /* Do some preliminary fix-ups. */
365     for (hp = Table; hp < ARRAY_END(Table); hp++) {
366         if (!hp->CanSet && hp->Value) {
367             snprintf(Error, sizeof(Error), "Can't set system %s header field",
368                      hp->Name);
369             return Error;
370         }
371         if (hp->Value) {
372             hp->Len = TrimSpaces(hp->Value);
373             /* If the header field body is empty, we just remove it.  We do
374              * not reject the article, contrary to what an injecting agent
375              * is supposed to do per Section 3.5 of RFC 5537.  (A revision
376              * to RFC 5537 may someday allow again that existing and useful
377              * feature.) */
378             if (hp->Len == 0) {
379                 hp->Value = hp->Body = NULL;
380             } else if (!IsValidHeaderBody(hp->Value)) {
381                 snprintf(Error, sizeof(Error),
382                          "Invalid syntax encountered in %s header field "
383                          "body (unexpected byte or empty content line)",
384                          hp->Name);
385                 return Error;
386             }
387         }
388     }
389 
390     /* Set the Injection-Date header field. */
391     /* Start with this header field because it MUST NOT be added in case
392      * the article already contains both Message-ID and Date
393      * header fields (possibility of multiple injection, see Sections 3.4.2
394      * and 3.5 of RFC 5537). */
395     if (HDR(HDR__INJECTION_DATE) == NULL) {
396         /* If moderation is needed, do not add an Injection-Date header field.
397          */
398         if (!needmoderation && PERMaccessconf->addinjectiondate) {
399             if ((HDR(HDR__MESSAGEID) == NULL) || (HDR(HDR__DATE) == NULL)) {
400                 HDR_SET(HDR__INJECTION_DATE, datebuff);
401             }
402         }
403     } else {
404         t = parsedate_rfc5322_lax(HDR(HDR__INJECTION_DATE));
405         if (t == (time_t) -1)
406             return "Can't parse Injection-Date header field body";
407         if (t > now + DATE_FUZZ)
408             return "Article injected in the future";
409     }
410 
411     /* If authorized, add the header field based on our info.  If not
412      * authorized, zap the Sender header field so we don't put out
413      * unauthenticated data. */
414     if (PERMaccessconf->nnrpdauthsender) {
415         if (PERMauthorized && PERMuser[0] != '\0') {
416             p = strchr(PERMuser, '@');
417             if (p == NULL) {
418                 snprintf(sendbuff, sizeof(sendbuff), "%s@%s", PERMuser,
419                          Client.host);
420             } else {
421                 snprintf(sendbuff, sizeof(sendbuff), "%s", PERMuser);
422             }
423             HDR_SET(HDR__SENDER, sendbuff);
424         } else {
425             HDR_CLEAR(HDR__SENDER);
426         }
427     }
428 
429     /* Set the Date header field. */
430     if (HDR(HDR__DATE) == NULL) {
431         if (PERMaccessconf->localtime) {
432             if (!makedate(-1, true, localdatebuff, sizeof(localdatebuff)))
433                 return "Can't generate local Date header field body";
434             HDR_SET(HDR__DATE, localdatebuff);
435         } else {
436             HDR_SET(HDR__DATE, datebuff);
437         }
438     } else {
439         t = parsedate_rfc5322_lax(HDR(HDR__DATE));
440         if (t == (time_t) -1)
441             return "Can't parse Date header field body";
442         if (t > now + DATE_FUZZ)
443             return "Article posted in the future";
444         /* This check is done for the Date header field by nnrpd.
445          * innd, as a relaying agent, does not check it when an Injection-Date
446          * header field is present. */
447         if (innconf->artcutoff != 0) {
448             long cutoff = innconf->artcutoff * 24 * 60 * 60;
449             if (t < now - cutoff)
450                 return "Article posted too far in the past (check still "
451                        "done for legacy reasons on the Date header field)";
452         }
453     }
454 
455     /* The Newsgroups header field is checked later. */
456 
457     if (HDR(HDR__CONTROL) != NULL) {
458         if ((error = CheckControl(HDR(HDR__CONTROL))) != NULL)
459             return error;
460     }
461 
462     /* Set the Message-ID header field. */
463     if (HDR(HDR__MESSAGEID) == NULL) {
464         HDR_SET(HDR__MESSAGEID, idbuff);
465     }
466     if (!IsValidMessageID(HDR(HDR__MESSAGEID), true, laxmid)) {
467         return "Can't parse Message-ID header field body";
468     }
469 
470     /* Set the Path header field. */
471     if (HDR(HDR__PATH) == NULL || PERMaccessconf->strippath) {
472         /* Note that innd will put host name here for us. */
473         /* If moderation is needed, do not update the Path header field. */
474         if (!needmoderation)
475             HDR_SET(HDR__PATH, (char *) PATHMASTER);
476         else if (PERMaccessconf->strippath)
477             HDR_CLEAR(HDR__PATH);
478 
479         if (VirtualPathlen > 0)
480             addvirtual = true;
481     } else {
482         /* Check that the article has not been injected yet. */
483         for (p = HDR(HDR__PATH); *p != '\0'; p++) {
484             if (*p == '.' && strncasecmp(p, ".POSTED", 7) == 0
485                 && (p[7] == '.' || p[7] == '!' || p[7] == ' ' || p[7] == '\t'
486                     || p[7] == '\r' || p[7] == '\n')
487                 && (p == HDR(HDR__PATH) || p[-1] == '!')) {
488                 return "Path header field shows a previous injection of the "
489                        "article";
490             }
491         }
492 
493         /* Check whether the virtual host name is required. */
494         if ((VirtualPathlen > 0)
495             && (p = strchr(HDR(HDR__PATH), '!')) != NULL) {
496             *p = '\0';
497             if (strcasecmp(HDR(HDR__PATH), PERMaccessconf->pathhost) != 0)
498                 addvirtual = true;
499             *p = '!';
500         } else if (VirtualPathlen > 0)
501             addvirtual = true;
502     }
503 
504     if (newpath != NULL)
505         free(newpath);
506     if (PERMaccessconf->addinjectionpostinghost) {
507         if (addvirtual) {
508             newpath = concat(VirtualPath, ".POSTED.", Client.host, "!",
509                              HDR(HDR__PATH), (char *) 0);
510         } else {
511             newpath = concat(".POSTED.", Client.host, "!", HDR(HDR__PATH),
512                              (char *) 0);
513         }
514     } else {
515         if (addvirtual) {
516             newpath =
517                 concat(VirtualPath, ".POSTED!", HDR(HDR__PATH), (char *) 0);
518         } else {
519             newpath = concat(".POSTED!", HDR(HDR__PATH), (char *) 0);
520         }
521     }
522     /* If moderation is needed, do not update the Path header field. */
523     if (!needmoderation)
524         HDR_SET(HDR__PATH, newpath);
525 
526     /* The Reply-To header field is left alone. */
527     /* The Sender header field is set above. */
528 
529     /* Check the Expires header field. */
530     if (HDR(HDR__EXPIRES) && parsedate_rfc5322_lax(HDR(HDR__EXPIRES)) == -1)
531         return "Can't parse Expires header field";
532 
533     /* The References header field is left alone. */
534     /* The Control header field is checked above. */
535 
536     /* Check the Distribution header field. */
537     if ((p = HDR(HDR__DISTRIBUTION)) != NULL) {
538         p = xstrdup(p);
539         error = CheckDistribution(p);
540         free(p);
541         if (error != NULL)
542             return error;
543     }
544 
545     /* Set the Organization header field. */
546     if (HDR(HDR__ORGANIZATION) == NULL
547         && (p = PERMaccessconf->organization) != NULL) {
548         strlcpy(orgbuff, p, sizeof(orgbuff));
549         HDR_SET(HDR__ORGANIZATION, orgbuff);
550     }
551 
552     /* The Keywords header field is left alone. */
553     /* The Summary header field is left alone. */
554     /* The Approved header field is left alone. */
555 
556     /* The Lines header field should not be generated. */
557 
558     /* The Supersedes header field is left alone. */
559 
560     /* Set the Injection-Info header field. */
561     /* Set the path identity. */
562     if (VirtualPathlen > 0) {
563         p = PERMaccessconf->domain;
564     } else {
565         fqdn = inn_getfqdn(PERMaccessconf->domain);
566         if (fqdn == NULL)
567             p = (char *) "unknown";
568         else
569             p = fqdn;
570     }
571     snprintf(pathidentitybuff, sizeof(pathidentitybuff), "%s", p);
572     free(fqdn);
573     p = NULL;
574 
575     /* Set the posting-account value. */
576     if (PERMaccessconf->addinjectionpostingaccount && PERMuser[0] != '\0') {
577         snprintf(postingaccountbuff, sizeof(postingaccountbuff),
578                  "; posting-account=\"%s\"", PERMuser);
579     }
580 
581     /* Set the posting-host identity.
582      * Check a proper definition of Client.host and Client.ip
583      * (we already saw the case of "localhost:" without IP),
584      * when getpeername fails. */
585     if ((strlen(Client.host) > 0) || (strlen(Client.ip) > 0)) {
586         if ((strcmp(Client.host, Client.ip) == 0)
587             || (strlen(Client.host) == 0)) {
588             snprintf(postinghostbuff, sizeof(postinghostbuff),
589                      "; posting-host=\"%s\"", Client.ip);
590         } else if (strlen(Client.ip) == 0) {
591             snprintf(postinghostbuff, sizeof(postinghostbuff),
592                      "; posting-host=\"%s\"", Client.host);
593         } else {
594             snprintf(postinghostbuff, sizeof(postinghostbuff),
595                      "; posting-host=\"%s:%s\"", Client.host, Client.ip);
596         }
597     }
598 
599     /* Set the logging-data attribute. */
600     pid = getpid();
601 
602     /* Set the mail-complaints-to attribute. */
603     if ((p = PERMaccessconf->complaints) != NULL) {
604         snprintf(complaintsbuff, sizeof(complaintsbuff), "%s", p);
605     } else {
606         static const char newsmaster[] = NEWSMASTER;
607 
608         if ((p = PERMaccessconf->fromhost) != NULL
609             && strchr(newsmaster, '@') == NULL) {
610             snprintf(complaintsbuff, sizeof(complaintsbuff), "%s@%s",
611                      newsmaster, p);
612         } else {
613             snprintf(complaintsbuff, sizeof(complaintsbuff), "%s", newsmaster);
614         }
615     }
616 
617     /* ARTpost() will convert bare LF to CRLF.  Do not use CRLF here.*/
618     snprintf(injectioninfobuff, sizeof(injectioninfobuff),
619              "%s%s%s;\n\tlogging-data=\"%ld\"; mail-complaints-to=\"%s\"",
620              pathidentitybuff,
621              PERMaccessconf->addinjectionpostingaccount && PERMuser[0] != '\0'
622                  ? postingaccountbuff
623                  : "",
624              PERMaccessconf->addinjectionpostinghost ? postinghostbuff : "",
625              (long) pid, complaintsbuff);
626 
627     /* If moderation is needed, do not add an Injection-Info header field. */
628     if (!needmoderation)
629         HDR_SET(HDR__INJECTION_INFO, injectioninfobuff);
630 
631     /* Clear out some header fields that should not be here. */
632     if (PERMaccessconf->strippostcc) {
633         HDR_CLEAR(HDR__CC);
634         HDR_CLEAR(HDR__BCC);
635         HDR_CLEAR(HDR__TO);
636     }
637 
638     /* Now make sure everything is there. */
639     for (hp = Table; hp < ARRAY_END(Table); hp++)
640         if (hp->Type == HTreq && hp->Value == NULL) {
641             snprintf(Error, sizeof(Error), "Missing required %s header field",
642                      hp->Name);
643             return Error;
644         }
645 
646     /* Check that all other header fields are valid. */
647     for (i = 0; i < OtherCount; i++) {
648         if (!IsValidHeaderField(OtherHeaders[i])) {
649             p = strchr(OtherHeaders[i], ':');
650             if (p == NULL || p == OtherHeaders[i])
651                 bad_header = xstrdup(OtherHeaders[i]);
652             else
653                 bad_header = xstrndup(OtherHeaders[i], p - OtherHeaders[i]);
654             snprintf(Error, sizeof(Error),
655                      "Invalid syntax encountered in header field (unexpected "
656                      "byte, no colon-space, or empty content line): %s",
657                      bad_header);
658             free(bad_header);
659             return Error;
660         }
661     }
662 
663     return NULL;
664 }
665 
666 
667 /*
668 **  See if the user has more included text than new text.  Simple-minded,
669 **  but reasonably effective for catching neophyte's mistakes.  Son-of-1036
670 **  says:
671 **
672 **      NOTE: While encouraging trimming is desirable, the 50% rule imposed
673 **      by some old posting agents is both inadequate and counterproductive.
674 **      Posters do not respond to it by being more selective about quoting;
675 **      they respond by padding short responses, or by using different
676 **      quoting styles to defeat automatic analysis.  The former adds
677 **      unnecessary noise and volume, while the latter also defeats more
678 **      useful forms of automatic analysis that reading agents might wish to
679 **      do.
680 **
681 **      NOTE: At the very least, if a minimum-unquoted quota is being set,
682 **      article bodies shorter than (say) 20 lines, or perhaps articles
683 **      which exceed the quota by only a few lines, should be exempt.  This
684 **      avoids the ridiculous situation of complaining about a 5-line
685 **      response to a 6-line quote.
686 **
687 **  Accordingly, bodies shorter than 20 lines are exempt.  A line starting
688 **  with >, |, or : is included text.  Decrement the count on lines starting
689 **  with < so that we don't reject diff(1) output.
690 */
691 static const char *
CheckIncludedText(const char * p,int lines)692 CheckIncludedText(const char *p, int lines)
693 {
694     int i;
695 
696     if (lines < 20)
697         return NULL;
698     for (i = 0;; p++) {
699         /* clang-format off */
700         switch (*p) {
701         case '>': i++; break;
702         case '|': i++; break;
703         case ':': i++; break;
704         case '<': i--; break;
705         default:       break;
706         }
707         /* clang-format on */
708         p = strchr(p, '\n');
709         if (p == NULL)
710             break;
711     }
712     if (i * 2 > lines)
713         return "Article not posted -- more included text than new text";
714     return NULL;
715 }
716 
717 
718 /*
719 **  Try to mail an article to the moderator of the group.
720 */
721 static const char *
MailArticle(char * group,char * article)722 MailArticle(char *group, char *article)
723 {
724     static char CANTSEND[] = "Can't send text to mailer";
725     FILE *F;
726     HEADER *hp;
727     size_t i;
728     int status;
729     char *address;
730     char buff[SMBUF];
731 
732     /* Try to get the address first. */
733     if ((address = GetModeratorAddress(NULL, NULL, group,
734                                        PERMaccessconf->moderatormailer))
735         == NULL) {
736         snprintf(Error, sizeof(Error), "No mailing address for \"%s\" -- %s",
737                  group, "ask your news administrator to fix this");
738         free(group);
739         return Error;
740     }
741     free(group);
742 
743     /* Now build up the command (ignore format/argument mismatch errors,
744      * in case %s isn't in inconf->mta) and send the headers. */
745     if (innconf->mta == NULL)
746         return "Can't start mailer -- mta not set";
747 #if __GNUC__ > 4 || defined(__llvm__) || defined(__clang__)
748 #    pragma GCC diagnostic ignored "-Wformat-nonliteral"
749 #endif
750     snprintf(buff, sizeof(buff), innconf->mta, address);
751 #if __GNUC__ > 4 || defined(__llvm__) || defined(__clang__)
752 #    pragma GCC diagnostic warning "-Wformat-nonliteral"
753 #endif
754     if ((F = popen(buff, "w")) == NULL)
755         return "Can't start mailer";
756     fprintf(F, "To: %s\n", address);
757     if (FLUSH_ERROR(F)) {
758         pclose(F);
759         return CANTSEND;
760     }
761 
762     /* Write the headers, a blank line, then the article. */
763     for (hp = Table; hp < ARRAY_END(Table); hp++)
764         if (hp->Value) {
765             if (*hp->Value == ' ' || *hp->Value == '\t')
766                 fprintf(F, "%s:%s\n", hp->Name, hp->Value);
767             else
768                 fprintf(F, "%s: %s\n", hp->Name, hp->Value);
769             if (FLUSH_ERROR(F)) {
770                 pclose(F);
771                 return CANTSEND;
772             }
773         }
774     for (i = 0; i < OtherCount; i++) {
775         fprintf(F, "%s\n", OtherHeaders[i]);
776         if (FLUSH_ERROR(F)) {
777             pclose(F);
778             return CANTSEND;
779         }
780     }
781     fprintf(F, "\n");
782     i = strlen(article);
783     if (fwrite(article, 1, i, F) != (size_t) i)
784         return "Can't send article";
785     if (FLUSH_ERROR(F)) {
786         pclose(F);
787         return CANTSEND;
788     }
789     status = pclose(F);
790     if (status != 0) {
791         snprintf(Error, sizeof(Error), "Mailer exited with status %d -- %s",
792                  status, "Article might not have been mailed");
793         return Error;
794     }
795     return NULL;
796 }
797 
798 
799 /*
800 **  Check the newsgroups and make sure they're all valid, that none are
801 **  moderated, etc.
802 */
803 static const char *
ValidNewsgroups(char * hdr,char ** modgroup)804 ValidNewsgroups(char *hdr, char **modgroup)
805 {
806     static char distbuff[SMBUF];
807     char *groups;
808     char *p;
809     bool approved;
810     struct _DDHANDLE *h;
811     char *grplist[2];
812     bool IsNewgroup;
813     bool FoundOne;
814     int flag;
815     bool hookpresent = false;
816 
817 #ifdef DO_PYTHON
818     hookpresent = PY_use_dynamic;
819 #endif /* DO_PYTHON */
820 
821     p = HDR(HDR__CONTROL);
822     IsNewgroup = (p && strncasecmp(p, "newgroup", 8) == 0);
823     groups = xstrdup(hdr);
824     if ((p = strtok(groups, NGSEPS)) == NULL) {
825         free(groups);
826         return "Can't parse Newsgroups header field body";
827     }
828     Error[0] = '\0';
829 
830     /* Reject all articles with Approved header fields unless the user is
831      * allowed to add them, even to unmoderated or local groups.  We want to
832      * reject them to unmoderated groups in case there's a disagreement of
833      * opinion between various sites as to the moderation status. */
834     approved = HDR(HDR__APPROVED) != NULL;
835     if (approved && !PERMaccessconf->allowapproved) {
836         snprintf(Error, sizeof(Error),
837                  "You are not allowed to approve postings");
838     }
839 
840     FoundOne = false;
841     h = DDstart((FILE *) NULL, (FILE *) NULL);
842     do {
843         if (innconf->mergetogroups && strncmp(p, "to.", 3) == 0)
844             p = (char *) "to";
845         if (!hookpresent && PERMspecified) {
846             grplist[0] = p;
847             grplist[1] = NULL;
848             if (!PERMmatch(PERMpostlist, grplist)) {
849                 snprintf(Error, sizeof(Error),
850                          "You are not allowed to post to %s\r\n", p);
851             }
852         }
853         if (!OVgroupstats(p, NULL, NULL, NULL, &flag))
854             continue;
855         FoundOne = true;
856         DDcheck(h, p);
857         switch (flag) {
858         case NF_FLAG_OK:
859 #ifdef DO_PYTHON
860             if (PY_use_dynamic) {
861                 char *reply;
862 
863                 /* Authorize user using Python module method dynamic. */
864                 if (PY_dynamic(PERMuser, p, true, &reply) < 0) {
865                     syslog(L_NOTICE, "PY_dynamic(): authorization skipped due "
866                                      "to no Python dynamic method defined");
867                 } else {
868                     if (reply != NULL) {
869                         syslog(L_TRACE,
870                                "PY_dynamic() returned a refuse string for "
871                                "user %s at %s who wants to post to %s: %s",
872                                PERMuser, Client.host, p, reply);
873                         snprintf(Error, sizeof(Error), "%s\r\n", reply);
874                         free(reply);
875                         break;
876                     }
877                 }
878             }
879 #endif /* DO_PYTHON */
880             break;
881         case NF_FLAG_MODERATED:
882             if (!approved && modgroup != NULL && !*modgroup)
883                 *modgroup = xstrdup(p);
884             break;
885         case NF_FLAG_IGNORE:
886         case NF_FLAG_JUNK:
887         case NF_FLAG_NOLOCAL:
888             if (!PERMaccessconf->locpost)
889                 snprintf(Error, sizeof(Error),
890                          "Postings to \"%s\" are not allowed here", p);
891             break;
892         case NF_FLAG_ALIAS:
893             snprintf(Error, sizeof(Error),
894                      "The newsgroup \"%s\" has been renamed\n", p);
895             break;
896         }
897     } while ((p = strtok((char *) NULL, NGSEPS)) != NULL);
898     free(groups);
899 
900     if (!FoundOne && !IsNewgroup)
901         snprintf(Error, sizeof(Error), "No valid newsgroups in \"%s\"",
902                  MaxLength(hdr, hdr));
903     if (Error[0]) {
904         tmpPtr = DDend(h);
905         free(tmpPtr);
906         if (modgroup != NULL && *modgroup != NULL) {
907             free(*modgroup);
908             *modgroup = NULL;
909         }
910         return Error;
911     }
912 
913     p = DDend(h);
914     if (HDR(HDR__DISTRIBUTION) == NULL && *p) {
915         strlcpy(distbuff, p, sizeof(distbuff));
916         HDR_SET(HDR__DISTRIBUTION, distbuff);
917     }
918     free(p);
919     return NULL;
920 }
921 
922 
923 /*
924 **  Send a QUIT message to the server, eat its reply.
925 */
926 static void
SendQuit(FILE * FromServer,FILE * ToServer)927 SendQuit(FILE *FromServer, FILE *ToServer)
928 {
929     char buff[NNTP_MAXLEN_COMMAND];
930 
931     fprintf(ToServer, "QUIT\r\n");
932     fflush(ToServer);
933     fclose(ToServer);
934     if (fgets(buff, sizeof buff, FromServer) == NULL) {
935         /* ignore: we don't care if we don't get the server reply */
936     }
937     fclose(FromServer);
938 }
939 
940 
941 /*
942 **  Offer the article to the server, return its reply.
943 */
944 static int
OfferArticle(char * buff,int buffsize,FILE * FromServer,FILE * ToServer)945 OfferArticle(char *buff, int buffsize, FILE *FromServer, FILE *ToServer)
946 {
947     /* We have a valid message-ID here (checked beforehand). */
948     fprintf(ToServer, "IHAVE %s\r\n", HDR(HDR__MESSAGEID));
949     if (FLUSH_ERROR(ToServer) || fgets(buff, buffsize, FromServer) == NULL) {
950         snprintf(buff, buffsize, "Can't send %s to server, %s", "IHAVE",
951                  strerror(errno));
952         return -1;
953     }
954     return atoi(buff);
955 }
956 
957 
958 /*
959 **  Spool article to temp file.
960 */
961 static const char *
SpoolitTo(char * article,char * err,char * SpoolDir)962 SpoolitTo(char *article, char *err, char *SpoolDir)
963 {
964     static char CANTSPOOL[NNTP_MAXLEN_COMMAND + 2];
965     HEADER *hp;
966     FILE *F = NULL;
967     size_t i;
968     int fd;
969     char *tmpspool = NULL;
970     char *spoolfile = NULL;
971     char *q;
972 
973     /* Initialize the returned error message. */
974     snprintf(CANTSPOOL, sizeof(CANTSPOOL),
975              "%s and can't write text to local spool file", err);
976 
977     /* Try to write it to the spool dir. */
978     tmpspool = concatpath(SpoolDir, ".XXXXXX");
979     fd = mkstemp(tmpspool);
980     if (fd < 0) {
981         syslog(L_FATAL, "can't create temporary spool file %s %m", tmpspool);
982         goto fail;
983     }
984     F = fdopen(fd, "w");
985     if (F == NULL) {
986         syslog(L_FATAL, "can't open %s %m", tmpspool);
987         goto fail;
988     }
989     fchmod(fileno(F), BATCHFILE_MODE);
990 
991     /* Write the headers and a blank line. */
992     for (hp = Table; hp < ARRAY_END(Table); hp++)
993         if (hp->Value) {
994             q = xstrndup(hp->Value, hp->Body - hp->Value + hp->Len);
995             if (*hp->Value == ' ' || *hp->Value == '\t')
996                 fprintf(F, "%s:%s\n", hp->Name, q);
997             else
998                 fprintf(F, "%s: %s\n", hp->Name, q);
999             if (FLUSH_ERROR(F)) {
1000                 fclose(F);
1001                 free(q);
1002                 goto fail;
1003             }
1004             free(q);
1005         }
1006     for (i = 0; i < OtherCount; i++) {
1007         fprintf(F, "%s\n", OtherHeaders[i]);
1008         if (FLUSH_ERROR(F)) {
1009             fclose(F);
1010             goto fail;
1011         }
1012     }
1013     fprintf(F, "\n");
1014 
1015     /* Write the article body. */
1016     i = strlen(article);
1017     if (fwrite(article, 1, i, F) != (size_t) i) {
1018         fclose(F);
1019         goto fail;
1020     }
1021 
1022     /* Flush and catch any errors. */
1023     if (fclose(F))
1024         goto fail;
1025 
1026     /* Rename the spool file to something rnews will pick up. */
1027     spoolfile = concatpath(SpoolDir, "XXXXXX");
1028     fd = mkstemp(spoolfile);
1029     if (fd < 0) {
1030         syslog(L_FATAL, "can't create spool file %s %m", spoolfile);
1031         goto fail;
1032     }
1033     close(fd);
1034     if (rename(tmpspool, spoolfile) < 0) {
1035         syslog(L_FATAL, "can't rename %s %s %m", tmpspool, spoolfile);
1036         goto fail;
1037     }
1038 
1039     /* Article has been spooled. */
1040     free(tmpspool);
1041     free(spoolfile);
1042     return NULL;
1043 
1044 fail:
1045     if (tmpspool != NULL)
1046         free(tmpspool);
1047     if (spoolfile != NULL)
1048         free(spoolfile);
1049     return CANTSPOOL;
1050 }
1051 
1052 
1053 /*
1054 **  Spool article to temp file.
1055 */
1056 static const char *
Spoolit(char * article,char * err)1057 Spoolit(char *article, char *err)
1058 {
1059     return SpoolitTo(article, err, innconf->pathincoming);
1060 }
1061 
1062 
1063 static char *
Towire(char * p)1064 Towire(char *p)
1065 {
1066     char *q, *r, *s;
1067     int curlen, len = BIG_BUFFER;
1068 
1069     for (r = p, q = s = xmalloc(len); *r != '\0';) {
1070         curlen = q - s;
1071         if (curlen + 3 > len) {
1072             len += BIG_BUFFER;
1073             s = xrealloc(s, len);
1074             q = s + curlen;
1075         }
1076         if (*r == '\n') {
1077             if (r > p) {
1078                 if (*(r - 1) != '\r')
1079                     *q++ = '\r';
1080             } else {
1081                 /* This should not happen. */
1082                 free(s);
1083                 return NULL;
1084             }
1085         }
1086         *q++ = *r++;
1087     }
1088     curlen = q - s;
1089     if (curlen + 1 > len) {
1090         len++;
1091         s = xrealloc(s, len);
1092         q = s + curlen;
1093     }
1094     *q = '\0';
1095     return s;
1096 }
1097 
1098 
1099 /*
1100 **  The main function which handles POST and IHAVE.
1101 */
1102 const char *
ARTpost(char * article,char * idbuff,bool * permanent)1103 ARTpost(char *article, char *idbuff, bool *permanent)
1104 {
1105     int i;
1106     size_t j;
1107     char *p, *q;
1108     char *next;
1109     HEADER *hp;
1110     FILE *ToServer;
1111     FILE *FromServer;
1112     char buff[NNTP_MAXLEN_COMMAND + 2], frombuf[SMBUF];
1113     char *modgroup = NULL;
1114     const char *error;
1115     char *TrackID;
1116     char *DirTrackID;
1117     FILE *ftd;
1118 
1119     /* Assume errors are permanent, until we discover otherwise. */
1120     *permanent = true;
1121 
1122     /* Set up the other header fields list. */
1123     if (OtherHeaders == NULL) {
1124         OtherSize = HEADER_DELTA;
1125         OtherHeaders = xmalloc(OtherSize * sizeof(char *));
1126     }
1127 
1128     /* Basic processing. */
1129     OtherCount = 0;
1130     for (hp = Table; hp < ARRAY_END(Table); hp++) {
1131         hp->Size = strlen(hp->Name);
1132         hp->Value = hp->Body = NULL;
1133     }
1134     if ((article = StripOffHeaders(article)) == NULL)
1135         return Error;
1136     for (i = 0, p = article; p; i++, p = next + 1)
1137         if ((next = strchr(p, '\n')) == NULL)
1138             break;
1139     if (PERMaccessconf->checkincludedtext) {
1140         if ((error = CheckIncludedText(article, i)) != NULL)
1141             return error;
1142     }
1143 
1144     /* modgroup is set when moderated newsgroups are found in the
1145      * Newsgroups header field, and the article does not contain
1146      * an Approved header field.
1147      * Therefore, moderation will be needed.
1148      *
1149      * Be sure to check that a Newsgroups header field exists
1150      * because ProcessHeaders() still has not been called.  It would
1151      * have rejected the message. */
1152     if (HDR(HDR__NEWSGROUPS) != NULL) {
1153         if ((error = ValidNewsgroups(HDR(HDR__NEWSGROUPS), &modgroup)) != NULL)
1154             return error;
1155     }
1156 
1157     if ((error = ProcessHeaders(idbuff, modgroup != NULL)) != NULL) {
1158         if (modgroup != NULL)
1159             free(modgroup);
1160         return error;
1161     }
1162 
1163     if (i == 0 && HDR(HDR__CONTROL) == NULL) {
1164         if (modgroup != NULL)
1165             free(modgroup);
1166         return "Article is empty";
1167     }
1168 
1169     strlcpy(frombuf, HDR(HDR__FROM), sizeof(frombuf));
1170     /* Unfold the From header field. */
1171     for (p = frombuf; p < frombuf + sizeof(frombuf);)
1172         if ((p = strchr(p, '\n')) == NULL)
1173             break;
1174         else
1175             *p++ = ' ';
1176     /* Try to rewrite the From header field in a cleaner format. */
1177     HeaderCleanFrom(frombuf);
1178     /* Now perform basic checks of the From header field.
1179      * Pass leading '@' chars because they are not part of an address. */
1180     p = frombuf;
1181     while (*p == '@') {
1182         p++;
1183     }
1184     p = strchr(p, '@');
1185     if (p != NULL) {
1186         p = strrchr(p + 1, '.');
1187         if (p == NULL) {
1188             if (modgroup)
1189                 free(modgroup);
1190             return "Address in From header field not in Internet syntax";
1191         }
1192     } else {
1193         if (modgroup)
1194             free(modgroup);
1195         return "Address in From header field not in Internet syntax";
1196     }
1197     if ((p = HDR(HDR__FOLLOWUPTO)) != NULL && strcmp(p, "poster") != 0
1198         && (error = ValidNewsgroups(p, (char **) NULL)) != NULL) {
1199         if (modgroup)
1200             free(modgroup);
1201         return error;
1202     }
1203     if ((PERMaccessconf->localmaxartsize != 0)
1204         && (strlen(article) > PERMaccessconf->localmaxartsize)) {
1205         snprintf(Error, sizeof(Error),
1206                  "Article is bigger than local limit of %lu bytes\n",
1207                  PERMaccessconf->localmaxartsize);
1208         if (modgroup)
1209             free(modgroup);
1210         return Error;
1211     }
1212 
1213 #if defined(DO_PERL)
1214     /* Calls the Perl subroutine for headers management.
1215      * The article may be modified, and its syntax may become invalid
1216      * but well... that's the news admin choice! */
1217     p = PERMaccessconf->nnrpdperlfilter ? HandleHeaders(article) : NULL;
1218     if (p != NULL) {
1219         char SDir[255];
1220 
1221         if (idbuff) {
1222             if (modgroup)
1223                 snprintf(idbuff, SMBUF, "(mailed to moderator for %s)",
1224                          modgroup);
1225             else if (HDR(HDR__MESSAGEID) != idbuff) {
1226                 strlcpy(idbuff, HDR(HDR__MESSAGEID), SMBUF);
1227             }
1228         }
1229         if (strncmp(p, "DROP", 4) == 0) {
1230             syslog(L_NOTICE, "%s post failed %s", Client.host, p);
1231             if (modgroup)
1232                 free(modgroup);
1233             return NULL;
1234         } else if (strncmp(p, "SPOOL", 5) == 0) {
1235             syslog(L_NOTICE, "%s post failed %s", Client.host, p);
1236             strlcpy(SDir, innconf->pathincoming, sizeof(SDir));
1237             if (modgroup) {
1238                 free(modgroup);
1239                 strlcat(SDir, "/spam/mod", sizeof(SDir));
1240                 return SpoolitTo(article, p, SDir);
1241             } else {
1242                 strlcat(SDir, "/spam", sizeof(SDir));
1243                 return SpoolitTo(article, p, SDir);
1244             }
1245         } else if (strncmp(p, "CLOSE", 5) == 0) {
1246             syslog(L_NOTICE, "%s post failed %s", Client.host, p);
1247             Reply("%d NNTP server unavailable; no posting\r\n",
1248                   NNTP_FAIL_TERMINATING);
1249             POSTrejected++;
1250             ExitWithStats(1, true);
1251         } else {
1252             if (modgroup)
1253                 free(modgroup);
1254             return p;
1255         }
1256     }
1257 #endif /* defined(DO_PERL) */
1258 
1259     /* Handle mailing to moderated groups. */
1260 
1261     if (modgroup) {
1262         if (idbuff != NULL) {
1263             const char *retstr;
1264             retstr = MailArticle(modgroup, article);
1265             /* MailArticle frees modgroup. */
1266             strlcpy(idbuff, "(mailed to moderator)", SMBUF);
1267             return retstr;
1268         }
1269         return MailArticle(modgroup, article);
1270     }
1271 
1272     if (idbuff != NULL && HDR(HDR__MESSAGEID) != idbuff) {
1273         strlcpy(idbuff, HDR(HDR__MESSAGEID), SMBUF);
1274     }
1275 
1276     if (PERMaccessconf->spoolfirst)
1277         return Spoolit(article, Error);
1278 
1279     if (Offlinepost)
1280         return Spoolit(article, Error);
1281 
1282     /* Open a local connection to the server. */
1283     if (PERMaccessconf->nnrpdposthost != NULL)
1284         i = NNTPconnect(PERMaccessconf->nnrpdposthost,
1285                         PERMaccessconf->nnrpdpostport, &FromServer, &ToServer,
1286                         buff, sizeof(buff));
1287     else {
1288 #if defined(HAVE_UNIX_DOMAIN_SOCKETS)
1289         i = NNTPlocalopen(&FromServer, &ToServer, buff, sizeof(buff));
1290 #else
1291         i = NNTPremoteopen(innconf->port, &FromServer, &ToServer, buff,
1292                            sizeof(buff));
1293 #endif /* defined(HAVE_UNIX_DOMAIN_SOCKETS) */
1294     }
1295 
1296     /* If we cannot open the connection, initialize the error message and
1297      * attempt to recover from this by spooling it locally. */
1298     if (i < 0) {
1299         if (buff[0])
1300             strlcpy(Error, buff, sizeof(Error));
1301         else {
1302             snprintf(Error, sizeof(Error),
1303                      "Can't send connect request to server, %s",
1304                      strerror(errno));
1305         }
1306         return Spoolit(article, Error);
1307     }
1308 
1309     if (Tracing)
1310         syslog(L_TRACE, "%s post_connect %s", Client.host,
1311                PERMaccessconf->nnrpdposthost ? PERMaccessconf->nnrpdposthost
1312                                              : "localhost");
1313 
1314     /* The code below ignores too many return values for my tastes.  At least
1315      * they are all inside cases that are most likely never going to happen --
1316      * for example, if the server crashes. */
1317 
1318     /* Offer article to server. */
1319     i = OfferArticle(buff, (int) sizeof buff, FromServer, ToServer);
1320     if (i == NNTP_FAIL_AUTH_NEEDED) {
1321         /* Send authorization. */
1322         if (NNTPsendpassword(PERMaccessconf->nnrpdposthost, FromServer,
1323                              ToServer)
1324             < 0) {
1325             snprintf(Error, sizeof(Error), "Can't authorize with %s",
1326                      PERMaccessconf->nnrpdposthost
1327                          ? PERMaccessconf->nnrpdposthost
1328                          : "innd");
1329             return Spoolit(article, Error);
1330         }
1331         i = OfferArticle(buff, (int) sizeof buff, FromServer, ToServer);
1332     }
1333     if (i != NNTP_CONT_IHAVE) {
1334         strlcpy(Error, buff, sizeof(Error));
1335         SendQuit(FromServer, ToServer);
1336         if (i == NNTP_FAIL_IHAVE_REJECT || i == NNTP_FAIL_IHAVE_DEFER) {
1337             *permanent = false;
1338         }
1339         /* As the syntax of the IHAVE command sent by nnrpd is valid,
1340          * the only valid case of response is a refusal. */
1341         if (i != NNTP_FAIL_IHAVE_REFUSE)
1342             return Spoolit(article, Error);
1343         return Error;
1344     }
1345     if (Tracing)
1346         syslog(L_TRACE, "%s post starting", Client.host);
1347 
1348     /* Write the headers and a blank line. */
1349     for (hp = Table; hp < ARRAY_END(Table); hp++)
1350         if (hp->Value) {
1351             q = xstrndup(hp->Value, hp->Body - hp->Value + hp->Len);
1352             if (strchr(q, '\n') != NULL) {
1353                 if ((p = Towire(q)) != NULL) {
1354                     /* There is no white space, if hp->Value and hp->Body are
1355                      * the same. */
1356                     if (*hp->Value == ' ' || *hp->Value == '\t')
1357                         fprintf(ToServer, "%s:%s\r\n", hp->Name, p);
1358                     else
1359                         fprintf(ToServer, "%s: %s\r\n", hp->Name, p);
1360                     free(p);
1361                 }
1362             } else {
1363                 /* There is no white space, if hp->Value and hp->Body are the
1364                  * same. */
1365                 if (*hp->Value == ' ' || *hp->Value == '\t')
1366                     fprintf(ToServer, "%s:%s\r\n", hp->Name, q);
1367                 else
1368                     fprintf(ToServer, "%s: %s\r\n", hp->Name, q);
1369             }
1370             free(q);
1371         }
1372     for (j = 0; j < OtherCount; j++) {
1373         if (strchr(OtherHeaders[j], '\n') != NULL) {
1374             if ((p = Towire(OtherHeaders[j])) != NULL) {
1375                 fprintf(ToServer, "%s\r\n", p);
1376                 free(p);
1377             }
1378         } else {
1379             fprintf(ToServer, "%s\r\n", OtherHeaders[j]);
1380         }
1381     }
1382     fprintf(ToServer, "\r\n");
1383     if (FLUSH_ERROR(ToServer)) {
1384         snprintf(Error, sizeof(Error), "Can't send headers to server, %s",
1385                  strerror(errno));
1386         fclose(FromServer);
1387         fclose(ToServer);
1388         return Spoolit(article, Error);
1389     }
1390 
1391     /* Send the article, get the server's reply. */
1392     if (NNTPsendarticle(article, ToServer, true) < 0
1393         || fgets(buff, sizeof buff, FromServer) == NULL) {
1394         snprintf(Error, sizeof(Error), "Can't send article to server, %s",
1395                  strerror(errno));
1396         fclose(FromServer);
1397         fclose(ToServer);
1398         return Spoolit(article, Error);
1399     }
1400 
1401     /* Did the server want the article? */
1402     if ((i = atoi(buff)) != NNTP_OK_IHAVE) {
1403         strlcpy(Error, buff, sizeof(Error));
1404         SendQuit(FromServer, ToServer);
1405         syslog(L_TRACE, "%s server rejects %s from %s", Client.host,
1406                HDR(HDR__MESSAGEID), HDR(HDR__PATH));
1407         if (i != NNTP_FAIL_IHAVE_REJECT && i != NNTP_FAIL_IHAVE_REFUSE)
1408             return Spoolit(article, Error);
1409         if (i == NNTP_FAIL_IHAVE_REJECT || i == NNTP_FAIL_IHAVE_DEFER) {
1410             *permanent = false;
1411         }
1412         return Error;
1413     }
1414 
1415     /* Send a quit and close down. */
1416     SendQuit(FromServer, ToServer);
1417 
1418     /* Tracking. */
1419     if (PERMaccessconf->readertrack) {
1420         TrackID = concat(innconf->pathlog, "/trackposts/track.",
1421                          HDR(HDR__MESSAGEID), (char *) 0);
1422         if ((ftd = fopen(TrackID, "w")) == NULL) {
1423             DirTrackID = concatpath(innconf->pathlog, "trackposts");
1424             MakeDirectory(DirTrackID, false);
1425             free(DirTrackID);
1426         }
1427         if (ftd == NULL && (ftd = fopen(TrackID, "w")) == NULL) {
1428             syslog(L_ERROR, "%s (%s) open %s: %m", Client.host, Username,
1429                    TrackID);
1430             free(TrackID);
1431             return NULL;
1432         }
1433         for (hp = Table; hp < ARRAY_END(Table); hp++)
1434             if (hp->Value) {
1435                 q = xstrndup(hp->Value, hp->Body - hp->Value + hp->Len);
1436                 if (strchr(q, '\n') != NULL) {
1437                     if ((p = Towire(q)) != NULL) {
1438                         /* There is no white space, if hp->Value and hp->Body
1439                          * are the same. */
1440                         if (*hp->Value == ' ' || *hp->Value == '\t')
1441                             fprintf(ftd, "%s:%s\r\n", hp->Name, p);
1442                         else
1443                             fprintf(ftd, "%s: %s\r\n", hp->Name, p);
1444                         free(p);
1445                     }
1446                 } else {
1447                     /* There is no white space, if hp->Value and hp->Body are
1448                      * the same. */
1449                     if (*hp->Value == ' ' || *hp->Value == '\t')
1450                         fprintf(ftd, "%s:%s\r\n", hp->Name, q);
1451                     else
1452                         fprintf(ftd, "%s: %s\r\n", hp->Name, q);
1453                 }
1454                 free(q);
1455             }
1456         for (j = 0; j < OtherCount; j++) {
1457             if (strchr(OtherHeaders[j], '\n') != NULL) {
1458                 if ((p = Towire(OtherHeaders[j])) != NULL) {
1459                     fprintf(ftd, "%s\r\n", p);
1460                     free(p);
1461                 }
1462             } else {
1463                 fprintf(ftd, "%s\r\n", OtherHeaders[j]);
1464             }
1465         }
1466         fprintf(ftd, "\r\n");
1467         NNTPsendarticle(article, ftd, true);
1468         if (fclose(ftd) != EOF) {
1469             syslog(L_NOTICE, "%s (%s) posttrack ok %s", Client.host, Username,
1470                    TrackID);
1471             if (LLOGenable)
1472                 fprintf(locallog, "%s (%s) posttrack ok %s\n", Client.host,
1473                         Username, TrackID);
1474         } else {
1475             syslog(L_ERROR, "%s (%s) posttrack error 2 %s", Client.host,
1476                    Username, TrackID);
1477         }
1478         free(TrackID);
1479     }
1480 
1481     return NULL;
1482 }
1483