chiark / gitweb /
import: beef up gpt importer to optionally make writable copy of read-only vendor...
[elogind.git] / src / import / import-gpt.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 <curl/curl.h>
24
25 #include "hashmap.h"
26 #include "utf8.h"
27 #include "curl-util.h"
28 #include "import-gpt.h"
29 #include "strv.h"
30 #include "copy.h"
31
32 typedef struct GptImportFile GptImportFile;
33
34 struct GptImportFile {
35         GptImport *import;
36
37         char *url;
38         char *local;
39
40         CURL *curl;
41         struct curl_slist *request_header;
42
43         char *temp_path;
44         char *final_path;
45         char *etag;
46         char **old_etags;
47
48         uint64_t content_length;
49         uint64_t written;
50
51         usec_t mtime;
52
53         bool force_local;
54         bool done;
55
56         int disk_fd;
57 };
58
59 struct GptImport {
60         sd_event *event;
61         CurlGlue *glue;
62
63         Hashmap *files;
64
65         gpt_import_on_finished on_finished;
66         void *userdata;
67
68         bool finished;
69 };
70
71 #define FILENAME_ESCAPE "/.#\"\'"
72
73 static GptImportFile *gpt_import_file_unref(GptImportFile *f) {
74         if (!f)
75                 return NULL;
76
77         if (f->import)
78                 curl_glue_remove_and_free(f->import->glue, f->curl);
79         curl_slist_free_all(f->request_header);
80
81         safe_close(f->disk_fd);
82
83         free(f->final_path);
84
85         if (f->temp_path) {
86                 unlink(f->temp_path);
87                 free(f->temp_path);
88         }
89
90         free(f->url);
91         free(f->local);
92         free(f->etag);
93         strv_free(f->old_etags);
94         free(f);
95
96         return NULL;
97 }
98
99 DEFINE_TRIVIAL_CLEANUP_FUNC(GptImportFile*, gpt_import_file_unref);
100
101 static void gpt_import_finish(GptImport *import, int error) {
102         assert(import);
103
104         if (import->finished)
105                 return;
106
107         import->finished = true;
108
109         if (import->on_finished)
110                 import->on_finished(import, error, import->userdata);
111         else
112                 sd_event_exit(import->event, error);
113 }
114
115 static int gpt_import_file_make_final_path(GptImportFile *f) {
116         _cleanup_free_ char *escaped_url = NULL, *escaped_etag = NULL;
117
118         assert(f);
119
120         if (f->final_path)
121                 return 0;
122
123         escaped_url = xescape(f->url, FILENAME_ESCAPE);
124         if (!escaped_url)
125                 return -ENOMEM;
126
127         if (f->etag) {
128                 escaped_etag = xescape(f->etag, FILENAME_ESCAPE);
129                 if (!escaped_etag)
130                         return -ENOMEM;
131
132                 f->final_path = strjoin("/var/lib/container/.gpt-", escaped_url, ".", escaped_etag, ".gpt", NULL);
133         } else
134                 f->final_path = strjoin("/var/lib/container/.gpt-", escaped_url, ".gpt", NULL);
135         if (!f->final_path)
136                 return -ENOMEM;
137
138         return 0;
139 }
140
141 static void gpt_import_file_success(GptImportFile *f) {
142         int r;
143
144         assert(f);
145
146         f->done = true;
147
148         if (f->local) {
149                 _cleanup_free_ char *tp = NULL;
150                 _cleanup_close_ int dfd = -1;
151                 const char *p;
152
153                 if (f->disk_fd >= 0) {
154                         if (lseek(f->disk_fd, SEEK_SET, 0) == (off_t) -1) {
155                                 r = log_error_errno(errno, "Failed to seek to beginning of vendor image: %m");
156                                 goto finish;
157                         }
158                 } else {
159                         r = gpt_import_file_make_final_path(f);
160                         if (r < 0) {
161                                 log_oom();
162                                 goto finish;
163                         }
164
165                         f->disk_fd = open(f->final_path, O_RDONLY|O_NOCTTY|O_CLOEXEC);
166                         if (f->disk_fd < 0) {
167                                 r = log_error_errno(errno, "Failed top open vendor image: %m");
168                                 goto finish;
169                         }
170                 }
171
172                 p = strappenda("/var/lib/container/", f->local, ".gpt");
173                 if (f->force_local)
174                         (void) rm_rf_dangerous(p, false, true, false);
175
176                 r = tempfn_random(p, &tp);
177                 if (r < 0) {
178                         log_oom();
179                         goto finish;
180                 }
181
182                 dfd = open(tp, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0664);
183                 if (dfd < 0) {
184                         r = log_error_errno(errno, "Failed to create writable copy of image: %m");
185                         goto finish;
186                 }
187
188                 r = copy_bytes(f->disk_fd, dfd, (off_t) -1, true);
189                 if (r < 0) {
190                         log_error_errno(r, "Failed to make writable copy of image: %m");
191                         unlink(tp);
192                         goto finish;
193                 }
194
195                 (void) copy_times(f->disk_fd, dfd);
196                 (void) copy_xattr(f->disk_fd, dfd);
197
198                 dfd = safe_close(dfd);
199
200                 r = rename(tp, p);
201                 if (r < 0) {
202                         r = log_error_errno(errno, "Failed to move writable image into place: %m");
203                         unlink(tp);
204                         goto finish;
205                 }
206
207                 log_info("Created new local image %s.", p);
208         }
209
210         f->disk_fd = safe_close(f->disk_fd);
211         r = 0;
212
213 finish:
214         gpt_import_finish(f->import, r);
215 }
216
217 static void gpt_import_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) {
218         GptImportFile *f = NULL;
219         struct stat st;
220         CURLcode code;
221         long status;
222         int r;
223
224         if (curl_easy_getinfo(curl, CURLINFO_PRIVATE, &f) != CURLE_OK)
225                 return;
226
227         if (!f || f->done)
228                 return;
229
230         f->done = true;
231
232         if (result != CURLE_OK) {
233                 log_error("Transfer failed: %s", curl_easy_strerror(result));
234                 r = -EIO;
235                 goto fail;
236         }
237
238         code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
239         if (code != CURLE_OK) {
240                 log_error("Failed to retrieve response code: %s", curl_easy_strerror(code));
241                 r = -EIO;
242                 goto fail;
243         } else if (status == 304) {
244                 log_info("Image already downloaded. Skipping download.");
245                 gpt_import_file_success(f);
246                 return;
247         } else if (status >= 300) {
248                 log_error("HTTP request to %s failed with code %li.", f->url, status);
249                 r = -EIO;
250                 goto fail;
251         } else if (status < 200) {
252                 log_error("HTTP request to %s finished with unexpected code %li.", f->url, status);
253                 r = -EIO;
254                 goto fail;
255         }
256
257         if (f->disk_fd < 0) {
258                 log_error("No data received.");
259                 r = -EIO;
260                 goto fail;
261         }
262
263         if (f->content_length != (uint64_t) -1 &&
264             f->content_length != f->written) {
265                 log_error("Download truncated.");
266                 r = -EIO;
267                 goto fail;
268         }
269
270         if (f->etag)
271                 (void) fsetxattr(f->disk_fd, "user.source_etag", f->etag, strlen(f->etag), 0);
272         if (f->url)
273                 (void) fsetxattr(f->disk_fd, "user.source_url", f->url, strlen(f->url), 0);
274
275         if (f->mtime != 0) {
276                 struct timespec ut[2];
277
278                 timespec_store(&ut[0], f->mtime);
279                 ut[1] = ut[0];
280                 (void) futimens(f->disk_fd, ut);
281
282                 fd_setcrtime(f->disk_fd, f->mtime);
283         }
284
285         if (fstat(f->disk_fd, &st) < 0) {
286                 r = log_error_errno(errno, "Failed to stat file: %m");
287                 goto fail;
288         }
289
290         /* Mark read-only */
291         (void) fchmod(f->disk_fd, st.st_mode & 07444);
292
293         assert(f->temp_path);
294         assert(f->final_path);
295
296         r = rename(f->temp_path, f->final_path);
297         if (r < 0) {
298                 r = log_error_errno(errno, "Failed to move GPT file into place: %m");
299                 goto fail;
300         }
301
302         free(f->temp_path);
303         f->temp_path = NULL;
304
305         log_info("Completed writing vendor image %s.", f->final_path);
306
307         gpt_import_file_success(f);
308         return;
309
310 fail:
311         gpt_import_finish(f->import, r);
312 }
313
314 static int gpt_import_file_open_disk_for_write(GptImportFile *f) {
315         int r;
316
317         assert(f);
318
319         if (f->disk_fd >= 0)
320                 return 0;
321
322         r = gpt_import_file_make_final_path(f);
323         if (r < 0)
324                 return log_oom();
325
326         if (!f->temp_path) {
327                 r = tempfn_random(f->final_path, &f->temp_path);
328                 if (r < 0)
329                         return log_oom();
330         }
331
332         f->disk_fd = open(f->temp_path, O_RDWR|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0644);
333         if (f->disk_fd < 0)
334                 return log_error_errno(errno, "Failed to create %s: %m", f->temp_path);
335
336         return 0;
337 }
338
339 static size_t gpt_import_file_write_callback(void *contents, size_t size, size_t nmemb, void *userdata) {
340         GptImportFile *f = userdata;
341         size_t sz = size * nmemb;
342         ssize_t n;
343         int r;
344
345         assert(contents);
346         assert(f);
347
348         if (f->done) {
349                 r = -ESTALE;
350                 goto fail;
351         }
352
353         r = gpt_import_file_open_disk_for_write(f);
354         if (r < 0)
355                 goto fail;
356
357         if (f->written + sz < f->written) {
358                 log_error("File too large, overflow");
359                 r = -EOVERFLOW;
360                 goto fail;
361         }
362
363         if (f->content_length != (uint64_t) -1 &&
364             f->written + sz > f->content_length) {
365                 log_error("Content length incorrect.");
366                 r = -EFBIG;
367                 goto fail;
368         }
369
370         n = write(f->disk_fd, contents, sz);
371         if (n < 0) {
372                 log_error_errno(errno, "Failed to write file: %m");
373                 goto fail;
374         }
375
376         if ((size_t) n < sz) {
377                 log_error("Short write");
378                 r = -EIO;
379                 goto fail;
380         }
381
382         f->written += sz;
383
384         return sz;
385
386 fail:
387         gpt_import_finish(f->import, r);
388         return 0;
389 }
390
391 static size_t gpt_import_file_header_callback(void *contents, size_t size, size_t nmemb, void *userdata) {
392         GptImportFile *f = userdata;
393         size_t sz = size * nmemb;
394         _cleanup_free_ char *length = NULL, *last_modified = NULL;
395         char *etag;
396         int r;
397
398         assert(contents);
399         assert(f);
400
401         if (f->done) {
402                 r = -ESTALE;
403                 goto fail;
404         }
405
406         r = curl_header_strdup(contents, sz, "ETag:", &etag);
407         if (r < 0) {
408                 log_oom();
409                 goto fail;
410         }
411         if (r > 0) {
412                 free(f->etag);
413                 f->etag = etag;
414
415                 if (strv_contains(f->old_etags, f->etag)) {
416                         log_info("Image already downloaded. Skipping download.");
417                         gpt_import_file_success(f);
418                         return sz;
419                 }
420
421                 return sz;
422         }
423
424         r = curl_header_strdup(contents, sz, "Content-Length:", &length);
425         if (r < 0) {
426                 log_oom();
427                 goto fail;
428         }
429         if (r > 0) {
430                 (void) safe_atou64(length, &f->content_length);
431                 return sz;
432         }
433
434         r = curl_header_strdup(contents, sz, "Last-Modified:", &last_modified);
435         if (r < 0) {
436                 log_oom();
437                 goto fail;
438         }
439         if (r > 0) {
440                 (void) curl_parse_http_time(last_modified, &f->mtime);
441                 return sz;
442         }
443
444         return sz;
445
446 fail:
447         gpt_import_finish(f->import, r);
448         return 0;
449 }
450
451 static bool etag_is_valid(const char *etag) {
452
453         if (!endswith(etag, "\""))
454                 return false;
455
456         if (!startswith(etag, "\"") && !startswith(etag, "W/\""))
457                 return false;
458
459         return true;
460 }
461
462 static int gpt_import_file_find_old_etags(GptImportFile *f) {
463         _cleanup_free_ char *escaped_url = NULL;
464         _cleanup_closedir_ DIR *d = NULL;
465         struct dirent *de;
466         int r;
467
468         escaped_url = xescape(f->url, FILENAME_ESCAPE);
469         if (!escaped_url)
470                 return -ENOMEM;
471
472         d = opendir("/var/lib/container/");
473         if (!d) {
474                 if (errno == ENOENT)
475                         return 0;
476
477                 return -errno;
478         }
479
480         FOREACH_DIRENT_ALL(de, d, return -errno) {
481                 const char *a, *b;
482                 char *u;
483
484                 if (de->d_type != DT_UNKNOWN &&
485                     de->d_type != DT_REG)
486                         continue;
487
488                 a = startswith(de->d_name, ".gpt-");
489                 if (!a)
490                         continue;
491
492                 a = startswith(a, escaped_url);
493                 if (!a)
494                         continue;
495
496                 a = startswith(a, ".");
497                 if (!a)
498                         continue;
499
500                 b = endswith(de->d_name, ".gpt");
501                 if (!b)
502                         continue;
503
504                 if (a >= b)
505                         continue;
506
507                 u = cunescape_length(a, b - a);
508                 if (!u)
509                         return -ENOMEM;
510
511                 if (!etag_is_valid(u)) {
512                         free(u);
513                         continue;
514                 }
515
516                 r = strv_consume(&f->old_etags, u);
517                 if (r < 0)
518                         return r;
519         }
520
521         return 0;
522 }
523
524 static int gpt_import_file_begin(GptImportFile *f) {
525         int r;
526
527         assert(f);
528         assert(!f->curl);
529
530         log_info("Getting %s.", f->url);
531
532         r = gpt_import_file_find_old_etags(f);
533         if (r < 0)
534                 return r;
535
536         r = curl_glue_make(&f->curl, f->url, f);
537         if (r < 0)
538                 return r;
539
540         if (!strv_isempty(f->old_etags)) {
541                 _cleanup_free_ char *cc = NULL, *hdr = NULL;
542
543                 cc = strv_join(f->old_etags, ", ");
544                 if (!cc)
545                         return -ENOMEM;
546
547                 hdr = strappend("If-None-Match: ", cc);
548                 if (!hdr)
549                         return -ENOMEM;
550
551                 f->request_header = curl_slist_new(hdr, NULL);
552                 if (!f->request_header)
553                         return -ENOMEM;
554
555                 if (curl_easy_setopt(f->curl, CURLOPT_HTTPHEADER, f->request_header) != CURLE_OK)
556                         return -EIO;
557         }
558
559         if (curl_easy_setopt(f->curl, CURLOPT_WRITEFUNCTION, gpt_import_file_write_callback) != CURLE_OK)
560                 return -EIO;
561
562         if (curl_easy_setopt(f->curl, CURLOPT_WRITEDATA, f) != CURLE_OK)
563                 return -EIO;
564
565         if (curl_easy_setopt(f->curl, CURLOPT_HEADERFUNCTION, gpt_import_file_header_callback) != CURLE_OK)
566                 return -EIO;
567
568         if (curl_easy_setopt(f->curl, CURLOPT_HEADERDATA, f) != CURLE_OK)
569                 return -EIO;
570
571         r = curl_glue_add(f->import->glue, f->curl);
572         if (r < 0)
573                 return r;
574
575         return 0;
576 }
577
578 int gpt_import_new(GptImport **import, sd_event *event, gpt_import_on_finished on_finished, void *userdata) {
579         _cleanup_(gpt_import_unrefp) GptImport *i = NULL;
580         int r;
581
582         assert(import);
583
584         i = new0(GptImport, 1);
585         if (!i)
586                 return -ENOMEM;
587
588         i->on_finished = on_finished;
589         i->userdata = userdata;
590
591         if (event)
592                 i->event = sd_event_ref(event);
593         else {
594                 r = sd_event_default(&i->event);
595                 if (r < 0)
596                         return r;
597         }
598
599         r = curl_glue_new(&i->glue, i->event);
600         if (r < 0)
601                 return r;
602
603         i->glue->on_finished = gpt_import_curl_on_finished;
604         i->glue->userdata = i;
605
606         *import = i;
607         i = NULL;
608
609         return 0;
610 }
611
612 GptImport* gpt_import_unref(GptImport *import) {
613         GptImportFile *f;
614
615         if (!import)
616                 return NULL;
617
618         while ((f = hashmap_steal_first(import->files)))
619                 gpt_import_file_unref(f);
620         hashmap_free(import->files);
621
622         curl_glue_unref(import->glue);
623         sd_event_unref(import->event);
624
625         free(import);
626
627         return NULL;
628 }
629
630 int gpt_import_cancel(GptImport *import, const char *url) {
631         GptImportFile *f;
632
633         assert(import);
634         assert(url);
635
636         f = hashmap_remove(import->files, url);
637         if (!f)
638                 return 0;
639
640         gpt_import_file_unref(f);
641         return 1;
642 }
643
644 int gpt_import_pull(GptImport *import, const char *url, const char *local, bool force_local) {
645         _cleanup_(gpt_import_file_unrefp) GptImportFile *f = NULL;
646         int r;
647
648         assert(import);
649         assert(gpt_url_is_valid(url));
650         assert(!local || machine_name_is_valid(local));
651
652         if (hashmap_get(import->files, url))
653                 return -EEXIST;
654
655         r = hashmap_ensure_allocated(&import->files, &string_hash_ops);
656         if (r < 0)
657                 return r;
658
659         f = new0(GptImportFile, 1);
660         if (!f)
661                 return -ENOMEM;
662
663         f->import = import;
664         f->disk_fd = -1;
665         f->content_length = (uint64_t) -1;
666
667         f->url = strdup(url);
668         if (!f->url)
669                 return -ENOMEM;
670
671         if (local) {
672                 f->local = strdup(local);
673                 if (!f->local)
674                         return -ENOMEM;
675
676                 f->force_local = force_local;
677         }
678
679         r = hashmap_put(import->files, f->url, f);
680         if (r < 0)
681                 return r;
682
683         r = gpt_import_file_begin(f);
684         if (r < 0) {
685                 gpt_import_cancel(import, f->url);
686                 f = NULL;
687                 return r;
688         }
689
690         f = NULL;
691         return 0;
692 }
693
694 bool gpt_url_is_valid(const char *url) {
695         if (isempty(url))
696                 return false;
697
698         if (!startswith(url, "http://") &&
699             !startswith(url, "https://"))
700                 return false;
701
702         return ascii_is_valid(url);
703 }