]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/import/import-raw.c
f1b36cbfcc242d91d0142e0f56f19146639a03b6
[thirdparty/systemd.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 <lzma.h>
26
27 #include "hashmap.h"
28 #include "utf8.h"
29 #include "curl-util.h"
30 #include "import-raw.h"
31 #include "strv.h"
32 #include "copy.h"
33
34 typedef struct RawImportFile RawImportFile;
35
36 struct RawImportFile {
37 RawImport *import;
38
39 char *url;
40 char *local;
41
42 CURL *curl;
43 struct curl_slist *request_header;
44
45 char *temp_path;
46 char *final_path;
47 char *etag;
48 char **old_etags;
49
50 uint64_t content_length;
51 uint64_t written_compressed;
52 uint64_t written_uncompressed;
53
54 void *payload;
55 size_t payload_size;
56
57 usec_t mtime;
58
59 bool force_local;
60 bool done;
61
62 int disk_fd;
63
64 lzma_stream lzma;
65 bool compressed;
66 };
67
68 struct RawImport {
69 sd_event *event;
70 CurlGlue *glue;
71
72 char *image_root;
73 Hashmap *files;
74
75 raw_import_on_finished on_finished;
76 void *userdata;
77
78 bool finished;
79 };
80
81 #define FILENAME_ESCAPE "/.#\"\'"
82
83 #define RAW_MAX_SIZE (1024LLU*1024LLU*1024LLU*8) /* 8 GB */
84
85 static RawImportFile *raw_import_file_unref(RawImportFile *f) {
86 if (!f)
87 return NULL;
88
89 if (f->import)
90 curl_glue_remove_and_free(f->import->glue, f->curl);
91 curl_slist_free_all(f->request_header);
92
93 safe_close(f->disk_fd);
94
95 free(f->final_path);
96
97 if (f->temp_path) {
98 unlink(f->temp_path);
99 free(f->temp_path);
100 }
101
102 free(f->url);
103 free(f->local);
104 free(f->etag);
105 strv_free(f->old_etags);
106 free(f->payload);
107 free(f);
108
109 return NULL;
110 }
111
112 DEFINE_TRIVIAL_CLEANUP_FUNC(RawImportFile*, raw_import_file_unref);
113
114 static void raw_import_finish(RawImport *import, int error) {
115 assert(import);
116
117 if (import->finished)
118 return;
119
120 import->finished = true;
121
122 if (import->on_finished)
123 import->on_finished(import, error, import->userdata);
124 else
125 sd_event_exit(import->event, error);
126 }
127
128 static int raw_import_file_make_final_path(RawImportFile *f) {
129 _cleanup_free_ char *escaped_url = NULL, *escaped_etag = NULL;
130
131 assert(f);
132
133 if (f->final_path)
134 return 0;
135
136 escaped_url = xescape(f->url, FILENAME_ESCAPE);
137 if (!escaped_url)
138 return -ENOMEM;
139
140 if (f->etag) {
141 escaped_etag = xescape(f->etag, FILENAME_ESCAPE);
142 if (!escaped_etag)
143 return -ENOMEM;
144
145 f->final_path = strjoin(f->import->image_root, "/.raw-", escaped_url, ".", escaped_etag, ".raw", NULL);
146 } else
147 f->final_path = strjoin(f->import->image_root, "/.raw-", escaped_url, ".raw", NULL);
148 if (!f->final_path)
149 return -ENOMEM;
150
151 return 0;
152 }
153
154 static void raw_import_file_success(RawImportFile *f) {
155 int r;
156
157 assert(f);
158
159 f->done = true;
160
161 if (f->local) {
162 _cleanup_free_ char *tp = NULL;
163 _cleanup_close_ int dfd = -1;
164 const char *p;
165
166 if (f->disk_fd >= 0) {
167 if (lseek(f->disk_fd, SEEK_SET, 0) == (off_t) -1) {
168 r = log_error_errno(errno, "Failed to seek to beginning of vendor image: %m");
169 goto finish;
170 }
171 } else {
172 r = raw_import_file_make_final_path(f);
173 if (r < 0) {
174 log_oom();
175 goto finish;
176 }
177
178 f->disk_fd = open(f->final_path, O_RDONLY|O_NOCTTY|O_CLOEXEC);
179 if (f->disk_fd < 0) {
180 r = log_error_errno(errno, "Failed to open vendor image: %m");
181 goto finish;
182 }
183 }
184
185 p = strappenda(f->import->image_root, "/", f->local, ".raw");
186 if (f->force_local)
187 (void) rm_rf_dangerous(p, false, true, false);
188
189 r = tempfn_random(p, &tp);
190 if (r < 0) {
191 log_oom();
192 goto finish;
193 }
194
195 dfd = open(tp, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0664);
196 if (dfd < 0) {
197 r = log_error_errno(errno, "Failed to create writable copy of image: %m");
198 goto finish;
199 }
200
201 /* Turn off COW writing. This should greatly improve
202 * performance on COW file systems like btrfs, since it
203 * reduces fragmentation caused by not allowing in-place
204 * writes. */
205 r = chattr_fd(dfd, true, FS_NOCOW_FL);
206 if (r < 0)
207 log_warning_errno(errno, "Failed to set file attributes on %s: %m", f->temp_path);
208
209 r = copy_bytes(f->disk_fd, dfd, (off_t) -1, true);
210 if (r < 0) {
211 log_error_errno(r, "Failed to make writable copy of image: %m");
212 unlink(tp);
213 goto finish;
214 }
215
216 (void) copy_times(f->disk_fd, dfd);
217 (void) copy_xattr(f->disk_fd, dfd);
218
219 dfd = safe_close(dfd);
220
221 r = rename(tp, p);
222 if (r < 0) {
223 r = log_error_errno(errno, "Failed to move writable image into place: %m");
224 unlink(tp);
225 goto finish;
226 }
227
228 log_info("Created new local image %s.", p);
229 }
230
231 f->disk_fd = safe_close(f->disk_fd);
232 r = 0;
233
234 finish:
235 raw_import_finish(f->import, r);
236 }
237
238 static void raw_import_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) {
239 RawImportFile *f = NULL;
240 struct stat st;
241 CURLcode code;
242 long status;
243 int r;
244
245 if (curl_easy_getinfo(curl, CURLINFO_PRIVATE, &f) != CURLE_OK)
246 return;
247
248 if (!f || f->done)
249 return;
250
251 f->done = true;
252
253 if (result != CURLE_OK) {
254 log_error("Transfer failed: %s", curl_easy_strerror(result));
255 r = -EIO;
256 goto fail;
257 }
258
259 code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
260 if (code != CURLE_OK) {
261 log_error("Failed to retrieve response code: %s", curl_easy_strerror(code));
262 r = -EIO;
263 goto fail;
264 } else if (status == 304) {
265 log_info("Image already downloaded. Skipping download.");
266 raw_import_file_success(f);
267 return;
268 } else if (status >= 300) {
269 log_error("HTTP request to %s failed with code %li.", f->url, status);
270 r = -EIO;
271 goto fail;
272 } else if (status < 200) {
273 log_error("HTTP request to %s finished with unexpected code %li.", f->url, status);
274 r = -EIO;
275 goto fail;
276 }
277
278 if (f->disk_fd < 0) {
279 log_error("No data received.");
280 r = -EIO;
281 goto fail;
282 }
283
284 if (f->content_length != (uint64_t) -1 &&
285 f->content_length != f->written_compressed) {
286 log_error("Download truncated.");
287 r = -EIO;
288 goto fail;
289 }
290
291 if (f->etag)
292 (void) fsetxattr(f->disk_fd, "user.source_etag", f->etag, strlen(f->etag), 0);
293 if (f->url)
294 (void) fsetxattr(f->disk_fd, "user.source_url", f->url, strlen(f->url), 0);
295
296 if (f->mtime != 0) {
297 struct timespec ut[2];
298
299 timespec_store(&ut[0], f->mtime);
300 ut[1] = ut[0];
301 (void) futimens(f->disk_fd, ut);
302
303 fd_setcrtime(f->disk_fd, f->mtime);
304 }
305
306 if (fstat(f->disk_fd, &st) < 0) {
307 r = log_error_errno(errno, "Failed to stat file: %m");
308 goto fail;
309 }
310
311 /* Mark read-only */
312 (void) fchmod(f->disk_fd, st.st_mode & 07444);
313
314 assert(f->temp_path);
315 assert(f->final_path);
316
317 r = rename(f->temp_path, f->final_path);
318 if (r < 0) {
319 r = log_error_errno(errno, "Failed to move RAW file into place: %m");
320 goto fail;
321 }
322
323 free(f->temp_path);
324 f->temp_path = NULL;
325
326 log_info("Completed writing vendor image %s.", f->final_path);
327
328 raw_import_file_success(f);
329 return;
330
331 fail:
332 raw_import_finish(f->import, r);
333 }
334
335 static int raw_import_file_open_disk_for_write(RawImportFile *f) {
336 int r;
337
338 assert(f);
339
340 if (f->disk_fd >= 0)
341 return 0;
342
343 r = raw_import_file_make_final_path(f);
344 if (r < 0)
345 return log_oom();
346
347 if (!f->temp_path) {
348 r = tempfn_random(f->final_path, &f->temp_path);
349 if (r < 0)
350 return log_oom();
351 }
352
353 f->disk_fd = open(f->temp_path, O_RDWR|O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC, 0644);
354 if (f->disk_fd < 0)
355 return log_error_errno(errno, "Failed to create %s: %m", f->temp_path);
356
357 return 0;
358 }
359
360 static int raw_import_file_write_uncompressed(RawImportFile *f, void *p, size_t sz) {
361 ssize_t n;
362
363 assert(f);
364 assert(p);
365 assert(sz > 0);
366 assert(f->disk_fd >= 0);
367
368 if (f->written_uncompressed + sz < f->written_uncompressed) {
369 log_error("File too large, overflow");
370 return -EOVERFLOW;
371 }
372
373 if (f->written_uncompressed + sz > RAW_MAX_SIZE) {
374 log_error("File overly large, refusing");
375 return -EFBIG;
376 }
377
378 n = write(f->disk_fd, p, sz);
379 if (n < 0) {
380 log_error_errno(errno, "Failed to write file: %m");
381 return -errno;
382 }
383 if ((size_t) n < sz) {
384 log_error("Short write");
385 return -EIO;
386 }
387
388 f->written_uncompressed += sz;
389
390 return 0;
391 }
392
393 static int raw_import_file_write_compressed(RawImportFile *f, void *p, size_t sz) {
394 int r;
395
396 assert(f);
397 assert(p);
398 assert(sz > 0);
399 assert(f->disk_fd >= 0);
400
401 if (f->written_compressed + sz < f->written_compressed) {
402 log_error("File too large, overflow");
403 return -EOVERFLOW;
404 }
405
406 if (f->content_length != (uint64_t) -1 &&
407 f->written_compressed + sz > f->content_length) {
408 log_error("Content length incorrect.");
409 return -EFBIG;
410 }
411
412 if (!f->compressed) {
413 r = raw_import_file_write_uncompressed(f, p, sz);
414 if (r < 0)
415 return r;
416 } else {
417 f->lzma.next_in = p;
418 f->lzma.avail_in = sz;
419
420 while (f->lzma.avail_in > 0) {
421 uint8_t buffer[16 * 1024];
422 lzma_ret lzr;
423
424 f->lzma.next_out = buffer;
425 f->lzma.avail_out = sizeof(buffer);
426
427 lzr = lzma_code(&f->lzma, LZMA_RUN);
428 if (lzr != LZMA_OK && lzr != LZMA_STREAM_END) {
429 log_error("Decompression error.");
430 return -EIO;
431 }
432
433 r = raw_import_file_write_uncompressed(f, buffer, sizeof(buffer) - f->lzma.avail_out);
434 if (r < 0)
435 return r;
436 }
437 }
438
439 f->written_compressed += sz;
440
441 return 0;
442 }
443
444 static int raw_import_file_detect_xz(RawImportFile *f) {
445 static const uint8_t xz_signature[] = {
446 '\xfd', '7', 'z', 'X', 'Z', '\x00'
447 };
448 lzma_ret lzr;
449 int r;
450
451 assert(f);
452
453 if (f->payload_size < sizeof(xz_signature))
454 return 0;
455
456 f->compressed = memcmp(f->payload, xz_signature, sizeof(xz_signature)) == 0;
457 log_debug("Stream is XZ compressed: %s", yes_no(f->compressed));
458
459 if (f->compressed) {
460 lzr = lzma_stream_decoder(&f->lzma, UINT64_MAX, LZMA_TELL_UNSUPPORTED_CHECK);
461 if (lzr != LZMA_OK) {
462 log_error("Failed to initialize LZMA decoder.");
463 return -EIO;
464 }
465 }
466
467 r = raw_import_file_open_disk_for_write(f);
468 if (r < 0)
469 return r;
470
471 r = raw_import_file_write_compressed(f, f->payload, f->payload_size);
472 if (r < 0)
473 return r;
474
475 free(f->payload);
476 f->payload = NULL;
477 f->payload_size = 0;
478
479 return 0;
480 }
481
482 static size_t raw_import_file_write_callback(void *contents, size_t size, size_t nmemb, void *userdata) {
483 RawImportFile *f = userdata;
484 size_t sz = size * nmemb;
485 int r;
486
487 assert(contents);
488 assert(f);
489
490 if (f->done) {
491 r = -ESTALE;
492 goto fail;
493 }
494
495 if (f->disk_fd < 0) {
496 uint8_t *p;
497
498 /* We haven't opened the file yet, let's first check what it actually is */
499
500 p = realloc(f->payload, f->payload_size + sz);
501 if (!p) {
502 r = log_oom();
503 goto fail;
504 }
505
506 memcpy(p + f->payload_size, contents, sz);
507 f->payload_size = sz;
508 f->payload = p;
509
510 r = raw_import_file_detect_xz(f);
511 if (r < 0)
512 goto fail;
513
514 return sz;
515 }
516
517 r = raw_import_file_write_compressed(f, contents, sz);
518 if (r < 0)
519 goto fail;
520
521 return sz;
522
523 fail:
524 raw_import_finish(f->import, r);
525 return 0;
526 }
527
528 static size_t raw_import_file_header_callback(void *contents, size_t size, size_t nmemb, void *userdata) {
529 RawImportFile *f = userdata;
530 size_t sz = size * nmemb;
531 _cleanup_free_ char *length = NULL, *last_modified = NULL;
532 char *etag;
533 int r;
534
535 assert(contents);
536 assert(f);
537
538 if (f->done) {
539 r = -ESTALE;
540 goto fail;
541 }
542
543 r = curl_header_strdup(contents, sz, "ETag:", &etag);
544 if (r < 0) {
545 log_oom();
546 goto fail;
547 }
548 if (r > 0) {
549 free(f->etag);
550 f->etag = etag;
551
552 if (strv_contains(f->old_etags, f->etag)) {
553 log_info("Image already downloaded. Skipping download.");
554 raw_import_file_success(f);
555 return sz;
556 }
557
558 return sz;
559 }
560
561 r = curl_header_strdup(contents, sz, "Content-Length:", &length);
562 if (r < 0) {
563 log_oom();
564 goto fail;
565 }
566 if (r > 0) {
567 (void) safe_atou64(length, &f->content_length);
568
569 if (f->content_length != (uint64_t) -1) {
570 char bytes[FORMAT_BYTES_MAX];
571 log_info("Downloading %s.", format_bytes(bytes, sizeof(bytes), f->content_length));
572 }
573
574 return sz;
575 }
576
577 r = curl_header_strdup(contents, sz, "Last-Modified:", &last_modified);
578 if (r < 0) {
579 log_oom();
580 goto fail;
581 }
582 if (r > 0) {
583 (void) curl_parse_http_time(last_modified, &f->mtime);
584 return sz;
585 }
586
587 return sz;
588
589 fail:
590 raw_import_finish(f->import, r);
591 return 0;
592 }
593
594 static bool etag_is_valid(const char *etag) {
595
596 if (!endswith(etag, "\""))
597 return false;
598
599 if (!startswith(etag, "\"") && !startswith(etag, "W/\""))
600 return false;
601
602 return true;
603 }
604
605 static int raw_import_file_find_old_etags(RawImportFile *f) {
606 _cleanup_free_ char *escaped_url = NULL;
607 _cleanup_closedir_ DIR *d = NULL;
608 struct dirent *de;
609 int r;
610
611 escaped_url = xescape(f->url, FILENAME_ESCAPE);
612 if (!escaped_url)
613 return -ENOMEM;
614
615 d = opendir(f->import->image_root);
616 if (!d) {
617 if (errno == ENOENT)
618 return 0;
619
620 return -errno;
621 }
622
623 FOREACH_DIRENT_ALL(de, d, return -errno) {
624 const char *a, *b;
625 char *u;
626
627 if (de->d_type != DT_UNKNOWN &&
628 de->d_type != DT_REG)
629 continue;
630
631 a = startswith(de->d_name, ".raw-");
632 if (!a)
633 continue;
634
635 a = startswith(a, escaped_url);
636 if (!a)
637 continue;
638
639 a = startswith(a, ".");
640 if (!a)
641 continue;
642
643 b = endswith(de->d_name, ".raw");
644 if (!b)
645 continue;
646
647 if (a >= b)
648 continue;
649
650 u = cunescape_length(a, b - a);
651 if (!u)
652 return -ENOMEM;
653
654 if (!etag_is_valid(u)) {
655 free(u);
656 continue;
657 }
658
659 r = strv_consume(&f->old_etags, u);
660 if (r < 0)
661 return r;
662 }
663
664 return 0;
665 }
666
667 static int raw_import_file_begin(RawImportFile *f) {
668 int r;
669
670 assert(f);
671 assert(!f->curl);
672
673 log_info("Getting %s.", f->url);
674
675 r = raw_import_file_find_old_etags(f);
676 if (r < 0)
677 return r;
678
679 r = curl_glue_make(&f->curl, f->url, f);
680 if (r < 0)
681 return r;
682
683 if (!strv_isempty(f->old_etags)) {
684 _cleanup_free_ char *cc = NULL, *hdr = NULL;
685
686 cc = strv_join(f->old_etags, ", ");
687 if (!cc)
688 return -ENOMEM;
689
690 hdr = strappend("If-None-Match: ", cc);
691 if (!hdr)
692 return -ENOMEM;
693
694 f->request_header = curl_slist_new(hdr, NULL);
695 if (!f->request_header)
696 return -ENOMEM;
697
698 if (curl_easy_setopt(f->curl, CURLOPT_HTTPHEADER, f->request_header) != CURLE_OK)
699 return -EIO;
700 }
701
702 if (curl_easy_setopt(f->curl, CURLOPT_WRITEFUNCTION, raw_import_file_write_callback) != CURLE_OK)
703 return -EIO;
704
705 if (curl_easy_setopt(f->curl, CURLOPT_WRITEDATA, f) != CURLE_OK)
706 return -EIO;
707
708 if (curl_easy_setopt(f->curl, CURLOPT_HEADERFUNCTION, raw_import_file_header_callback) != CURLE_OK)
709 return -EIO;
710
711 if (curl_easy_setopt(f->curl, CURLOPT_HEADERDATA, f) != CURLE_OK)
712 return -EIO;
713
714 r = curl_glue_add(f->import->glue, f->curl);
715 if (r < 0)
716 return r;
717
718 return 0;
719 }
720
721 int raw_import_new(RawImport **import, sd_event *event, const char *image_root, raw_import_on_finished on_finished, void *userdata) {
722 _cleanup_(raw_import_unrefp) RawImport *i = NULL;
723 int r;
724
725 assert(import);
726 assert(image_root);
727
728 i = new0(RawImport, 1);
729 if (!i)
730 return -ENOMEM;
731
732 i->on_finished = on_finished;
733 i->userdata = userdata;
734
735 i->image_root = strdup(image_root);
736 if (!i->image_root)
737 return -ENOMEM;
738
739 if (event)
740 i->event = sd_event_ref(event);
741 else {
742 r = sd_event_default(&i->event);
743 if (r < 0)
744 return r;
745 }
746
747 r = curl_glue_new(&i->glue, i->event);
748 if (r < 0)
749 return r;
750
751 i->glue->on_finished = raw_import_curl_on_finished;
752 i->glue->userdata = i;
753
754 *import = i;
755 i = NULL;
756
757 return 0;
758 }
759
760 RawImport* raw_import_unref(RawImport *import) {
761 RawImportFile *f;
762
763 if (!import)
764 return NULL;
765
766 while ((f = hashmap_steal_first(import->files)))
767 raw_import_file_unref(f);
768 hashmap_free(import->files);
769
770 curl_glue_unref(import->glue);
771 sd_event_unref(import->event);
772
773 free(import->image_root);
774 free(import);
775
776 return NULL;
777 }
778
779 int raw_import_cancel(RawImport *import, const char *url) {
780 RawImportFile *f;
781
782 assert(import);
783 assert(url);
784
785 f = hashmap_remove(import->files, url);
786 if (!f)
787 return 0;
788
789 raw_import_file_unref(f);
790 return 1;
791 }
792
793 int raw_import_pull(RawImport *import, const char *url, const char *local, bool force_local) {
794 _cleanup_(raw_import_file_unrefp) RawImportFile *f = NULL;
795 int r;
796
797 assert(import);
798 assert(raw_url_is_valid(url));
799 assert(!local || machine_name_is_valid(local));
800
801 if (hashmap_get(import->files, url))
802 return -EEXIST;
803
804 r = hashmap_ensure_allocated(&import->files, &string_hash_ops);
805 if (r < 0)
806 return r;
807
808 f = new0(RawImportFile, 1);
809 if (!f)
810 return -ENOMEM;
811
812 f->import = import;
813 f->disk_fd = -1;
814 f->content_length = (uint64_t) -1;
815
816 f->url = strdup(url);
817 if (!f->url)
818 return -ENOMEM;
819
820 if (local) {
821 f->local = strdup(local);
822 if (!f->local)
823 return -ENOMEM;
824
825 f->force_local = force_local;
826 }
827
828 r = hashmap_put(import->files, f->url, f);
829 if (r < 0)
830 return r;
831
832 r = raw_import_file_begin(f);
833 if (r < 0) {
834 raw_import_cancel(import, f->url);
835 f = NULL;
836 return r;
837 }
838
839 f = NULL;
840 return 0;
841 }
842
843 bool raw_url_is_valid(const char *url) {
844 if (isempty(url))
845 return false;
846
847 if (!startswith(url, "http://") &&
848 !startswith(url, "https://"))
849 return false;
850
851 return ascii_is_valid(url);
852 }