cgi-io: support SHA256 checksums for file uploads
[project/cgi-io.git] / src / main.c
1 /*
2 * cgi-io - LuCI non-RPC helper
3 *
4 * Copyright (C) 2013 Jo-Philipp Wich <jo@mein.io>
5 *
6 * Permission to use, copy, modify, and/or distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <stdbool.h>
22 #include <unistd.h>
23 #include <string.h>
24 #include <errno.h>
25 #include <fcntl.h>
26 #include <ctype.h>
27 #include <sys/stat.h>
28 #include <sys/wait.h>
29
30 #include <libubus.h>
31 #include <libubox/blobmsg.h>
32
33 #include "multipart_parser.h"
34
35
36 enum part {
37 PART_UNKNOWN,
38 PART_SESSIONID,
39 PART_FILENAME,
40 PART_FILEMODE,
41 PART_FILEDATA
42 };
43
44 const char *parts[] = {
45 "(bug)",
46 "sessionid",
47 "filename",
48 "filemode",
49 "filedata",
50 };
51
52 struct state
53 {
54 bool is_content_disposition;
55 enum part parttype;
56 char *sessionid;
57 char *filename;
58 bool filedata;
59 int filemode;
60 int filefd;
61 int tempfd;
62 };
63
64 enum {
65 SES_ACCESS,
66 __SES_MAX,
67 };
68
69 static const struct blobmsg_policy ses_policy[__SES_MAX] = {
70 [SES_ACCESS] = { .name = "access", .type = BLOBMSG_TYPE_BOOL },
71 };
72
73
74 static struct state st;
75
76 static void
77 session_access_cb(struct ubus_request *req, int type, struct blob_attr *msg)
78 {
79 struct blob_attr *tb[__SES_MAX];
80 bool *allow = (bool *)req->priv;
81
82 if (!msg)
83 return;
84
85 blobmsg_parse(ses_policy, __SES_MAX, tb, blob_data(msg), blob_len(msg));
86
87 if (tb[SES_ACCESS])
88 *allow = blobmsg_get_bool(tb[SES_ACCESS]);
89 }
90
91 static bool
92 session_access(const char *sid, const char *obj, const char *func)
93 {
94 uint32_t id;
95 bool allow = false;
96 struct ubus_context *ctx;
97 static struct blob_buf req;
98
99 ctx = ubus_connect(NULL);
100
101 if (!ctx || ubus_lookup_id(ctx, "session", &id))
102 goto out;
103
104 blob_buf_init(&req, 0);
105 blobmsg_add_string(&req, "ubus_rpc_session", sid);
106 blobmsg_add_string(&req, "scope", "cgi-io");
107 blobmsg_add_string(&req, "object", obj);
108 blobmsg_add_string(&req, "function", func);
109
110 ubus_invoke(ctx, id, "access", req.head, session_access_cb, &allow, 500);
111
112 out:
113 if (ctx)
114 ubus_free(ctx);
115
116 return allow;
117 }
118
119 static char *
120 checksum(const char *applet, size_t sumlen, const char *file)
121 {
122 pid_t pid;
123 int fds[2];
124 static char chksum[65];
125
126 if (pipe(fds))
127 return NULL;
128
129 switch ((pid = fork()))
130 {
131 case -1:
132 return NULL;
133
134 case 0:
135 uloop_done();
136
137 dup2(fds[1], 1);
138
139 close(0);
140 close(2);
141 close(fds[0]);
142 close(fds[1]);
143
144 if (execl("/bin/busybox", "/bin/busybox", applet, file, NULL))
145 return NULL;
146
147 break;
148
149 default:
150 memset(chksum, 0, sizeof(chksum));
151 read(fds[0], chksum, sumlen);
152 waitpid(pid, NULL, 0);
153 close(fds[0]);
154 close(fds[1]);
155 }
156
157 return chksum;
158 }
159
160 static char *
161 datadup(const void *in, size_t len)
162 {
163 char *out = malloc(len + 1);
164
165 if (!out)
166 return NULL;
167
168 memcpy(out, in, len);
169
170 *(out + len) = 0;
171
172 return out;
173 }
174
175 static bool
176 urldecode(char *buf)
177 {
178 char *c, *p;
179
180 if (!buf || !*buf)
181 return true;
182
183 #define hex(x) \
184 (((x) <= '9') ? ((x) - '0') : \
185 (((x) <= 'F') ? ((x) - 'A' + 10) : \
186 ((x) - 'a' + 10)))
187
188 for (c = p = buf; *p; c++)
189 {
190 if (*p == '%')
191 {
192 if (!isxdigit(*(p + 1)) || !isxdigit(*(p + 2)))
193 return false;
194
195 *c = (char)(16 * hex(*(p + 1)) + hex(*(p + 2)));
196
197 p += 3;
198 }
199 else if (*p == '+')
200 {
201 *c = ' ';
202 p++;
203 }
204 else
205 {
206 *c = *p++;
207 }
208 }
209
210 *c = 0;
211
212 return true;
213 }
214
215 static bool
216 postdecode(char **fields, int n_fields)
217 {
218 char *p;
219 const char *var;
220 static char buf[1024];
221 int i, len, field, found = 0;
222
223 var = getenv("CONTENT_TYPE");
224
225 if (!var || strncmp(var, "application/x-www-form-urlencoded", 33))
226 return false;
227
228 memset(buf, 0, sizeof(buf));
229
230 if ((len = read(0, buf, sizeof(buf) - 1)) > 0)
231 {
232 for (p = buf, i = 0; i <= len; i++)
233 {
234 if (buf[i] == '=')
235 {
236 buf[i] = 0;
237
238 for (field = 0; field < (n_fields * 2); field += 2)
239 {
240 if (!strcmp(p, fields[field]))
241 {
242 fields[field + 1] = buf + i + 1;
243 found++;
244 }
245 }
246 }
247 else if (buf[i] == '&' || buf[i] == '\0')
248 {
249 buf[i] = 0;
250
251 if (found >= n_fields)
252 break;
253
254 p = buf + i + 1;
255 }
256 }
257 }
258
259 for (field = 0; field < (n_fields * 2); field += 2)
260 if (!urldecode(fields[field + 1]))
261 return false;
262
263 return (found >= n_fields);
264 }
265
266 static int
267 response(bool success, const char *message)
268 {
269 char *chksum;
270 struct stat s;
271
272 printf("Status: 200 OK\r\n");
273 printf("Content-Type: text/plain\r\n\r\n{\n");
274
275 if (success)
276 {
277 if (!stat(st.filename, &s))
278 printf("\t\"size\": %u,\n", (unsigned int)s.st_size);
279 else
280 printf("\t\"size\": null,\n");
281
282 chksum = checksum("md5sum", 32, st.filename);
283 printf("\t\"checksum\": %s%s%s,\n",
284 chksum ? "\"" : "",
285 chksum ? chksum : "null",
286 chksum ? "\"" : "");
287
288 chksum = checksum("sha256sum", 64, st.filename);
289 printf("\t\"sha256sum\": %s%s%s\n",
290 chksum ? "\"" : "",
291 chksum ? chksum : "null",
292 chksum ? "\"" : "");
293 }
294 else
295 {
296 if (message)
297 printf("\t\"message\": \"%s\",\n", message);
298
299 printf("\t\"failure\": [ %u, \"%s\" ]\n", errno, strerror(errno));
300
301 if (st.filefd > -1)
302 unlink(st.filename);
303 }
304
305 printf("}\n");
306
307 return -1;
308 }
309
310 static int
311 failure(int e, const char *message)
312 {
313 printf("Status: 500 Internal Server failure\r\n");
314 printf("Content-Type: text/plain\r\n\r\n");
315 printf("%s", message);
316
317 if (e)
318 printf(": %s", strerror(e));
319
320 return -1;
321 }
322
323 static int
324 filecopy(void)
325 {
326 int len;
327 char buf[4096];
328
329 if (!st.filedata)
330 {
331 close(st.tempfd);
332 errno = EINVAL;
333 return response(false, "No file data received");
334 }
335
336 if (lseek(st.tempfd, 0, SEEK_SET) < 0)
337 {
338 close(st.tempfd);
339 return response(false, "Failed to rewind temp file");
340 }
341
342 st.filefd = open(st.filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
343
344 if (st.filefd < 0)
345 {
346 close(st.tempfd);
347 return response(false, "Failed to open target file");
348 }
349
350 while ((len = read(st.tempfd, buf, sizeof(buf))) > 0)
351 {
352 if (write(st.filefd, buf, len) != len)
353 {
354 close(st.tempfd);
355 close(st.filefd);
356 return response(false, "I/O failure while writing target file");
357 }
358 }
359
360 close(st.tempfd);
361 close(st.filefd);
362
363 if (chmod(st.filename, st.filemode))
364 return response(false, "Failed to chmod target file");
365
366 return 0;
367 }
368
369 static int
370 header_field(multipart_parser *p, const char *data, size_t len)
371 {
372 st.is_content_disposition = !strncasecmp(data, "Content-Disposition", len);
373 return 0;
374 }
375
376 static int
377 header_value(multipart_parser *p, const char *data, size_t len)
378 {
379 int i, j;
380
381 if (!st.is_content_disposition)
382 return 0;
383
384 if (len < 10 || strncasecmp(data, "form-data", 9))
385 return 0;
386
387 for (data += 9, len -= 9; *data == ' ' || *data == ';'; data++, len--);
388
389 if (len < 8 || strncasecmp(data, "name=\"", 6))
390 return 0;
391
392 for (data += 6, len -= 6, i = 0; i <= len; i++)
393 {
394 if (*(data + i) != '"')
395 continue;
396
397 for (j = 1; j < sizeof(parts) / sizeof(parts[0]); j++)
398 if (!strncmp(data, parts[j], i))
399 st.parttype = j;
400
401 break;
402 }
403
404 return 0;
405 }
406
407 static int
408 data_begin_cb(multipart_parser *p)
409 {
410 char tmpname[24] = "/tmp/luci-upload.XXXXXX";
411
412 if (st.parttype == PART_FILEDATA)
413 {
414 if (!st.sessionid)
415 return response(false, "File data without session");
416
417 if (!st.filename)
418 return response(false, "File data without name");
419
420 st.tempfd = mkstemp(tmpname);
421
422 if (st.tempfd < 0)
423 return response(false, "Failed to create temporary file");
424
425 unlink(tmpname);
426 }
427
428 return 0;
429 }
430
431 static int
432 data_cb(multipart_parser *p, const char *data, size_t len)
433 {
434 switch (st.parttype)
435 {
436 case PART_SESSIONID:
437 st.sessionid = datadup(data, len);
438 break;
439
440 case PART_FILENAME:
441 st.filename = datadup(data, len);
442 break;
443
444 case PART_FILEMODE:
445 st.filemode = strtoul(data, NULL, 8);
446 break;
447
448 case PART_FILEDATA:
449 if (write(st.tempfd, data, len) != len)
450 {
451 close(st.tempfd);
452 return response(false, "I/O failure while writing temporary file");
453 }
454
455 if (!st.filedata)
456 st.filedata = !!len;
457
458 break;
459
460 default:
461 break;
462 }
463
464 return 0;
465 }
466
467 static int
468 data_end_cb(multipart_parser *p)
469 {
470 if (st.parttype == PART_SESSIONID)
471 {
472 if (!session_access(st.sessionid, "upload", "write"))
473 {
474 errno = EPERM;
475 return response(false, "Upload permission denied");
476 }
477 }
478 else if (st.parttype == PART_FILEDATA)
479 {
480 if (st.tempfd < 0)
481 return response(false, "Internal program failure");
482
483 #if 0
484 /* prepare directory */
485 for (ptr = st.filename; *ptr; ptr++)
486 {
487 if (*ptr == '/')
488 {
489 *ptr = 0;
490
491 if (mkdir(st.filename, 0755))
492 {
493 unlink(st.tmpname);
494 return response(false, "Failed to create destination directory");
495 }
496
497 *ptr = '/';
498 }
499 }
500 #endif
501
502 if (filecopy())
503 return -1;
504
505 return response(true, NULL);
506 }
507
508 st.parttype = PART_UNKNOWN;
509 return 0;
510 }
511
512 static multipart_parser *
513 init_parser(void)
514 {
515 char *boundary;
516 const char *var;
517
518 multipart_parser *p;
519 static multipart_parser_settings s = {
520 .on_part_data = data_cb,
521 .on_headers_complete = data_begin_cb,
522 .on_part_data_end = data_end_cb,
523 .on_header_field = header_field,
524 .on_header_value = header_value
525 };
526
527 var = getenv("CONTENT_TYPE");
528
529 if (!var || strncmp(var, "multipart/form-data;", 20))
530 return NULL;
531
532 for (var += 20; *var && *var != '='; var++);
533
534 if (*var++ != '=')
535 return NULL;
536
537 boundary = malloc(strlen(var) + 3);
538
539 if (!boundary)
540 return NULL;
541
542 strcpy(boundary, "--");
543 strcpy(boundary + 2, var);
544
545 st.tempfd = -1;
546 st.filefd = -1;
547 st.filemode = 0600;
548
549 p = multipart_parser_init(boundary, &s);
550
551 free(boundary);
552
553 return p;
554 }
555
556 static int
557 main_upload(int argc, char *argv[])
558 {
559 int rem, len;
560 char buf[4096];
561 multipart_parser *p;
562
563 p = init_parser();
564
565 if (!p)
566 {
567 errno = EINVAL;
568 return response(false, "Invalid request");
569 }
570
571 while ((len = read(0, buf, sizeof(buf))) > 0)
572 {
573 rem = multipart_parser_execute(p, buf, len);
574
575 if (rem < len)
576 break;
577 }
578
579 multipart_parser_free(p);
580
581 /* read remaining post data */
582 while ((len = read(0, buf, sizeof(buf))) > 0);
583
584 return 0;
585 }
586
587 static int
588 main_backup(int argc, char **argv)
589 {
590 pid_t pid;
591 time_t now;
592 int len;
593 int status;
594 int fds[2];
595 char buf[4096];
596 char datestr[16] = { 0 };
597 char hostname[64] = { 0 };
598 char *fields[] = { "sessionid", NULL };
599
600 if (!postdecode(fields, 1) || !session_access(fields[1], "backup", "read"))
601 return failure(0, "Backup permission denied");
602
603 if (pipe(fds))
604 return failure(errno, "Failed to spawn pipe");
605
606 switch ((pid = fork()))
607 {
608 case -1:
609 return failure(errno, "Failed to fork process");
610
611 case 0:
612 dup2(fds[1], 1);
613
614 close(0);
615 close(2);
616 close(fds[0]);
617 close(fds[1]);
618
619 chdir("/");
620
621 execl("/sbin/sysupgrade", "/sbin/sysupgrade",
622 "--create-backup", "-", NULL);
623
624 return -1;
625
626 default:
627 fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | O_NONBLOCK);
628 now = time(NULL);
629 strftime(datestr, sizeof(datestr) - 1, "%Y-%m-%d", localtime(&now));
630
631 if (gethostname(hostname, sizeof(hostname) - 1))
632 sprintf(hostname, "OpenWrt");
633
634 printf("Status: 200 OK\r\n");
635 printf("Content-Type: application/x-targz\r\n");
636 printf("Content-Disposition: attachment; "
637 "filename=\"backup-%s-%s.tar.gz\"\r\n\r\n", hostname, datestr);
638
639 do {
640 waitpid(pid, &status, 0);
641
642 while ((len = read(fds[0], buf, sizeof(buf))) > 0) {
643 fwrite(buf, len, 1, stdout);
644 fflush(stdout);
645 }
646
647 } while (!WIFEXITED(status));
648
649 close(fds[0]);
650 close(fds[1]);
651
652 return 0;
653 }
654 }
655
656 int main(int argc, char **argv)
657 {
658 if (strstr(argv[0], "cgi-upload"))
659 return main_upload(argc, argv);
660 else if (strstr(argv[0], "cgi-backup"))
661 return main_backup(argc, argv);
662
663 return -1;
664 }