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