1 /*
2 Copyright 2021 Northern.tech AS
3
4 This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the
8 Free Software Foundation; version 3.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
18
19 To the extent this program is licensed as part of the Enterprise
20 versions of CFEngine, the applicable Commercial Open Source License
21 (COSL) may apply to this file if you as a licensee so wish it. See
22 included file COSL.txt.
23 */
24
25 #include <files_lib.h>
26
27 #include <files_interfaces.h>
28 #include <files_names.h>
29 #include <files_copy.h>
30 #include <item_lib.h>
31 #include <promises.h>
32 #include <matching.h>
33 #include <misc_lib.h>
34 #include <dir.h>
35 #include <policy.h>
36 #include <string_lib.h>
37 #include <eval_context.h> /* MakingChanges(), RecordFailure() */
38 #include <actuator.h> /* PromiseResultUpdate() */
39
40
41 static Item *ROTATED = NULL; /* GLOBAL_X */
42
43
44 /*********************************************************************/
45
PurgeItemList(Item ** list,char * name)46 void PurgeItemList(Item **list, char *name)
47 {
48 Item *ip, *copy = NULL;
49 struct stat sb;
50
51 CopyList(©, *list);
52
53 for (ip = copy; ip != NULL; ip = ip->next)
54 {
55 if (stat(ip->name, &sb) == -1)
56 {
57 Log(LOG_LEVEL_VERBOSE,
58 "Purging file '%s' from '%s' list as it no longer exists",
59 ip->name, name);
60 DeleteItemLiteral(list, ip->name);
61 }
62 }
63
64 DeleteItemList(copy);
65 }
66
FileWriteOver(char * filename,char * contents)67 bool FileWriteOver(char *filename, char *contents)
68 {
69 FILE *fp = safe_fopen_create_perms(filename, "w", CF_PERMS_DEFAULT);
70
71 if(fp == NULL)
72 {
73 return false;
74 }
75
76 size_t bytes_to_write = strlen(contents);
77
78 size_t bytes_written = fwrite(contents, 1, bytes_to_write, fp);
79
80 bool res = true;
81
82 if(bytes_written != bytes_to_write)
83 {
84 res = false;
85 }
86
87 if(fclose(fp) != 0)
88 {
89 res = false;
90 }
91
92 return res;
93 }
94
95
96 /*********************************************************************/
97
98 static bool MakeParentDirectoryImpl(EvalContext *ctx, const Promise *pp, const Attributes *attr,
99 PromiseResult *result, const char *parentandchild,
100 bool force, bool internal, bool *created);
101
MakeParentDirectory(const char * parentandchild,bool force,bool * created)102 bool MakeParentDirectory(const char *parentandchild, bool force, bool *created)
103 {
104 /* just use the complex function with no promise info */
105 return MakeParentDirectoryImpl(NULL, NULL, NULL, NULL,
106 parentandchild, force, false, created);
107 }
108
MakeParentInternalDirectory(const char * parentandchild,bool force,bool * created)109 bool MakeParentInternalDirectory(const char *parentandchild, bool force, bool *created)
110 {
111 /* just use the complex function with no promise info */
112 return MakeParentDirectoryImpl(NULL, NULL, NULL, NULL,
113 parentandchild, force, true, created);
114 }
115
MakeParentDirectoryForPromise(EvalContext * ctx,const Promise * pp,const Attributes * attr,PromiseResult * result,const char * parentandchild,bool force,bool * created)116 bool MakeParentDirectoryForPromise(EvalContext *ctx, const Promise *pp, const Attributes *attr,
117 PromiseResult *result, const char *parentandchild,
118 bool force, bool *created)
119 {
120 return MakeParentDirectoryImpl(ctx, pp, attr, result, parentandchild, force, false, created);
121 }
122
MakeParentDirectoryImpl(EvalContext * ctx,const Promise * pp,const Attributes * attr,PromiseResult * result,const char * parentandchild,bool force,bool internal,bool * created)123 static bool MakeParentDirectoryImpl(EvalContext *ctx, const Promise *pp, const Attributes *attr,
124 PromiseResult *result, const char *parentandchild,
125 bool force, bool internal, bool *created)
126 {
127 char *sp;
128 char currentpath[CF_BUFSIZE];
129 char pathbuf[CF_BUFSIZE];
130 struct stat statbuf;
131 mode_t mask;
132 int rootlen;
133
134 const char *changes_parentandchild = parentandchild;
135 if (!internal && ChrootChanges())
136 {
137 changes_parentandchild = ToChangesChroot(parentandchild);
138 }
139
140 const bool have_promise_info = ((ctx != NULL) && (pp != NULL) && (attr != NULL) && (result != NULL));
141
142 if (created != NULL)
143 {
144 *created = false;
145 }
146
147 #ifdef __APPLE__
148 /* Keeps track of if dealing w. resource fork */
149 int rsrcfork;
150
151 rsrcfork = 0;
152
153 char *tmpstr;
154 #endif
155
156 Log(LOG_LEVEL_DEBUG, "Trying to create a parent directory%s for: %s",
157 force ? " (force applied)" : "",
158 parentandchild);
159
160 if (!IsAbsoluteFileName(parentandchild))
161 {
162 Log(LOG_LEVEL_ERR,
163 "Will not create directories for a relative filename: %s",
164 parentandchild);
165 return false;
166 }
167
168 strlcpy(pathbuf, changes_parentandchild, CF_BUFSIZE); /* local copy */
169
170 #ifdef __APPLE__
171 if (strstr(pathbuf, _PATH_RSRCFORKSPEC) != NULL)
172 {
173 rsrcfork = 1;
174 }
175 #endif
176
177 /* skip link name */
178
179 sp = (char *) LastFileSeparator(pathbuf); /* de-constify */
180
181 if (sp == NULL)
182 {
183 sp = pathbuf;
184 }
185 *sp = '\0';
186
187 DeleteSlash(pathbuf);
188
189 if (lstat(pathbuf, &statbuf) != -1)
190 {
191 if (S_ISLNK(statbuf.st_mode))
192 {
193 Log(LOG_LEVEL_VERBOSE, "'%s' is a symbolic link, not a directory",
194 pathbuf);
195 }
196
197 if (force) /* force in-the-way directories aside */
198 {
199 struct stat dir;
200 stat(pathbuf, &dir);
201
202 /* If the target directory exists as a directory, no problem. */
203 /* If the target directory exists but is not a directory, then
204 * rename it to ".cf-moved": */
205 if (!S_ISDIR(dir.st_mode))
206 {
207 struct stat sbuf;
208
209 strcpy(currentpath, pathbuf);
210 DeleteSlash(currentpath);
211 /* TODO overflow check! */
212 strlcat(currentpath, ".cf-moved", sizeof(currentpath));
213 Log(LOG_LEVEL_VERBOSE,
214 "Moving obstructing file/link %s to %s to make directory",
215 pathbuf, currentpath);
216
217 if (have_promise_info &&
218 !MakingChanges(ctx, pp, attr, result,
219 "move obstructing file/link '%s' to '%s' to make directories for '%s'",
220 pathbuf, currentpath, parentandchild))
221 {
222 return true;
223 }
224
225 /* Remove possibly pre-existing ".cf-moved" backup object. */
226 if (lstat(currentpath, &sbuf) != -1)
227 {
228 if (S_ISDIR(sbuf.st_mode)) /* directory */
229 {
230 if (!DeleteDirectoryTree(currentpath))
231 {
232 Log(LOG_LEVEL_WARNING,
233 "Failed to remove directory '%s' while trying to remove a backup",
234 currentpath);
235 }
236 }
237 else /* not a directory */
238 {
239 if (unlink(currentpath) == -1)
240 {
241 Log(LOG_LEVEL_WARNING,
242 "Couldn't remove file/link '%s' while trying to remove a backup"
243 " (unlink: %s)", currentpath, GetErrorStr());
244 }
245 }
246 }
247
248 /* And then rename the current object to ".cf-moved". */
249 if (rename(pathbuf, currentpath) == -1)
250 {
251 if (have_promise_info)
252 {
253 RecordFailure(ctx, pp, attr,
254 "Couldn't rename '%s' to .cf-moved (rename: %s)",
255 pathbuf, GetErrorStr());
256 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
257 }
258 else
259 {
260 Log(LOG_LEVEL_ERR,
261 "Couldn't rename '%s' to .cf-moved (rename: %s)",
262 pathbuf, GetErrorStr());
263 }
264 return false;
265 }
266 else if (have_promise_info)
267 {
268 RecordChange(ctx, pp, attr,
269 "Moved obstructing file/link '%s' to '%s' to make directories for '%s'",
270 pathbuf, currentpath, parentandchild);
271 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
272 }
273 }
274 }
275 else
276 {
277 if (!S_ISLNK(statbuf.st_mode) && !S_ISDIR(statbuf.st_mode))
278 {
279 if (have_promise_info)
280 {
281 RecordFailure(ctx, pp, attr,
282 "The object '%s' is not a directory."
283 " Cannot make a new directory without deleting it.",
284 pathbuf);
285 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
286 }
287 else
288 {
289 Log(LOG_LEVEL_ERR, "The object '%s' is not a directory."
290 " Cannot make a new directory without deleting it.",
291 pathbuf);
292 }
293 return false;
294 }
295 }
296 }
297
298 /* Now we make directories descending from the root folder down to the leaf */
299
300 currentpath[0] = '\0';
301
302 rootlen = RootDirLength(changes_parentandchild);
303 /* currentpath is not NULL terminated on purpose! */
304 strncpy(currentpath, changes_parentandchild, rootlen);
305
306 for (size_t z = rootlen; changes_parentandchild[z] != '\0'; z++)
307 {
308 const char c = changes_parentandchild[z];
309
310 /* Copy up to the next separator. */
311 if (!IsFileSep(c))
312 {
313 currentpath[z] = c;
314 continue;
315 }
316
317 const char path_file_separator = c;
318 currentpath[z] = '\0';
319
320 /* currentpath is complete path for each of the parent directories. */
321
322 if (currentpath[0] == '\0')
323 {
324 /* We are at dir "/" of an absolute path, no need to create. */
325 }
326 /* WARNING: on Windows stat() fails if path has a trailing slash! */
327 else if (stat(currentpath, &statbuf) == -1)
328 {
329 if (!have_promise_info ||
330 MakingChanges(ctx, pp, attr, result,
331 "make directory '%s' for '%s'", currentpath, parentandchild))
332 {
333 mask = umask(0);
334
335 if (mkdir(currentpath, DEFAULTMODE) == -1)
336 {
337 if (errno != EEXIST)
338 {
339 if (have_promise_info)
340 {
341 RecordFailure(ctx, pp, attr, "Failed to make directory: %s (mkdir: %s)",
342 currentpath, GetErrorStr());
343 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
344 }
345 else
346 {
347 Log(LOG_LEVEL_ERR,
348 "Failed to make directory: %s (mkdir: %s)",
349 currentpath, GetErrorStr());
350 }
351 umask(mask);
352 return false;
353 }
354 }
355 else
356 {
357 if (created != NULL)
358 {
359 *created = true;
360 }
361 }
362 umask(mask);
363 }
364 }
365 else
366 {
367 if (!S_ISDIR(statbuf.st_mode))
368 {
369 #ifdef __APPLE__
370 /* Ck if rsrc fork */
371 if (rsrcfork)
372 {
373 tmpstr = xmalloc(CF_BUFSIZE);
374 strlcpy(tmpstr, currentpath, CF_BUFSIZE);
375 strncat(tmpstr, _PATH_FORKSPECIFIER, CF_BUFSIZE);
376
377 /* CFEngine removed terminating slashes */
378 DeleteSlash(tmpstr);
379
380 if (strncmp(tmpstr, pathbuf, CF_BUFSIZE) == 0)
381 {
382 free(tmpstr);
383 return true;
384 }
385 free(tmpstr);
386 }
387 #endif
388 if (have_promise_info)
389 {
390 RecordFailure(ctx, pp, attr,
391 "Cannot make %s - %s is not a directory!",
392 pathbuf, currentpath);
393 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
394 }
395 else
396 {
397 Log(LOG_LEVEL_ERR,
398 "Cannot make %s - %s is not a directory!",
399 pathbuf, currentpath);
400 }
401 return false;
402 }
403 }
404
405 currentpath[z] = path_file_separator;
406 }
407
408 Log(LOG_LEVEL_DEBUG, "Directory for '%s' exists. Okay", parentandchild);
409 return true;
410 }
411
LoadFileAsItemList(Item ** liststart,const char * file,EditDefaults edits)412 bool LoadFileAsItemList(Item **liststart, const char *file, EditDefaults edits)
413 {
414 {
415 struct stat statbuf;
416 if (stat(file, &statbuf) == -1)
417 {
418 Log(LOG_LEVEL_VERBOSE, "The proposed file '%s' could not be loaded. (stat: %s)", file, GetErrorStr());
419 return false;
420 }
421
422 if (edits.maxfilesize != 0 && statbuf.st_size > edits.maxfilesize)
423 {
424 Log(LOG_LEVEL_INFO, "File '%s' is bigger than the edit limit. max_file_size = %jd > %d bytes", file,
425 (intmax_t) statbuf.st_size, edits.maxfilesize);
426 return false;
427 }
428
429 if (!S_ISREG(statbuf.st_mode))
430 {
431 Log(LOG_LEVEL_INFO, "%s is not a plain file", file);
432 return false;
433 }
434 }
435
436 FILE *fp = safe_fopen(file, "rt");
437 if (!fp)
438 {
439 Log(LOG_LEVEL_INFO, "Couldn't read file '%s' for editing. (fopen: %s)", file, GetErrorStr());
440 return false;
441 }
442
443 Buffer *concat = BufferNew();
444
445 size_t line_size = CF_BUFSIZE;
446 char *line = xmalloc(line_size);
447 bool result = true;
448
449 for (;;)
450 {
451 ssize_t num_read = CfReadLine(&line, &line_size, fp);
452 if (num_read == -1)
453 {
454 if (!feof(fp))
455 {
456 Log(LOG_LEVEL_ERR,
457 "Unable to read contents of file: %s (fread: %s)",
458 file, GetErrorStr());
459 result = false;
460 }
461 break;
462 }
463
464 if (edits.joinlines && *(line + strlen(line) - 1) == '\\')
465 {
466 *(line + strlen(line) - 1) = '\0';
467
468 BufferAppend(concat, line, num_read);
469 }
470 else
471 {
472 BufferAppend(concat, line, num_read);
473 if (!feof(fp) || (BufferSize(concat) > 0))
474 {
475 AppendItem(liststart, BufferData(concat), NULL);
476 }
477 }
478
479 BufferClear(concat);
480 }
481
482 free(line);
483 BufferDestroy(concat);
484 fclose(fp);
485 return result;
486 }
487
TraverseDirectoryTreeInternal(const char * base_path,const char * current_path,int (* callback)(const char *,const struct stat *,void *),void * user_data)488 bool TraverseDirectoryTreeInternal(const char *base_path,
489 const char *current_path,
490 int (*callback)(const char *, const struct stat *, void *),
491 void *user_data)
492 {
493 Dir *dirh = DirOpen(base_path);
494 if (!dirh)
495 {
496 if (errno == ENOENT)
497 {
498 return true;
499 }
500
501 Log(LOG_LEVEL_INFO, "Unable to open directory '%s' during traversal of directory tree '%s' (opendir: %s)",
502 current_path, base_path, GetErrorStr());
503 return false;
504 }
505
506 bool failed = false;
507 for (const struct dirent *dirp = DirRead(dirh); dirp != NULL; dirp = DirRead(dirh))
508 {
509 if (!strcmp(dirp->d_name, ".") || !strcmp(dirp->d_name, ".."))
510 {
511 continue;
512 }
513
514 char sub_path[CF_BUFSIZE];
515 snprintf(sub_path, CF_BUFSIZE, "%s" FILE_SEPARATOR_STR "%s", current_path, dirp->d_name);
516
517 struct stat lsb;
518 if (lstat(sub_path, &lsb) == -1)
519 {
520 if (errno == ENOENT)
521 {
522 /* File disappeared on its own */
523 continue;
524 }
525
526 Log(LOG_LEVEL_VERBOSE, "Unable to stat file '%s' during traversal of directory tree '%s' (lstat: %s)",
527 current_path, base_path, GetErrorStr());
528 failed = true;
529 }
530 else
531 {
532 if (S_ISDIR(lsb.st_mode))
533 {
534 if (!TraverseDirectoryTreeInternal(base_path, sub_path, callback, user_data))
535 {
536 failed = true;
537 }
538 }
539 else
540 {
541 if (callback(sub_path, &lsb, user_data) == -1)
542 {
543 failed = true;
544 }
545 }
546 }
547 }
548
549 DirClose(dirh);
550 return !failed;
551 }
552
TraverseDirectoryTree(const char * path,int (* callback)(const char *,const struct stat *,void *),void * user_data)553 bool TraverseDirectoryTree(const char *path,
554 int (*callback)(const char *, const struct stat *, void *),
555 void *user_data)
556 {
557 return TraverseDirectoryTreeInternal(path, path, callback, user_data);
558 }
559
560 typedef struct
561 {
562 unsigned char buffer[1024];
563 const char **extensions_filter;
564 EVP_MD_CTX *crypto_context;
565 unsigned char **digest;
566 } HashDirectoryTreeState;
567
HashDirectoryTreeCallback(const char * filename,ARG_UNUSED const struct stat * sb,void * user_data)568 int HashDirectoryTreeCallback(const char *filename, ARG_UNUSED const struct stat *sb, void *user_data)
569 {
570 HashDirectoryTreeState *state = user_data;
571 bool ignore = true;
572 for (size_t i = 0; state->extensions_filter[i]; i++)
573 {
574 if (StringEndsWith(filename, state->extensions_filter[i]))
575 {
576 ignore = false;
577 break;
578 }
579 }
580
581 if (ignore)
582 {
583 return 0;
584 }
585
586 FILE *file = fopen(filename, "rb");
587 if (!file)
588 {
589 Log(LOG_LEVEL_ERR, "Cannot open file for hashing '%s'. (fopen: %s)", filename, GetErrorStr());
590 return -1;
591 }
592
593 size_t len = 0;
594 char buffer[1024];
595 while ((len = fread(buffer, 1, 1024, file)))
596 {
597 EVP_DigestUpdate(state->crypto_context, state->buffer, len);
598 }
599
600 fclose(file);
601 return 0;
602 }
603
HashDirectoryTree(const char * path,const char ** extensions_filter,EVP_MD_CTX * crypto_context)604 bool HashDirectoryTree(const char *path,
605 const char **extensions_filter,
606 EVP_MD_CTX *crypto_context)
607 {
608 HashDirectoryTreeState state;
609 memset(state.buffer, 0, 1024);
610 state.extensions_filter = extensions_filter;
611 state.crypto_context = crypto_context;
612
613 return TraverseDirectoryTree(path, HashDirectoryTreeCallback, &state);
614 }
615
RotateFiles(const char * name,int number)616 void RotateFiles(const char *name, int number)
617 {
618 int i, fd;
619 struct stat statbuf;
620 char from[CF_BUFSIZE], to[CF_BUFSIZE];
621
622 if (IsItemIn(ROTATED, name))
623 {
624 return;
625 }
626
627 PrependItem(&ROTATED, name, NULL);
628
629 if (stat(name, &statbuf) == -1)
630 {
631 Log(LOG_LEVEL_VERBOSE, "No access to file %s", name);
632 return;
633 }
634
635 for (i = number - 1; i > 0; i--)
636 {
637 snprintf(from, CF_BUFSIZE, "%s.%d", name, i);
638 snprintf(to, CF_BUFSIZE, "%s.%d", name, i + 1);
639
640 if (rename(from, to) == -1)
641 {
642 Log(LOG_LEVEL_DEBUG, "Rename failed in RotateFiles '%s' -> '%s'", name, from);
643 }
644
645 snprintf(from, CF_BUFSIZE, "%s.%d.gz", name, i);
646 snprintf(to, CF_BUFSIZE, "%s.%d.gz", name, i + 1);
647
648 if (rename(from, to) == -1)
649 {
650 Log(LOG_LEVEL_DEBUG, "Rename failed in RotateFiles '%s' -> '%s'", name, from);
651 }
652
653 snprintf(from, CF_BUFSIZE, "%s.%d.Z", name, i);
654 snprintf(to, CF_BUFSIZE, "%s.%d.Z", name, i + 1);
655
656 if (rename(from, to) == -1)
657 {
658 Log(LOG_LEVEL_DEBUG, "Rename failed in RotateFiles '%s' -> '%s'", name, from);
659 }
660
661 snprintf(from, CF_BUFSIZE, "%s.%d.bz", name, i);
662 snprintf(to, CF_BUFSIZE, "%s.%d.bz", name, i + 1);
663
664 if (rename(from, to) == -1)
665 {
666 Log(LOG_LEVEL_DEBUG, "Rename failed in RotateFiles '%s' -> '%s'", name, from);
667 }
668
669 snprintf(from, CF_BUFSIZE, "%s.%d.bz2", name, i);
670 snprintf(to, CF_BUFSIZE, "%s.%d.bz2", name, i + 1);
671
672 if (rename(from, to) == -1)
673 {
674 Log(LOG_LEVEL_DEBUG, "Rename failed in RotateFiles '%s' -> '%s'", name, from);
675 }
676 }
677
678 snprintf(to, CF_BUFSIZE, "%s.1", name);
679
680 if (CopyRegularFileDisk(name, to) == false)
681 {
682 Log(LOG_LEVEL_DEBUG, "Copy failed in RotateFiles '%s' -> '%s'", name, to);
683 return;
684 }
685
686 safe_chmod(to, statbuf.st_mode);
687 if (safe_chown(to, statbuf.st_uid, statbuf.st_gid))
688 {
689 UnexpectedError("Failed to chown %s", to);
690 }
691 safe_chmod(name, 0600); /* File must be writable to empty .. */
692
693 if ((fd = safe_creat(name, statbuf.st_mode)) == -1)
694 {
695 Log(LOG_LEVEL_ERR, "Failed to create new '%s' in disable(rotate). (create: %s)",
696 name, GetErrorStr());
697 }
698 else
699 {
700 if (safe_chown(name, statbuf.st_uid, statbuf.st_gid)) /* NT doesn't have fchown */
701 {
702 UnexpectedError("Failed to chown '%s'", name);
703 }
704 fchmod(fd, statbuf.st_mode);
705 close(fd);
706 }
707 }
708
709 #ifndef __MINGW32__
710
CreateEmptyFile(char * name)711 void CreateEmptyFile(char *name)
712 {
713 if (unlink(name) == -1)
714 {
715 if (errno != ENOENT)
716 {
717 Log(LOG_LEVEL_DEBUG, "Unable to remove existing file '%s'. (unlink: %s)", name, GetErrorStr());
718 }
719 }
720
721 int tempfd = safe_open(name, O_CREAT | O_EXCL | O_WRONLY);
722 if (tempfd < 0)
723 {
724 Log(LOG_LEVEL_ERR, "Couldn't open a file '%s'. (open: %s)", name, GetErrorStr());
725 }
726
727 close(tempfd);
728 }
729
730 #endif
731