diff --git a/net/cgi-io/Makefile b/net/cgi-io/Makefile index 3a4fc565f..92cca4fa0 100644 --- a/net/cgi-io/Makefile +++ b/net/cgi-io/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=cgi-io -PKG_RELEASE:=7 +PKG_RELEASE:=14 PKG_LICENSE:=GPL-2.0-or-later @@ -38,6 +38,7 @@ define Package/cgi-io/install $(INSTALL_DIR) $(1)/usr/libexec $(1)/www/cgi-bin/ $(INSTALL_BIN) $(PKG_BUILD_DIR)/cgi-io $(1)/usr/libexec $(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-upload + $(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-download $(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-backup endef diff --git a/net/cgi-io/src/CMakeLists.txt b/net/cgi-io/src/CMakeLists.txt index 6d8b1585a..c7c9d40ca 100644 --- a/net/cgi-io/src/CMakeLists.txt +++ b/net/cgi-io/src/CMakeLists.txt @@ -5,9 +5,12 @@ PROJECT(cgi-io C) INCLUDE(CheckFunctionExists) FIND_PATH(ubus_include_dir libubus.h) +FIND_LIBRARY(ubox NAMES ubox) +FIND_LIBRARY(ubus NAMES ubus) INCLUDE_DIRECTORIES(${ubus_include_dir}) -ADD_DEFINITIONS(-Os -Wall -Werror --std=gnu99 -g3 -Wmissing-declarations) +ADD_DEFINITIONS(-Os -Wall -Werror -Wextra --std=gnu99 -g3) +ADD_DEFINITIONS(-Wno-unused-parameter -Wmissing-declarations) SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") @@ -17,6 +20,6 @@ IF(APPLE) ENDIF() ADD_EXECUTABLE(cgi-io main.c multipart_parser.c) -TARGET_LINK_LIBRARIES(cgi-io ubox ubus) +TARGET_LINK_LIBRARIES(cgi-io ${ubox} ${ubus}) INSTALL(TARGETS cgi-io RUNTIME DESTINATION sbin) diff --git a/net/cgi-io/src/main.c b/net/cgi-io/src/main.c index 2bfec623b..3530284c6 100644 --- a/net/cgi-io/src/main.c +++ b/net/cgi-io/src/main.c @@ -16,6 +16,8 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#define _GNU_SOURCE /* splice(), SPLICE_F_MORE */ + #include #include #include @@ -26,12 +28,16 @@ #include #include #include +#include +#include +#include #include #include #include "multipart_parser.h" +#define READ_BLOCK 4096 enum part { PART_UNKNOWN, @@ -89,7 +95,7 @@ session_access_cb(struct ubus_request *req, int type, struct blob_attr *msg) } static bool -session_access(const char *sid, const char *obj, const char *func) +session_access(const char *sid, const char *scope, const char *obj, const char *func) { uint32_t id; bool allow = false; @@ -103,7 +109,7 @@ session_access(const char *sid, const char *obj, const char *func) blob_buf_init(&req, 0); blobmsg_add_string(&req, "ubus_rpc_session", sid); - blobmsg_add_string(&req, "scope", "cgi-io"); + blobmsg_add_string(&req, "scope", scope); blobmsg_add_string(&req, "object", obj); blobmsg_add_string(&req, "function", func); @@ -120,6 +126,7 @@ static char * checksum(const char *applet, size_t sumlen, const char *file) { pid_t pid; + int r; int fds[2]; static char chksum[65]; @@ -148,10 +155,14 @@ checksum(const char *applet, size_t sumlen, const char *file) default: memset(chksum, 0, sizeof(chksum)); - read(fds[0], chksum, sumlen); + r = read(fds[0], chksum, sumlen); + waitpid(pid, NULL, 0); close(fds[0]); close(fds[1]); + + if (r < 0) + return NULL; } return chksum; @@ -263,6 +274,64 @@ postdecode(char **fields, int n_fields) return (found >= n_fields); } +static char * +canonicalize_path(const char *path, size_t len) +{ + char *canonpath, *cp; + const char *p, *e; + + if (path == NULL || *path == '\0') + return NULL; + + canonpath = datadup(path, len); + + if (canonpath == NULL) + return NULL; + + /* normalize */ + for (cp = canonpath, p = path, e = path + len; p < e; ) { + if (*p != '/') + goto next; + + /* skip repeating / */ + if ((p + 1 < e) && (p[1] == '/')) { + p++; + continue; + } + + /* /./ or /../ */ + if ((p + 1 < e) && (p[1] == '.')) { + /* skip /./ */ + if ((p + 2 >= e) || (p[2] == '/')) { + p += 2; + continue; + } + + /* collapse /x/../ */ + if ((p + 2 < e) && (p[2] == '.') && ((p + 3 >= e) || (p[3] == '/'))) { + while ((cp > canonpath) && (*--cp != '/')) + ; + + p += 3; + continue; + } + } + +next: + *cp++ = *p++; + } + + /* remove trailing slash if not root / */ + if ((cp > canonpath + 1) && (cp[-1] == '/')) + cp--; + else if (cp == canonpath) + *cp++ = '/'; + + *cp = '\0'; + + return canonpath; +} + static int response(bool success, const char *message) { @@ -308,15 +377,17 @@ response(bool success, const char *message) } static int -failure(int e, const char *message) +failure(int code, int e, const char *message) { - printf("Status: 500 Internal Server failure\r\n"); + printf("Status: %d %s\r\n", code, message); printf("Content-Type: text/plain\r\n\r\n"); printf("%s", message); if (e) printf(": %s", strerror(e)); + printf("\n"); + return -1; } @@ -324,7 +395,7 @@ static int filecopy(void) { int len; - char buf[4096]; + char buf[READ_BLOCK]; if (!st.filedata) { @@ -376,7 +447,7 @@ header_field(multipart_parser *p, const char *data, size_t len) static int header_value(multipart_parser *p, const char *data, size_t len) { - int i, j; + size_t i, j; if (!st.is_content_disposition) return 0; @@ -417,6 +488,9 @@ data_begin_cb(multipart_parser *p) if (!st.filename) return response(false, "File data without name"); + if (!session_access(st.sessionid, "file", st.filename, "write")) + return response(false, "Access to path denied by ACL"); + st.tempfd = mkstemp(tmpname); if (st.tempfd < 0) @@ -431,6 +505,8 @@ data_begin_cb(multipart_parser *p) static int data_cb(multipart_parser *p, const char *data, size_t len) { + int wlen = len; + switch (st.parttype) { case PART_SESSIONID: @@ -438,7 +514,7 @@ data_cb(multipart_parser *p, const char *data, size_t len) break; case PART_FILENAME: - st.filename = datadup(data, len); + st.filename = canonicalize_path(data, len); break; case PART_FILEMODE: @@ -446,14 +522,14 @@ data_cb(multipart_parser *p, const char *data, size_t len) break; case PART_FILEDATA: - if (write(st.tempfd, data, len) != len) + if (write(st.tempfd, data, len) != wlen) { close(st.tempfd); return response(false, "I/O failure while writing temporary file"); } if (!st.filedata) - st.filedata = !!len; + st.filedata = !!wlen; break; @@ -469,7 +545,7 @@ data_end_cb(multipart_parser *p) { if (st.parttype == PART_SESSIONID) { - if (!session_access(st.sessionid, "upload", "write")) + if (!session_access(st.sessionid, "cgi-io", "upload", "write")) { errno = EPERM; return response(false, "Upload permission denied"); @@ -557,7 +633,8 @@ static int main_upload(int argc, char *argv[]) { int rem, len; - char buf[4096]; + bool done = false; + char buf[READ_BLOCK]; multipart_parser *p; p = init_parser(); @@ -570,16 +647,91 @@ main_upload(int argc, char *argv[]) while ((len = read(0, buf, sizeof(buf))) > 0) { - rem = multipart_parser_execute(p, buf, len); - - if (rem < len) - break; + if (!done) { + rem = multipart_parser_execute(p, buf, len); + done = (rem < len); + } } multipart_parser_free(p); - /* read remaining post data */ - while ((len = read(0, buf, sizeof(buf))) > 0); + return 0; +} + +static int +main_download(int argc, char **argv) +{ + char *fields[] = { "sessionid", NULL, "path", NULL, "filename", NULL, "mimetype", NULL }; + unsigned long long size = 0; + char *p, buf[READ_BLOCK]; + ssize_t len = 0; + struct stat s; + int rfd; + + postdecode(fields, 4); + + if (!fields[1] || !session_access(fields[1], "cgi-io", "download", "read")) + return failure(403, 0, "Download permission denied"); + + if (!fields[3] || !session_access(fields[1], "file", fields[3], "read")) + return failure(403, 0, "Access to path denied by ACL"); + + if (stat(fields[3], &s)) + return failure(404, errno, "Failed to stat requested path"); + + if (!S_ISREG(s.st_mode) && !S_ISBLK(s.st_mode)) + return failure(403, 0, "Requested path is not a regular file or block device"); + + for (p = fields[5]; p && *p; p++) + if (!isalnum(*p) && !strchr(" ()<>@,;:[]?.=%-", *p)) + return failure(400, 0, "Invalid characters in filename"); + + for (p = fields[7]; p && *p; p++) + if (!isalnum(*p) && !strchr(" .;=/-", *p)) + return failure(400, 0, "Invalid characters in mimetype"); + + rfd = open(fields[3], O_RDONLY); + + if (rfd < 0) + return failure(500, errno, "Failed to open requested path"); + + if (S_ISBLK(s.st_mode)) + ioctl(rfd, BLKGETSIZE64, &size); + else + size = (unsigned long long)s.st_size; + + printf("Status: 200 OK\r\n"); + printf("Content-Type: %s\r\n", fields[7] ? fields[7] : "application/octet-stream"); + + if (fields[5]) + printf("Content-Disposition: attachment; filename=\"%s\"\r\n", fields[5]); + + printf("Content-Length: %llu\r\n\r\n", size); + fflush(stdout); + + while (size > 0) { + len = sendfile(1, rfd, NULL, size); + + if (len == -1) { + if (errno == ENOSYS || errno == EINVAL) { + while ((len = read(rfd, buf, sizeof(buf))) > 0) + fwrite(buf, len, 1, stdout); + + fflush(stdout); + break; + } + + if (errno == EINTR || errno == EAGAIN) + continue; + } + + if (len <= 0) + break; + + size -= len; + } + + close(rfd); return 0; } @@ -589,24 +741,24 @@ main_backup(int argc, char **argv) { pid_t pid; time_t now; + int r; int len; int status; int fds[2]; - char buf[4096]; char datestr[16] = { 0 }; char hostname[64] = { 0 }; char *fields[] = { "sessionid", NULL }; - if (!postdecode(fields, 1) || !session_access(fields[1], "backup", "read")) - return failure(0, "Backup permission denied"); + if (!postdecode(fields, 1) || !session_access(fields[1], "cgi-io", "backup", "read")) + return failure(403, 0, "Backup permission denied"); if (pipe(fds)) - return failure(errno, "Failed to spawn pipe"); + return failure(500, errno, "Failed to spawn pipe"); switch ((pid = fork())) { case -1: - return failure(errno, "Failed to fork process"); + return failure(500, errno, "Failed to fork process"); case 0: dup2(fds[1], 1); @@ -616,7 +768,9 @@ main_backup(int argc, char **argv) close(fds[0]); close(fds[1]); - chdir("/"); + r = chdir("/"); + if (r < 0) + return failure(500, errno, "Failed chdir('/')"); execl("/sbin/sysupgrade", "/sbin/sysupgrade", "--create-backup", "-", NULL); @@ -624,7 +778,6 @@ main_backup(int argc, char **argv) return -1; default: - fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | O_NONBLOCK); now = time(NULL); strftime(datestr, sizeof(datestr) - 1, "%Y-%m-%d", localtime(&now)); @@ -636,15 +789,13 @@ main_backup(int argc, char **argv) printf("Content-Disposition: attachment; " "filename=\"backup-%s-%s.tar.gz\"\r\n\r\n", hostname, datestr); + fflush(stdout); + do { - waitpid(pid, &status, 0); + len = splice(fds[0], NULL, 1, NULL, READ_BLOCK, SPLICE_F_MORE); + } while (len > 0); - while ((len = read(fds[0], buf, sizeof(buf))) > 0) { - fwrite(buf, len, 1, stdout); - fflush(stdout); - } - - } while (!WIFEXITED(status)); + waitpid(pid, &status, 0); close(fds[0]); close(fds[1]); @@ -657,6 +808,8 @@ int main(int argc, char **argv) { if (strstr(argv[0], "cgi-upload")) return main_upload(argc, argv); + else if (strstr(argv[0], "cgi-download")) + return main_download(argc, argv); else if (strstr(argv[0], "cgi-backup")) return main_backup(argc, argv);