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