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_operators.h>
26 
27 #include <actuator.h>
28 #include <eval_context.h>
29 #include <promises.h>
30 #include <dir.h>
31 #include <dbm_api.h>
32 #include <files_names.h>
33 #include <files_interfaces.h>
34 #include <hash.h>
35 #include <files_copy.h>
36 #include <vars.h>
37 #include <item_lib.h>
38 #include <conversion.h>
39 #include <expand.h>
40 #include <scope.h>
41 #include <matching.h>
42 #include <attributes.h>
43 #include <client_code.h>
44 #include <pipes.h>
45 #include <locks.h>
46 #include <string_lib.h>
47 #include <files_repository.h>
48 #include <files_lib.h>
49 #include <buffer.h>
50 
51 
MoveObstruction(EvalContext * ctx,char * from,const Attributes * attr,const Promise * pp,PromiseResult * result)52 bool MoveObstruction(EvalContext *ctx, char *from, const Attributes *attr, const Promise *pp, PromiseResult *result)
53 {
54     assert(attr != NULL);
55     struct stat sb;
56     char stamp[CF_BUFSIZE], saved[CF_BUFSIZE];
57     time_t now_stamp = time((time_t *) NULL);
58 
59     const char *changes_from = from;
60     if (ChrootChanges())
61     {
62         changes_from = ToChangesChroot(from);
63     }
64 
65     if (lstat(from, &sb) == 0)
66     {
67         if (!attr->move_obstructions)
68         {
69             RecordFailure(ctx, pp, attr, "Object '%s' is obstructing promise", from);
70             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
71             return false;
72         }
73 
74         if (!S_ISDIR(sb.st_mode))
75         {
76             if (!MakingChanges(ctx, pp, attr, result, "move aside object '%s' obstructing promise", from))
77             {
78                 return false;
79             }
80 
81             saved[0] = '\0';
82             strlcpy(saved, changes_from, sizeof(saved));
83 
84             if (attr->copy.backup == BACKUP_OPTION_TIMESTAMP || attr->edits.backup == BACKUP_OPTION_TIMESTAMP)
85             {
86                 snprintf(stamp, CF_BUFSIZE, "_%jd_%s", (intmax_t) CFSTARTTIME, CanonifyName(ctime(&now_stamp)));
87                 strlcat(saved, stamp, sizeof(saved));
88             }
89 
90             strlcat(saved, CF_SAVED, sizeof(saved));
91 
92             if (rename(changes_from, saved) == -1)
93             {
94                 RecordFailure(ctx, pp, attr,
95                               "Can't rename '%s' to '%s'. (rename: %s)", from, saved, GetErrorStr());
96                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
97                 return false;
98             }
99             RecordChange(ctx, pp, attr, "Moved obstructing object '%s' to '%s'", from, saved);
100             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
101 
102             if (ArchiveToRepository(saved, attr))
103             {
104                 RecordChange(ctx, pp, attr, "Archived '%s'", saved);
105                 unlink(saved);
106             }
107 
108             return true;
109         }
110 
111         if (S_ISDIR(sb.st_mode))
112         {
113             if (!MakingChanges(ctx, pp, attr, result, "move aside directory '%s' obstructing", from))
114             {
115                 return false;
116             }
117 
118             saved[0] = '\0';
119             strlcpy(saved, changes_from, sizeof(saved));
120 
121             snprintf(stamp, CF_BUFSIZE, "_%jd_%s", (intmax_t) CFSTARTTIME, CanonifyName(ctime(&now_stamp)));
122             strlcat(saved, stamp, sizeof(saved));
123             strlcat(saved, CF_SAVED, sizeof(saved));
124             strlcat(saved, ".dir", sizeof(saved));
125 
126             if (stat(saved, &sb) != -1)
127             {
128                 RecordFailure(ctx, pp, attr,
129                               "Couldn't move directory '%s' aside, since '%s' exists already",
130                               from, saved);
131                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
132                 return false;
133             }
134 
135             if (rename(changes_from, saved) == -1)
136             {
137                 RecordFailure(ctx, pp, attr, "Can't rename '%s' to '%s'. (rename: %s)",
138                               from, saved, GetErrorStr());
139                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
140                 return false;
141             }
142             RecordChange(ctx, pp, attr, "Moved directory '%s' to '%s%s'", from, from, CF_SAVED);
143             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
144         }
145     }
146 
147     return true;
148 }
149 
150 /*********************************************************************/
151 
SaveAsFile(SaveCallbackFn callback,void * param,const char * file,const Attributes * a,NewLineMode new_line_mode)152 bool SaveAsFile(SaveCallbackFn callback, void *param, const char *file, const Attributes *a, NewLineMode new_line_mode)
153 {
154     assert(a != NULL);
155     struct stat statbuf;
156     char new[CF_BUFSIZE], backup[CF_BUFSIZE];
157     char stamp[CF_BUFSIZE];
158     time_t stamp_now;
159     Buffer *deref_file = BufferNewFrom(file, strlen(file));
160     Buffer *pretty_file = BufferNew();
161     bool ret = false;
162 
163     BufferPrintf(pretty_file, "'%s'", file);
164 
165     stamp_now = time((time_t *) NULL);
166 
167     while (1)
168     {
169         if (lstat(BufferData(deref_file), &statbuf) == -1)
170         {
171             Log(LOG_LEVEL_ERR, "Can no longer access file %s, which needed editing. (lstat: %s)", BufferData(pretty_file), GetErrorStr());
172             goto end;
173         }
174 #ifndef __MINGW32__
175         if (S_ISLNK(statbuf.st_mode))
176         {
177             char buf[statbuf.st_size + 1];
178             // Careful. readlink() doesn't add '\0' byte.
179             ssize_t linksize = readlink(BufferData(deref_file), buf, statbuf.st_size);
180             if (linksize == 0)
181             {
182                 Log(LOG_LEVEL_WARNING, "readlink() failed with 0 bytes. Should not happen (bug?).");
183                 goto end;
184             }
185             else if (linksize < 0)
186             {
187                 Log(LOG_LEVEL_ERR, "Could not read link %s. (readlink: %s)", BufferData(pretty_file), GetErrorStr());
188                 goto end;
189             }
190             buf[linksize] = '\0';
191             if (!IsAbsPath(buf))
192             {
193                 char dir[BufferSize(deref_file) + 1];
194                 strcpy(dir, BufferData(deref_file));
195                 ChopLastNode(dir);
196                 BufferPrintf(deref_file, "%s/%s", dir, buf);
197             }
198             else
199             {
200                 BufferSet(deref_file, buf, linksize);
201             }
202             BufferPrintf(pretty_file, "'%s' (from symlink '%s')", BufferData(deref_file), file);
203         }
204         else
205 #endif
206         {
207             break;
208         }
209     }
210 
211     strcpy(backup, BufferData(deref_file));
212 
213     if (a->edits.backup == BACKUP_OPTION_TIMESTAMP)
214     {
215         snprintf(stamp, CF_BUFSIZE, "_%jd_%s", (intmax_t) CFSTARTTIME, CanonifyName(ctime(&stamp_now)));
216         strcat(backup, stamp);
217     }
218 
219     strcat(backup, ".cf-before-edit");
220 
221     strcpy(new, BufferData(deref_file));
222     strcat(new, ".cf-after-edit");
223     unlink(new);                /* Just in case of races */
224 
225     if ((*callback)(new, param, new_line_mode) == false)
226     {
227         goto end;
228     }
229 
230     if (!CopyFilePermissionsDisk(BufferData(deref_file), new))
231     {
232         Log(LOG_LEVEL_ERR, "Can't copy file permissions from %s to '%s' - so promised edits could not be moved into place.",
233             BufferData(pretty_file), new);
234         goto end;
235     }
236 
237     unlink(backup);
238 #ifndef __MINGW32__
239     if (link(BufferData(deref_file), backup) == -1)
240     {
241         Log(LOG_LEVEL_VERBOSE, "Can't link %s to '%s' - falling back to copy. (link: %s)",
242             BufferData(pretty_file), backup, GetErrorStr());
243 #else
244     /* No hardlinks on Windows, go straight to copying */
245     {
246 #endif
247         if (!CopyRegularFileDisk(BufferData(deref_file), backup))
248         {
249             Log(LOG_LEVEL_ERR, "Can't copy %s to '%s' - so promised edits could not be moved into place.",
250                 BufferData(pretty_file), backup);
251             goto end;
252         }
253         if (!CopyFilePermissionsDisk(BufferData(deref_file), backup))
254         {
255             Log(LOG_LEVEL_ERR, "Can't copy permissions %s to '%s' - so promised edits could not be moved into place.",
256                 BufferData(pretty_file), backup);
257             goto end;
258         }
259     }
260 
261     if (a->edits.backup == BACKUP_OPTION_ROTATE)
262     {
263         RotateFiles(backup, a->edits.rotate);
264         unlink(backup);
265     }
266 
267     if (a->edits.backup != BACKUP_OPTION_NO_BACKUP)
268     {
269         if (ArchiveToRepository(backup, a))
270         {
271             unlink(backup);
272         }
273     }
274 
275     else
276     {
277         unlink(backup);
278     }
279 
280     if (rename(new, BufferData(deref_file)) == -1)
281     {
282         Log(LOG_LEVEL_ERR, "Can't rename '%s' to %s - so promised edits could not be moved into place. (rename: %s)",
283             new, BufferData(pretty_file), GetErrorStr());
284         goto end;
285     }
286 
287     ret = true;
288 
289 end:
290     BufferDestroy(pretty_file);
291     BufferDestroy(deref_file);
292     return ret;
293 }
294 
295 /*********************************************************************/
296 
297 static bool SaveItemListCallback(const char *dest_filename, void *param, NewLineMode new_line_mode)
298 {
299     Item *liststart = param, *ip;
300 
301     //saving list to file
302     FILE *fp = safe_fopen(
303         dest_filename, (new_line_mode == NewLineMode_Native) ? "wt" : "w");
304     if (fp == NULL)
305     {
306         Log(LOG_LEVEL_ERR, "Unable to open destination file '%s' for writing. (fopen: %s)",
307             dest_filename, GetErrorStr());
308         return false;
309     }
310 
311     for (ip = liststart; ip != NULL; ip = ip->next)
312     {
313         if (fprintf(fp, "%s\n", ip->name) < 0)
314         {
315             Log(LOG_LEVEL_ERR, "Unable to write into destination file '%s'. (fprintf: %s)",
316                 dest_filename, GetErrorStr());
317             fclose(fp);
318             return false;
319         }
320     }
321 
322     if (fclose(fp) == -1)
323     {
324         Log(LOG_LEVEL_ERR, "Unable to close file '%s' after writing. (fclose: %s)",
325             dest_filename, GetErrorStr());
326         return false;
327     }
328 
329     return true;
330 }
331 
332 /*********************************************************************/
333 
334 bool SaveItemListAsFile(Item *liststart, const char *file, const Attributes *a, NewLineMode new_line_mode)
335 {
336     assert(a != NULL);
337     return SaveAsFile(&SaveItemListCallback, liststart, file, a, new_line_mode);
338 }
339 
340 // Some complex logic here to enable warnings of diffs to be given
341 
342 static Item *NextItem(const Item *ip)
343 {
344     if (ip)
345     {
346         return ip->next;
347     }
348     else
349     {
350         return NULL;
351     }
352 }
353 
354 static bool ItemListsEqual(EvalContext *ctx, const Item *list1, const Item *list2, int warnings,
355                            const Attributes *a, const Promise *pp, PromiseResult *result)
356 {
357     assert(a != NULL);
358     bool retval = true;
359 
360     const Item *ip1 = list1;
361     const Item *ip2 = list2;
362 
363     while (true)
364     {
365         if ((ip1 == NULL) && (ip2 == NULL))
366         {
367             return retval;
368         }
369 
370         if ((ip1 == NULL) || (ip2 == NULL))
371         {
372             if (warnings)
373             {
374                 if ((ip1 == list1) || (ip2 == list2))
375                 {
376                     cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, "File content wants to change from from/to full/empty but only a warning promised");
377                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
378                 }
379                 else
380                 {
381                     if (ip1 != NULL)
382                     {
383                         cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, " ! edit_line change warning promised: (remove) %s",
384                              ip1->name);
385                         *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
386                     }
387 
388                     if (ip2 != NULL)
389                     {
390                         cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, " ! edit_line change warning promised: (add) %s", ip2->name);
391                         *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
392                     }
393                 }
394             }
395 
396             if (warnings)
397             {
398                 if (ip1 || ip2)
399                 {
400                     retval = false;
401                     ip1 = NextItem(ip1);
402                     ip2 = NextItem(ip2);
403                     continue;
404                 }
405             }
406 
407             return false;
408         }
409 
410         if (strcmp(ip1->name, ip2->name) != 0)
411         {
412             if (!warnings)
413             {
414                 // No need to wait
415                 return false;
416             }
417             else
418             {
419                 // If we want to see warnings, we need to scan the whole file
420 
421                 cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, "edit_line warning promised: - %s", ip1->name);
422                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
423                 retval = false;
424             }
425         }
426 
427         ip1 = NextItem(ip1);
428         ip2 = NextItem(ip2);
429     }
430 
431     return retval;
432 }
433 
434 /* returns true if file on disk is identical to file in memory */
435 
436 bool CompareToFile(
437     EvalContext *ctx,
438     const Item *liststart,
439     const char *file,
440     const Attributes *a,
441     const Promise *pp,
442     PromiseResult *result)
443 {
444     assert(a != NULL);
445     struct stat statbuf;
446     Item *cmplist = NULL;
447 
448     if (stat(file, &statbuf) == -1)
449     {
450         return false;
451     }
452 
453     if ((liststart == NULL) && (statbuf.st_size == 0))
454     {
455         return true;
456     }
457 
458     if (liststart == NULL)
459     {
460         return false;
461     }
462 
463     if (!LoadFileAsItemList(&cmplist, file, a->edits))
464     {
465         return false;
466     }
467 
468     if (!ItemListsEqual(ctx, cmplist, liststart, (a->transaction.action == cfa_warn), a, pp, result))
469     {
470         DeleteItemList(cmplist);
471         return false;
472     }
473 
474     DeleteItemList(cmplist);
475     return (true);
476 }
477