1 /*
2 * history file bashing
3 *
4 * B 2.10.3+ rnews puts out a leading space before received
5 * time if the article contains an Expires: header; tough.
6 * C news does this right instead of compatibly.
7 *
8 * The second history field is really two: time-received and Expires: value,
9 * separated by a tilde. This is an attempt at partial compatibility with
10 * B news, in that C expire can cope with B news history files.
11 *
12 * There is no point to storing seek offsets in network byte order in the
13 * dbm file, since dbm files are machine-dependent and so can't be shared
14 * by dissimilar machines anyway.
15 */
16
17 #include <stdio.h>
18 #include <stdlib.h>
19 #include <string.h> /* for memcpy */
20 #include <time.h>
21 #include <errno.h>
22 #include "fixerrno.h"
23 #include <sys/types.h>
24 #include "libc.h"
25 #include "news.h"
26 #include "config.h"
27 #include "dbz.h"
28 #include "fgetmfs.h"
29 #include "headers.h"
30 #include "relay.h"
31 #include "history.h"
32 #include "msgs.h"
33 #include "rerror.h"
34
35 #define HISTNAME "history" /* name of the history file in $NEWSCTL */
36 #define FIELDSEP '\t'
37 #define SUBFIELDSEP '~'
38
39 /* give 0 & 2 pretty, SVIDish names */
40 #ifndef SEEK_SET
41 #define SEEK_SET 0
42 #define SEEK_END 2
43 #endif
44
45 /* private data */
46 static FILE *fp = NULL;
47 static char *filename; /* absolute name of the ascii history file */
48 static boolean writable;
49
50 /* libdbm imports */
51 extern int dbminit(), store();
52 extern datum fetch();
53
54 /* other imports */
55 extern void prefuse(register struct article *art);
56
57 /* forwards */
58 int msgidok(register struct article *art);
59
60
61 STATIC void
histname()62 histname()
63 {
64 if (filename == NULL)
65 filename = strsave(ctlfile(HISTNAME));
66 }
67
68 /*
69 * open the history files: ascii first, then dbm.
70 * Try a+ mode first, then r mode, as dbm(3) does nowadays,
71 * so that this routine can be used by any user to read history files.
72 */
73 STATIC boolean
openhist()74 openhist()
75 {
76 histname();
77 if (fp == NULL) {
78 if ((fp = fopenclex(filename, "a+")) != NULL)
79 writable = YES;
80 else if ((fp = fopenwclex(filename, "r")) != NULL)
81 writable = NO;
82 /* else fp==NULL and fopenwclex just complained */
83
84 errno = 0;
85 if (fp != NULL && dbminit(filename) < 0) {
86 /*
87 * no luck. dbm's dbminit will have just honked (on
88 * stdout, alas) but dbz's won't have, so bitch.
89 */
90 persistent(NOART, 'f',
91 "database files for `%s' incomprehensible or unavailable",
92 filename);
93 (void) nfclose(fp); /* close ascii file */
94 fp = NULL; /* and mark it closed */
95 }
96 }
97 return fp != NULL;
98 }
99
100 /*
101 * Turn \n & FIELDSEP into ' ' in s.
102 */
103 STATIC void
sanitise(s)104 sanitise(s)
105 register char *s;
106 {
107 for (; *s != '\0'; ++s)
108 if (*s == FIELDSEP || *s == '\n')
109 *s = ' ';
110 }
111
112 /*
113 * Turn SUBFIELDSEP into ' ' in s.
114 */
115 STATIC void
subsanitise(s)116 subsanitise(s)
117 register char *s;
118 {
119 stranslit(s, SUBFIELDSEP, ' ');
120 }
121
122 STATIC datum
getposhist(msgid)123 getposhist(msgid) /* return seek offset of history entry */
124 char *msgid;
125 {
126 register char *clnmsgid;
127 datum msgidkey, keypos;
128
129 msgidkey.dptr = NULL;
130 msgidkey.dsize = 0;
131 if (!openhist())
132 return msgidkey;
133 clnmsgid = strsave(msgid);
134 sanitise(clnmsgid);
135 msgidkey.dptr = clnmsgid;
136 msgidkey.dsize = strlen(clnmsgid) + SIZENUL;
137 keypos = dbzfetch(msgidkey); /* offset into ascii file */
138 free(clnmsgid);
139 return keypos;
140 }
141
142 boolean
alreadyseen(msgid)143 alreadyseen(msgid) /* return true if found in the data base */
144 char *msgid;
145 {
146 datum posdatum;
147
148 posdatum = getposhist(msgid);
149 return posdatum.dptr != NULL;
150 }
151
152 char * /* NULL if no history entry; else malloced */
gethistory(msgid)153 gethistory(msgid) /* return existing history entry, if any */
154 char *msgid;
155 {
156 long pos = 0;
157 datum posdatum;
158
159 posdatum = getposhist(msgid);
160 if (posdatum.dptr != NULL && posdatum.dsize == sizeof pos) {
161 static char *histent = NULL;
162
163 (void) memcpy((char *)&pos, posdatum.dptr, sizeof pos); /* align */
164 nnfree(&histent);
165 if (fseek(fp, pos, SEEK_SET) != -1 &&
166 (histent = fgetms(fp)) != NULL)
167 return histent; /* could note move from EOF */
168 }
169 return NULL;
170 }
171
172 /*
173 * Return a pointer to the "files" field of a history entry.
174 * Side-effect: trims \n from the history entry.
175 */
176 char *
findfiles(histent)177 findfiles(histent)
178 char *histent;
179 {
180 register char *tabp;
181
182 trim(histent);
183 /* find start of 2nd field (arrival~expiry) */
184 tabp = strchr(histent, FIELDSEP);
185 if (tabp == NULL)
186 return NULL; /* mangled entry */
187 /* find start of 3rd field (files list) */
188 else if ((tabp = strchr(tabp + 1, FIELDSEP)) == NULL)
189 return NULL; /* cancelled or expired art. */
190 else
191 return tabp + 1;
192 }
193
194 /*
195 * Internal interface to generate a history file entry,
196 * assuming all sanity checking has been done already.
197 * Record the (msgid, position) pair in the data base.
198 *
199 * The fflush is crash-proofing.
200 */
201 STATIC void
mkhistent(art,msgid,now,expiry)202 mkhistent(art, msgid, now, expiry)
203 register struct article *art;
204 char *msgid, *expiry;
205 time_t now;
206 {
207 long pos;
208 datum msgidkey, posdatum;
209
210 pos = ftell(fp); /* get seek ptr for dbm; could keep track instead */
211
212 if (fprintf(fp, "%s%c%ld%c%s%c", msgid, FIELDSEP,
213 (long)now, SUBFIELDSEP, expiry, SUBFIELDSEP) == EOF)
214 fulldisk(art, filename);
215 if (art->a_charswritten > 0 &&
216 fprintf(fp, "%ld", (long)art->a_charswritten) == EOF)
217 fulldisk(art, filename);
218 /* don't write 3rd field for cancelled but unseen articles */
219 if (art->a_files != NULL && art->a_files[0] != '\0')
220 if (fprintf(fp, "%c%s", FIELDSEP, art->a_files) == EOF)
221 fulldisk(art, filename);
222 (void) putc('\n', fp);
223 if (fflush(fp) == EOF)
224 fulldisk(art, filename);
225
226 msgidkey.dptr = msgid;
227 msgidkey.dsize = strlen(msgid) + SIZENUL;
228 posdatum.dptr = (char *)&pos;
229 posdatum.dsize = sizeof pos;
230 if (dbzstore(msgidkey, posdatum) < 0)
231 fulldisk(art, filename);
232 }
233
234 /*
235 * Generate a history entry from art.
236 * The history entry will have tabs and newlines deleted from the
237 * interior of fields, to keep the file format sane.
238 * Optionally print the start of an "accepted" log file line (no \n)
239 * (transmit() prints site names).
240 */
241 void
history(art,startlog)242 history(art, startlog)
243 register struct article *art;
244 boolean startlog;
245 {
246 register char *msgid, *expiry;
247 time_t now;
248
249 if (!msgidok(art)) /* complains in log if unhappy */
250 return; /* refuse to corrupt history */
251 msgid = strsave(nullify(art->h.h_msgid));
252 sanitise(msgid); /* RFC 1036 forbids whitespace in msg-ids */
253 expiry = strsave(nullify(art->h.h_expiry));
254 sanitise(expiry);
255 subsanitise(expiry);
256
257 if (startlog) {
258 timestamp(stdout, &now);
259 if (printf(" %s + %s", sendersite(nullify(art->h.h_path)),
260 msgid) == EOF)
261 fulldisk(art, "stdout");
262 } else
263 now = time(&now);
264 if (!openhist())
265 persistent(art, '\0', "can't open history", "");
266 else if (!writable)
267 persistent(art, 'f', "no write permission on `%s'", filename);
268 else if (fseek(fp, 0L, SEEK_END) == -1)
269 /* could avoid fseek if still at EOF */
270 persistent(art, 'f', "can't seek to end of `%s'", filename);
271 else
272 mkhistent(art, msgid, now, expiry);
273 free(msgid);
274 free(expiry);
275 }
276
277 void
decline(art)278 decline(art) /* mark art as undesirable */
279 struct article *art;
280 {
281 transient(art, '\0', "article is a turkey", "");
282 if (!opts.okrefusal)
283 art->a_status |= ST_DROPPED;
284 }
285
286 const char *
ismsgidbad(msgid)287 ismsgidbad(msgid) /* if bad, return error */
288 register char *msgid;
289 {
290 if (msgid == NULL || msgid[0] == '\0')
291 return "missing Message-ID";
292 else if (strchr(msgid, '@') == NULL)
293 return "no @ in Message-ID";
294 else if (strchr(msgid, ' ') != NULL || strchr(msgid, '\t') != NULL)
295 return "whitespace in Message-ID";
296 else if (msgid[0] != '<' || msgid[strlen(msgid)-1] != '>')
297 return "Message-ID not bracketed by <>";
298 else
299 return NULL;
300 }
301
302 int
msgidok(art)303 msgidok(art) /* if bad, complain in log */
304 register struct article *art;
305 {
306 register const char *err = ismsgidbad(art->h.h_msgid);
307
308 if (err == NULL)
309 return YES;
310 else {
311 prefuse(art);
312 (void) fputs(err, stdout);
313 decline(art);
314 return NO;
315 }
316 }
317
318 /*
319 * Generate a fake history file entry, given a message-id, an Expires:
320 * value, and a "file" list ("net.foo/123").
321 */
322 statust
fakehist(fkmsgid,fkexpiry,fkfiles)323 fakehist(fkmsgid, fkexpiry, fkfiles)
324 char *fkmsgid, *fkexpiry, *fkfiles;
325 {
326 struct article art;
327
328 artinit(&art);
329 art.h.h_msgid = fkmsgid;
330 art.h.h_expiry = fkexpiry;
331 art.a_files = fkfiles;
332 history(&art, STARTLOG);
333 return art.a_status;
334 }
335
336 /* Append "group/artnumstr" to the file list in *art. */
337 void
histupdfiles(art,group,artnumstr)338 histupdfiles(art, group, artnumstr)
339 register struct article *art;
340 register char *group;
341 register char *artnumstr;
342 {
343 unsigned addlen = strlen(group) + STRLEN(SFNDELIM) +
344 strlen(artnumstr) + SIZENUL;
345
346 if (art->a_files == NULL) {
347 art->a_files = nemalloc(addlen);
348 art->a_files[0] = '\0';
349 } else {
350 art->a_files = realloc(art->a_files, (unsigned)
351 strlen(art->a_files) + STRLEN(" ") + addlen);
352 if (art->a_files == NULL)
353 errunlock("can't grow a_files", "");
354 (void) strcat(art->a_files, " ");
355 }
356 (void) strcat(art->a_files, group); /* normal case */
357 (void) strcat(art->a_files, SFNDELIM);
358 (void) strcat(art->a_files, artnumstr);
359 }
360
361 statust
closehist()362 closehist()
363 {
364 register statust status = ST_OKAY;
365
366 if (fp != NULL) {
367 /* dbmclose is only needed by dbz, to flush statistics to disk */
368 if (dbmclose() < 0) {
369 persistent(NOART, 'f', "error closing dbm history file",
370 "");
371 status |= ST_DROPPED;
372 }
373 if (nfclose(fp) == EOF) {
374 persistent(NOART, 'f', "error closing history file",
375 "");
376 status |= ST_DROPPED;
377 }
378 fp = NULL; /* mark file closed */
379 }
380 return status;
381 }
382