1 /*
2 **  LIST commands.
3 */
4 
5 #include "portable/system.h"
6 
7 #include "inn/innconf.h"
8 #include "inn/messages.h"
9 #include "inn/ov.h"
10 #include "inn/overview.h"
11 #include "nnrpd.h"
12 
13 typedef struct _LISTINFO {
14     const char *method;
15     const char *File;
16     void (*impl)(struct _LISTINFO *, int ac, char *av[]);
17     bool Required;
18     const char *Items;
19     const char *Format;
20 } LISTINFO;
21 
22 static void cmd_list_schema(LISTINFO *lp, int ac, char *av[]);
23 static void cmd_list_headers(LISTINFO *lp, int ac, char *av[]);
24 
25 static LISTINFO INFOactive = {"ACTIVE",
26                               INN_PATH_ACTIVE,
27                               NULL,
28                               true,
29                               "list of active newsgroups",
30                               "Newsgroups in form \"group high low status\""};
31 static LISTINFO INFOactivetimes = {
32     "ACTIVE.TIMES",
33     INN_PATH_ACTIVETIMES,
34     NULL,
35     false,
36     "list of newsgroup creation times",
37     "Newsgroup creation times in form \"group time who\""};
38 static LISTINFO INFOcounts = {
39     "COUNTS",
40     INN_PATH_ACTIVE,
41     NULL,
42     true,
43     "list of active newsgroups with estimated counts",
44     "Newsgroups in form \"group high low count status\""};
45 static LISTINFO INFOdistribs = {
46     "DISTRIBUTIONS",
47     INN_PATH_NNRPDIST,
48     NULL,
49     false,
50     "list of newsgroup distributions",
51     "Distributions in form \"distribution description\""};
52 static LISTINFO INFOheaders = {"HEADERS",
53                                NULL,
54                                cmd_list_headers,
55                                true,
56                                "list of supported headers and metadata items",
57                                "Headers and metadata items supported"};
58 static LISTINFO INFOsubs = {"SUBSCRIPTIONS",
59                             INN_PATH_NNRPSUBS,
60                             NULL,
61                             false,
62                             "list of recommended newsgroup subscriptions",
63                             "Recommended subscriptions in form \"group\""};
64 static LISTINFO INFOdistribpats = {
65     "DISTRIB.PATS",
66     INN_PATH_DISTPATS,
67     NULL,
68     false,
69     "list of distribution patterns",
70     "Default distributions in form \"weight:group-pattern:distribution\""};
71 static LISTINFO INFOgroups = {
72     "NEWSGROUPS",
73     INN_PATH_NEWSGROUPS,
74     NULL,
75     true,
76     "list of newsgroup descriptions",
77     "Newsgroup descriptions in form \"group description\""};
78 static LISTINFO INFOmoderators = {
79     "MODERATORS",
80     INN_PATH_MODERATORS,
81     NULL,
82     false,
83     "list of submission templates",
84     "Newsgroup moderators in form \"group-pattern:submission-template\""};
85 static LISTINFO INFOschema = {
86     "OVERVIEW.FMT",    NULL,
87     cmd_list_schema,   true,
88     "overview format", "Order of fields in overview database"};
89 static LISTINFO INFOmotd = {
90     "MOTD", INN_PATH_MOTD_NNRPD,  NULL,
91     false,  "message of the day", "Message of the day text in UTF-8"};
92 
93 static LISTINFO *info[] = {
94     &INFOactive,     &INFOactivetimes, &INFOcounts,      &INFOdistribs,
95     &INFOheaders,    &INFOsubs,        &INFOdistribpats, &INFOgroups,
96     &INFOmoderators, &INFOschema,      &INFOmotd,
97 };
98 
99 
100 /*
101 **  List the overview schema (standard and extra fields).
102 */
103 static void
cmd_list_schema(LISTINFO * lp,int ac UNUSED,char * av[]UNUSED)104 cmd_list_schema(LISTINFO *lp, int ac UNUSED, char *av[] UNUSED)
105 {
106     const struct cvector *standard;
107     unsigned int i;
108 
109     Reply("%d %s\r\n", NNTP_OK_LIST, lp->Format);
110     standard = overview_fields();
111     for (i = 0; i < standard->count; ++i) {
112         Printf("%s:\r\n", standard->strings[i]);
113     }
114     for (i = 0; i < OVextra->count; ++i) {
115         Printf("%s:full\r\n", OVextra->strings[i]);
116     }
117     Printf(".\r\n");
118 }
119 
120 
121 /*
122 **  List supported headers and metadata information.
123 */
124 static void
cmd_list_headers(LISTINFO * lp,int ac,char * av[])125 cmd_list_headers(LISTINFO *lp, int ac, char *av[])
126 {
127     bool range;
128 
129     range = (ac > 2 && strcasecmp(av[2], "RANGE") == 0);
130 
131     if (ac > 2 && (strcasecmp(av[2], "MSGID") != 0) && !range) {
132         Reply("%d Syntax error in arguments\r\n", NNTP_ERR_SYNTAX);
133         return;
134     }
135     Reply("%d %s\r\n", NNTP_OK_LIST, lp->Format);
136     Printf(":\r\n");
137     if (range) {
138         /* These information are only known by the overview system,
139          * and are only accessible with a range. */
140         Printf(":bytes\r\n");
141         Printf(":lines\r\n");
142     }
143     Printf(".\r\n");
144 }
145 
146 
147 /*
148 **  List a single newsgroup.  Called by LIST ACTIVE with a single argument.
149 **  This is quicker than parsing the whole active file, but only works with
150 **  single groups.  It also doesn't work for aliased groups, since overview
151 **  doesn't know what group the group is aliased to (yet).  Returns whether we
152 **  were able to answer the command.
153 */
154 static bool
CMD_list_single(char * group)155 CMD_list_single(char *group)
156 {
157     char *grplist[2] = {NULL, NULL};
158     int lo, hi, count, flag;
159 
160     if (PERMspecified) {
161         grplist[0] = group;
162         if (!PERMmatch(PERMreadlist, grplist))
163             return false;
164     }
165     if (OVgroupstats(group, &lo, &hi, &count, &flag)
166         && flag != NF_FLAG_ALIAS) {
167         /* When the connected user has the right to locally post, mention it.
168          */
169         if (PERMaccessconf->locpost
170             && (flag == NF_FLAG_IGNORE || flag == NF_FLAG_JUNK
171                 || flag == NF_FLAG_NOLOCAL))
172             flag = NF_FLAG_OK;
173         /* When a newsgroup is empty, the high water mark should be one less
174          * than the low water mark according to RFC 3977. */
175         if (count == 0)
176             lo = hi + 1;
177         Reply("%d %s\r\n", NNTP_OK_LIST, INFOactive.Format);
178         Printf("%s %0*u %0*u %c\r\n", group, ARTNUMPRINTSIZE, hi,
179                ARTNUMPRINTSIZE, lo, flag);
180         Printf(".\r\n");
181         return true;
182     }
183     return false;
184 }
185 
186 
187 /*
188 **  Main LIST function.
189 */
190 void
CMDlist(int ac,char * av[])191 CMDlist(int ac, char *av[])
192 {
193     QIOSTATE *qp;
194     char *p;
195     char *save;
196     char *path;
197     char *q;
198     char *grplist[2];
199     LISTINFO *lp;
200     char *wildarg = NULL;
201     char savec;
202     unsigned int i;
203     int lo, hi, count, flag;
204 
205     p = av[1];
206 
207     /* LIST ACTIVE is the default LIST command.  If a keyword is provided,
208      * we check whether it is defined. */
209     if (p == NULL) {
210         lp = &INFOactive;
211     } else {
212         lp = NULL;
213         for (i = 0; i < ARRAY_SIZE(info); ++i) {
214             if (strcasecmp(p, info[i]->method) == 0) {
215                 lp = info[i];
216                 break;
217             }
218         }
219     }
220 
221     /* If no defined LIST keyword is found, we return. */
222     if (lp == NULL) {
223         Reply("%d Unknown LIST keyword\r\n", NNTP_ERR_SYNTAX);
224         return;
225     }
226 
227     if (lp == &INFOactive) {
228         if (ac == 3) {
229             wildarg = av[2];
230             /* No need to parse the active file for a single group. */
231             if (CMD_list_single(wildarg))
232                 return;
233         }
234     } else if (lp == &INFOgroups || lp == &INFOactivetimes || lp == &INFOcounts
235                || lp == &INFOheaders || lp == &INFOsubs) {
236         if (ac == 3)
237             wildarg = av[2];
238     }
239 
240     /* Three arguments can be passed only when ACTIVE, ACTIVE.TIMES, COUNTS
241      * HEADERS, NEWSGROUPS or SUBSCRIPTIONS keywords are used. */
242     if (ac > 2 && !wildarg) {
243         Reply("%d Unexpected wildmat or argument\r\n", NNTP_ERR_SYNTAX);
244         return;
245     }
246 
247     /* If a function is provided for the given keyword, we call it. */
248     if (lp->impl != NULL) {
249         lp->impl(lp, ac, av);
250         return;
251     }
252 
253     path = innconf->pathetc;
254 
255     /* The active, active.times and newsgroups files are in pathdb. */
256     if ((strstr(lp->File, "active") != NULL)
257         || (strstr(lp->File, "newsgroups") != NULL))
258         path = innconf->pathdb;
259     path = concatpath(path, lp->File);
260     qp = QIOopen(path);
261     free(path);
262     if (qp == NULL) {
263         Reply("%d No %s available\r\n", NNTP_ERR_UNAVAILABLE, lp->Items);
264         /* Only the active and newsgroups files are required. */
265         if (lp->Required || errno != ENOENT) {
266             /* %m outputs strerror(errno). */
267             syslog(L_ERROR, "%s can't fopen %s %m", Client.host, lp->File);
268         }
269         return;
270     }
271 
272     Reply("%d %s\r\n", NNTP_OK_LIST, lp->Format);
273     if (!PERMspecified && lp != &INFOmotd) {
274         /* Optimize for unlikely case of no permissions and false default. */
275         QIOclose(qp);
276         Printf(".\r\n");
277         return;
278     }
279 
280     /* Set up group list terminator. */
281     grplist[1] = NULL;
282 
283     /* Read lines, ignore long ones. */
284     while ((p = QIOread(qp)) != NULL) {
285         /* Check that the output does not break the NNTP protocol. */
286         if (p[0] == '.' && p[1] != '.') {
287             syslog(L_ERROR, "%s bad dot-stuffing in %s", Client.host,
288                    lp->File);
289             continue;
290         }
291         if (lp == &INFOmotd) {
292             if (is_valid_utf8(p)) {
293                 Printf("%s\r\n", p);
294             } else {
295                 syslog(L_ERROR, "%s bad encoding in %s (UTF-8 expected)",
296                        Client.host, lp->File);
297             }
298             continue;
299         }
300         /* Matching patterns against patterns is not that
301          * good but it is better than nothing... */
302         if (lp == &INFOdistribpats) {
303             if (*p == '\0' || *p == '#' || *p == ';' || *p == ' ')
304                 continue;
305             if (PERMspecified) {
306                 if ((q = strchr(p, ':')) == NULL)
307                     continue;
308                 q++;
309                 if ((save = strchr(q, ':')) == NULL)
310                     continue;
311                 *save = '\0';
312                 grplist[0] = q;
313                 if (!PERMmatch(PERMreadlist, grplist))
314                     continue;
315                 *save = ':';
316             }
317             Printf("%s\r\n", p);
318             continue;
319         }
320         if (lp == &INFOdistribs || lp == &INFOmoderators) {
321             if (*p != '\0' && *p != '#' && *p != ';' && *p != ' ') {
322                 if (is_valid_utf8(p)) {
323                     Printf("%s\r\n", p);
324                 } else if (lp == &INFOdistribs) {
325                     syslog(L_ERROR, "%s bad encoding in %s (UTF-8 expected)",
326                            Client.host, lp->File);
327                 }
328             }
329             continue;
330         }
331         savec = '\0';
332         for (save = p; *save != '\0'; save++) {
333             if (*save == ' ' || *save == '\t') {
334                 savec = *save;
335                 *save = '\0';
336                 break;
337             }
338         }
339 
340         /* Check whether the reader has access to the newsgroup. */
341         if (PERMspecified) {
342             grplist[0] = p;
343             if (!PERMmatch(PERMreadlist, grplist))
344                 continue;
345         }
346 
347         /* Check whether the newsgroup matches the wildmat pattern,
348          * if given. */
349         if (wildarg && !uwildmat(p, wildarg))
350             continue;
351 
352         if (lp == &INFOcounts) {
353             if (OVgroupstats(p, &lo, &hi, &count, &flag)) {
354                 /* When a newsgroup is empty, the high water mark should be
355                  * one less than the low water mark according to RFC 3977. */
356                 if (count == 0)
357                     lo = hi + 1;
358 
359                 if (flag != NF_FLAG_ALIAS) {
360                     Printf("%s %u %u %u %c\r\n", p, hi, lo, count,
361                            PERMaccessconf->locpost
362                                    && (flag == NF_FLAG_IGNORE
363                                        || flag == NF_FLAG_JUNK
364                                        || flag == NF_FLAG_NOLOCAL)
365                                ? NF_FLAG_OK
366                                : flag);
367                 } else if (savec != '\0') {
368                     *save = savec;
369 
370                     if ((q = strrchr(p, NF_FLAG_ALIAS)) != NULL) {
371                         *save = '\0';
372                         Printf("%s %u %u %u %s\r\n", p, hi, lo, count, q);
373                     }
374                 }
375             }
376 
377             continue;
378         }
379 
380         if (savec != '\0')
381             *save = savec;
382 
383         if (lp == &INFOactive) {
384             /* When the connected user has the right to locally post, mention
385              * it. */
386             if (PERMaccessconf->locpost && (q = strrchr(p, ' ')) != NULL) {
387                 q++;
388                 if (*q == NF_FLAG_IGNORE || *q == NF_FLAG_JUNK
389                     || *q == NF_FLAG_NOLOCAL)
390                     *q = NF_FLAG_OK;
391             }
392         }
393 
394         Printf("%s\r\n", p);
395     }
396     QIOclose(qp);
397 
398     Printf(".\r\n");
399 }
400