chiark / gitweb /
machined: beef up machined image listing with creation/modification times of subvolumes
[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
30 typedef struct GptImportFile GptImportFile;
31
32 struct GptImportFile {
33         GptImport *import;
34
35         char *url;
36         char *local;
37
38         CURL *curl;
39         struct curl_slist *request_header;
40
41         char *temp_path;
42         char *final_path;
43         char *etag;
44         char *old_etag;
45
46         uint64_t content_length;
47         uint64_t written;
48
49         usec_t mtime;
50
51         bool force_local;
52         bool done;
53
54         int disk_fd;
55 };
56
57 struct GptImport {
58         sd_event *event;
59         CurlGlue *glue;
60
61         Hashmap *files;
62
63         gpt_import_on_finished on_finished;
64         void *userdata;
65
66         bool finished;
67 };
68
69 static GptImportFile *gpt_import_file_unref(GptImportFile *f) {
70         if (!f)
71                 return NULL;
72
73         if (f->import)
74                 curl_glue_remove_and_free(f->import->glue, f->curl);
75         curl_slist_free_all(f->request_header);
76
77         safe_close(f->disk_fd);
78
79         free(f->final_path);
80
81         if (f->temp_path) {
82                 unlink(f->temp_path);
83                 free(f->temp_path);
84         }
85
86         free(f->url);
87         free(f->local);
88         free(f->etag);
89         free(f->old_etag);
90         free(f);
91
92         return NULL;
93 }
94
95 DEFINE_TRIVIAL_CLEANUP_FUNC(GptImportFile*, gpt_import_file_unref);
96
97 static void gpt_import_finish(GptImport *import, int error) {
98         assert(import);
99
100         if (import->finished)
101                 return;
102
103         import->finished = true;
104
105         if (import->on_finished)
106                 import->on_finished(import, error, import->userdata);
107         else
108                 sd_event_exit(import->event, error);
109 }
110
111 static void gpt_import_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) {
112         GptImportFile *f = NULL;
113         struct stat st;
114         CURLcode code;
115         long status;
116         int r;
117
118         if (curl_easy_getinfo(curl, CURLINFO_PRIVATE, &f) != CURLE_OK)
119                 return;
120
121         if (!f)
122                 return;
123
124         f->done = true;
125
126         if (result != CURLE_OK) {
127                 log_error("Transfer failed: %s", curl_easy_strerror(result));
128                 r = -EIO;
129                 goto fail;
130         }
131
132         code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
133         if (code != CURLE_OK) {
134                 log_error("Failed to retrieve response code: %s", curl_easy_strerror(code));
135                 r = -EIO;
136                 goto fail;
137         } else if (status == 304) {
138                 log_info("File unmodified.");
139                 r = 0;
140                 goto fail;
141         } else if (status >= 300) {
142                 log_error("HTTP request to %s failed with code %li.", f->url, status);
143                 r = -EIO;
144                 goto fail;
145         } else if (status < 200) {
146                 log_error("HTTP request to %s finished with unexpected code %li.", f->url, status);
147                 r = -EIO;
148                 goto fail;
149         }
150
151         if (f->disk_fd < 0) {
152                 log_error("No data received.");
153                 r = -EIO;
154                 goto fail;
155         }
156
157         if (f->content_length != (uint64_t) -1 &&
158             f->content_length != f->written) {
159                 log_error("Download truncated.");
160                 r = -EIO;
161                 goto fail;
162         }
163
164         if (f->etag)
165                 (void) fsetxattr(f->disk_fd, "user.etag", f->etag, strlen(f->etag), XATTR_CREATE);
166
167         if (f->mtime != 0) {
168                 struct timespec ut[2];
169
170                 timespec_store(&ut[0], f->mtime);
171                 ut[1] = ut[0];
172
173                 (void) futimens(f->disk_fd, ut);
174
175                 fd_setcrtime(f->disk_fd, f->mtime);
176         }
177
178         if (fstat(f->disk_fd, &st) < 0) {
179                 r = log_error_errno(errno, "Failed to stat file: %m");
180                 goto fail;
181         }
182
183         /* Mark read-only */
184         (void) fchmod(f->disk_fd, st.st_mode & 07444);
185
186         f->disk_fd = safe_close(f->disk_fd);
187
188         assert(f->temp_path);
189         assert(f->final_path);
190
191         r = rename(f->temp_path, f->final_path);
192         if (r < 0) {
193                 r = log_error_errno(errno, "Failed to move GPT file into place: %m");
194                 goto fail;
195         }
196
197         r = 0;
198
199 fail:
200         gpt_import_finish(f->import, r);
201 }
202
203 static int gpt_import_file_open_disk(GptImportFile *f) {
204         int r;
205
206         assert(f);
207
208         if (f->disk_fd >= 0)
209                 return 0;
210
211         assert(f->final_path);
212
213         if (!f->temp_path) {
214                 r = tempfn_random(f->final_path, &f->temp_path);
215                 if (r < 0)
216                         return log_oom();
217         }
218
219         f->disk_fd = open(f->temp_path, O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC|O_WRONLY, 0644);
220         if (f->disk_fd < 0)
221                 return log_error_errno(errno, "Failed to create %s: %m", f->temp_path);
222
223         return 0;
224 }
225
226 static size_t gpt_import_file_write_callback(void *contents, size_t size, size_t nmemb, void *userdata) {
227         GptImportFile *f = userdata;
228         size_t sz = size * nmemb;
229         ssize_t n;
230         int r;
231
232         assert(contents);
233         assert(f);
234
235         r = gpt_import_file_open_disk(f);
236         if (r < 0)
237                 goto fail;
238
239         if (f->written + sz < f->written) {
240                 log_error("File too large, overflow");
241                 r = -EOVERFLOW;
242                 goto fail;
243         }
244
245         if (f->content_length != (uint64_t) -1 &&
246             f->written + sz > f->content_length) {
247                 log_error("Content length incorrect.");
248                 r = -EFBIG;
249                 goto fail;
250         }
251
252         n = write(f->disk_fd, contents, sz);
253         if (n < 0) {
254                 log_error_errno(errno, "Failed to write file: %m");
255                 goto fail;
256         }
257
258         if ((size_t) n < sz) {
259                 log_error("Short write");
260                 r = -EIO;
261                 goto fail;
262         }
263
264         f->written += sz;
265
266         return sz;
267
268 fail:
269         gpt_import_finish(f->import, r);
270         return 0;
271 }
272
273 static size_t gpt_import_file_header_callback(void *contents, size_t size, size_t nmemb, void *userdata) {
274         GptImportFile *f = userdata;
275         size_t sz = size * nmemb;
276         _cleanup_free_ char *length = NULL, *last_modified = NULL;
277         char *etag;
278         int r;
279
280         assert(contents);
281         assert(f);
282
283         r = curl_header_strdup(contents, sz, "ETag:", &etag);
284         if (r < 0) {
285                 log_oom();
286                 goto fail;
287         }
288         if (r > 0) {
289                 free(f->etag);
290                 f->etag = etag;
291
292                 if (streq_ptr(f->old_etag, f->etag)) {
293                         log_info("Image already up to date. Finishing.");
294                         gpt_import_finish(f->import, 0);
295                         return sz;
296                 }
297
298                 return sz;
299         }
300
301         r = curl_header_strdup(contents, sz, "Content-Length:", &length);
302         if (r < 0) {
303                 log_oom();
304                 goto fail;
305         }
306         if (r > 0) {
307                 (void) safe_atou64(length, &f->content_length);
308                 return sz;
309         }
310
311         r = curl_header_strdup(contents, sz, "Last-Modified:", &last_modified);
312         if (r < 0) {
313                 log_oom();
314                 goto fail;
315         }
316         if (r > 0) {
317                 (void) curl_parse_http_time(last_modified, &f->mtime);
318                 return sz;
319         }
320
321         return sz;
322
323 fail:
324         gpt_import_finish(f->import, r);
325         return 0;
326 }
327
328 static int gpt_import_file_begin(GptImportFile *f) {
329         int r;
330
331         assert(f);
332         assert(!f->curl);
333
334         log_info("Getting %s.", f->url);
335
336         r = curl_glue_make(&f->curl, f->url, f);
337         if (r < 0)
338                 return r;
339
340         if (f->old_etag) {
341                 const char *hdr;
342
343                 hdr = strappenda("If-None-Match: ", f->old_etag);
344
345                 f->request_header = curl_slist_new(hdr, NULL);
346                 if (!f->request_header)
347                         return -ENOMEM;
348
349                 if (curl_easy_setopt(f->curl, CURLOPT_HTTPHEADER, f->request_header) != CURLE_OK)
350                         return -EIO;
351         }
352
353         if (curl_easy_setopt(f->curl, CURLOPT_WRITEFUNCTION, gpt_import_file_write_callback) != CURLE_OK)
354                 return -EIO;
355
356         if (curl_easy_setopt(f->curl, CURLOPT_WRITEDATA, f) != CURLE_OK)
357                 return -EIO;
358
359         if (curl_easy_setopt(f->curl, CURLOPT_HEADERFUNCTION, gpt_import_file_header_callback) != CURLE_OK)
360                 return -EIO;
361
362         if (curl_easy_setopt(f->curl, CURLOPT_HEADERDATA, f) != CURLE_OK)
363                 return -EIO;
364
365         r = curl_glue_add(f->import->glue, f->curl);
366         if (r < 0)
367                 return r;
368
369         return 0;
370 }
371
372 int gpt_import_new(GptImport **import, sd_event *event, gpt_import_on_finished on_finished, void *userdata) {
373         _cleanup_(gpt_import_unrefp) GptImport *i = NULL;
374         int r;
375
376         assert(import);
377
378         i = new0(GptImport, 1);
379         if (!i)
380                 return -ENOMEM;
381
382         i->on_finished = on_finished;
383         i->userdata = userdata;
384
385         if (event)
386                 i->event = sd_event_ref(event);
387         else {
388                 r = sd_event_default(&i->event);
389                 if (r < 0)
390                         return r;
391         }
392
393         r = curl_glue_new(&i->glue, i->event);
394         if (r < 0)
395                 return r;
396
397         i->glue->on_finished = gpt_import_curl_on_finished;
398         i->glue->userdata = i;
399
400         *import = i;
401         i = NULL;
402
403         return 0;
404 }
405
406 GptImport* gpt_import_unref(GptImport *import) {
407         GptImportFile *f;
408
409         if (!import)
410                 return NULL;
411
412         while ((f = hashmap_steal_first(import->files)))
413                 gpt_import_file_unref(f);
414         hashmap_free(import->files);
415
416         curl_glue_unref(import->glue);
417         sd_event_unref(import->event);
418
419         free(import);
420
421         return NULL;
422 }
423
424 int gpt_import_cancel(GptImport *import, const char *url) {
425         GptImportFile *f;
426
427         assert(import);
428         assert(url);
429
430         f = hashmap_remove(import->files, url);
431         if (!f)
432                 return 0;
433
434         gpt_import_file_unref(f);
435         return 1;
436 }
437
438 int gpt_import_pull(GptImport *import, const char *url, const char *local, bool force_local) {
439         _cleanup_(gpt_import_file_unrefp) GptImportFile *f = NULL;
440         char etag[LINE_MAX];
441         ssize_t n;
442         int r;
443
444         assert(import);
445         assert(gpt_url_is_valid(url));
446         assert(machine_name_is_valid(local));
447
448         if (hashmap_get(import->files, url))
449                 return -EEXIST;
450
451         r = hashmap_ensure_allocated(&import->files, &string_hash_ops);
452         if (r < 0)
453                 return r;
454
455         f = new0(GptImportFile, 1);
456         if (!f)
457                 return -ENOMEM;
458
459         f->import = import;
460         f->disk_fd = -1;
461         f->content_length = (uint64_t) -1;
462
463         f->url = strdup(url);
464         if (!f->url)
465                 return -ENOMEM;
466
467         f->local = strdup(local);
468         if (!f->local)
469                 return -ENOMEM;
470
471         f->final_path = strjoin("/var/lib/container/", local, ".gpt", NULL);
472         if (!f->final_path)
473                 return -ENOMEM;
474
475         n = getxattr(f->final_path, "user.etag", etag, sizeof(etag));
476         if (n > 0) {
477                 f->old_etag = strndup(etag, n);
478                 if (!f->old_etag)
479                         return -ENOMEM;
480         }
481
482         r = hashmap_put(import->files, f->url, f);
483         if (r < 0)
484                 return r;
485
486         r = gpt_import_file_begin(f);
487         if (r < 0) {
488                 gpt_import_cancel(import, f->url);
489                 f = NULL;
490                 return r;
491         }
492
493         f = NULL;
494         return 0;
495 }
496
497 bool gpt_url_is_valid(const char *url) {
498         if (isempty(url))
499                 return false;
500
501         if (!startswith(url, "http://") &&
502             !startswith(url, "https://"))
503                 return false;
504
505         return ascii_is_valid(url);
506 }