1 /*  $Id: sm.c 10405 2020-11-21 07:02:43Z iulius $
2 **
3 **  Provide a command line interface to the storage manager.
4 */
5 
6 #include "config.h"
7 #include "clibrary.h"
8 #include <sys/uio.h>
9 
10 #include "inn/buffer.h"
11 #include "inn/innconf.h"
12 #include "inn/messages.h"
13 #include "inn/qio.h"
14 #include "inn/wire.h"
15 #include "inn/libinn.h"
16 #include "inn/storage.h"
17 
18 #define WIRE_CHUNK_SIZE 0x10000
19 
20 static const char usage[] = "\
21 Usage: sm [-cdHiqrRSs] [token ...]\n\
22 \n\
23 Command-line interface to the INN storage manager.  The default action is\n\
24 to display the complete article associated with each token given.  If no\n\
25 tokens are specified on the command line, they're read from stdin, one per\n\
26 line.\n\
27 \n\
28     -c          Show clear information about a token\n\
29     -d, -r      Delete the articles associated with the given tokens\n\
30     -H          Display the headers of articles only\n\
31     -i          Translate tokens into newsgroup names and article numbers\n\
32     -q          Suppress all error messages except usage\n\
33     -R          Display the raw article rather than undoing wire format\n\
34                 or consume wire format articles when storing\n\
35     -S          Output articles in rnews batch file format\n\
36     -s          Store the article provided on stdin\n";
37 
38 /* The options that can be set on the command line, used to determine what to
39    do with each token. */
40 struct options {
41     bool artinfo;               /* Show newsgroup and article number. */
42     bool clearinfo;             /* Show clear information about a token. */
43     bool delete;                /* Delete articles instead of showing them. */
44     bool header;                /* Display article headers only. */
45     bool raw;                   /* Display or consume wire-format articles. */
46     bool rnews;                 /* Output articles as rnews batch files. */
47 };
48 
49 
50 /*
51 **  This basically duplicates what INN does, except that INN has to
52 **  do more complex things to add the Path header.
53 **
54 **  Note that we make no attempt to add history or overview information, at
55 **  least right now.
56 */
57 static bool
store_article_common(char * text,size_t size)58 store_article_common(char *text, size_t size)
59 {
60     char *start, *end;
61     ARTHANDLE handle = ARTHANDLE_INITIALIZER;
62     TOKEN token;
63 
64     /* Build the basic article handle. */
65     handle.type = TOKEN_EMPTY;
66     handle.data = text;
67     handle.iov = xmalloc(sizeof(struct iovec));
68     handle.iov->iov_base = text;
69     handle.iov->iov_len = size;
70     handle.iovcnt = 1;
71     handle.len = size;
72     handle.arrived = 0;
73     handle.expires = 0;
74 
75     /* Find the expiration time, if any. */
76     start = wire_findheader(text, size, "Expires", true);
77     if (start != NULL) {
78         char *expires;
79 
80         end = wire_endheader(start, text + size - 1);
81         if (end == NULL)
82             die("cannot find end of Expires header");
83         expires = xstrndup(start, end - start);
84         handle.expires = parsedate_rfc5322_lax(expires);
85         free(expires);
86         if (handle.expires == (time_t) -1)
87             handle.expires = 0;
88     }
89 
90     /* Find the appropriate newsgroups header. */
91     if (innconf->storeonxref) {
92         start = wire_findheader(text, size, "Xref", true);
93         if (start == NULL)
94             die("no Xref header found in message");
95         end = wire_endheader(start, text + size - 1);
96         if (end == NULL)
97             die("cannot find end of Xref header");
98         for (; *start != ' ' && start < end; start++)
99             ;
100         if (start >= end)
101             die("malformed Xref header");
102         start++;
103     } else {
104         start = wire_findheader(text, size, "Newsgroups", true);
105         if (start == NULL)
106             die("no Newsgroups header found in message");
107         end = wire_endheader(start, text + size - 1);
108         if (end == NULL)
109             die("cannot find end of Newsgroups header");
110     }
111     handle.groups = start;
112     handle.groupslen = end - start;
113 
114     /* Store the article. */
115     token = SMstore(handle);
116     free(handle.iov);
117 
118     if (token.type == TOKEN_EMPTY) {
119         warn("failed to store article: %s", SMerrorstr);
120         return false;
121     } else {
122         printf("%s\n", TokenToText(token));
123         return true;
124     }
125 }
126 
127 
128 /*
129 **  Given a file descriptor, read a post from that file descriptor and then
130 **  store it.
131 */
132 static bool
store_article(int fd)133 store_article(int fd)
134 {
135     struct buffer *article;
136     size_t size;
137     char *text;
138     bool result;
139 
140     article = buffer_new();
141     if (!buffer_read_file(article, fd)) {
142         sysdie("cannot read article");
143     }
144     text = wire_from_native(article->data, article->left, &size);
145     buffer_free(article);
146     result = store_article_common(text, size);
147     free(text);
148     return result;
149 }
150 
151 
152 /*
153 **  Given a file descriptor, read any number of posts in wire format
154 **  from that file descriptor and store them.
155 */
156 static bool
store_wire_articles(int fd)157 store_wire_articles(int fd)
158 {
159     bool result = true;
160     bool skipping = false;
161     struct buffer *input;
162     size_t offset;
163     ssize_t got;
164 
165     input = buffer_new();
166     buffer_resize(input, WIRE_CHUNK_SIZE);
167     offset = 0;
168     for (;;) {
169         got = buffer_read(input, fd);
170         if (got < 0) {
171             result = false;
172             syswarn("article read failed");
173             break;
174         }
175         if (got == 0)
176             break;
177 
178         while (buffer_find_string(input, "\r\n.\r\n", offset, &offset)) {
179             size_t size;
180 
181             size = offset + 5;
182             if (skipping) {
183                 skipping = false;
184             } else {
185                 char *text;
186 
187                 text = input->data + input->used;
188                 if (!store_article_common(text, size))
189                     result = false;
190             }
191             input->used += size;
192             input->left -= size;
193             offset = 0;
194         }
195 
196         if (!skipping && input->used <= 0 && input->left >= input->size) {
197             if (input->size >= innconf->maxartsize) {
198                 skipping = true;
199                 result = false;
200                 warn("article too large, skipping");
201             } else {
202                 buffer_resize(input, input->size + WIRE_CHUNK_SIZE);
203             }
204         }
205         if (skipping && input->left > 4) {
206             input->used += input->left - 4;
207             input->left = 4;
208         }
209         if (input->left >= 4) {
210             offset = input->left - 4;
211         } else {
212             offset = 0;
213         }
214         if (input->used > 0)
215             buffer_compact(input);
216     }
217 
218     if (input->left > 0)
219         warn("trailing garbage at end of input");
220 
221     buffer_free(input);
222     return result;
223 }
224 
225 
226 /*
227 **  Process a single token, performing the operations specified in the given
228 **  options struct.  Calls warn and die to display error messages; -q is
229 **  implemented by removing all the warn and die error handlers.
230 */
231 static bool
process_token(const char * id,const struct options * options)232 process_token(const char *id, const struct options *options)
233 {
234     TOKEN token;
235     struct artngnum artinfo;
236     ARTHANDLE *article;
237     size_t length;
238     char *text;
239 
240     if (!IsToken(id)) {
241         warn("%s is not a storage token", id);
242         return false;
243     }
244     token = TextToToken(id);
245 
246     if (options->artinfo) {
247         if (!SMprobe(SMARTNGNUM, &token, &artinfo)) {
248             warn("could not get article information for %s", id);
249             return false;
250         } else {
251             printf("%s: %lu\n", artinfo.groupname, artinfo.artnum);
252             free(artinfo.groupname);
253         }
254     } else if (options->clearinfo) {
255         text = SMexplaintoken(token);
256         printf("%s %s\n", id, text);
257         free(text);
258     } else if (options->delete) {
259         if (!SMcancel(token)) {
260             warn("could not remove %s: %s", id, SMerrorstr);
261             return false;
262         }
263     } else {
264         article = SMretrieve(token, options->header ? RETR_HEAD : RETR_ALL);
265         if (article == NULL) {
266             warn("could not retrieve %s", id);
267             return false;
268         }
269         if (options->raw) {
270             if (fwrite(article->data, article->len, 1, stdout) != 1)
271                 die("output failed");
272         } else {
273             text = wire_to_native(article->data, article->len, &length);
274             if (options->rnews)
275                 printf("#! rnews %lu\n", (unsigned long) length);
276             if (fwrite(text, length, 1, stdout) != 1)
277                 die("output failed");
278             free(text);
279         }
280         SMfreearticle(article);
281     }
282     return true;
283 }
284 
285 
286 int
main(int argc,char * argv[])287 main(int argc, char *argv[])
288 {
289     int option;
290     bool okay, status;
291     struct options options = { false, false, false, false, false, false };
292     bool store = false;
293 
294     /* Suppress notice messages like tradspool rebuilding its map. */
295     message_handlers_notice(0);
296 
297     message_program_name = "sm";
298 
299     if (!innconf_read(NULL))
300         exit(1);
301 
302     while ((option = getopt(argc, argv, "cdHiqrRSs")) != EOF) {
303         switch (option) {
304         case 'c':
305             options.clearinfo = true;
306             break;
307         case 'd':
308         case 'r':
309             options.delete = true;
310             break;
311         case 'H':
312             options.header = true;
313             break;
314         case 'i':
315             options.artinfo = true;
316             break;
317         case 'q':
318             message_handlers_warn(0);
319             message_handlers_die(0);
320             break;
321         case 'R':
322             options.raw = true;
323             break;
324         case 'S':
325             options.rnews = true;
326             break;
327         case 's':
328             store = true;
329             break;
330         default:
331             fprintf(stderr, usage);
332             exit(1);
333         }
334     }
335 
336     /* Check options for consistency. */
337     if (options.artinfo && options.delete)
338         die("-i cannot be used with -r or -d");
339     if (options.artinfo && (options.header || options.raw || options.rnews))
340         die("-i cannot be used with -H, -R, or -S");
341     if (options.delete && (options.header || options.rnews))
342         die("-r or -d cannot be used with -H or -S");
343     if (options.raw && options.rnews)
344         die("-R cannot be used with -S");
345     if (options.header && options.rnews)
346         die("-H cannot be used with -S");
347     if (store && (options.artinfo || options.delete
348                   || options.header || options.rnews))
349         die("-s cannot be used with -i, -r, -d, -H, or -S");
350     if (options.clearinfo && (options.artinfo || options.delete
351                               || options.header || options.raw
352                               || options.rnews || store))
353         die("-c cannot be used with -i, -r, -d, -H, -R, -S, or -s");
354 
355     /* Initialize the storage manager.  If we're doing article deletions, we
356        need to open it read/write. */
357     if (store || options.delete) {
358         bool value = true;
359 
360         if (!SMsetup(SM_RDWR, &value))
361             die("cannot set up storage manager");
362     }
363     if (!SMinit())
364         die("cannot initialize storage manager: %s", SMerrorstr);
365 
366     /* If we're storing articles, do that and then exit. */
367     if (store) {
368         if (options.raw) {
369             status = store_wire_articles(fileno(stdin));
370         } else {
371             status = store_article(fileno(stdin));
372         }
373         exit(status ? 0 : 1);
374     }
375 
376     /* Process tokens.  If no arguments were given on the command line,
377        process tokens from stdin.  Otherwise, walk through the remaining
378        command line arguments. */
379     okay = true;
380     if (optind == argc) {
381         QIOSTATE *qp;
382         char *line;
383 
384         qp = QIOfdopen(fileno(stdin));
385         for (line = QIOread(qp); line != NULL; line = QIOread(qp)) {
386             status = process_token(line, &options);
387             okay = okay && status;
388         }
389         if (QIOerror(qp)) {
390             if (QIOtoolong(qp))
391                 die("input line too long");
392             sysdie("error reading stdin");
393         }
394         QIOclose(qp);
395     } else {
396         int i;
397 
398         for (i = optind; i < argc; i++) {
399             status = process_token(argv[i], &options);
400             okay = okay && status;
401         }
402     }
403 
404     SMshutdown();
405     exit(okay ? 0 : 1);
406 }
407