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