]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/import/import-gpt.c
machined: beef up machined image listing with creation/modification times of subvolumes
[thirdparty/systemd.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 }