chiark / gitweb /
503f1e64cf6121e49c9504474e5048a176eb3d5f
[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
176         if (fstat(f->disk_fd, &st) < 0) {
177                 r = log_error_errno(errno, "Failed to stat file: %m");
178                 goto fail;
179         }
180
181         /* Mark read-only */
182         (void) fchmod(f->disk_fd, st.st_mode & 07444);
183
184         f->disk_fd = safe_close(f->disk_fd);
185
186         assert(f->temp_path);
187         assert(f->final_path);
188
189         r = rename(f->temp_path, f->final_path);
190         if (r < 0) {
191                 r = log_error_errno(errno, "Failed to move GPT file into place: %m");
192                 goto fail;
193         }
194
195         r = 0;
196
197 fail:
198         gpt_import_finish(f->import, r);
199 }
200
201 static int gpt_import_file_open_disk(GptImportFile *f) {
202         int r;
203
204         assert(f);
205
206         if (f->disk_fd >= 0)
207                 return 0;
208
209         assert(f->final_path);
210
211         if (!f->temp_path) {
212                 r = tempfn_random(f->final_path, &f->temp_path);
213                 if (r < 0)
214                         return log_oom();
215         }
216
217         f->disk_fd = open(f->temp_path, O_CREAT|O_EXCL|O_NOCTTY|O_CLOEXEC|O_WRONLY, 0644);
218         if (f->disk_fd < 0)
219                 return log_error_errno(errno, "Failed to create %s: %m", f->temp_path);
220
221         return 0;
222 }
223
224 static size_t gpt_import_file_write_callback(void *contents, size_t size, size_t nmemb, void *userdata) {
225         GptImportFile *f = userdata;
226         size_t sz = size * nmemb;
227         ssize_t n;
228         int r;
229
230         assert(contents);
231         assert(f);
232
233         r = gpt_import_file_open_disk(f);
234         if (r < 0)
235                 goto fail;
236
237         if (f->written + sz < f->written) {
238                 log_error("File too large, overflow");
239                 r = -EOVERFLOW;
240                 goto fail;
241         }
242
243         if (f->content_length != (uint64_t) -1 &&
244             f->written + sz > f->content_length) {
245                 log_error("Content length incorrect.");
246                 r = -EFBIG;
247                 goto fail;
248         }
249
250         n = write(f->disk_fd, contents, sz);
251         if (n < 0) {
252                 log_error_errno(errno, "Failed to write file: %m");
253                 goto fail;
254         }
255
256         if ((size_t) n < sz) {
257                 log_error("Short write");
258                 r = -EIO;
259                 goto fail;
260         }
261
262         f->written += sz;
263
264         return sz;
265
266 fail:
267         gpt_import_finish(f->import, r);
268         return 0;
269 }
270
271 static size_t gpt_import_file_header_callback(void *contents, size_t size, size_t nmemb, void *userdata) {
272         GptImportFile *f = userdata;
273         size_t sz = size * nmemb;
274         _cleanup_free_ char *length = NULL, *last_modified = NULL;
275         char *etag;
276         int r;
277
278         assert(contents);
279         assert(f);
280
281         r = curl_header_strdup(contents, sz, "ETag:", &etag);
282         if (r < 0) {
283                 log_oom();
284                 goto fail;
285         }
286         if (r > 0) {
287                 free(f->etag);
288                 f->etag = etag;
289
290                 if (streq_ptr(f->old_etag, f->etag)) {
291                         log_info("Image already up to date. Finishing.");
292                         gpt_import_finish(f->import, 0);
293                         return sz;
294                 }
295
296                 return sz;
297         }
298
299         r = curl_header_strdup(contents, sz, "Content-Length:", &length);
300         if (r < 0) {
301                 log_oom();
302                 goto fail;
303         }
304         if (r > 0) {
305                 (void) safe_atou64(length, &f->content_length);
306                 return sz;
307         }
308
309         r = curl_header_strdup(contents, sz, "Last-Modified:", &last_modified);
310         if (r < 0) {
311                 log_oom();
312                 goto fail;
313         }
314         if (r > 0) {
315                 (void) curl_parse_http_time(last_modified, &f->mtime);
316                 return sz;
317         }
318
319         return sz;
320
321 fail:
322         gpt_import_finish(f->import, r);
323         return 0;
324 }
325
326 static int gpt_import_file_begin(GptImportFile *f) {
327         int r;
328
329         assert(f);
330         assert(!f->curl);
331
332         log_info("Getting %s.", f->url);
333
334         r = curl_glue_make(&f->curl, f->url, f);
335         if (r < 0)
336                 return r;
337
338         if (f->old_etag) {
339                 const char *hdr;
340
341                 hdr = strappenda("If-None-Match: ", f->old_etag);
342
343                 f->request_header = curl_slist_new(hdr, NULL);
344                 if (!f->request_header)
345                         return -ENOMEM;
346
347                 if (curl_easy_setopt(f->curl, CURLOPT_HTTPHEADER, f->request_header) != CURLE_OK)
348                         return -EIO;
349         }
350
351         if (curl_easy_setopt(f->curl, CURLOPT_WRITEFUNCTION, gpt_import_file_write_callback) != CURLE_OK)
352                 return -EIO;
353
354         if (curl_easy_setopt(f->curl, CURLOPT_WRITEDATA, f) != CURLE_OK)
355                 return -EIO;
356
357         if (curl_easy_setopt(f->curl, CURLOPT_HEADERFUNCTION, gpt_import_file_header_callback) != CURLE_OK)
358                 return -EIO;
359
360         if (curl_easy_setopt(f->curl, CURLOPT_HEADERDATA, f) != CURLE_OK)
361                 return -EIO;
362
363         r = curl_glue_add(f->import->glue, f->curl);
364         if (r < 0)
365                 return r;
366
367         return 0;
368 }
369
370 int gpt_import_new(GptImport **import, sd_event *event, gpt_import_on_finished on_finished, void *userdata) {
371         _cleanup_(gpt_import_unrefp) GptImport *i = NULL;
372         int r;
373
374         assert(import);
375
376         i = new0(GptImport, 1);
377         if (!i)
378                 return -ENOMEM;
379
380         i->on_finished = on_finished;
381         i->userdata = userdata;
382
383         if (event)
384                 i->event = sd_event_ref(event);
385         else {
386                 r = sd_event_default(&i->event);
387                 if (r < 0)
388                         return r;
389         }
390
391         r = curl_glue_new(&i->glue, i->event);
392         if (r < 0)
393                 return r;
394
395         i->glue->on_finished = gpt_import_curl_on_finished;
396         i->glue->userdata = i;
397
398         *import = i;
399         i = NULL;
400
401         return 0;
402 }
403
404 GptImport* gpt_import_unref(GptImport *import) {
405         GptImportFile *f;
406
407         if (!import)
408                 return NULL;
409
410         while ((f = hashmap_steal_first(import->files)))
411                 gpt_import_file_unref(f);
412         hashmap_free(import->files);
413
414         curl_glue_unref(import->glue);
415         sd_event_unref(import->event);
416
417         free(import);
418
419         return NULL;
420 }
421
422 int gpt_import_cancel(GptImport *import, const char *url) {
423         GptImportFile *f;
424
425         assert(import);
426         assert(url);
427
428         f = hashmap_remove(import->files, url);
429         if (!f)
430                 return 0;
431
432         gpt_import_file_unref(f);
433         return 1;
434 }
435
436 int gpt_import_pull(GptImport *import, const char *url, const char *local, bool force_local) {
437         _cleanup_(gpt_import_file_unrefp) GptImportFile *f = NULL;
438         char etag[LINE_MAX];
439         ssize_t n;
440         int r;
441
442         assert(import);
443         assert(gpt_url_is_valid(url));
444         assert(machine_name_is_valid(local));
445
446         if (hashmap_get(import->files, url))
447                 return -EEXIST;
448
449         r = hashmap_ensure_allocated(&import->files, &string_hash_ops);
450         if (r < 0)
451                 return r;
452
453         f = new0(GptImportFile, 1);
454         if (!f)
455                 return -ENOMEM;
456
457         f->import = import;
458         f->disk_fd = -1;
459         f->content_length = (uint64_t) -1;
460
461         f->url = strdup(url);
462         if (!f->url)
463                 return -ENOMEM;
464
465         f->local = strdup(local);
466         if (!f->local)
467                 return -ENOMEM;
468
469         f->final_path = strjoin("/var/lib/container/", local, ".gpt", NULL);
470         if (!f->final_path)
471                 return -ENOMEM;
472
473         n = getxattr(f->final_path, "user.etag", etag, sizeof(etag));
474         if (n > 0) {
475                 f->old_etag = strndup(etag, n);
476                 if (!f->old_etag)
477                         return -ENOMEM;
478         }
479
480         r = hashmap_put(import->files, f->url, f);
481         if (r < 0)
482                 return r;
483
484         r = gpt_import_file_begin(f);
485         if (r < 0) {
486                 gpt_import_cancel(import, f->url);
487                 f = NULL;
488                 return r;
489         }
490
491         f = NULL;
492         return 0;
493 }
494
495 bool gpt_url_is_valid(const char *url) {
496         if (isempty(url))
497                 return false;
498
499         if (!startswith(url, "http://") &&
500             !startswith(url, "https://"))
501                 return false;
502
503         return ascii_is_valid(url);
504 }