chiark / gitweb /
import: add a simple scheme for validating the SHA256 sums of downloaded raw files
[elogind.git] / src / import / import-raw.c
1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3 /***
4   This file is part of systemd.
5
6   Copyright 2014 Lennart Poettering
7
8   systemd is free software; you can redistribute it and/or modify it
9   under the terms of the GNU Lesser General Public License as published by
10   the Free Software Foundation; either version 2.1 of the License, or
11   (at your option) any later version.
12
13   systemd is distributed in the hope that it will be useful, but
14   WITHOUT ANY WARRANTY; without even the implied warranty of
15   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16   Lesser General Public License for more details.
17
18   You should have received a copy of the GNU Lesser General Public License
19   along with systemd; If not, see <http://www.gnu.org/licenses/>.
20 ***/
21
22 #include <sys/xattr.h>
23 #include <linux/fs.h>
24 #include <curl/curl.h>
25 #include <gcrypt.h>
26
27 #include "utf8.h"
28 #include "strv.h"
29 #include "copy.h"
30 #include "btrfs-util.h"
31 #include "util.h"
32 #include "macro.h"
33 #include "mkdir.h"
34 #include "curl-util.h"
35 #include "qcow2-util.h"
36 #include "import-job.h"
37 #include "import-util.h"
38 #include "import-raw.h"
39
40 typedef struct RawImportFile RawImportFile;
41
42 struct RawImport {
43         sd_event *event;
44         CurlGlue *glue;
45
46         char *image_root;
47
48         ImportJob *raw_job;
49         ImportJob *sha256sums_job;
50
51         RawImportFinished on_finished;
52         void *userdata;
53
54         char *local;
55         bool force_local;
56
57         char *temp_path;
58         char *final_path;
59 };
60
61 RawImport* raw_import_unref(RawImport *i) {
62         if (!i)
63                 return NULL;
64
65         import_job_unref(i->raw_job);
66
67         curl_glue_unref(i->glue);
68         sd_event_unref(i->event);
69
70         if (i->temp_path) {
71                 (void) unlink(i->temp_path);
72                 free(i->temp_path);
73         }
74
75         free(i->final_path);
76         free(i->image_root);
77         free(i->local);
78         free(i);
79
80         return NULL;
81 }
82
83 int raw_import_new(RawImport **ret, sd_event *event, const char *image_root, RawImportFinished on_finished, void *userdata) {
84         _cleanup_(raw_import_unrefp) RawImport *i = NULL;
85         int r;
86
87         assert(ret);
88
89         i = new0(RawImport, 1);
90         if (!i)
91                 return -ENOMEM;
92
93         i->on_finished = on_finished;
94         i->userdata = userdata;
95
96         i->image_root = strdup(image_root ?: "/var/lib/machines");
97         if (!i->image_root)
98                 return -ENOMEM;
99
100         if (event)
101                 i->event = sd_event_ref(event);
102         else {
103                 r = sd_event_default(&i->event);
104                 if (r < 0)
105                         return r;
106         }
107
108         r = curl_glue_new(&i->glue, i->event);
109         if (r < 0)
110                 return r;
111
112         i->glue->on_finished = import_job_curl_on_finished;
113         i->glue->userdata = i;
114
115         *ret = i;
116         i = NULL;
117
118         return 0;
119 }
120
121 static int raw_import_maybe_convert_qcow2(RawImport *i) {
122         _cleanup_close_ int converted_fd = -1;
123         _cleanup_free_ char *t = NULL;
124         int r;
125
126         assert(i);
127         assert(i->raw_job);
128
129         r = qcow2_detect(i->raw_job->disk_fd);
130         if (r < 0)
131                 return log_error_errno(r, "Failed to detect whether this is a QCOW2 image: %m");
132         if (r == 0)
133                 return 0;
134
135         /* This is a QCOW2 image, let's convert it */
136         r = tempfn_random(i->final_path, &t);
137         if (r < 0)
138                 return log_oom();
139
140         converted_fd = open(t, O_RDWR|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0644);
141         if (converted_fd < 0)
142                 return log_error_errno(errno, "Failed to create %s: %m", t);
143
144         r = chattr_fd(converted_fd, true, FS_NOCOW_FL);
145         if (r < 0)
146                 log_warning_errno(errno, "Failed to set file attributes on %s: %m", t);
147
148         log_info("Unpacking QCOW2 file.");
149
150         r = qcow2_convert(i->raw_job->disk_fd, converted_fd);
151         if (r < 0) {
152                 unlink(t);
153                 return log_error_errno(r, "Failed to convert qcow2 image: %m");
154         }
155
156         unlink(i->temp_path);
157         free(i->temp_path);
158
159         i->temp_path = t;
160         t = NULL;
161
162         safe_close(i->raw_job->disk_fd);
163         i->raw_job->disk_fd = converted_fd;
164         converted_fd = -1;
165
166         return 1;
167 }
168
169 static int raw_import_make_local_copy(RawImport *i) {
170         _cleanup_free_ char *tp = NULL;
171         _cleanup_close_ int dfd = -1;
172         const char *p;
173         int r;
174
175         assert(i);
176         assert(i->raw_job);
177
178         if (!i->local)
179                 return 0;
180
181         if (i->raw_job->etag_exists) {
182                 /* We have downloaded this one previously, reopen it */
183
184                 assert(i->raw_job->disk_fd < 0);
185
186                 if (!i->final_path) {
187                         r = import_make_path(i->raw_job->url, i->raw_job->etag, i->image_root, ".raw-", ".raw", &i->final_path);
188                         if (r < 0)
189                                 return log_oom();
190                 }
191
192                 i->raw_job->disk_fd = open(i->final_path, O_RDONLY|O_NOCTTY|O_CLOEXEC);
193                 if (i->raw_job->disk_fd < 0)
194                         return log_error_errno(errno, "Failed to open vendor image: %m");
195         } else {
196                 /* We freshly downloaded the image, use it */
197
198                 assert(i->raw_job->disk_fd >= 0);
199
200                 if (lseek(i->raw_job->disk_fd, SEEK_SET, 0) == (off_t) -1)
201                         return log_error_errno(errno, "Failed to seek to beginning of vendor image: %m");
202         }
203
204         p = strappenda(i->image_root, "/", i->local, ".raw");
205
206         if (i->force_local) {
207                 (void) btrfs_subvol_remove(p);
208                 (void) rm_rf_dangerous(p, false, true, false);
209         }
210
211         r = tempfn_random(p, &tp);
212         if (r < 0)
213                 return log_oom();
214
215         dfd = open(tp, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0664);
216         if (dfd < 0)
217                 return log_error_errno(errno, "Failed to create writable copy of image: %m");
218
219         /* Turn off COW writing. This should greatly improve
220          * performance on COW file systems like btrfs, since it
221          * reduces fragmentation caused by not allowing in-place
222          * writes. */
223         r = chattr_fd(dfd, true, FS_NOCOW_FL);
224         if (r < 0)
225                 log_warning_errno(errno, "Failed to set file attributes on %s: %m", tp);
226
227         r = copy_bytes(i->raw_job->disk_fd, dfd, (off_t) -1, true);
228         if (r < 0) {
229                 unlink(tp);
230                 return log_error_errno(r, "Failed to make writable copy of image: %m");
231         }
232
233         (void) copy_times(i->raw_job->disk_fd, dfd);
234         (void) copy_xattr(i->raw_job->disk_fd, dfd);
235
236         dfd = safe_close(dfd);
237
238         r = rename(tp, p);
239         if (r < 0)  {
240                 unlink(tp);
241                 return log_error_errno(errno, "Failed to move writable image into place: %m");
242         }
243
244         log_info("Created new local image '%s'.", i->local);
245         return 0;
246 }
247
248 static int raw_import_verify_sha256sum(RawImport *i) {
249         _cleanup_free_ char *fn = NULL;
250         const char *p, *line;
251         int r;
252
253         assert(i);
254
255         assert(i->raw_job);
256         assert(i->raw_job->sha256);
257
258         assert(i->sha256sums_job);
259         assert(i->sha256sums_job->payload);
260         assert(i->sha256sums_job->payload_size > 0);
261
262         r = import_url_last_component(i->raw_job->url, &fn);
263         if (r < 0)
264                 return log_oom();
265
266         if (!filename_is_valid(fn)) {
267                 log_error("Cannot verify checksum, could not determine valid server-side file name.");
268                 return -EBADMSG;
269         }
270
271         line = strappenda(i->raw_job->sha256, " *", fn, "\n");
272
273         p = memmem(i->sha256sums_job->payload,
274                    i->sha256sums_job->payload_size,
275                    line,
276                    strlen(line));
277
278         if (!p || (p != (char*) i->sha256sums_job->payload && p[-1] != '\n')) {
279                 log_error("Checksum did not check out, payload has been tempered with.");
280                 return -EBADMSG;
281         }
282
283         log_info("SHA256 checksum of %s is valid.", i->raw_job->url);
284
285         return 0;
286 }
287
288 static int raw_import_finalize(RawImport *i) {
289         int r;
290
291         assert(i);
292
293         if (!IMPORT_JOB_STATE_IS_COMPLETE(i->raw_job) ||
294             !IMPORT_JOB_STATE_IS_COMPLETE(i->sha256sums_job))
295                 return 0;
296
297         if (!i->raw_job->etag_exists) {
298                 assert(i->temp_path);
299                 assert(i->final_path);
300                 assert(i->raw_job->disk_fd >= 0);
301
302                 r = raw_import_verify_sha256sum(i);
303                 if (r < 0)
304                         return r;
305
306                 r = rename(i->temp_path, i->final_path);
307                 if (r < 0)
308                         return log_error_errno(errno, "Failed to move RAW file into place: %m");
309
310                 free(i->temp_path);
311                 i->temp_path = NULL;
312         }
313
314         r = raw_import_make_local_copy(i);
315         if (r < 0)
316                 return r;
317
318         i->raw_job->disk_fd = safe_close(i->raw_job->disk_fd);
319
320         return 1;
321 }
322
323 static void raw_import_invoke_finished(RawImport *i, int r) {
324         assert(i);
325
326         if (i->on_finished)
327                 i->on_finished(i, r, i->userdata);
328         else
329                 sd_event_exit(i->event, r);
330 }
331
332 static void raw_import_raw_job_on_finished(ImportJob *j) {
333         RawImport *i;
334         int r;
335
336         assert(j);
337         assert(j->userdata);
338
339         i = j->userdata;
340         if (j->error != 0) {
341                 r = j->error;
342                 goto finish;
343         }
344
345         /* This is invoked if either the download completed
346          * successfully, or the download was skipped because we
347          * already have the etag. In this case ->etag_exists is
348          * true. */
349
350         if (!j->etag_exists) {
351                 assert(j->disk_fd >= 0);
352
353                 r = raw_import_maybe_convert_qcow2(i);
354                 if (r < 0)
355                         goto finish;
356
357                 r = import_make_read_only_fd(j->disk_fd);
358                 if (r < 0)
359                         goto finish;
360         }
361
362         r = raw_import_finalize(i);
363         if (r < 0)
364                 goto finish;
365         if (r == 0)
366                 return;
367
368         r = 0;
369
370 finish:
371         raw_import_invoke_finished(i, r);
372 }
373
374 static void raw_import_sha256sums_job_on_finished(ImportJob *j) {
375         RawImport *i;
376         int r;
377
378         assert(j);
379         assert(j->userdata);
380
381         i = j->userdata;
382         if (j->error != 0) {
383                 r = j->error;
384                 goto finish;
385         }
386
387         r = raw_import_finalize(i);
388         if (r < 0)
389                 goto finish;
390         if (r == 0)
391                 return;
392
393         r = 0;
394 finish:
395         raw_import_invoke_finished(i, r);
396 }
397
398 static int raw_import_raw_job_on_open_disk(ImportJob *j) {
399         RawImport *i;
400         int r;
401
402         assert(j);
403         assert(j->userdata);
404
405         i = j->userdata;
406
407         r = import_make_path(j->url, j->etag, i->image_root, ".raw-", ".raw", &i->final_path);
408         if (r < 0)
409                 return log_oom();
410
411         r = tempfn_random(i->final_path, &i->temp_path);
412         if (r <0)
413                 return log_oom();
414
415         mkdir_parents_label(i->temp_path, 0700);
416
417         j->disk_fd = open(i->temp_path, O_RDWR|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0644);
418         if (j->disk_fd < 0)
419                 return log_error_errno(errno, "Failed to create %s: %m", i->temp_path);
420
421         r = chattr_fd(j->disk_fd, true, FS_NOCOW_FL);
422         if (r < 0)
423                 log_warning_errno(errno, "Failed to set file attributes on %s: %m", i->temp_path);
424
425         return 0;
426 }
427
428 int raw_import_pull(RawImport *i, const char *url, const char *local, bool force_local) {
429         _cleanup_free_ char *sha256sums_url = NULL;
430         int r;
431
432         assert(i);
433
434         if (i->raw_job)
435                 return -EBUSY;
436
437         if (!http_url_is_valid(url))
438                 return -EINVAL;
439
440         if (local && !machine_name_is_valid(local))
441                 return -EINVAL;
442
443         r = free_and_strdup(&i->local, local);
444         if (r < 0)
445                 return r;
446         i->force_local = force_local;
447
448         /* Queue job for the image itself */
449         r = import_job_new(&i->raw_job, url, i->glue, i);
450         if (r < 0)
451                 return r;
452
453         i->raw_job->on_finished = raw_import_raw_job_on_finished;
454         i->raw_job->on_open_disk = raw_import_raw_job_on_open_disk;
455         i->raw_job->calc_hash = true;
456
457         r = import_find_old_etags(url, i->image_root, DT_REG, ".raw-", ".raw", &i->raw_job->old_etags);
458         if (r < 0)
459                 return r;
460
461         /* Queue job for the SHA256SUMS file for the image */
462         r = import_url_change_last_component(url, "SHA256SUMS", &sha256sums_url);
463         if (r < 0)
464                 return r;
465
466         r = import_job_new(&i->sha256sums_job, sha256sums_url, i->glue, i);
467         if (r < 0)
468                 return r;
469
470         i->sha256sums_job->on_finished = raw_import_sha256sums_job_on_finished;
471         i->sha256sums_job->uncompressed_max = i->sha256sums_job->compressed_max = 1ULL * 1024ULL * 1024ULL;
472
473         r = import_job_begin(i->raw_job);
474         if (r < 0)
475                 return r;
476
477         r = import_job_begin(i->sha256sums_job);
478         if (r < 0)
479                 return r;
480
481         return 0;
482 }