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