cgi-io: pass appropriate HTTP error codes to failure()
[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 #include <sys/sendfile.h>
30 #include <sys/ioctl.h>
31 #include <linux/fs.h>
32
33 #include <libubus.h>
34 #include <libubox/blobmsg.h>
35
36 #include "multipart_parser.h"
37
38
39 enum part {
40 PART_UNKNOWN,
41 PART_SESSIONID,
42 PART_FILENAME,
43 PART_FILEMODE,
44 PART_FILEDATA
45 };
46
47 const char *parts[] = {
48 "(bug)",
49 "sessionid",
50 "filename",
51 "filemode",
52 "filedata",
53 };
54
55 struct state
56 {
57 bool is_content_disposition;
58 enum part parttype;
59 char *sessionid;
60 char *filename;
61 bool filedata;
62 int filemode;
63 int filefd;
64 int tempfd;
65 };
66
67 enum {
68 SES_ACCESS,
69 __SES_MAX,
70 };
71
72 static const struct blobmsg_policy ses_policy[__SES_MAX] = {
73 [SES_ACCESS] = { .name = "access", .type = BLOBMSG_TYPE_BOOL },
74 };
75
76
77 static struct state st;
78
79 static void
80 session_access_cb(struct ubus_request *req, int type, struct blob_attr *msg)
81 {
82 struct blob_attr *tb[__SES_MAX];
83 bool *allow = (bool *)req->priv;
84
85 if (!msg)
86 return;
87
88 blobmsg_parse(ses_policy, __SES_MAX, tb, blob_data(msg), blob_len(msg));
89
90 if (tb[SES_ACCESS])
91 *allow = blobmsg_get_bool(tb[SES_ACCESS]);
92 }
93
94 static bool
95 session_access(const char *sid, const char *scope, const char *obj, const char *func)
96 {
97 uint32_t id;
98 bool allow = false;
99 struct ubus_context *ctx;
100 static struct blob_buf req;
101
102 ctx = ubus_connect(NULL);
103
104 if (!ctx || ubus_lookup_id(ctx, "session", &id))
105 goto out;
106
107 blob_buf_init(&req, 0);
108 blobmsg_add_string(&req, "ubus_rpc_session", sid);
109 blobmsg_add_string(&req, "scope", scope);
110 blobmsg_add_string(&req, "object", obj);
111 blobmsg_add_string(&req, "function", func);
112
113 ubus_invoke(ctx, id, "access", req.head, session_access_cb, &allow, 500);
114
115 out:
116 if (ctx)
117 ubus_free(ctx);
118
119 return allow;
120 }
121
122 static char *
123 checksum(const char *applet, size_t sumlen, const char *file)
124 {
125 pid_t pid;
126 int fds[2];
127 static char chksum[65];
128
129 if (pipe(fds))
130 return NULL;
131
132 switch ((pid = fork()))
133 {
134 case -1:
135 return NULL;
136
137 case 0:
138 uloop_done();
139
140 dup2(fds[1], 1);
141
142 close(0);
143 close(2);
144 close(fds[0]);
145 close(fds[1]);
146
147 if (execl("/bin/busybox", "/bin/busybox", applet, file, NULL))
148 return NULL;
149
150 break;
151
152 default:
153 memset(chksum, 0, sizeof(chksum));
154 read(fds[0], chksum, sumlen);
155 waitpid(pid, NULL, 0);
156 close(fds[0]);
157 close(fds[1]);
158 }
159
160 return chksum;
161 }
162
163 static char *
164 datadup(const void *in, size_t len)
165 {
166 char *out = malloc(len + 1);
167
168 if (!out)
169 return NULL;
170
171 memcpy(out, in, len);
172
173 *(out + len) = 0;
174
175 return out;
176 }
177
178 static bool
179 urldecode(char *buf)
180 {
181 char *c, *p;
182
183 if (!buf || !*buf)
184 return true;
185
186 #define hex(x) \
187 (((x) <= '9') ? ((x) - '0') : \
188 (((x) <= 'F') ? ((x) - 'A' + 10) : \
189 ((x) - 'a' + 10)))
190
191 for (c = p = buf; *p; c++)
192 {
193 if (*p == '%')
194 {
195 if (!isxdigit(*(p + 1)) || !isxdigit(*(p + 2)))
196 return false;
197
198 *c = (char)(16 * hex(*(p + 1)) + hex(*(p + 2)));
199
200 p += 3;
201 }
202 else if (*p == '+')
203 {
204 *c = ' ';
205 p++;
206 }
207 else
208 {
209 *c = *p++;
210 }
211 }
212
213 *c = 0;
214
215 return true;
216 }
217
218 static bool
219 postdecode(char **fields, int n_fields)
220 {
221 char *p;
222 const char *var;
223 static char buf[1024];
224 int i, len, field, found = 0;
225
226 var = getenv("CONTENT_TYPE");
227
228 if (!var || strncmp(var, "application/x-www-form-urlencoded", 33))
229 return false;
230
231 memset(buf, 0, sizeof(buf));
232
233 if ((len = read(0, buf, sizeof(buf) - 1)) > 0)
234 {
235 for (p = buf, i = 0; i <= len; i++)
236 {
237 if (buf[i] == '=')
238 {
239 buf[i] = 0;
240
241 for (field = 0; field < (n_fields * 2); field += 2)
242 {
243 if (!strcmp(p, fields[field]))
244 {
245 fields[field + 1] = buf + i + 1;
246 found++;
247 }
248 }
249 }
250 else if (buf[i] == '&' || buf[i] == '\0')
251 {
252 buf[i] = 0;
253
254 if (found >= n_fields)
255 break;
256
257 p = buf + i + 1;
258 }
259 }
260 }
261
262 for (field = 0; field < (n_fields * 2); field += 2)
263 if (!urldecode(fields[field + 1]))
264 return false;
265
266 return (found >= n_fields);
267 }
268
269 static char *
270 canonicalize_path(const char *path, size_t len)
271 {
272 char *canonpath, *cp;
273 const char *p, *e;
274
275 if (path == NULL || *path == '\0')
276 return NULL;
277
278 canonpath = datadup(path, len);
279
280 if (canonpath == NULL)
281 return NULL;
282
283 /* normalize */
284 for (cp = canonpath, p = path, e = path + len; p < e; ) {
285 if (*p != '/')
286 goto next;
287
288 /* skip repeating / */
289 if ((p + 1 < e) && (p[1] == '/')) {
290 p++;
291 continue;
292 }
293
294 /* /./ or /../ */
295 if ((p + 1 < e) && (p[1] == '.')) {
296 /* skip /./ */
297 if ((p + 2 >= e) || (p[2] == '/')) {
298 p += 2;
299 continue;
300 }
301
302 /* collapse /x/../ */
303 if ((p + 2 < e) && (p[2] == '.') && ((p + 3 >= e) || (p[3] == '/'))) {
304 while ((cp > canonpath) && (*--cp != '/'))
305 ;
306
307 p += 3;
308 continue;
309 }
310 }
311
312 next:
313 *cp++ = *p++;
314 }
315
316 /* remove trailing slash if not root / */
317 if ((cp > canonpath + 1) && (cp[-1] == '/'))
318 cp--;
319 else if (cp == canonpath)
320 *cp++ = '/';
321
322 *cp = '\0';
323
324 return canonpath;
325 }
326
327 static int
328 response(bool success, const char *message)
329 {
330 char *chksum;
331 struct stat s;
332
333 printf("Status: 200 OK\r\n");
334 printf("Content-Type: text/plain\r\n\r\n{\n");
335
336 if (success)
337 {
338 if (!stat(st.filename, &s))
339 printf("\t\"size\": %u,\n", (unsigned int)s.st_size);
340 else
341 printf("\t\"size\": null,\n");
342
343 chksum = checksum("md5sum", 32, st.filename);
344 printf("\t\"checksum\": %s%s%s,\n",
345 chksum ? "\"" : "",
346 chksum ? chksum : "null",
347 chksum ? "\"" : "");
348
349 chksum = checksum("sha256sum", 64, st.filename);
350 printf("\t\"sha256sum\": %s%s%s\n",
351 chksum ? "\"" : "",
352 chksum ? chksum : "null",
353 chksum ? "\"" : "");
354 }
355 else
356 {
357 if (message)
358 printf("\t\"message\": \"%s\",\n", message);
359
360 printf("\t\"failure\": [ %u, \"%s\" ]\n", errno, strerror(errno));
361
362 if (st.filefd > -1)
363 unlink(st.filename);
364 }
365
366 printf("}\n");
367
368 return -1;
369 }
370
371 static int
372 failure(int code, int e, const char *message)
373 {
374 printf("Status: %d %s\r\n", code, message);
375 printf("Content-Type: text/plain\r\n\r\n");
376 printf("%s", message);
377
378 if (e)
379 printf(": %s", strerror(e));
380
381 printf("\n");
382
383 return -1;
384 }
385
386 static int
387 filecopy(void)
388 {
389 int len;
390 char buf[4096];
391
392 if (!st.filedata)
393 {
394 close(st.tempfd);
395 errno = EINVAL;
396 return response(false, "No file data received");
397 }
398
399 if (lseek(st.tempfd, 0, SEEK_SET) < 0)
400 {
401 close(st.tempfd);
402 return response(false, "Failed to rewind temp file");
403 }
404
405 st.filefd = open(st.filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
406
407 if (st.filefd < 0)
408 {
409 close(st.tempfd);
410 return response(false, "Failed to open target file");
411 }
412
413 while ((len = read(st.tempfd, buf, sizeof(buf))) > 0)
414 {
415 if (write(st.filefd, buf, len) != len)
416 {
417 close(st.tempfd);
418 close(st.filefd);
419 return response(false, "I/O failure while writing target file");
420 }
421 }
422
423 close(st.tempfd);
424 close(st.filefd);
425
426 if (chmod(st.filename, st.filemode))
427 return response(false, "Failed to chmod target file");
428
429 return 0;
430 }
431
432 static int
433 header_field(multipart_parser *p, const char *data, size_t len)
434 {
435 st.is_content_disposition = !strncasecmp(data, "Content-Disposition", len);
436 return 0;
437 }
438
439 static int
440 header_value(multipart_parser *p, const char *data, size_t len)
441 {
442 int i, j;
443
444 if (!st.is_content_disposition)
445 return 0;
446
447 if (len < 10 || strncasecmp(data, "form-data", 9))
448 return 0;
449
450 for (data += 9, len -= 9; *data == ' ' || *data == ';'; data++, len--);
451
452 if (len < 8 || strncasecmp(data, "name=\"", 6))
453 return 0;
454
455 for (data += 6, len -= 6, i = 0; i <= len; i++)
456 {
457 if (*(data + i) != '"')
458 continue;
459
460 for (j = 1; j < sizeof(parts) / sizeof(parts[0]); j++)
461 if (!strncmp(data, parts[j], i))
462 st.parttype = j;
463
464 break;
465 }
466
467 return 0;
468 }
469
470 static int
471 data_begin_cb(multipart_parser *p)
472 {
473 char tmpname[24] = "/tmp/luci-upload.XXXXXX";
474
475 if (st.parttype == PART_FILEDATA)
476 {
477 if (!st.sessionid)
478 return response(false, "File data without session");
479
480 if (!st.filename)
481 return response(false, "File data without name");
482
483 if (!session_access(st.sessionid, "file", st.filename, "write"))
484 return response(false, "Access to path denied by ACL");
485
486 st.tempfd = mkstemp(tmpname);
487
488 if (st.tempfd < 0)
489 return response(false, "Failed to create temporary file");
490
491 unlink(tmpname);
492 }
493
494 return 0;
495 }
496
497 static int
498 data_cb(multipart_parser *p, const char *data, size_t len)
499 {
500 switch (st.parttype)
501 {
502 case PART_SESSIONID:
503 st.sessionid = datadup(data, len);
504 break;
505
506 case PART_FILENAME:
507 st.filename = canonicalize_path(data, len);
508 break;
509
510 case PART_FILEMODE:
511 st.filemode = strtoul(data, NULL, 8);
512 break;
513
514 case PART_FILEDATA:
515 if (write(st.tempfd, data, len) != len)
516 {
517 close(st.tempfd);
518 return response(false, "I/O failure while writing temporary file");
519 }
520
521 if (!st.filedata)
522 st.filedata = !!len;
523
524 break;
525
526 default:
527 break;
528 }
529
530 return 0;
531 }
532
533 static int
534 data_end_cb(multipart_parser *p)
535 {
536 if (st.parttype == PART_SESSIONID)
537 {
538 if (!session_access(st.sessionid, "cgi-io", "upload", "write"))
539 {
540 errno = EPERM;
541 return response(false, "Upload permission denied");
542 }
543 }
544 else if (st.parttype == PART_FILEDATA)
545 {
546 if (st.tempfd < 0)
547 return response(false, "Internal program failure");
548
549 #if 0
550 /* prepare directory */
551 for (ptr = st.filename; *ptr; ptr++)
552 {
553 if (*ptr == '/')
554 {
555 *ptr = 0;
556
557 if (mkdir(st.filename, 0755))
558 {
559 unlink(st.tmpname);
560 return response(false, "Failed to create destination directory");
561 }
562
563 *ptr = '/';
564 }
565 }
566 #endif
567
568 if (filecopy())
569 return -1;
570
571 return response(true, NULL);
572 }
573
574 st.parttype = PART_UNKNOWN;
575 return 0;
576 }
577
578 static multipart_parser *
579 init_parser(void)
580 {
581 char *boundary;
582 const char *var;
583
584 multipart_parser *p;
585 static multipart_parser_settings s = {
586 .on_part_data = data_cb,
587 .on_headers_complete = data_begin_cb,
588 .on_part_data_end = data_end_cb,
589 .on_header_field = header_field,
590 .on_header_value = header_value
591 };
592
593 var = getenv("CONTENT_TYPE");
594
595 if (!var || strncmp(var, "multipart/form-data;", 20))
596 return NULL;
597
598 for (var += 20; *var && *var != '='; var++);
599
600 if (*var++ != '=')
601 return NULL;
602
603 boundary = malloc(strlen(var) + 3);
604
605 if (!boundary)
606 return NULL;
607
608 strcpy(boundary, "--");
609 strcpy(boundary + 2, var);
610
611 st.tempfd = -1;
612 st.filefd = -1;
613 st.filemode = 0600;
614
615 p = multipart_parser_init(boundary, &s);
616
617 free(boundary);
618
619 return p;
620 }
621
622 static int
623 main_upload(int argc, char *argv[])
624 {
625 int rem, len;
626 char buf[4096];
627 multipart_parser *p;
628
629 p = init_parser();
630
631 if (!p)
632 {
633 errno = EINVAL;
634 return response(false, "Invalid request");
635 }
636
637 while ((len = read(0, buf, sizeof(buf))) > 0)
638 {
639 rem = multipart_parser_execute(p, buf, len);
640
641 if (rem < len)
642 break;
643 }
644
645 multipart_parser_free(p);
646
647 /* read remaining post data */
648 while ((len = read(0, buf, sizeof(buf))) > 0);
649
650 return 0;
651 }
652
653 static int
654 main_download(int argc, char **argv)
655 {
656 char *fields[] = { "sessionid", NULL, "path", NULL, "filename", NULL, "mimetype", NULL };
657 unsigned long long size = 0;
658 char *p, buf[4096];
659 ssize_t len = 0;
660 struct stat s;
661 int rfd;
662
663 postdecode(fields, 4);
664
665 if (!fields[1] || !session_access(fields[1], "cgi-io", "download", "read"))
666 return failure(403, 0, "Download permission denied");
667
668 if (!fields[3] || !session_access(fields[1], "file", fields[3], "read"))
669 return failure(403, 0, "Access to path denied by ACL");
670
671 if (stat(fields[3], &s))
672 return failure(404, errno, "Failed to stat requested path");
673
674 if (!S_ISREG(s.st_mode) && !S_ISBLK(s.st_mode))
675 return failure(403, 0, "Requested path is not a regular file or block device");
676
677 for (p = fields[5]; p && *p; p++)
678 if (!isalnum(*p) && !strchr(" ()<>@,;:[]?.=%", *p))
679 return failure(400, 0, "Invalid characters in filename");
680
681 for (p = fields[7]; p && *p; p++)
682 if (!isalnum(*p) && !strchr(" .;=/-", *p))
683 return failure(400, 0, "Invalid characters in mimetype");
684
685 rfd = open(fields[3], O_RDONLY);
686
687 if (rfd < 0)
688 return failure(500, errno, "Failed to open requested path");
689
690 if (S_ISBLK(s.st_mode))
691 ioctl(rfd, BLKGETSIZE64, &size);
692 else
693 size = (unsigned long long)s.st_size;
694
695 printf("Status: 200 OK\r\n");
696 printf("Content-Type: %s\r\n", fields[7] ? fields[7] : "application/octet-stream");
697
698 if (fields[5])
699 printf("Content-Disposition: attachment; filename=\"%s\"\r\n", fields[5]);
700
701 printf("Content-Length: %llu\r\n\r\n", size);
702 fflush(stdout);
703
704 while (size > 0) {
705 len = sendfile(1, rfd, NULL, size);
706
707 if (len == -1) {
708 if (errno == ENOSYS || errno == EINVAL) {
709 while ((len = read(rfd, buf, sizeof(buf))) > 0)
710 fwrite(buf, len, 1, stdout);
711
712 fflush(stdout);
713 break;
714 }
715
716 if (errno == EINTR || errno == EAGAIN)
717 continue;
718 }
719
720 if (len <= 0)
721 break;
722
723 size -= len;
724 }
725
726 close(rfd);
727
728 return 0;
729 }
730
731 static int
732 main_backup(int argc, char **argv)
733 {
734 pid_t pid;
735 time_t now;
736 int len;
737 int status;
738 int fds[2];
739 char buf[4096];
740 char datestr[16] = { 0 };
741 char hostname[64] = { 0 };
742 char *fields[] = { "sessionid", NULL };
743
744 if (!postdecode(fields, 1) || !session_access(fields[1], "cgi-io", "backup", "read"))
745 return failure(403, 0, "Backup permission denied");
746
747 if (pipe(fds))
748 return failure(500, errno, "Failed to spawn pipe");
749
750 switch ((pid = fork()))
751 {
752 case -1:
753 return failure(500, errno, "Failed to fork process");
754
755 case 0:
756 dup2(fds[1], 1);
757
758 close(0);
759 close(2);
760 close(fds[0]);
761 close(fds[1]);
762
763 chdir("/");
764
765 execl("/sbin/sysupgrade", "/sbin/sysupgrade",
766 "--create-backup", "-", NULL);
767
768 return -1;
769
770 default:
771 fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | O_NONBLOCK);
772 now = time(NULL);
773 strftime(datestr, sizeof(datestr) - 1, "%Y-%m-%d", localtime(&now));
774
775 if (gethostname(hostname, sizeof(hostname) - 1))
776 sprintf(hostname, "OpenWrt");
777
778 printf("Status: 200 OK\r\n");
779 printf("Content-Type: application/x-targz\r\n");
780 printf("Content-Disposition: attachment; "
781 "filename=\"backup-%s-%s.tar.gz\"\r\n\r\n", hostname, datestr);
782
783 do {
784 waitpid(pid, &status, 0);
785
786 while ((len = read(fds[0], buf, sizeof(buf))) > 0) {
787 fwrite(buf, len, 1, stdout);
788 fflush(stdout);
789 }
790
791 } while (!WIFEXITED(status));
792
793 close(fds[0]);
794 close(fds[1]);
795
796 return 0;
797 }
798 }
799
800 int main(int argc, char **argv)
801 {
802 if (strstr(argv[0], "cgi-upload"))
803 return main_upload(argc, argv);
804 else if (strstr(argv[0], "cgi-download"))
805 return main_download(argc, argv);
806 else if (strstr(argv[0], "cgi-backup"))
807 return main_backup(argc, argv);
808
809 return -1;
810 }