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