44a520576028ef722829681528de17017474539a
[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 *scope, 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", scope);
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 char *
267 canonicalize_path(const char *path, size_t len)
268 {
269 char *canonpath, *cp;
270 const char *p, *e;
271
272 if (path == NULL || *path == '\0')
273 return NULL;
274
275 canonpath = datadup(path, len);
276
277 if (canonpath == NULL)
278 return NULL;
279
280 /* normalize */
281 for (cp = canonpath, p = path, e = path + len; p < e; ) {
282 if (*p != '/')
283 goto next;
284
285 /* skip repeating / */
286 if ((p + 1 < e) && (p[1] == '/')) {
287 p++;
288 continue;
289 }
290
291 /* /./ or /../ */
292 if ((p + 1 < e) && (p[1] == '.')) {
293 /* skip /./ */
294 if ((p + 2 >= e) || (p[2] == '/')) {
295 p += 2;
296 continue;
297 }
298
299 /* collapse /x/../ */
300 if ((p + 2 < e) && (p[2] == '.') && ((p + 3 >= e) || (p[3] == '/'))) {
301 while ((cp > canonpath) && (*--cp != '/'))
302 ;
303
304 p += 3;
305 continue;
306 }
307 }
308
309 next:
310 *cp++ = *p++;
311 }
312
313 /* remove trailing slash if not root / */
314 if ((cp > canonpath + 1) && (cp[-1] == '/'))
315 cp--;
316 else if (cp == canonpath)
317 *cp++ = '/';
318
319 *cp = '\0';
320
321 return canonpath;
322 }
323
324 static int
325 response(bool success, const char *message)
326 {
327 char *chksum;
328 struct stat s;
329
330 printf("Status: 200 OK\r\n");
331 printf("Content-Type: text/plain\r\n\r\n{\n");
332
333 if (success)
334 {
335 if (!stat(st.filename, &s))
336 printf("\t\"size\": %u,\n", (unsigned int)s.st_size);
337 else
338 printf("\t\"size\": null,\n");
339
340 chksum = checksum("md5sum", 32, st.filename);
341 printf("\t\"checksum\": %s%s%s,\n",
342 chksum ? "\"" : "",
343 chksum ? chksum : "null",
344 chksum ? "\"" : "");
345
346 chksum = checksum("sha256sum", 64, st.filename);
347 printf("\t\"sha256sum\": %s%s%s\n",
348 chksum ? "\"" : "",
349 chksum ? chksum : "null",
350 chksum ? "\"" : "");
351 }
352 else
353 {
354 if (message)
355 printf("\t\"message\": \"%s\",\n", message);
356
357 printf("\t\"failure\": [ %u, \"%s\" ]\n", errno, strerror(errno));
358
359 if (st.filefd > -1)
360 unlink(st.filename);
361 }
362
363 printf("}\n");
364
365 return -1;
366 }
367
368 static int
369 failure(int e, const char *message)
370 {
371 printf("Status: 500 Internal Server failure\r\n");
372 printf("Content-Type: text/plain\r\n\r\n");
373 printf("%s", message);
374
375 if (e)
376 printf(": %s", strerror(e));
377
378 return -1;
379 }
380
381 static int
382 filecopy(void)
383 {
384 int len;
385 char buf[4096];
386
387 if (!st.filedata)
388 {
389 close(st.tempfd);
390 errno = EINVAL;
391 return response(false, "No file data received");
392 }
393
394 if (lseek(st.tempfd, 0, SEEK_SET) < 0)
395 {
396 close(st.tempfd);
397 return response(false, "Failed to rewind temp file");
398 }
399
400 st.filefd = open(st.filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
401
402 if (st.filefd < 0)
403 {
404 close(st.tempfd);
405 return response(false, "Failed to open target file");
406 }
407
408 while ((len = read(st.tempfd, buf, sizeof(buf))) > 0)
409 {
410 if (write(st.filefd, buf, len) != len)
411 {
412 close(st.tempfd);
413 close(st.filefd);
414 return response(false, "I/O failure while writing target file");
415 }
416 }
417
418 close(st.tempfd);
419 close(st.filefd);
420
421 if (chmod(st.filename, st.filemode))
422 return response(false, "Failed to chmod target file");
423
424 return 0;
425 }
426
427 static int
428 header_field(multipart_parser *p, const char *data, size_t len)
429 {
430 st.is_content_disposition = !strncasecmp(data, "Content-Disposition", len);
431 return 0;
432 }
433
434 static int
435 header_value(multipart_parser *p, const char *data, size_t len)
436 {
437 int i, j;
438
439 if (!st.is_content_disposition)
440 return 0;
441
442 if (len < 10 || strncasecmp(data, "form-data", 9))
443 return 0;
444
445 for (data += 9, len -= 9; *data == ' ' || *data == ';'; data++, len--);
446
447 if (len < 8 || strncasecmp(data, "name=\"", 6))
448 return 0;
449
450 for (data += 6, len -= 6, i = 0; i <= len; i++)
451 {
452 if (*(data + i) != '"')
453 continue;
454
455 for (j = 1; j < sizeof(parts) / sizeof(parts[0]); j++)
456 if (!strncmp(data, parts[j], i))
457 st.parttype = j;
458
459 break;
460 }
461
462 return 0;
463 }
464
465 static int
466 data_begin_cb(multipart_parser *p)
467 {
468 char tmpname[24] = "/tmp/luci-upload.XXXXXX";
469
470 if (st.parttype == PART_FILEDATA)
471 {
472 if (!st.sessionid)
473 return response(false, "File data without session");
474
475 if (!st.filename)
476 return response(false, "File data without name");
477
478 if (!session_access(st.sessionid, "file", st.filename, "write"))
479 return response(false, "Access to path denied by ACL");
480
481 st.tempfd = mkstemp(tmpname);
482
483 if (st.tempfd < 0)
484 return response(false, "Failed to create temporary file");
485
486 unlink(tmpname);
487 }
488
489 return 0;
490 }
491
492 static int
493 data_cb(multipart_parser *p, const char *data, size_t len)
494 {
495 switch (st.parttype)
496 {
497 case PART_SESSIONID:
498 st.sessionid = datadup(data, len);
499 break;
500
501 case PART_FILENAME:
502 st.filename = canonicalize_path(data, len);
503 break;
504
505 case PART_FILEMODE:
506 st.filemode = strtoul(data, NULL, 8);
507 break;
508
509 case PART_FILEDATA:
510 if (write(st.tempfd, data, len) != len)
511 {
512 close(st.tempfd);
513 return response(false, "I/O failure while writing temporary file");
514 }
515
516 if (!st.filedata)
517 st.filedata = !!len;
518
519 break;
520
521 default:
522 break;
523 }
524
525 return 0;
526 }
527
528 static int
529 data_end_cb(multipart_parser *p)
530 {
531 if (st.parttype == PART_SESSIONID)
532 {
533 if (!session_access(st.sessionid, "cgi-io", "upload", "write"))
534 {
535 errno = EPERM;
536 return response(false, "Upload permission denied");
537 }
538 }
539 else if (st.parttype == PART_FILEDATA)
540 {
541 if (st.tempfd < 0)
542 return response(false, "Internal program failure");
543
544 #if 0
545 /* prepare directory */
546 for (ptr = st.filename; *ptr; ptr++)
547 {
548 if (*ptr == '/')
549 {
550 *ptr = 0;
551
552 if (mkdir(st.filename, 0755))
553 {
554 unlink(st.tmpname);
555 return response(false, "Failed to create destination directory");
556 }
557
558 *ptr = '/';
559 }
560 }
561 #endif
562
563 if (filecopy())
564 return -1;
565
566 return response(true, NULL);
567 }
568
569 st.parttype = PART_UNKNOWN;
570 return 0;
571 }
572
573 static multipart_parser *
574 init_parser(void)
575 {
576 char *boundary;
577 const char *var;
578
579 multipart_parser *p;
580 static multipart_parser_settings s = {
581 .on_part_data = data_cb,
582 .on_headers_complete = data_begin_cb,
583 .on_part_data_end = data_end_cb,
584 .on_header_field = header_field,
585 .on_header_value = header_value
586 };
587
588 var = getenv("CONTENT_TYPE");
589
590 if (!var || strncmp(var, "multipart/form-data;", 20))
591 return NULL;
592
593 for (var += 20; *var && *var != '='; var++);
594
595 if (*var++ != '=')
596 return NULL;
597
598 boundary = malloc(strlen(var) + 3);
599
600 if (!boundary)
601 return NULL;
602
603 strcpy(boundary, "--");
604 strcpy(boundary + 2, var);
605
606 st.tempfd = -1;
607 st.filefd = -1;
608 st.filemode = 0600;
609
610 p = multipart_parser_init(boundary, &s);
611
612 free(boundary);
613
614 return p;
615 }
616
617 static int
618 main_upload(int argc, char *argv[])
619 {
620 int rem, len;
621 char buf[4096];
622 multipart_parser *p;
623
624 p = init_parser();
625
626 if (!p)
627 {
628 errno = EINVAL;
629 return response(false, "Invalid request");
630 }
631
632 while ((len = read(0, buf, sizeof(buf))) > 0)
633 {
634 rem = multipart_parser_execute(p, buf, len);
635
636 if (rem < len)
637 break;
638 }
639
640 multipart_parser_free(p);
641
642 /* read remaining post data */
643 while ((len = read(0, buf, sizeof(buf))) > 0);
644
645 return 0;
646 }
647
648 static int
649 main_backup(int argc, char **argv)
650 {
651 pid_t pid;
652 time_t now;
653 int len;
654 int status;
655 int fds[2];
656 char buf[4096];
657 char datestr[16] = { 0 };
658 char hostname[64] = { 0 };
659 char *fields[] = { "sessionid", NULL };
660
661 if (!postdecode(fields, 1) || !session_access(fields[1], "cgi-io", "backup", "read"))
662 return failure(0, "Backup permission denied");
663
664 if (pipe(fds))
665 return failure(errno, "Failed to spawn pipe");
666
667 switch ((pid = fork()))
668 {
669 case -1:
670 return failure(errno, "Failed to fork process");
671
672 case 0:
673 dup2(fds[1], 1);
674
675 close(0);
676 close(2);
677 close(fds[0]);
678 close(fds[1]);
679
680 chdir("/");
681
682 execl("/sbin/sysupgrade", "/sbin/sysupgrade",
683 "--create-backup", "-", NULL);
684
685 return -1;
686
687 default:
688 fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | O_NONBLOCK);
689 now = time(NULL);
690 strftime(datestr, sizeof(datestr) - 1, "%Y-%m-%d", localtime(&now));
691
692 if (gethostname(hostname, sizeof(hostname) - 1))
693 sprintf(hostname, "OpenWrt");
694
695 printf("Status: 200 OK\r\n");
696 printf("Content-Type: application/x-targz\r\n");
697 printf("Content-Disposition: attachment; "
698 "filename=\"backup-%s-%s.tar.gz\"\r\n\r\n", hostname, datestr);
699
700 do {
701 waitpid(pid, &status, 0);
702
703 while ((len = read(fds[0], buf, sizeof(buf))) > 0) {
704 fwrite(buf, len, 1, stdout);
705 fflush(stdout);
706 }
707
708 } while (!WIFEXITED(status));
709
710 close(fds[0]);
711 close(fds[1]);
712
713 return 0;
714 }
715 }
716
717 int main(int argc, char **argv)
718 {
719 if (strstr(argv[0], "cgi-upload"))
720 return main_upload(argc, argv);
721 else if (strstr(argv[0], "cgi-backup"))
722 return main_backup(argc, argv);
723
724 return -1;
725 }