1 /*
2 ** Article-related routines.
3 */
4
5 #include "portable/system.h"
6
7 #include <assert.h>
8 #if HAVE_LIMITS_H
9 # include <limits.h>
10 #endif
11 #include <ctype.h>
12 #include <sys/uio.h>
13
14 #include "cache.h"
15 #include "inn/innconf.h"
16 #include "inn/messages.h"
17 #include "inn/ov.h"
18 #include "inn/overview.h"
19 #include "inn/wire.h"
20 #include "nnrpd.h"
21 #include "tls.h"
22
23 /*
24 ** Data structures for use in ARTICLE/HEAD/BODY/STAT common code.
25 */
26 typedef enum _SENDTYPE
27 {
28 STarticle,
29 SThead,
30 STbody,
31 STstat
32 } SENDTYPE;
33
34 typedef struct _SENDDATA {
35 SENDTYPE Type;
36 int ReplyCode;
37 const char *Item;
38 } SENDDATA;
39
40 static ARTHANDLE *ARThandle = NULL;
41
42 /* clang-format off */
43 static SENDDATA SENDbody = {
44 STbody, NNTP_OK_BODY, "body"
45 };
46 static SENDDATA SENDarticle = {
47 STarticle, NNTP_OK_ARTICLE, "article"
48 };
49 static SENDDATA SENDstat = {
50 STstat, NNTP_OK_STAT, "status"
51 };
52 static SENDDATA SENDhead = {
53 SThead, NNTP_OK_HEAD, "head"
54 };
55 /* clang-format on */
56
57 static struct iovec iov[IOV_MAX > 1024 ? 1024 : IOV_MAX];
58 static int queued_iov = 0;
59
60 static void
PushIOvHelper(struct iovec * vec,int * countp)61 PushIOvHelper(struct iovec *vec, int *countp)
62 {
63 int result = 0;
64
65 TMRstart(TMR_NNTPWRITE);
66
67 #if defined(HAVE_ZLIB)
68 if (compression_layer_on) {
69 int i;
70
71 for (i = 0; i < *countp; i++) {
72 if (i + 1 == *countp) {
73 /* Time to flush the compressed output stream. */
74 zstream_flush_needed = true;
75 }
76 write_buffer(vec[i].iov_base, vec[i].iov_len);
77 }
78
79 *countp = 0;
80
81 TMRstop(TMR_NNTPWRITE);
82 return;
83 }
84 #endif /* HAVE_ZLIB */
85
86 #ifdef HAVE_SASL
87 if (sasl_conn && sasl_ssf) {
88 int i;
89
90 for (i = 0; i < *countp; i++) {
91 write_buffer(vec[i].iov_base, vec[i].iov_len);
92 }
93 } else {
94 #endif /* HAVE_SASL */
95
96 #ifdef HAVE_OPENSSL
97 if (tls_conn) {
98 Again:
99 result = SSL_writev(tls_conn, vec, *countp);
100 switch (SSL_get_error(tls_conn, result)) {
101 case SSL_ERROR_NONE:
102 break;
103 case SSL_ERROR_WANT_WRITE:
104 goto Again;
105 /* NOTREACHED */
106 case SSL_ERROR_ZERO_RETURN:
107 SSL_shutdown(tls_conn);
108 goto fallthrough;
109 case SSL_ERROR_SSL:
110 case SSL_ERROR_SYSCALL:
111 fallthrough:
112 /* SSL_shutdown() must not be called. */
113 tls_conn = NULL;
114 errno = ECONNRESET;
115 break;
116 }
117 } else
118 #endif /* HAVE_OPENSSL */
119 result = xwritev(STDOUT_FILENO, vec, *countp);
120
121 #ifdef HAVE_SASL
122 }
123 #endif
124
125 TMRstop(TMR_NNTPWRITE);
126
127 if (result == -1) {
128 /* We can't recover, since we can't resynchronise with our
129 * peer. */
130 ExitWithStats(1, true);
131 }
132 *countp = 0;
133 }
134
135 static void
PushIOvRateLimited(void)136 PushIOvRateLimited(void)
137 {
138 double start, end, elapsed, target;
139 struct iovec newiov[IOV_MAX > 1024 ? 1024 : IOV_MAX];
140 int newiov_len;
141 int sentiov;
142 int i;
143 int bytesfound;
144 int chunkbittenoff;
145 struct timeval waittime;
146
147 while (queued_iov) {
148 bytesfound = newiov_len = 0;
149 sentiov = 0;
150 for (i = 0; (i < queued_iov) && (bytesfound < MaxBytesPerSecond);
151 i++) {
152 if ((signed) iov[i].iov_len + bytesfound > MaxBytesPerSecond) {
153 chunkbittenoff = MaxBytesPerSecond - bytesfound;
154 newiov[newiov_len].iov_base = iov[i].iov_base;
155 newiov[newiov_len++].iov_len = chunkbittenoff;
156 iov[i].iov_base = (char *) iov[i].iov_base + chunkbittenoff;
157 iov[i].iov_len -= chunkbittenoff;
158 bytesfound += chunkbittenoff;
159 } else {
160 newiov[newiov_len++] = iov[i];
161 sentiov++;
162 bytesfound += iov[i].iov_len;
163 }
164 }
165 assert(sentiov <= queued_iov);
166 start = TMRnow_double();
167 PushIOvHelper(newiov, &newiov_len);
168 end = TMRnow_double();
169 target = (double) bytesfound / (double) MaxBytesPerSecond;
170 elapsed = end - start;
171 if (elapsed < 1 && elapsed < target) {
172 waittime.tv_sec = 0;
173 waittime.tv_usec = (long) ((target - elapsed) * 1e6);
174 start = TMRnow_double();
175 if (select(0, NULL, NULL, NULL, &waittime) != 0)
176 syswarn("%s: select in PushIOvRateLimit failed", Client.host);
177 end = TMRnow_double();
178 IDLEtime += end - start;
179 }
180 memmove(iov, &iov[sentiov],
181 (queued_iov - sentiov) * sizeof(struct iovec));
182 queued_iov -= sentiov;
183 }
184 }
185
186 static void
PushIOv(void)187 PushIOv(void)
188 {
189 TMRstart(TMR_NNTPWRITE);
190 fflush(stdout);
191 TMRstop(TMR_NNTPWRITE);
192 if (MaxBytesPerSecond != 0)
193 PushIOvRateLimited();
194 else
195 PushIOvHelper(iov, &queued_iov);
196 }
197
198 static void
SendIOv(const char * p,int len)199 SendIOv(const char *p, int len)
200 {
201 char *q;
202
203 if (queued_iov) {
204 q = (char *) iov[queued_iov - 1].iov_base
205 + iov[queued_iov - 1].iov_len;
206 if (p == q) {
207 iov[queued_iov - 1].iov_len += len;
208 return;
209 }
210 }
211 iov[queued_iov].iov_base = (char *) p;
212 iov[queued_iov++].iov_len = len;
213 if (queued_iov == IOV_MAX)
214 PushIOv();
215 }
216
217 static char *_IO_buffer_ = NULL;
218 static int highwater = 0;
219
220 static void
PushIOb(void)221 PushIOb(void)
222 {
223 #if defined(HAVE_ZLIB)
224 /* Last line of a multi-line data block response.
225 * Time to flush the compressed output stream. */
226 zstream_flush_needed = true;
227 #endif /* HAVE_ZLIB */
228
229 write_buffer(_IO_buffer_, highwater);
230 highwater = 0;
231 }
232
233 static void
SendIOb(const char * p,int len)234 SendIOb(const char *p, int len)
235 {
236 int tocopy;
237
238 if (_IO_buffer_ == NULL)
239 _IO_buffer_ = xmalloc(BIG_BUFFER);
240
241 while (len > 0) {
242 tocopy =
243 (len > (BIG_BUFFER - highwater)) ? (BIG_BUFFER - highwater) : len;
244 memcpy(&_IO_buffer_[highwater], p, tocopy);
245 p += tocopy;
246 highwater += tocopy;
247 len -= tocopy;
248 if (highwater == BIG_BUFFER)
249 PushIOb();
250 }
251 }
252
253
254 /*
255 ** If we have an article open, close it.
256 */
257 void
ARTclose(void)258 ARTclose(void)
259 {
260 if (ARThandle) {
261 SMfreearticle(ARThandle);
262 ARThandle = NULL;
263 }
264 }
265
266 bool
ARTinstorebytoken(TOKEN token)267 ARTinstorebytoken(TOKEN token)
268 {
269 ARTHANDLE *art;
270 struct timeval stv, etv;
271
272 if (PERMaccessconf->nnrpdoverstats) {
273 gettimeofday(&stv, NULL);
274 }
275 art = SMretrieve(token, RETR_STAT);
276 /* XXX This isn't really overstats, is it? */
277 if (PERMaccessconf->nnrpdoverstats) {
278 gettimeofday(&etv, NULL);
279 OVERartcheck += (etv.tv_sec - stv.tv_sec) * 1000;
280 OVERartcheck += (etv.tv_usec - stv.tv_usec) / 1000;
281 }
282 if (art) {
283 SMfreearticle(art);
284 return true;
285 }
286 return false;
287 }
288
289 /*
290 ** If the article name is valid, open it and stuff in the ID.
291 */
292 static bool
ARTopen(ARTNUM artnum)293 ARTopen(ARTNUM artnum)
294 {
295 static ARTNUM save_artnum;
296 TOKEN token;
297
298 /* Re-use article if it's the same one. */
299 if (save_artnum == artnum) {
300 if (ARThandle)
301 return true;
302 }
303 ARTclose();
304
305 if (!OVgetartinfo(GRPcur, artnum, &token))
306 return false;
307
308 TMRstart(TMR_READART);
309 ARThandle = SMretrieve(token, RETR_ALL);
310 TMRstop(TMR_READART);
311 if (ARThandle == NULL) {
312 return false;
313 }
314
315 save_artnum = artnum;
316 return true;
317 }
318
319
320 /*
321 ** Open the article for a given Message-ID.
322 */
323 static bool
ARTopenbyid(char * msg_id,ARTNUM * ap,bool final)324 ARTopenbyid(char *msg_id, ARTNUM *ap, bool final)
325 {
326 TOKEN token;
327
328 *ap = 0;
329 token = cache_get(HashMessageID(msg_id), final);
330 if (token.type == TOKEN_EMPTY) {
331 if (History == NULL) {
332 time_t statinterval;
333
334 /* Do lazy opens of the history file: lots of clients
335 * will never ask for anything by Message-ID, so put off
336 * doing the work until we have to. */
337 History = HISopen(HISTORY, innconf->hismethod, HIS_RDONLY);
338 if (!History) {
339 syslog(L_NOTICE, "can't initialize history");
340 Reply("%d NNTP server unavailable; try later\r\n",
341 NNTP_FAIL_TERMINATING);
342 ExitWithStats(1, true);
343 }
344 statinterval = 30;
345 HISctl(History, HISCTLS_STATINTERVAL, &statinterval);
346 }
347 if (!HISlookup(History, msg_id, NULL, NULL, NULL, &token))
348 return false;
349 }
350 if (token.type == TOKEN_EMPTY)
351 return false;
352 TMRstart(TMR_READART);
353 ARThandle = SMretrieve(token, RETR_ALL);
354 TMRstop(TMR_READART);
355 if (ARThandle == NULL) {
356 return false;
357 }
358
359 return true;
360 }
361
362 /*
363 ** Send a (part of) a file to stdout, doing newline and dot conversion.
364 */
365 static void
ARTsendmmap(SENDTYPE what)366 ARTsendmmap(SENDTYPE what)
367 {
368 const char *p, *q, *r;
369 const char *s, *path, *xref, *endofpath;
370 char lastchar;
371
372 ARTcount++;
373 GRParticles++;
374 lastchar = -1;
375
376 /* Get the headers and detect if wire format. */
377 if (what == STarticle) {
378 q = ARThandle->data;
379 p = ARThandle->data + ARThandle->len;
380 } else {
381 for (q = p = ARThandle->data; p < (ARThandle->data + ARThandle->len);
382 p++) {
383 if (*p == '\r')
384 continue;
385 if (*p == '\n') {
386 if (lastchar == '\n') {
387 if (what == SThead) {
388 if (*(p - 1) == '\r')
389 p--;
390 break;
391 } else {
392 q = p + 1;
393 p = ARThandle->data + ARThandle->len;
394 break;
395 }
396 }
397 }
398 lastchar = *p;
399 }
400 }
401
402 /* q points to the start of the article buffer, p to the end of it. */
403 if (VirtualPathlen > 0 && (what != STbody)) {
404 path = wire_findheader(ARThandle->data, ARThandle->len, "Path", true);
405 if (path == NULL) {
406 SendIOv(".\r\n", 3);
407 ARTgetsize += 3;
408 PushIOv();
409 ARTget++;
410 return;
411 } else {
412 xref =
413 wire_findheader(ARThandle->data, ARThandle->len, "Xref", true);
414 if (xref == NULL) {
415 SendIOv(".\r\n", 3);
416 ARTgetsize += 3;
417 PushIOv();
418 ARTget++;
419 return;
420 }
421 }
422 endofpath = wire_endheader(path, ARThandle->data + ARThandle->len - 1);
423 if (endofpath == NULL) {
424 SendIOv(".\r\n", 3);
425 ARTgetsize += 3;
426 PushIOv();
427 ARTget++;
428 return;
429 }
430 if ((r = memchr(xref, ' ', p - xref)) == NULL || r == p) {
431 SendIOv(".\r\n", 3);
432 ARTgetsize += 3;
433 PushIOv();
434 ARTget++;
435 return;
436 }
437 /* r points to the first space in the Xref header field body. */
438
439 for (s = path, lastchar = '\0'; s + VirtualPathlen + 1 < endofpath;
440 lastchar = *s++) {
441 if ((lastchar != '\0' && lastchar != '!') || *s != *VirtualPath
442 || strncasecmp(s, VirtualPath, VirtualPathlen - 1) != 0)
443 continue;
444 if (*(s + VirtualPathlen - 1) != '\0'
445 && *(s + VirtualPathlen - 1) != '!')
446 continue;
447 break;
448 }
449 if (s + VirtualPathlen + 1 < endofpath) {
450 if (xref > path) {
451 SendIOv(q, path - q);
452 SendIOv(s, xref - s);
453 SendIOv(VirtualPath, VirtualPathlen - 1);
454 SendIOv(r, p - r);
455 } else {
456 SendIOv(q, xref - q);
457 SendIOv(VirtualPath, VirtualPathlen - 1);
458 SendIOv(r, path - r);
459 SendIOv(s, p - s);
460 }
461 } else {
462 /* Double the '!' (thus, adding one '!') in Path header field. */
463 if (xref > path) {
464 SendIOv(q, path - q);
465 SendIOv(VirtualPath, VirtualPathlen);
466 SendIOv("!", 1);
467 SendIOv(path, xref - path);
468 SendIOv(VirtualPath, VirtualPathlen - 1);
469 SendIOv(r, p - r);
470 } else {
471 SendIOv(q, xref - q);
472 SendIOv(VirtualPath, VirtualPathlen - 1);
473 SendIOv(r, path - r);
474 SendIOv(VirtualPath, VirtualPathlen);
475 SendIOv("!", 1);
476 SendIOv(path, p - path);
477 }
478 }
479 } else
480 SendIOv(q, p - q);
481 ARTgetsize += p - q;
482 if (what == SThead) {
483 SendIOv(".\r\n", 3);
484 ARTgetsize += 3;
485 } else if (memcmp((ARThandle->data + ARThandle->len - 5), "\r\n.\r\n",
486 5)) {
487 if (memcmp((ARThandle->data + ARThandle->len - 2), "\r\n", 2)) {
488 SendIOv("\r\n.\r\n", 5);
489 ARTgetsize += 5;
490 } else {
491 SendIOv(".\r\n", 3);
492 ARTgetsize += 3;
493 }
494 }
495 PushIOv();
496
497 ARTget++;
498 }
499
500 /*
501 ** Return the header field from the specified file, or NULL if not found.
502 */
503 char *
GetHeader(const char * header,bool stripspaces)504 GetHeader(const char *header, bool stripspaces)
505 {
506 const char *p, *q, *r, *s, *t;
507 char *w, *wnew, prevchar;
508 /* Bogus value here to make sure that it isn't initialized to \n. */
509 char lastchar = ' ';
510 const char *limit;
511 const char *cmplimit;
512 static char *retval = NULL;
513 static int retlen = 0;
514 int headerlen;
515 bool pathheader = false;
516 bool xrefheader = false;
517
518 limit = ARThandle->data + ARThandle->len;
519 cmplimit = ARThandle->data + ARThandle->len - strlen(header) - 1;
520 for (p = ARThandle->data; p < cmplimit; p++) {
521 if (*p == '\r')
522 continue;
523 if ((lastchar == '\n') && (*p == '\n')) {
524 return NULL;
525 }
526 if ((lastchar == '\n') || (p == ARThandle->data)) {
527 headerlen = strlen(header);
528 if (strncasecmp(p, header, headerlen) == 0 && p[headerlen] == ':'
529 && ISWHITE(p[headerlen + 1])) {
530 p += headerlen + 2;
531 if (stripspaces) {
532 for (; (p < limit) && ISWHITE(*p); p++)
533 ;
534 }
535 for (q = p; q < limit; q++)
536 if ((*q == '\r') || (*q == '\n')) {
537 /* Check for continuation header lines. */
538 t = q + 1;
539 if (t < limit) {
540 if ((*q == '\r' && *t == '\n')) {
541 t++;
542 if (t == limit)
543 break;
544 }
545 if ((*t == '\t' || *t == ' ')) {
546 for (; (t < limit) && ISWHITE(*t); t++)
547 ;
548 q = t;
549 } else {
550 break;
551 }
552 } else {
553 break;
554 }
555 }
556 if (q == limit)
557 return NULL;
558 if (strncasecmp("Path", header, headerlen) == 0)
559 pathheader = true;
560 else if (strncasecmp("Xref", header, headerlen) == 0)
561 xrefheader = true;
562 if (retval == NULL) {
563 /* Possibly add '!' (a second one) at the end of the
564 * virtual path. So it is +2 because of '\0'. */
565 retlen = q - p + VirtualPathlen + 2;
566 retval = xmalloc(retlen);
567 } else {
568 if ((q - p + VirtualPathlen + 2) > retlen) {
569 retlen = q - p + VirtualPathlen + 2;
570 retval = xrealloc(retval, retlen);
571 }
572 }
573 if (pathheader && (VirtualPathlen > 0)) {
574 const char *endofpath;
575 const char *endofarticle;
576
577 endofarticle = ARThandle->data + ARThandle->len - 1;
578 endofpath = wire_endheader(p, endofarticle);
579 if (endofpath == NULL)
580 return NULL;
581 for (s = p, prevchar = '\0';
582 s + VirtualPathlen + 1 < endofpath; prevchar = *s++) {
583 if ((prevchar != '\0' && prevchar != '!')
584 || *s != *VirtualPath
585 || strncasecmp(s, VirtualPath, VirtualPathlen - 1)
586 != 0)
587 continue;
588 if (*(s + VirtualPathlen - 1) != '\0'
589 && *(s + VirtualPathlen - 1) != '!')
590 continue;
591 break;
592 }
593 if (s + VirtualPathlen + 1 < endofpath) {
594 memcpy(retval, s, q - s);
595 *(retval + (int) (q - s)) = '\0';
596 } else {
597 memcpy(retval, VirtualPath, VirtualPathlen);
598 *(retval + VirtualPathlen) = '!';
599 memcpy(retval + VirtualPathlen + 1, p, q - p);
600 *(retval + (int) (q - p) + VirtualPathlen + 1) = '\0';
601 }
602 } else if (xrefheader && (VirtualPathlen > 0)) {
603 if ((r = memchr(p, ' ', q - p)) == NULL)
604 return NULL;
605 for (; (r < q) && isspace((unsigned char) *r); r++)
606 ;
607 if (r == q)
608 return NULL;
609 /* Copy the virtual path without its final '!'. */
610 memcpy(retval, VirtualPath, VirtualPathlen - 1);
611 memcpy(retval + VirtualPathlen - 1, r - 1, q - r + 1);
612 *(retval + (int) (q - r) + VirtualPathlen) = '\0';
613 } else {
614 memcpy(retval, p, q - p);
615 *(retval + (int) (q - p)) = '\0';
616 }
617 for (w = retval, wnew = retval; *w; w++) {
618 if (*w == '\r' && w[1] == '\n') {
619 w++;
620 continue;
621 }
622 if (*w == '\0' || *w == '\t' || *w == '\r' || *w == '\n') {
623 *wnew = ' ';
624 } else {
625 *wnew = *w;
626 }
627 wnew++;
628 }
629 *wnew = '\0';
630 return retval;
631 }
632 }
633 lastchar = *p;
634 }
635 return NULL;
636 }
637
638 /*
639 ** Fetch part or all of an article and send it to the client.
640 */
641 void
CMDfetch(int ac,char * av[])642 CMDfetch(int ac, char *av[])
643 {
644 char buff[SMBUF];
645 SENDDATA *what;
646 bool mid, ok;
647 ARTNUM art;
648 char *msgid;
649 ARTNUM tart;
650 bool final = false;
651
652 mid = (ac > 1 && IsValidMessageID(av[1], true, laxmid));
653
654 /* Check the syntax of the arguments first. */
655 if (ac > 1 && !IsValidArticleNumber(av[1])) {
656 /* It is better to check for a number before a Message-ID because
657 * '<' could have been forgotten and the error message should then
658 * report a syntax error in the Message-ID. */
659 if (isdigit((unsigned char) av[1][0])) {
660 Reply("%d Syntax error in article number\r\n", NNTP_ERR_SYNTAX);
661 return;
662 } else if (!mid) {
663 Reply("%d Syntax error in Message-ID\r\n", NNTP_ERR_SYNTAX);
664 return;
665 }
666 }
667
668 /* Find what to send; get permissions. */
669 ok = PERMcanread;
670 switch (*av[0]) {
671 default:
672 what = &SENDbody;
673 final = true;
674 break;
675 case 'a':
676 case 'A':
677 what = &SENDarticle;
678 final = true;
679 break;
680 case 's':
681 case 'S':
682 what = &SENDstat;
683 break;
684 case 'h':
685 case 'H':
686 what = &SENDhead;
687 /* Poster might do a HEAD command to verify the article. */
688 ok = PERMcanread || PERMcanpost;
689 break;
690 }
691
692 /* Trying to read. */
693 if (GRPcount == 0 && !mid) {
694 Reply("%d Not in a newsgroup\r\n", NNTP_FAIL_NO_GROUP);
695 return;
696 }
697
698 /* Check authorizations. If an article number is requested
699 * (not a Message-ID), we check whether the group is still readable. */
700 if (!ok || (!mid && PERMgroupmadeinvalid)) {
701 Reply("%d Read access denied\r\n",
702 PERMcanauthenticate ? NNTP_FAIL_AUTH_NEEDED : NNTP_ERR_ACCESS);
703 return;
704 }
705
706 /* Requesting by Message-ID? */
707 if (mid) {
708 if (!ARTopenbyid(av[1], &art, final)) {
709 Reply("%d No such article\r\n", NNTP_FAIL_MSGID_NOTFOUND);
710 return;
711 }
712 if (!PERMartok()) {
713 ARTclose();
714 Reply("%d Read access denied for this article\r\n",
715 PERMcanauthenticate ? NNTP_FAIL_AUTH_NEEDED
716 : NNTP_ERR_ACCESS);
717 return;
718 }
719 Reply("%d %lu %s %s\r\n", what->ReplyCode, art, av[1], what->Item);
720 if (what->Type != STstat) {
721 ARTsendmmap(what->Type);
722 }
723 ARTclose();
724 return;
725 }
726
727 /* Default is to get current article, or specified article. */
728 if (ac == 1) {
729 if (ARTnumber < ARTlow || ARTnumber > ARThigh) {
730 Reply("%d Current article number %lu is invalid\r\n",
731 NNTP_FAIL_ARTNUM_INVALID, ARTnumber);
732 return;
733 }
734 snprintf(buff, sizeof(buff), "%lu", ARTnumber);
735 tart = ARTnumber;
736 } else {
737 /* We have already checked that the article number is valid. */
738 strlcpy(buff, av[1], sizeof(buff));
739 tart = (ARTNUM) atol(buff);
740 }
741
742 /* Open the article and send the reply. */
743 if (!ARTopen(tart)) {
744 Reply("%d No such article number %lu\r\n", NNTP_FAIL_ARTNUM_NOTFOUND,
745 tart);
746 return;
747 }
748 if ((msgid = GetHeader("Message-ID", true)) == NULL) {
749 ARTclose();
750 Reply("%d No such article number %lu\r\n", NNTP_FAIL_ARTNUM_NOTFOUND,
751 tart);
752 return;
753 }
754 if (ac > 1)
755 ARTnumber = tart;
756
757 /* A Message-ID does not have more than 250 octets. */
758 Reply("%d %s %.250s %s\r\n", what->ReplyCode, buff, msgid, what->Item);
759 if (what->Type != STstat)
760 ARTsendmmap(what->Type);
761 ARTclose();
762 }
763
764
765 /*
766 ** Go to the next or last (really previous) article in the group.
767 */
768 void
CMDnextlast(int ac UNUSED,char * av[])769 CMDnextlast(int ac UNUSED, char *av[])
770 {
771 char *msgid;
772 int save, delta, errcode;
773 bool next;
774 const char *message;
775
776 /* Trying to read. */
777 if (GRPcount == 0) {
778 Reply("%d Not in a newsgroup\r\n", NNTP_FAIL_NO_GROUP);
779 return;
780 }
781
782 /* No syntax to check. Only check authorizations. */
783 if (!PERMcanread || PERMgroupmadeinvalid) {
784 Reply("%d Read access denied\r\n",
785 PERMcanauthenticate ? NNTP_FAIL_AUTH_NEEDED : NNTP_ERR_ACCESS);
786 return;
787 }
788
789 if (ARTnumber < ARTlow || ARTnumber > ARThigh) {
790 Reply("%d Current article number %lu is invalid\r\n",
791 NNTP_FAIL_ARTNUM_INVALID, ARTnumber);
792 return;
793 }
794
795 /* NEXT? */
796 next = (av[0][0] == 'n' || av[0][0] == 'N');
797 if (next) {
798 delta = 1;
799 errcode = NNTP_FAIL_NEXT;
800 message = "next";
801 } else {
802 delta = -1;
803 errcode = NNTP_FAIL_PREV;
804 message = "previous";
805 }
806
807 save = ARTnumber;
808 msgid = NULL;
809 do {
810 ARTnumber += delta;
811 if (ARTnumber < ARTlow || ARTnumber > ARThigh) {
812 Reply("%d No %s article to retrieve\r\n", errcode, message);
813 ARTnumber = save;
814 return;
815 }
816 if (!ARTopen(ARTnumber))
817 continue;
818 msgid = GetHeader("Message-ID", true);
819 ARTclose();
820 } while (msgid == NULL);
821
822 Reply("%d %lu %s Article retrieved; request text separately\r\n",
823 NNTP_OK_STAT, ARTnumber, msgid);
824 }
825
826
827 /*
828 ** Parse a range (in av[1]) which may be any of the following:
829 ** - An article number.
830 ** - An article number followed by a dash to indicate all following.
831 ** - An article number followed by a dash followed by another article
832 ** number.
833 **
834 ** In the last case, if the second number is less than the first number,
835 ** then the range contains no articles.
836 **
837 ** In addition to RFC 3977, we also accept:
838 ** - A dash followed by an article number to indicate all previous.
839 ** - A dash for everything.
840 **
841 ** ac is the number of arguments in the command:
842 ** LISTGROUP news.software.nntp 12-42
843 ** gives ac=3 and av[1] should match "12-42" (whence the "av+1" call
844 ** of CMDgetrange).
845 **
846 ** rp->Low and rp->High will contain the values of the range.
847 ** *DidReply will be true if this function sends an answer.
848 */
849 bool
CMDgetrange(int ac,char * av[],ARTRANGE * rp,bool * DidReply)850 CMDgetrange(int ac, char *av[], ARTRANGE *rp, bool *DidReply)
851 {
852 char *p;
853
854 *DidReply = false;
855
856 if (ac == 1) {
857 /* No arguments, do only current article. */
858 if (ARTnumber < ARTlow || ARTnumber > ARThigh) {
859 Reply("%d Current article number %lu is invalid\r\n",
860 NNTP_FAIL_ARTNUM_INVALID, ARTnumber);
861 *DidReply = true;
862 return false;
863 }
864 rp->High = rp->Low = ARTnumber;
865 return true;
866 }
867
868 /* Check the syntax. */
869 if (!IsValidRange(av[1])) {
870 Reply("%d Syntax error in range\r\n", NNTP_ERR_SYNTAX);
871 *DidReply = true;
872 return false;
873 }
874
875 /* Got just a single number? */
876 if ((p = strchr(av[1], '-')) == NULL) {
877 rp->Low = rp->High = atol(av[1]);
878 return true;
879 }
880
881 /* "-" becomes "\0" and we parse the low water mark.
882 * Note that atol() returns 0 if no valid number
883 * is found at the beginning of *p. */
884 *p++ = '\0';
885 rp->Low = atol(av[1]);
886
887 /* Adjust the low water mark. */
888 if (rp->Low < ARTlow)
889 rp->Low = ARTlow;
890
891 /* Parse and adjust the high water mark.
892 * "12-" gives everything from 12 to the end.
893 * We do not bother about "42-12" or "42-0" in this function. */
894 if ((*p == '\0') || ((rp->High = atol(p)) > ARThigh))
895 rp->High = ARThigh;
896
897 p--;
898 *p = '-';
899
900 return true;
901 }
902
903
904 /*
905 ** Apply virtual hosting to an Xref header field body.
906 */
907 static char *
vhost_xref(char * p)908 vhost_xref(char *p)
909 {
910 char *space;
911 size_t offset;
912 char *field = NULL;
913
914 space = strchr(p, ' ');
915 if (space == NULL) {
916 warn("malformed Xref header field: `%s'", p);
917 goto fail;
918 }
919 offset = space - p;
920 space = strchr(p + offset, ' ');
921 if (space == NULL) {
922 warn("malformed Xref header field: `%s'", p);
923 goto fail;
924 }
925 field = concat(PERMaccessconf->domain, space, NULL);
926 fail:
927 free(p);
928 return field;
929 }
930
931
932 /*
933 ** Dump parts of the overview database with the OVER command.
934 ** The legacy XOVER is also kept, with its specific behaviour.
935 */
936 void
CMDover(int ac,char * av[])937 CMDover(int ac, char *av[])
938 {
939 bool DidReply, HasNotReplied;
940 ARTRANGE range;
941 struct timeval stv, etv;
942 ARTNUM artnum;
943 void *handle;
944 char *data, *r;
945 const char *p, *q;
946 int len, useIOb = 0;
947 TOKEN token;
948 struct cvector *vector = NULL;
949 bool xover, mid;
950
951 xover = (strcasecmp(av[0], "XOVER") == 0);
952 mid = (ac > 1 && IsValidMessageID(av[1], true, laxmid));
953
954 if (mid && !xover) {
955 /* FIXME: We still do not support OVER MSGID, sorry! */
956 Reply("%d Overview by Message-ID unsupported\r\n",
957 NNTP_ERR_UNAVAILABLE);
958 return;
959 }
960
961 /* Check the syntax of the arguments first.
962 * We do not accept a Message-ID for XOVER, contrary to OVER. A range
963 * is accepted for both of them. */
964 if (ac > 1 && !IsValidRange(av[1])) {
965 /* It is better to check for a range before a Message-ID because
966 * '<' could have been forgotten and the error message should then
967 * report a syntax error in the Message-ID. */
968 if (xover || isdigit((unsigned char) av[1][0]) || av[1][0] == '-') {
969 Reply("%d Syntax error in range\r\n", NNTP_ERR_SYNTAX);
970 return;
971 } else if (!mid) {
972 Reply("%d Syntax error in Message-ID\r\n", NNTP_ERR_SYNTAX);
973 return;
974 }
975 }
976
977 /* Trying to read. */
978 if (GRPcount == 0) {
979 Reply("%d Not in a newsgroup\r\n", NNTP_FAIL_NO_GROUP);
980 return;
981 }
982
983 /* Check authorizations. If a range is requested (not a Message-ID),
984 * we check whether the group is still readable. */
985 if (!PERMcanread || (!mid && PERMgroupmadeinvalid)) {
986 Reply("%d Read access denied\r\n",
987 PERMcanauthenticate ? NNTP_FAIL_AUTH_NEEDED : NNTP_ERR_ACCESS);
988 return;
989 }
990
991 /* Parse range. CMDgetrange() correctly sets the range when
992 * there is no arguments. */
993 if (!CMDgetrange(ac, av, &range, &DidReply))
994 if (DidReply)
995 return;
996
997 if (PERMaccessconf->nnrpdoverstats) {
998 OVERcount++;
999 gettimeofday(&stv, NULL);
1000 }
1001 if ((handle = (void *) OVopensearch(GRPcur, range.Low, range.High))
1002 == NULL) {
1003 /* The response code for OVER is different if a range is provided.
1004 * Note that XOVER answers OK. */
1005 if (ac > 1)
1006 Reply("%d No articles in %s\r\n",
1007 xover ? NNTP_OK_OVER : NNTP_FAIL_ARTNUM_NOTFOUND, av[1]);
1008 else
1009 Reply("%d No such article number %lu\r\n",
1010 xover ? NNTP_OK_OVER : NNTP_FAIL_ARTNUM_NOTFOUND, ARTnumber);
1011 if (xover)
1012 Printf(".\r\n");
1013 return;
1014 }
1015 if (PERMaccessconf->nnrpdoverstats) {
1016 gettimeofday(&etv, NULL);
1017 OVERtime += (etv.tv_sec - stv.tv_sec) * 1000;
1018 OVERtime += (etv.tv_usec - stv.tv_usec) / 1000;
1019 }
1020
1021 if (PERMaccessconf->nnrpdoverstats)
1022 gettimeofday(&stv, NULL);
1023
1024 /* If OVSTATICSEARCH is true, then the data returned by OVsearch is only
1025 valid until the next call to OVsearch. In this case, we must use
1026 SendIOb because it copies the data. */
1027 OVctl(OVSTATICSEARCH, &useIOb);
1028
1029 HasNotReplied = true;
1030 while (OVsearch(handle, &artnum, &data, &len, &token, NULL)) {
1031 if (PERMaccessconf->nnrpdoverstats) {
1032 gettimeofday(&etv, NULL);
1033 OVERtime += (etv.tv_sec - stv.tv_sec) * 1000;
1034 OVERtime += (etv.tv_usec - stv.tv_usec) / 1000;
1035 }
1036 if (len == 0
1037 || (PERMaccessconf->nnrpdcheckart && !ARTinstorebytoken(token))) {
1038 if (PERMaccessconf->nnrpdoverstats) {
1039 OVERmiss++;
1040 gettimeofday(&stv, NULL);
1041 }
1042 continue;
1043 }
1044 if (PERMaccessconf->nnrpdoverstats) {
1045 OVERhit++;
1046 OVERsize += len;
1047 }
1048
1049 if (HasNotReplied) {
1050 if (ac > 1)
1051 Reply("%d Overview information for %s follows\r\n",
1052 NNTP_OK_OVER, av[1]);
1053 else
1054 Reply("%d Overview information for %lu follows\r\n",
1055 NNTP_OK_OVER, ARTnumber);
1056 fflush(stdout);
1057 HasNotReplied = false;
1058 }
1059
1060 vector = overview_split(data, len, NULL, vector);
1061 r = overview_get_standard_header(vector, OVERVIEW_MESSAGE_ID);
1062 if (r == NULL) {
1063 if (PERMaccessconf->nnrpdoverstats) {
1064 gettimeofday(&stv, NULL);
1065 }
1066 continue;
1067 }
1068 cache_add(HashMessageID(r), token);
1069 free(r);
1070 if (VirtualPathlen > 0 && overhdr_xref != -1) {
1071 if ((size_t)(overhdr_xref + 1) >= vector->count) {
1072 if (PERMaccessconf->nnrpdoverstats) {
1073 gettimeofday(&stv, NULL);
1074 }
1075 continue;
1076 }
1077 p = vector->strings[overhdr_xref] + sizeof("Xref: ") - 1;
1078 while ((p < data + len) && *p == ' ')
1079 ++p;
1080 q = memchr(p, ' ', data + len - p);
1081 if (q == NULL) {
1082 if (PERMaccessconf->nnrpdoverstats) {
1083 gettimeofday(&stv, NULL);
1084 }
1085 continue;
1086 }
1087 /* Copy the virtual path without its final '!'. */
1088 if (useIOb) {
1089 SendIOb(data, p - data);
1090 SendIOb(VirtualPath, VirtualPathlen - 1);
1091 SendIOb(q, len - (q - data));
1092 } else {
1093 SendIOv(data, p - data);
1094 SendIOv(VirtualPath, VirtualPathlen - 1);
1095 SendIOv(q, len - (q - data));
1096 }
1097 } else {
1098 if (useIOb)
1099 SendIOb(data, len);
1100 else
1101 SendIOv(data, len);
1102 }
1103 if (PERMaccessconf->nnrpdoverstats)
1104 gettimeofday(&stv, NULL);
1105 }
1106
1107 if (PERMaccessconf->nnrpdoverstats) {
1108 gettimeofday(&etv, NULL);
1109 OVERtime += (etv.tv_sec - stv.tv_sec) * 1000;
1110 OVERtime += (etv.tv_usec - stv.tv_usec) / 1000;
1111 }
1112
1113 if (vector)
1114 cvector_free(vector);
1115
1116 if (HasNotReplied) {
1117 /* The response code for OVER is different if a range is provided.
1118 * Note that XOVER answers OK. */
1119 if (ac > 1)
1120 Reply("%d No articles in %s\r\n",
1121 xover ? NNTP_OK_OVER : NNTP_FAIL_ARTNUM_NOTFOUND, av[1]);
1122 else
1123 Reply("%d No such article number %lu\r\n",
1124 xover ? NNTP_OK_OVER : NNTP_FAIL_ARTNUM_NOTFOUND, ARTnumber);
1125 if (xover)
1126 Printf(".\r\n");
1127 } else {
1128 if (useIOb) {
1129 SendIOb(".\r\n", 3);
1130 PushIOb();
1131 } else {
1132 SendIOv(".\r\n", 3);
1133 PushIOv();
1134 }
1135 }
1136
1137 if (PERMaccessconf->nnrpdoverstats)
1138 gettimeofday(&stv, NULL);
1139 OVclosesearch(handle);
1140 if (PERMaccessconf->nnrpdoverstats) {
1141 gettimeofday(&etv, NULL);
1142 OVERtime += (etv.tv_sec - stv.tv_sec) * 1000;
1143 OVERtime += (etv.tv_usec - stv.tv_usec) / 1000;
1144 }
1145 }
1146
1147 /*
1148 ** Access specific fields from an article with HDR.
1149 ** The legacy XHDR and XPAT are also kept, with their specific behaviours.
1150 */
1151 void
CMDpat(int ac,char * av[])1152 CMDpat(int ac, char *av[])
1153 {
1154 char *p;
1155 unsigned long i;
1156 ARTRANGE range;
1157 bool IsBytes, IsLines;
1158 bool IsMetaBytes, IsMetaLines;
1159 bool DidReply, HasNotReplied;
1160 const char *header;
1161 char *pattern;
1162 char *text;
1163 int Overview;
1164 ARTNUM artnum;
1165 char buff[SPOOLNAMEBUFF];
1166 void *handle;
1167 char *data;
1168 int len;
1169 TOKEN token;
1170 struct cvector *vector = NULL;
1171 bool hdr, mid;
1172
1173 hdr = (strcasecmp(av[0], "HDR") == 0);
1174 mid = (ac > 2 && IsValidMessageID(av[2], true, laxmid));
1175
1176 /* Check the syntax of the arguments first. */
1177 if (ac > 2 && !IsValidRange(av[2])) {
1178 /* It is better to check for a range before a Message-ID because
1179 * '<' could have been forgotten and the error message should then
1180 * report a syntax error in the Message-ID. */
1181 if (isdigit((unsigned char) av[2][0]) || av[2][0] == '-') {
1182 Reply("%d Syntax error in range\r\n", NNTP_ERR_SYNTAX);
1183 return;
1184 } else if (!mid) {
1185 Reply("%d Syntax error in Message-ID\r\n", NNTP_ERR_SYNTAX);
1186 return;
1187 }
1188 }
1189
1190 header = av[1];
1191
1192 /* If metadata is asked for, convert it to headers that
1193 * the overview database knows. */
1194 IsBytes = (strcasecmp(header, "Bytes") == 0);
1195 IsLines = (strcasecmp(header, "Lines") == 0);
1196 IsMetaBytes = (strcasecmp(header, ":bytes") == 0);
1197 IsMetaLines = (strcasecmp(header, ":lines") == 0);
1198 /* Make these changes because our overview database does
1199 * not currently know metadata names. */
1200 if (IsMetaBytes)
1201 header = "Bytes";
1202 if (IsMetaLines)
1203 header = "Lines";
1204
1205 /* We only allow :bytes and :lines for metadata. */
1206 if (!IsMetaLines && !IsMetaBytes) {
1207 p = av[1];
1208 p++;
1209 if (strncasecmp(header, ":", 1) == 0 && IsValidHeaderName(p)) {
1210 Reply("%d Unsupported metadata request\r\n", NNTP_ERR_UNAVAILABLE);
1211 return;
1212 } else if (!IsValidHeaderName(header)) {
1213 Reply("%d Syntax error in header field name\r\n", NNTP_ERR_SYNTAX);
1214 return;
1215 }
1216 }
1217
1218 /* Trying to read. */
1219 if (GRPcount == 0 && !mid) {
1220 Reply("%d Not in a newsgroup\r\n", NNTP_FAIL_NO_GROUP);
1221 return;
1222 }
1223
1224 /* Check authorizations. If a range is requested (not a Message-ID),
1225 * we check whether the group is still readable. */
1226 if (!PERMcanread || (!mid && PERMgroupmadeinvalid)) {
1227 Reply("%d Read access denied\r\n",
1228 PERMcanauthenticate ? NNTP_FAIL_AUTH_NEEDED : NNTP_ERR_ACCESS);
1229 return;
1230 }
1231
1232 if (ac > 3) /* Necessarily XPAT. */
1233 pattern = Glom(&av[3]);
1234 else
1235 pattern = NULL;
1236
1237 /* We will only do the loop once. It is just in order to easily break. */
1238 do {
1239 /* Message-ID specified? */
1240 if (mid) {
1241 /* FIXME: We do not handle metadata requests by Message-ID. */
1242 if (hdr && (IsMetaBytes || IsMetaLines)) {
1243 Reply("%d Metadata requests by Message-ID unsupported\r\n",
1244 NNTP_ERR_UNAVAILABLE);
1245 break;
1246 }
1247
1248 p = av[2];
1249 if (!ARTopenbyid(p, &artnum, false)) {
1250 Reply("%d No such article\r\n", NNTP_FAIL_MSGID_NOTFOUND);
1251 break;
1252 }
1253
1254 if (!PERMartok()) {
1255 ARTclose();
1256 Reply("%d Read access denied for this article\r\n",
1257 PERMcanauthenticate ? NNTP_FAIL_AUTH_NEEDED
1258 : NNTP_ERR_ACCESS);
1259 break;
1260 }
1261
1262 Reply(
1263 "%d Header information for %s follows (from the article)\r\n",
1264 hdr ? NNTP_OK_HDR : NNTP_OK_HEAD, av[1]);
1265
1266 if ((text = GetHeader(av[1], false)) != NULL
1267 && (!pattern || uwildmat_simple(text, pattern)))
1268 Printf("%s %s\r\n", hdr ? "0" : p, text);
1269 else if (hdr) {
1270 /* We always have to answer something with HDR. */
1271 Printf("0 \r\n");
1272 }
1273
1274 ARTclose();
1275 Printf(".\r\n");
1276 break;
1277 }
1278
1279 /* Parse range. CMDgetrange() correctly sets the range when
1280 * there is no arguments. */
1281 if (!CMDgetrange(ac - 1, av + 1, &range, &DidReply))
1282 if (DidReply)
1283 break;
1284
1285 /* In overview? */
1286 Overview = overview_index(header, OVextra);
1287
1288 HasNotReplied = true;
1289
1290 /* Not in overview, we have to fish headers out from the articles. */
1291 if (Overview < 0 || IsBytes || IsLines) {
1292 for (i = range.Low; i <= range.High && range.High > 0; i++) {
1293 if (!ARTopen(i))
1294 continue;
1295 if (HasNotReplied) {
1296 Reply("%d Header information for %s follows (from "
1297 "articles)\r\n",
1298 hdr ? NNTP_OK_HDR : NNTP_OK_HEAD, av[1]);
1299 HasNotReplied = false;
1300 }
1301 p = GetHeader(header, false);
1302 if (p && (!pattern || uwildmat_simple(p, pattern))) {
1303 snprintf(buff, sizeof(buff), "%lu ", i);
1304 SendIOb(buff, strlen(buff));
1305 SendIOb(p, strlen(p));
1306 SendIOb("\r\n", 2);
1307 } else if (hdr) {
1308 /* We always have to answer something with HDR. */
1309 snprintf(buff, sizeof(buff), "%lu \r\n", i);
1310 SendIOb(buff, strlen(buff));
1311 }
1312 ARTclose();
1313 }
1314 if (HasNotReplied) {
1315 if (hdr) {
1316 if (ac > 2)
1317 Reply("%d No articles in %s\r\n",
1318 NNTP_FAIL_ARTNUM_NOTFOUND, av[2]);
1319 else
1320 Reply("%d No such article number %lu\r\n",
1321 NNTP_FAIL_ARTNUM_NOTFOUND, ARTnumber);
1322 } else {
1323 Reply("%d No header information for %s follows (from "
1324 "articles)\r\n",
1325 NNTP_OK_HEAD, av[1]);
1326 Printf(".\r\n");
1327 }
1328 break;
1329 } else {
1330 SendIOb(".\r\n", 3);
1331 }
1332 PushIOb();
1333 break;
1334 }
1335
1336 /* Okay then, we can grab values from the overview database. */
1337 handle = (void *) OVopensearch(GRPcur, range.Low, range.High);
1338 if (handle == NULL) {
1339 if (hdr) {
1340 if (ac > 2)
1341 Reply("%d No articles in %s\r\n",
1342 NNTP_FAIL_ARTNUM_NOTFOUND, av[2]);
1343 else
1344 Reply("%d No such article number %lu\r\n",
1345 NNTP_FAIL_ARTNUM_NOTFOUND, ARTnumber);
1346 } else {
1347 Reply("%d No header information for %s follows (from "
1348 "overview)\r\n",
1349 NNTP_OK_HEAD, av[1]);
1350 Printf(".\r\n");
1351 }
1352 break;
1353 }
1354
1355 while (OVsearch(handle, &artnum, &data, &len, &token, NULL)) {
1356 if (len == 0
1357 || (PERMaccessconf->nnrpdcheckart
1358 && !ARTinstorebytoken(token)))
1359 continue;
1360 if (HasNotReplied) {
1361 Reply("%d Header or metadata information for %s follows (from "
1362 "overview)\r\n",
1363 hdr ? NNTP_OK_HDR : NNTP_OK_HEAD, av[1]);
1364 HasNotReplied = false;
1365 }
1366 vector = overview_split(data, len, NULL, vector);
1367 if (Overview < OVERVIEW_MAX) {
1368 p = overview_get_standard_header(vector, Overview);
1369 } else {
1370 p = overview_get_extra_header(vector, header);
1371 }
1372 if (p != NULL) {
1373 if (PERMaccessconf->virtualhost && Overview == overhdr_xref) {
1374 p = vhost_xref(p);
1375 if (p == NULL) {
1376 if (hdr) {
1377 snprintf(buff, sizeof(buff), "%lu \r\n", artnum);
1378 SendIOb(buff, strlen(buff));
1379 }
1380 continue;
1381 }
1382 }
1383 if (!pattern || uwildmat_simple(p, pattern)) {
1384 snprintf(buff, sizeof(buff), "%lu ", artnum);
1385 SendIOb(buff, strlen(buff));
1386 SendIOb(p, strlen(p));
1387 SendIOb("\r\n", 2);
1388 }
1389 /* No need to have another condition for HDR because
1390 * pattern is NULL for it, and p is not NULL here. */
1391 free(p);
1392 } else if (hdr) {
1393 snprintf(buff, sizeof(buff), "%lu \r\n", artnum);
1394 SendIOb(buff, strlen(buff));
1395 }
1396 }
1397 if (HasNotReplied) {
1398 if (hdr) {
1399 if (ac > 2)
1400 Reply("%d No articles in %s\r\n",
1401 NNTP_FAIL_ARTNUM_NOTFOUND, av[2]);
1402 else
1403 Reply("%d Current article number %lu is invalid\r\n",
1404 NNTP_FAIL_ARTNUM_INVALID, ARTnumber);
1405 } else {
1406 Reply("%d No header or metadata information for %s follows "
1407 "(from overview)\r\n",
1408 NNTP_OK_HEAD, av[1]);
1409 Printf(".\r\n");
1410 }
1411 break;
1412 } else {
1413 SendIOb(".\r\n", 3);
1414 }
1415 PushIOb();
1416 OVclosesearch(handle);
1417 } while (0);
1418
1419 if (vector)
1420 cvector_free(vector);
1421
1422 if (pattern)
1423 free(pattern);
1424 }
1425