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