1 /*
2  *  implements file generations support for NTP
3  *  logfiles and statistic files
4  *
5  *
6  * Copyright Rainer Pruy
7  *           Friedrich-Alexander Universitaet Erlangen-Nuernberg, Germany
8  * Copyright the NTPsec Project contributors
9  * SPDX-License-Identifier: BSD-2-Clause
10  */
11 
12 #include "config.h"
13 
14 #include <stdio.h>
15 #include <sys/types.h>
16 #include <sys/stat.h>
17 #include <string.h>
18 
19 #include "ntpd.h"
20 #include "ntp_io.h"
21 #include "ntp_calendar.h"
22 #include "ntp_filegen.h"
23 #include "ntp_stdlib.h"
24 
25 /*
26  * NTP is intended to run long periods of time without restart.
27  * Thus log and statistic files generated by NTP will grow large.
28  *
29  * this set of routines provides a central interface
30  * to generating files using file generations
31  *
32  * the generation of a file is changed according to file generation type
33  */
34 
35 
36 /*
37  * redefine this if your system dislikes filename suffixes like
38  * X.19910101 or X.1992W50 or ....
39  */
40 #define SUFFIX_SEP '.'
41 
42 static	void	filegen_open	(FILEGEN *, const time_t);
43 static	int	valid_fileref	(const char *, const char *)
44 			         __attribute__((pure));
45 static	void	filegen_init	(const char *, const char *, FILEGEN *);
46 #ifdef	DEBUG
47 static	void	filegen_uninit		(FILEGEN *);
48 #endif	/* DEBUG */
49 
50 
51 /*
52  * filegen_init
53  */
54 
55 static void
filegen_init(const char * dir,const char * fname,FILEGEN * fgp)56 filegen_init(
57 	const char *	dir,
58 	const char *	fname,
59 	FILEGEN *	fgp
60 	)
61 {
62 	fgp->fp = NULL;
63 	fgp->dir = estrdup(dir);
64 	fgp->fname = estrdup(fname);
65 	fgp->id_lo = 0;
66 	fgp->id_hi = 0;
67 	fgp->type = FILEGEN_DAY;
68 	fgp->flag = FGEN_FLAG_LINK; /* not yet enabled !!*/
69 }
70 
71 
72 /*
73  * filegen_uninit - free memory allocated by filegen_init
74  */
75 #ifdef DEBUG
76 static void
filegen_uninit(FILEGEN * fgp)77 filegen_uninit(
78 	FILEGEN *fgp
79 	)
80 {
81 	free(fgp->dir);
82 	free(fgp->fname);
83 }
84 #endif
85 
86 
87 /*
88  * open a file generation according to the current settings of gen
89  * will also provide a link to basename if requested to do so
90  */
91 
92 static void
filegen_open(FILEGEN * gen,const time_t stamp)93 filegen_open(
94 	FILEGEN *	gen,
95 	const time_t 	stamp
96 	)
97 {
98 	char *savename;	/* temp store for name collision handling */
99 	char *fullname;	/* name with any designation extension */
100 	char *filename;	/* name without designation extension */
101 	char *suffix;	/* where to print suffix extension */
102 	unsigned int len, suflen;
103 	FILE *fp;
104 	struct tm tm;
105 
106 	/* get basic filename in buffer, leave room for extensions */
107 	len = strlen(gen->dir) + strlen(gen->fname) + 65;
108 	filename = emalloc(len);
109 	fullname = emalloc(len);
110 	savename = NULL;
111 	snprintf(filename, len, "%s%s", gen->dir, gen->fname);
112 
113 	/* where to place suffix */
114 	suflen = strlcpy(fullname, filename, len);
115 	suffix = fullname + suflen;
116 	suflen = len - suflen;
117 
118 	/* last octet of fullname set to '\0' for truncation check */
119 	fullname[len - 1] = '\0';
120 
121 	switch (gen->type) {
122 
123 	default:
124 		msyslog(LOG_ERR,
125 			"LOG: unsupported file generations type %d for "
126 			"\"%s\" - reverting to FILEGEN_NONE",
127 			gen->type, filename);
128 		gen->type = FILEGEN_NONE;
129 		break;
130 
131 	case FILEGEN_NONE:
132 		/* no suffix, all set */
133 		break;
134 
135 	case FILEGEN_PID:
136 		gen->id_lo = getpid();
137 		gen->id_hi = 0;
138 		snprintf(suffix, suflen, "%c#%lld",
139 			 SUFFIX_SEP, (long long)gen->id_lo);
140 		break;
141 
142 	case FILEGEN_DAY:
143 		/*
144 		 * You can argue here in favor of using MJD, but I
145 		 * would assume it to be easier for humans to interpret
146 		 * dates in a format they are used to in everyday life.
147 		 */
148 		gmtime_r(&stamp, &tm);
149 		snprintf(suffix, suflen, "%c%04d%02d%02d",
150 			 SUFFIX_SEP, tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday);
151 		gen->id_lo = stamp - (stamp % SECSPERDAY);
152 		gen->id_hi = gen->id_lo + SECSPERDAY;
153 		break;
154 
155 	case FILEGEN_WEEK:
156 		/* week number is day-of-year mod 7 */
157 		gmtime_r(&stamp, &tm);
158 		snprintf(suffix, suflen, "%c%04dw%02d",
159 			 SUFFIX_SEP, tm.tm_year+1900, tm.tm_yday % 7);
160 		/* See comment below at MONTH */
161 		gen->id_lo = stamp - (stamp % SECSPERDAY);
162 		gen->id_hi = gen->id_lo + SECSPERDAY;
163 		break;
164 
165 	case FILEGEN_MONTH:
166 		gmtime_r(&stamp, &tm);
167 		snprintf(suffix, suflen, "%c%04d%02d",
168 			 SUFFIX_SEP, tm.tm_year+1900, tm.tm_mon+1);
169 		/* If we had a mktime that didn't use the local time zone
170 		 * we could setup id_lo and id_hi to bracket the month.
171 		 * This will have to recalculate things each day.
172 		 */
173 		gen->id_lo = stamp - (stamp % SECSPERDAY);
174 		gen->id_hi = gen->id_lo + SECSPERDAY;
175 		break;
176 
177 	case FILEGEN_YEAR:
178 		gmtime_r(&stamp, &tm);
179 		snprintf(suffix, suflen, "%c%04d",
180 			 SUFFIX_SEP, tm.tm_year+1900);
181 		/* See comment above at MONTH */
182 		gen->id_lo = stamp - (stamp % SECSPERDAY);
183 		gen->id_hi = gen->id_lo + SECSPERDAY;
184 		break;
185 
186 	case FILEGEN_AGE:
187 	    gen->id_lo = (time_t)(current_time - (current_time % SECSPERDAY));
188 	    gen->id_hi = gen->id_lo + SECSPERDAY;
189 	    snprintf(suffix, suflen, "%ca%08lld",
190 		     SUFFIX_SEP, (long long)gen->id_lo);
191 	}
192 
193 	/* check possible truncation */
194 	if ('\0' != fullname[len - 1]) {
195 		fullname[len - 1] = '\0';
196 		msyslog(LOG_ERR, "LOG: logfile name truncated: \"%s\"",
197 			fullname);
198 	}
199 
200 	if (FILEGEN_NONE != gen->type) {
201 		/*
202 		 * check for existence of a file with name 'basename'
203 		 * as we disallow such a file
204 		 * if FGEN_FLAG_LINK is set create a link
205 		 */
206 		struct stat stats;
207 		/*
208 		 * try to resolve name collisions
209 		 */
210 		static unsigned long conflicts = 0;
211 
212 #ifndef	S_ISREG
213 #define	S_ISREG(mode)	(((mode) & S_IFREG) == S_IFREG)
214 #endif
215 		/* coverity[toctou] */
216 		if (stat(filename, &stats) == 0) {
217 			/* Hm, file exists... */
218 			if (S_ISREG(stats.st_mode)) {
219 				if (stats.st_nlink <= 1)	{
220 					/*
221 					 * Oh, it is not linked - try to save it
222 					 */
223 					savename = emalloc(len);
224 					snprintf(savename, len,
225 						"%s%c%dC%lu",
226 						filename, SUFFIX_SEP,
227 						(int)getpid(), conflicts++);
228 
229 					if (rename(filename, savename) != 0)
230 						msyslog(LOG_ERR,
231 							"LOG: couldn't save %s: %s",
232 							filename, strerror(errno));
233 					free(savename);
234 				} else {
235 					/*
236 					 * there is at least a second link to
237 					 * this file.
238 					 * just remove the conflicting one
239 					 */
240 					/* coverity[toctou] */
241 					if (unlink(filename) != 0)
242 						msyslog(LOG_ERR,
243 							"LOG: couldn't unlink %s: %s",
244 							filename, strerror(errno));
245 				}
246 			} else {
247 				/*
248 				 * Ehh? Not a regular file ?? strange !!!!
249 				 */
250 				msyslog(LOG_ERR,
251 					"LOG: expected regular file for %s "
252 					"(found mode 0%lo)",
253 					filename,
254 					(unsigned long)stats.st_mode);
255 			}
256 		} else {
257 			/*
258 			 * stat(..) failed, but it is absolutely correct for
259 			 * 'basename' not to exist
260 			 */
261 			if (ENOENT != errno)
262 				msyslog(LOG_ERR, "LOG: stat(%s) failed: %s",
263 						 filename, strerror(errno));
264 		}
265 	}
266 
267 	/*
268 	 * now, try to open new file generation...
269 	 */
270 	DPRINT(4, ("opening filegen (type=%d/stamp=%lld) \"%s\"\n",
271 		   gen->type, (long long)stamp, fullname));
272 
273 	fp = fopen(fullname, "a");
274 
275 	if (NULL == fp)	{
276 		/* open failed -- keep previous state
277 		 *
278 		 * If the file was open before keep the previous generation.
279 		 * This will cause output to end up in the 'wrong' file,
280 		 * but I think this is still better than losing output
281 		 *
282 		 * ignore errors due to missing directories
283 		 */
284 
285 		if (ENOENT != errno)
286 			msyslog(LOG_ERR, "LOG: can't open %s: %s", fullname, strerror(errno));
287 	} else {
288 		if (NULL != gen->fp) {
289 			fclose(gen->fp);
290 			gen->fp = NULL;
291 		}
292 		gen->fp = fp;
293 
294 		if (gen->flag & FGEN_FLAG_LINK) {
295 			/*
296 			 * need to link file to basename
297 			 * have to use hardlink for now as I want to allow
298 			 * gen->basename spanning directory levels
299 			 * this would make it more complex to get the correct
300 			 * fullname for symlink
301 			 *
302 			 * Ok, it would just mean taking the part following
303 			 * the last '/' in the name.... Should add it later....
304 			 */
305 			if (link(fullname, filename) != 0)
306 				if (EEXIST != errno)
307 					msyslog(LOG_ERR,
308 						"LOG: can't link(%s, %s): %s",
309 						fullname, filename, strerror(errno));
310 		}		/* flags & FGEN_FLAG_LINK */
311 	}			/* else fp == NULL */
312 
313 	free(filename);
314 	free(fullname);
315 	return;
316 }
317 
318 /*
319  * this function sets up gen->fp to point to the correct
320  * generation of the file for the time specified by 'now'
321  */
322 
323 void
filegen_setup(FILEGEN * gen,time_t now)324 filegen_setup(
325 	FILEGEN * gen,
326 	time_t now
327 	)
328 {
329 	bool	current;
330 
331 	if (!(gen->flag & FGEN_FLAG_ENABLED)) {
332 		if (NULL != gen->fp) {
333 			fclose(gen->fp);
334 			gen->fp = NULL;
335 		}
336 		return;
337 	}
338 
339 	switch (gen->type) {
340 
341 	default:
342 	case FILEGEN_NONE:
343 		current = true;
344 		break;
345 
346 	case FILEGEN_PID:
347 		current = ((int)gen->id_lo == getpid());
348 		break;
349 
350 	case FILEGEN_AGE:
351 		current = (gen->id_lo <= (long)current_time) &&
352 			  (gen->id_hi > (long)current_time);
353 		break;
354 
355 	case FILEGEN_DAY:
356 	case FILEGEN_WEEK:
357 	case FILEGEN_MONTH:
358 	case FILEGEN_YEAR:
359 		current = (gen->id_lo <= now) &&
360 			  (gen->id_hi > now);
361 		break;
362 	}
363 	/*
364 	 * try to open file if not yet open
365 	 * reopen new file generation file on change of generation id
366 	 */
367 	if (NULL == gen->fp || !current) {
368 		DPRINT(1, ("filegen  %0x %lld\n", gen->type, (long long)now));
369 		filegen_open(gen, now);
370 	}
371 }
372 
373 
374 /*
375  * change settings for filegen files
376  */
377 void
filegen_config(FILEGEN * gen,const char * dir,const char * fname,unsigned int type,unsigned int flag)378 filegen_config(
379 	FILEGEN *	gen,
380 	const char *	dir,
381 	const char *	fname,
382 	unsigned int	type,
383 	unsigned int	flag
384 	)
385 {
386 	bool file_existed;
387 
388 
389 	/*
390 	 * if nothing would be changed...
391 	 */
392 	if (strcmp(dir, gen->dir) == 0 && strcmp(fname, gen->fname) == 0
393 	    && type == gen->type && flag == gen->flag)
394 		return;
395 
396 	/*
397 	 * validate parameters
398 	 */
399 	if (!valid_fileref(dir, fname)) {
400 		return;
401 }
402 
403 	if (NULL != gen->fp) {
404 		fclose(gen->fp);
405 		gen->fp = NULL;
406 		file_existed = true;
407 	} else {
408 		file_existed = false;
409 	}
410 
411 	DPRINT(3, ("configuring filegen:\n"
412 		   "\tdir:\t%s -> %s\n"
413 		   "\tfname:\t%s -> %s\n"
414 		   "\ttype:\t%d -> %u\n"
415 		   "\tflag: %x -> %x\n",
416 		   gen->dir, dir,
417 		   gen->fname, fname,
418 		   gen->type, type,
419 		   gen->flag, flag));
420 
421 	if (strcmp(gen->dir, dir) != 0) {
422 		free(gen->dir);
423 		gen->dir = estrdup(dir);
424 	}
425 
426 	if (strcmp(gen->fname, fname) != 0) {
427 		free(gen->fname);
428 		gen->fname = estrdup(fname);
429 	}
430 	gen->type = (uint8_t)type;
431 	gen->flag = (uint8_t)flag;
432 
433 	/*
434 	 * make filegen use the new settings
435 	 * special action is only required when a generation file
436 	 * is currently open
437 	 * otherwise the new settings will be used anyway at the next open
438 	 */
439 	if (file_existed) {
440 		filegen_setup(gen, time(NULL));
441 	}
442 }
443 
444 
445 /*
446  * check whether concatenating prefix and basename
447  * yields a legal filename
448  */
449 static int
valid_fileref(const char * dir,const char * fname)450 valid_fileref(
451 	const char *	dir,
452 	const char *	fname
453 	)
454 {
455 	/*
456 	 * dir cannot be changed dynamically
457 	 * (within the context of filegen)
458 	 * so just reject basenames containing '..'
459 	 *
460 	 * ASSUMPTION:
461 	 *		file system parts 'below' dir may be
462 	 *		specified without infringement of security
463 	 *
464 	 *		restricting dir to legal values
465 	 *		has to be ensured by other means
466 	 * (however, it would be possible to perform some checks here...)
467 	 */
468 	const char *p;
469 
470 	/*
471 	 * Just to catch, dumb errors opening up the world...
472 	 */
473 	if (NULL == dir || '\0' == dir[0])
474 		return false;
475 
476 	if (NULL == fname)
477 		return false;
478 
479 	for (p = fname; p != NULL; p = strchr(p, DIR_SEP)) {
480 		if ('.' == p[0] && '.' == p[1]
481 		    && ('\0' == p[2] || DIR_SEP == p[2]))
482 			return false;
483 	}
484 
485 	return true;
486 }
487 
488 
489 /*
490  * filegen registry
491  */
492 
493 static struct filegen_entry {
494 	char *			name;
495 	FILEGEN *		filegen;
496 	struct filegen_entry *	next;
497 } *filegen_registry = NULL;
498 
499 
500 FILEGEN *
filegen_get(const char * name)501 filegen_get(
502 	const char *	name
503 	)
504 {
505 	struct filegen_entry *f = filegen_registry;
506 
507 	while (f) {
508 		if (f->name == name || strcmp(name, f->name) == 0) {
509 			DPRINT(4, ("filegen_get(%s) = %p\n",
510 				   name, f->filegen));
511 			return f->filegen;
512 		}
513 		f = f->next;
514 	}
515 	DPRINT(4, ("filegen_get(%s) = NULL\n", name));
516 	return NULL;
517 }
518 
519 
520 void
filegen_register(const char * dir,const char * name,FILEGEN * filegen)521 filegen_register(
522 	const char *	dir,
523 	const char *	name,
524 	FILEGEN *	filegen
525 	)
526 {
527 	struct filegen_entry **ppfe;
528 
529 	DPRINT(4, ("filegen_register(%s, %p)\n", name, filegen));
530 
531 	filegen_init(dir, name, filegen);
532 
533 	ppfe = &filegen_registry;
534 	while (NULL != *ppfe) {
535 		if ((*ppfe)->name == name
536 		    || !strcmp((*ppfe)->name, name)) {
537 
538 			DPRINT(5, ("replacing filegen %p\n",
539 				   (*ppfe)->filegen));
540 
541 			(*ppfe)->filegen = filegen;
542 			return;
543 		}
544 		ppfe = &((*ppfe)->next);
545 	}
546 
547 	*ppfe = emalloc(sizeof **ppfe);
548 
549 	(*ppfe)->next = NULL;
550 	(*ppfe)->name = estrdup(name);
551 	(*ppfe)->filegen = filegen;
552 
553 	DPRINT(6, ("adding new filegen\n"));
554 
555 	return;
556 }
557 
558 
559 /*
560  * filegen_statsdir() - reset each filegen entry's dir to statsdir.
561  */
562 void
filegen_statsdir(void)563 filegen_statsdir(void)
564 {
565 	struct filegen_entry *f;
566 
567 	for (f = filegen_registry; f != NULL; f = f->next)
568 		filegen_config(f->filegen, statsdir, f->filegen->fname,
569 			       f->filegen->type, f->filegen->flag);
570 }
571 
572 
573 /*
574  * filegen_unregister frees memory allocated by filegen_register for
575  * name.
576  */
577 #ifdef DEBUG
578 void
filegen_unregister(const char * name)579 filegen_unregister(
580 	const char *name
581 	)
582 {
583 	struct filegen_entry **	ppfe;
584 	struct filegen_entry *	pfe;
585 	FILEGEN *		fg;
586 
587 	DPRINT(4, ("filegen_unregister(%s)\n", name));
588 
589 	ppfe = &filegen_registry;
590 
591 	while (NULL != *ppfe) {
592 		if ((*ppfe)->name == name
593 		    || !strcmp((*ppfe)->name, name)) {
594 			pfe = *ppfe;
595 			*ppfe = (*ppfe)->next;
596 			fg = pfe->filegen;
597 			free(pfe->name);
598 			free(pfe);
599 			filegen_uninit(fg);
600 			break;
601 		}
602 		ppfe = &((*ppfe)->next);
603 	}
604 }
605 #endif	/* DEBUG */
606