2 * cgi-io - LuCI non-RPC helper
4 * Copyright (C) 2013 Jo-Philipp Wich <jo@mein.io>
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.
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.
19 #define _GNU_SOURCE /* splice(), SPLICE_F_MORE */
31 #include <sys/sendfile.h>
32 #include <sys/ioctl.h>
36 #include <libubox/blobmsg.h>
39 #include "multipart_parser.h"
42 #define O_TMPFILE (020000000 | O_DIRECTORY)
45 #define READ_BLOCK 4096
55 const char *parts
[] = {
65 bool is_content_disposition
;
75 static struct state st
;
84 static const struct blobmsg_policy ses_policy
[__SES_MAX
] = {
85 [SES_ACCESS
] = { .name
= "access", .type
= BLOBMSG_TYPE_BOOL
},
89 session_access_cb(struct ubus_request
*req
, int type
, struct blob_attr
*msg
)
91 struct blob_attr
*tb
[__SES_MAX
];
92 bool *allow
= (bool *)req
->priv
;
97 blobmsg_parse(ses_policy
, __SES_MAX
, tb
, blob_data(msg
), blob_len(msg
));
100 *allow
= blobmsg_get_bool(tb
[SES_ACCESS
]);
105 session_access(const char *sid
, const char *scope
, const char *obj
, const char *func
)
112 struct ubus_context
*ctx
;
113 static struct blob_buf req
;
115 ctx
= ubus_connect(NULL
);
117 if (!ctx
|| !obj
|| ubus_lookup_id(ctx
, "session", &id
))
120 blob_buf_init(&req
, 0);
121 blobmsg_add_string(&req
, "ubus_rpc_session", sid
);
122 blobmsg_add_string(&req
, "scope", scope
);
123 blobmsg_add_string(&req
, "object", obj
);
124 blobmsg_add_string(&req
, "function", func
);
126 ubus_invoke(ctx
, id
, "access", req
.head
, session_access_cb
, &allow
, 500);
137 checksum(const char *applet
, size_t sumlen
, const char *file
)
142 static char chksum
[65];
147 switch ((pid
= fork()))
162 if (execl("/bin/busybox", "/bin/busybox", applet
, file
, NULL
))
168 memset(chksum
, 0, sizeof(chksum
));
169 r
= read(fds
[0], chksum
, sumlen
);
171 waitpid(pid
, NULL
, 0);
183 response(bool success
, const char *message
)
188 printf("Status: 200 OK\r\n");
189 printf("Content-Type: text/plain\r\n\r\n{\n");
193 if (!stat(st
.filename
, &s
))
194 printf("\t\"size\": %u,\n", (unsigned int)s
.st_size
);
196 printf("\t\"size\": null,\n");
198 chksum
= checksum("md5sum", 32, st
.filename
);
199 printf("\t\"checksum\": %s%s%s,\n",
201 chksum
? chksum
: "null",
204 chksum
= checksum("sha256sum", 64, st
.filename
);
205 printf("\t\"sha256sum\": %s%s%s\n",
207 chksum
? chksum
: "null",
213 printf("\t\"message\": \"%s\",\n", message
);
215 printf("\t\"failure\": [ %u, \"%s\" ]\n", errno
, strerror(errno
));
217 if (st
.filefd
> -1 && st
.filename
)
227 failure(int code
, int e
, const char *message
)
229 printf("Status: %d %s\r\n", code
, message
);
230 printf("Content-Type: text/plain\r\n\r\n");
231 printf("%s", message
);
234 printf(": %s", strerror(e
));
245 char buf
[READ_BLOCK
];
251 return response(false, "No file data received");
254 snprintf(buf
, sizeof(buf
), "/proc/self/fd/%d", st
.tempfd
);
256 if (unlink(st
.filename
) < 0 && errno
!= ENOENT
)
259 return response(false, "Failed to unlink existing file");
262 if (linkat(AT_FDCWD
, buf
, AT_FDCWD
, st
.filename
, AT_SYMLINK_FOLLOW
) < 0)
264 if (lseek(st
.tempfd
, 0, SEEK_SET
) < 0)
267 return response(false, "Failed to rewind temp file");
270 st
.filefd
= open(st
.filename
, O_CREAT
| O_TRUNC
| O_WRONLY
, 0600);
275 return response(false, "Failed to open target file");
278 while ((len
= read(st
.tempfd
, buf
, sizeof(buf
))) > 0)
280 if (write(st
.filefd
, buf
, len
) != len
)
284 return response(false, "I/O failure while writing target file");
293 if (chmod(st
.filename
, st
.filemode
))
294 return response(false, "Failed to chmod target file");
300 header_field(multipart_parser
*p
, const char *data
, size_t len
)
302 st
.is_content_disposition
= !strncasecmp(data
, "Content-Disposition", len
);
307 header_value(multipart_parser
*p
, const char *data
, size_t len
)
311 if (!st
.is_content_disposition
)
314 if (len
< 10 || strncasecmp(data
, "form-data", 9))
317 for (data
+= 9, len
-= 9; *data
== ' ' || *data
== ';'; data
++, len
--);
319 if (len
< 8 || strncasecmp(data
, "name=\"", 6))
322 for (data
+= 6, len
-= 6, i
= 0; i
<= len
; i
++)
324 if (*(data
+ i
) != '"')
327 for (j
= 1; j
< sizeof(parts
) / sizeof(parts
[0]); j
++)
328 if (!strncmp(data
, parts
[j
], i
))
338 data_begin_cb(multipart_parser
*p
)
340 if (st
.parttype
== PART_FILEDATA
)
343 return response(false, "File data without session");
346 return response(false, "File data without name");
348 if (!session_access(st
.sessionid
, "file", st
.filename
, "write"))
349 return response(false, "Access to path denied by ACL");
351 st
.tempfd
= open("/tmp", O_TMPFILE
| O_RDWR
, S_IRUSR
| S_IWUSR
);
354 return response(false, "Failed to create temporary file");
361 data_cb(multipart_parser
*p
, const char *data
, size_t len
)
368 st
.sessionid
= datadup(data
, len
);
372 st
.filename
= canonicalize_path(data
, len
);
376 st
.filemode
= strtoul(data
, NULL
, 8);
380 if (write(st
.tempfd
, data
, len
) != wlen
)
383 return response(false, "I/O failure while writing temporary file");
387 st
.filedata
= !!wlen
;
399 data_end_cb(multipart_parser
*p
)
401 if (st
.parttype
== PART_SESSIONID
)
403 if (!session_access(st
.sessionid
, "cgi-io", "upload", "write"))
406 return response(false, "Upload permission denied");
409 else if (st
.parttype
== PART_FILEDATA
)
412 return response(false, "Internal program failure");
415 /* prepare directory */
416 for (ptr
= st
.filename
; *ptr
; ptr
++)
422 if (mkdir(st
.filename
, 0755))
425 return response(false, "Failed to create destination directory");
436 return response(true, NULL
);
439 st
.parttype
= PART_UNKNOWN
;
443 static multipart_parser
*
450 static multipart_parser_settings s
= {
451 .on_part_data
= data_cb
,
452 .on_headers_complete
= data_begin_cb
,
453 .on_part_data_end
= data_end_cb
,
454 .on_header_field
= header_field
,
455 .on_header_value
= header_value
458 var
= getenv("CONTENT_TYPE");
460 if (!var
|| strncmp(var
, "multipart/form-data;", 20))
463 for (var
+= 20; *var
&& *var
!= '='; var
++);
468 boundary
= malloc(strlen(var
) + 3);
473 strcpy(boundary
, "--");
474 strcpy(boundary
+ 2, var
);
480 p
= multipart_parser_init(boundary
, &s
);
488 main_upload(int argc
, char *argv
[])
492 char buf
[READ_BLOCK
];
500 return response(false, "Invalid request");
503 while ((len
= read(0, buf
, sizeof(buf
))) > 0)
506 rem
= multipart_parser_execute(p
, buf
, len
);
511 multipart_parser_free(p
);
517 free_charp(char **ptr
)
522 #define autochar __attribute__((__cleanup__(free_charp))) char
525 main_download(int argc
, char **argv
)
527 char *fields
[] = { "sessionid", NULL
, "path", NULL
, "filename", NULL
, "mimetype", NULL
};
528 unsigned long long size
= 0;
529 char *p
, buf
[READ_BLOCK
];
534 autochar
*post
= postdecode(fields
, 4);
537 if (!fields
[1] || !session_access(fields
[1], "cgi-io", "download", "read"))
538 return failure(403, 0, "Download permission denied");
540 if (!fields
[3] || !session_access(fields
[1], "file", fields
[3], "read"))
541 return failure(403, 0, "Access to path denied by ACL");
543 if (stat(fields
[3], &s
))
544 return failure(404, errno
, "Failed to stat requested path");
546 if (!S_ISREG(s
.st_mode
) && !S_ISBLK(s
.st_mode
))
547 return failure(403, 0, "Requested path is not a regular file or block device");
549 for (p
= fields
[5]; p
&& *p
; p
++)
550 if (!isalnum(*p
) && !strchr(" ()<>@,;:[]?.=%-", *p
))
551 return failure(400, 0, "Invalid characters in filename");
553 for (p
= fields
[7]; p
&& *p
; p
++)
554 if (!isalnum(*p
) && !strchr(" .;=/-", *p
))
555 return failure(400, 0, "Invalid characters in mimetype");
557 rfd
= open(fields
[3], O_RDONLY
);
560 return failure(500, errno
, "Failed to open requested path");
562 if (S_ISBLK(s
.st_mode
))
563 ioctl(rfd
, BLKGETSIZE64
, &size
);
565 size
= (unsigned long long)s
.st_size
;
567 printf("Status: 200 OK\r\n");
568 printf("Content-Type: %s\r\n", fields
[7] ? fields
[7] : "application/octet-stream");
571 printf("Content-Disposition: attachment; filename=\"%s\"\r\n", fields
[5]);
574 printf("Content-Length: %llu\r\n\r\n", size
);
578 len
= sendfile(1, rfd
, NULL
, size
);
581 if (errno
== ENOSYS
|| errno
== EINVAL
) {
582 while ((len
= read(rfd
, buf
, sizeof(buf
))) > 0)
583 fwrite(buf
, len
, 1, stdout
);
589 if (errno
== EINTR
|| errno
== EAGAIN
)
602 while ((len
= read(rfd
, buf
, sizeof(buf
))) > 0)
603 fwrite(buf
, len
, 1, stdout
);
614 main_backup(int argc
, char **argv
)
622 char datestr
[16] = { 0 };
623 char hostname
[64] = { 0 };
624 char *fields
[] = { "sessionid", NULL
};
626 autochar
*post
= postdecode(fields
, 1);
629 if (!fields
[1] || !session_access(fields
[1], "cgi-io", "backup", "read"))
630 return failure(403, 0, "Backup permission denied");
633 return failure(500, errno
, "Failed to spawn pipe");
635 switch ((pid
= fork()))
638 return failure(500, errno
, "Failed to fork process");
650 return failure(500, errno
, "Failed chdir('/')");
652 execl("/sbin/sysupgrade", "/sbin/sysupgrade",
653 "--create-backup", "-", NULL
);
661 strftime(datestr
, sizeof(datestr
) - 1, "%Y-%m-%d", localtime(&now
));
663 if (gethostname(hostname
, sizeof(hostname
) - 1))
664 sprintf(hostname
, "OpenWrt");
666 printf("Status: 200 OK\r\n");
667 printf("Content-Type: application/x-targz\r\n");
668 printf("Content-Disposition: attachment; "
669 "filename=\"backup-%s-%s.tar.gz\"\r\n\r\n", hostname
, datestr
);
674 len
= splice(fds
[0], NULL
, 1, NULL
, READ_BLOCK
, SPLICE_F_MORE
);
675 } while (len
> 0 || (len
== -1 && errno
== EINTR
));
677 waitpid(pid
, &status
, 0);
687 lookup_executable(const char *cmd
)
689 size_t plen
= 0, clen
;
690 static char path
[PATH_MAX
];
697 clen
= strlen(cmd
) + 1;
699 if (!stat(cmd
, &s
) && S_ISREG(s
.st_mode
))
702 search
= getenv("PATH");
705 search
= "/bin:/usr/bin:/sbin:/usr/sbin";
710 if (*p
!= ':' && *p
!= '\0')
715 if ((plen
+ clen
) >= sizeof(path
))
718 strncpy(path
, search
, plen
);
719 sprintf(path
+ plen
, "/%s", cmd
);
721 if (!stat(path
, &s
) && S_ISREG(s
.st_mode
))
731 main_exec(int argc
, char **argv
)
733 char *fields
[] = { "sessionid", NULL
, "command", NULL
, "filename", NULL
, "mimetype", NULL
};
734 int i
, devnull
, status
, fds
[2];
735 bool allowed
= false;
741 autochar
*post
= postdecode(fields
, 4);
744 if (!fields
[1] || !session_access(fields
[1], "cgi-io", "exec", "read"))
745 return failure(403, 0, "Exec permission denied");
747 for (p
= fields
[5]; p
&& *p
; p
++)
748 if (!isalnum(*p
) && !strchr(" ()<>@,;:[]?.=%-", *p
))
749 return failure(400, 0, "Invalid characters in filename");
751 for (p
= fields
[7]; p
&& *p
; p
++)
752 if (!isalnum(*p
) && !strchr(" .;=/-", *p
))
753 return failure(400, 0, "Invalid characters in mimetype");
755 args
= fields
[3] ? parse_command(fields
[3]) : NULL
;
758 return failure(400, 0, "Invalid command parameter");
760 /* First check if we find an ACL match for the whole cmdline ... */
761 allowed
= session_access(fields
[1], "file", args
[0], "exec");
763 /* Now split the command vector... */
764 for (i
= 1; args
[i
]; i
++)
767 /* Find executable... */
768 exe
= lookup_executable(args
[0]);
772 return failure(404, 0, "Executable not found");
775 /* If there was no ACL match, check for a match on the executable */
776 if (!allowed
&& !session_access(fields
[1], "file", exe
, "exec")) {
778 return failure(403, 0, "Access to command denied by ACL");
783 return failure(500, errno
, "Failed to spawn pipe");
786 switch ((pid
= fork()))
792 return failure(500, errno
, "Failed to fork process");
795 devnull
= open("/dev/null", O_RDWR
);
811 if (chdir("/") < 0) {
813 return failure(500, errno
, "Failed chdir('/')");
816 if (execv(exe
, args
) < 0) {
818 return failure(500, errno
, "Failed execv(...)");
826 printf("Status: 200 OK\r\n");
827 printf("Content-Type: %s\r\n",
828 fields
[7] ? fields
[7] : "application/octet-stream");
831 printf("Content-Disposition: attachment; filename=\"%s\"\r\n",
838 len
= splice(fds
[0], NULL
, 1, NULL
, READ_BLOCK
, SPLICE_F_MORE
);
839 } while (len
> 0 || (len
== -1 && errno
== EINTR
));
841 waitpid(pid
, &status
, 0);
850 int main(int argc
, char **argv
)
852 if (strstr(argv
[0], "cgi-upload"))
853 return main_upload(argc
, argv
);
854 else if (strstr(argv
[0], "cgi-download"))
855 return main_download(argc
, argv
);
856 else if (strstr(argv
[0], "cgi-backup"))
857 return main_backup(argc
, argv
);
858 else if (strstr(argv
[0], "cgi-exec"))
859 return main_exec(argc
, argv
);