mirror of
https://github.com/novatiq/packages.git
synced 2026-07-08 04:49:32 +01:00
b2a890f6ad
Implement a new "cgi-exec" applet which allows to invoke remote commands
and stream their stdandard output back to the client via HTTP. This is
needed in cases where large amounts of data or binary encoded contents
such as tar archives need to be transferred, which are unsuitable to be
transported via ubus directly.
The exec call is guarded by the same ACL semantics as rpcd's file plugin,
means in order to be able to execute a command remotely, the ubus session
identified by the given session ID must have read access to the "exec"
function of the "cgi-io" scope and an explicit "exec" permission rule for
the invoked command in the "file" scope.
In order to initiate a transfer, a POST request in x-www-form-urlencoded
format must be sent to the applet, with one field "sessionid" holding
the login session and another field "command" specifiying the commandline
to invoke.
Further optional fields are "filename" which - if present - will cause
the download applet to set a Content-Dispostition header and "mimetype"
which allows to let the applet respond with a specific type instead of
the default "application/octet-stream".
Below is an example for the required ACL rules to grant exec access to
both the "date" and "iptables" commands. The "date" rule specifies the
base name of the executable and thus allows invocation with arbitrary
parameters while the latter "iptables" rule merely allows one specific
set of arguments which must appear exactly in the given order.
ubus call session grant '{
"ubus_rpc_session": "...",
"scope": "cgi-io",
"objects": [
[ "exec", "read" ]
]
}'
ubus call session grant '{
"ubus_rpc_session": "...",
"scope": "file",
"objects": [
[ "/bin/date", "exec" ],
[ "/usr/sbin/iptables -n -v -L", "exec" ]
]
}'
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
1051 lines
19 KiB
C
1051 lines
19 KiB
C
/*
|
|
* cgi-io - LuCI non-RPC helper
|
|
*
|
|
* Copyright (C) 2013 Jo-Philipp Wich <jo@mein.io>
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
#define _GNU_SOURCE /* splice(), SPLICE_F_MORE */
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <ctype.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/sendfile.h>
|
|
#include <sys/ioctl.h>
|
|
#include <linux/fs.h>
|
|
|
|
#include <libubus.h>
|
|
#include <libubox/blobmsg.h>
|
|
|
|
#include "multipart_parser.h"
|
|
|
|
#define READ_BLOCK 4096
|
|
|
|
enum part {
|
|
PART_UNKNOWN,
|
|
PART_SESSIONID,
|
|
PART_FILENAME,
|
|
PART_FILEMODE,
|
|
PART_FILEDATA
|
|
};
|
|
|
|
const char *parts[] = {
|
|
"(bug)",
|
|
"sessionid",
|
|
"filename",
|
|
"filemode",
|
|
"filedata",
|
|
};
|
|
|
|
struct state
|
|
{
|
|
bool is_content_disposition;
|
|
enum part parttype;
|
|
char *sessionid;
|
|
char *filename;
|
|
bool filedata;
|
|
int filemode;
|
|
int filefd;
|
|
int tempfd;
|
|
};
|
|
|
|
enum {
|
|
SES_ACCESS,
|
|
__SES_MAX,
|
|
};
|
|
|
|
static const struct blobmsg_policy ses_policy[__SES_MAX] = {
|
|
[SES_ACCESS] = { .name = "access", .type = BLOBMSG_TYPE_BOOL },
|
|
};
|
|
|
|
|
|
static struct state st;
|
|
|
|
static void
|
|
session_access_cb(struct ubus_request *req, int type, struct blob_attr *msg)
|
|
{
|
|
struct blob_attr *tb[__SES_MAX];
|
|
bool *allow = (bool *)req->priv;
|
|
|
|
if (!msg)
|
|
return;
|
|
|
|
blobmsg_parse(ses_policy, __SES_MAX, tb, blob_data(msg), blob_len(msg));
|
|
|
|
if (tb[SES_ACCESS])
|
|
*allow = blobmsg_get_bool(tb[SES_ACCESS]);
|
|
}
|
|
|
|
static bool
|
|
session_access(const char *sid, const char *scope, const char *obj, const char *func)
|
|
{
|
|
uint32_t id;
|
|
bool allow = false;
|
|
struct ubus_context *ctx;
|
|
static struct blob_buf req;
|
|
|
|
ctx = ubus_connect(NULL);
|
|
|
|
if (!ctx || ubus_lookup_id(ctx, "session", &id))
|
|
goto out;
|
|
|
|
blob_buf_init(&req, 0);
|
|
blobmsg_add_string(&req, "ubus_rpc_session", sid);
|
|
blobmsg_add_string(&req, "scope", scope);
|
|
blobmsg_add_string(&req, "object", obj);
|
|
blobmsg_add_string(&req, "function", func);
|
|
|
|
ubus_invoke(ctx, id, "access", req.head, session_access_cb, &allow, 500);
|
|
|
|
out:
|
|
if (ctx)
|
|
ubus_free(ctx);
|
|
|
|
return allow;
|
|
}
|
|
|
|
static char *
|
|
checksum(const char *applet, size_t sumlen, const char *file)
|
|
{
|
|
pid_t pid;
|
|
int r;
|
|
int fds[2];
|
|
static char chksum[65];
|
|
|
|
if (pipe(fds))
|
|
return NULL;
|
|
|
|
switch ((pid = fork()))
|
|
{
|
|
case -1:
|
|
return NULL;
|
|
|
|
case 0:
|
|
uloop_done();
|
|
|
|
dup2(fds[1], 1);
|
|
|
|
close(0);
|
|
close(2);
|
|
close(fds[0]);
|
|
close(fds[1]);
|
|
|
|
if (execl("/bin/busybox", "/bin/busybox", applet, file, NULL))
|
|
return NULL;
|
|
|
|
break;
|
|
|
|
default:
|
|
memset(chksum, 0, sizeof(chksum));
|
|
r = read(fds[0], chksum, sumlen);
|
|
|
|
waitpid(pid, NULL, 0);
|
|
close(fds[0]);
|
|
close(fds[1]);
|
|
|
|
if (r < 0)
|
|
return NULL;
|
|
}
|
|
|
|
return chksum;
|
|
}
|
|
|
|
static char *
|
|
datadup(const void *in, size_t len)
|
|
{
|
|
char *out = malloc(len + 1);
|
|
|
|
if (!out)
|
|
return NULL;
|
|
|
|
memcpy(out, in, len);
|
|
|
|
*(out + len) = 0;
|
|
|
|
return out;
|
|
}
|
|
|
|
static bool
|
|
urldecode(char *buf)
|
|
{
|
|
char *c, *p;
|
|
|
|
if (!buf || !*buf)
|
|
return true;
|
|
|
|
#define hex(x) \
|
|
(((x) <= '9') ? ((x) - '0') : \
|
|
(((x) <= 'F') ? ((x) - 'A' + 10) : \
|
|
((x) - 'a' + 10)))
|
|
|
|
for (c = p = buf; *p; c++)
|
|
{
|
|
if (*p == '%')
|
|
{
|
|
if (!isxdigit(*(p + 1)) || !isxdigit(*(p + 2)))
|
|
return false;
|
|
|
|
*c = (char)(16 * hex(*(p + 1)) + hex(*(p + 2)));
|
|
|
|
p += 3;
|
|
}
|
|
else if (*p == '+')
|
|
{
|
|
*c = ' ';
|
|
p++;
|
|
}
|
|
else
|
|
{
|
|
*c = *p++;
|
|
}
|
|
}
|
|
|
|
*c = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
postdecode(char **fields, int n_fields)
|
|
{
|
|
char *p;
|
|
const char *var;
|
|
static char buf[1024];
|
|
int i, len, field, found = 0;
|
|
|
|
var = getenv("CONTENT_TYPE");
|
|
|
|
if (!var || strncmp(var, "application/x-www-form-urlencoded", 33))
|
|
return false;
|
|
|
|
memset(buf, 0, sizeof(buf));
|
|
|
|
if ((len = read(0, buf, sizeof(buf) - 1)) > 0)
|
|
{
|
|
for (p = buf, i = 0; i <= len; i++)
|
|
{
|
|
if (buf[i] == '=')
|
|
{
|
|
buf[i] = 0;
|
|
|
|
for (field = 0; field < (n_fields * 2); field += 2)
|
|
{
|
|
if (!strcmp(p, fields[field]))
|
|
{
|
|
fields[field + 1] = buf + i + 1;
|
|
found++;
|
|
}
|
|
}
|
|
}
|
|
else if (buf[i] == '&' || buf[i] == '\0')
|
|
{
|
|
buf[i] = 0;
|
|
|
|
if (found >= n_fields)
|
|
break;
|
|
|
|
p = buf + i + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (field = 0; field < (n_fields * 2); field += 2)
|
|
if (!urldecode(fields[field + 1]))
|
|
return false;
|
|
|
|
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)
|
|
{
|
|
char *chksum;
|
|
struct stat s;
|
|
|
|
printf("Status: 200 OK\r\n");
|
|
printf("Content-Type: text/plain\r\n\r\n{\n");
|
|
|
|
if (success)
|
|
{
|
|
if (!stat(st.filename, &s))
|
|
printf("\t\"size\": %u,\n", (unsigned int)s.st_size);
|
|
else
|
|
printf("\t\"size\": null,\n");
|
|
|
|
chksum = checksum("md5sum", 32, st.filename);
|
|
printf("\t\"checksum\": %s%s%s,\n",
|
|
chksum ? "\"" : "",
|
|
chksum ? chksum : "null",
|
|
chksum ? "\"" : "");
|
|
|
|
chksum = checksum("sha256sum", 64, st.filename);
|
|
printf("\t\"sha256sum\": %s%s%s\n",
|
|
chksum ? "\"" : "",
|
|
chksum ? chksum : "null",
|
|
chksum ? "\"" : "");
|
|
}
|
|
else
|
|
{
|
|
if (message)
|
|
printf("\t\"message\": \"%s\",\n", message);
|
|
|
|
printf("\t\"failure\": [ %u, \"%s\" ]\n", errno, strerror(errno));
|
|
|
|
if (st.filefd > -1)
|
|
unlink(st.filename);
|
|
}
|
|
|
|
printf("}\n");
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
failure(int code, int e, const char *message)
|
|
{
|
|
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;
|
|
}
|
|
|
|
static int
|
|
filecopy(void)
|
|
{
|
|
int len;
|
|
char buf[READ_BLOCK];
|
|
|
|
if (!st.filedata)
|
|
{
|
|
close(st.tempfd);
|
|
errno = EINVAL;
|
|
return response(false, "No file data received");
|
|
}
|
|
|
|
if (lseek(st.tempfd, 0, SEEK_SET) < 0)
|
|
{
|
|
close(st.tempfd);
|
|
return response(false, "Failed to rewind temp file");
|
|
}
|
|
|
|
st.filefd = open(st.filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
|
|
|
|
if (st.filefd < 0)
|
|
{
|
|
close(st.tempfd);
|
|
return response(false, "Failed to open target file");
|
|
}
|
|
|
|
while ((len = read(st.tempfd, buf, sizeof(buf))) > 0)
|
|
{
|
|
if (write(st.filefd, buf, len) != len)
|
|
{
|
|
close(st.tempfd);
|
|
close(st.filefd);
|
|
return response(false, "I/O failure while writing target file");
|
|
}
|
|
}
|
|
|
|
close(st.tempfd);
|
|
close(st.filefd);
|
|
|
|
if (chmod(st.filename, st.filemode))
|
|
return response(false, "Failed to chmod target file");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
header_field(multipart_parser *p, const char *data, size_t len)
|
|
{
|
|
st.is_content_disposition = !strncasecmp(data, "Content-Disposition", len);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
header_value(multipart_parser *p, const char *data, size_t len)
|
|
{
|
|
size_t i, j;
|
|
|
|
if (!st.is_content_disposition)
|
|
return 0;
|
|
|
|
if (len < 10 || strncasecmp(data, "form-data", 9))
|
|
return 0;
|
|
|
|
for (data += 9, len -= 9; *data == ' ' || *data == ';'; data++, len--);
|
|
|
|
if (len < 8 || strncasecmp(data, "name=\"", 6))
|
|
return 0;
|
|
|
|
for (data += 6, len -= 6, i = 0; i <= len; i++)
|
|
{
|
|
if (*(data + i) != '"')
|
|
continue;
|
|
|
|
for (j = 1; j < sizeof(parts) / sizeof(parts[0]); j++)
|
|
if (!strncmp(data, parts[j], i))
|
|
st.parttype = j;
|
|
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
data_begin_cb(multipart_parser *p)
|
|
{
|
|
char tmpname[24] = "/tmp/luci-upload.XXXXXX";
|
|
|
|
if (st.parttype == PART_FILEDATA)
|
|
{
|
|
if (!st.sessionid)
|
|
return response(false, "File data without session");
|
|
|
|
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)
|
|
return response(false, "Failed to create temporary file");
|
|
|
|
unlink(tmpname);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
data_cb(multipart_parser *p, const char *data, size_t len)
|
|
{
|
|
int wlen = len;
|
|
|
|
switch (st.parttype)
|
|
{
|
|
case PART_SESSIONID:
|
|
st.sessionid = datadup(data, len);
|
|
break;
|
|
|
|
case PART_FILENAME:
|
|
st.filename = canonicalize_path(data, len);
|
|
break;
|
|
|
|
case PART_FILEMODE:
|
|
st.filemode = strtoul(data, NULL, 8);
|
|
break;
|
|
|
|
case PART_FILEDATA:
|
|
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 = !!wlen;
|
|
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
data_end_cb(multipart_parser *p)
|
|
{
|
|
if (st.parttype == PART_SESSIONID)
|
|
{
|
|
if (!session_access(st.sessionid, "cgi-io", "upload", "write"))
|
|
{
|
|
errno = EPERM;
|
|
return response(false, "Upload permission denied");
|
|
}
|
|
}
|
|
else if (st.parttype == PART_FILEDATA)
|
|
{
|
|
if (st.tempfd < 0)
|
|
return response(false, "Internal program failure");
|
|
|
|
#if 0
|
|
/* prepare directory */
|
|
for (ptr = st.filename; *ptr; ptr++)
|
|
{
|
|
if (*ptr == '/')
|
|
{
|
|
*ptr = 0;
|
|
|
|
if (mkdir(st.filename, 0755))
|
|
{
|
|
unlink(st.tmpname);
|
|
return response(false, "Failed to create destination directory");
|
|
}
|
|
|
|
*ptr = '/';
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (filecopy())
|
|
return -1;
|
|
|
|
return response(true, NULL);
|
|
}
|
|
|
|
st.parttype = PART_UNKNOWN;
|
|
return 0;
|
|
}
|
|
|
|
static multipart_parser *
|
|
init_parser(void)
|
|
{
|
|
char *boundary;
|
|
const char *var;
|
|
|
|
multipart_parser *p;
|
|
static multipart_parser_settings s = {
|
|
.on_part_data = data_cb,
|
|
.on_headers_complete = data_begin_cb,
|
|
.on_part_data_end = data_end_cb,
|
|
.on_header_field = header_field,
|
|
.on_header_value = header_value
|
|
};
|
|
|
|
var = getenv("CONTENT_TYPE");
|
|
|
|
if (!var || strncmp(var, "multipart/form-data;", 20))
|
|
return NULL;
|
|
|
|
for (var += 20; *var && *var != '='; var++);
|
|
|
|
if (*var++ != '=')
|
|
return NULL;
|
|
|
|
boundary = malloc(strlen(var) + 3);
|
|
|
|
if (!boundary)
|
|
return NULL;
|
|
|
|
strcpy(boundary, "--");
|
|
strcpy(boundary + 2, var);
|
|
|
|
st.tempfd = -1;
|
|
st.filefd = -1;
|
|
st.filemode = 0600;
|
|
|
|
p = multipart_parser_init(boundary, &s);
|
|
|
|
free(boundary);
|
|
|
|
return p;
|
|
}
|
|
|
|
static int
|
|
main_upload(int argc, char *argv[])
|
|
{
|
|
int rem, len;
|
|
bool done = false;
|
|
char buf[READ_BLOCK];
|
|
multipart_parser *p;
|
|
|
|
p = init_parser();
|
|
|
|
if (!p)
|
|
{
|
|
errno = EINVAL;
|
|
return response(false, "Invalid request");
|
|
}
|
|
|
|
while ((len = read(0, buf, sizeof(buf))) > 0)
|
|
{
|
|
if (!done) {
|
|
rem = multipart_parser_execute(p, buf, len);
|
|
done = (rem < len);
|
|
}
|
|
}
|
|
|
|
multipart_parser_free(p);
|
|
|
|
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;
|
|
}
|
|
|
|
static int
|
|
main_backup(int argc, char **argv)
|
|
{
|
|
pid_t pid;
|
|
time_t now;
|
|
int r;
|
|
int len;
|
|
int status;
|
|
int fds[2];
|
|
char datestr[16] = { 0 };
|
|
char hostname[64] = { 0 };
|
|
char *fields[] = { "sessionid", NULL };
|
|
|
|
if (!postdecode(fields, 1) || !session_access(fields[1], "cgi-io", "backup", "read"))
|
|
return failure(403, 0, "Backup permission denied");
|
|
|
|
if (pipe(fds))
|
|
return failure(500, errno, "Failed to spawn pipe");
|
|
|
|
switch ((pid = fork()))
|
|
{
|
|
case -1:
|
|
return failure(500, errno, "Failed to fork process");
|
|
|
|
case 0:
|
|
dup2(fds[1], 1);
|
|
|
|
close(0);
|
|
close(2);
|
|
close(fds[0]);
|
|
close(fds[1]);
|
|
|
|
r = chdir("/");
|
|
if (r < 0)
|
|
return failure(500, errno, "Failed chdir('/')");
|
|
|
|
execl("/sbin/sysupgrade", "/sbin/sysupgrade",
|
|
"--create-backup", "-", NULL);
|
|
|
|
return -1;
|
|
|
|
default:
|
|
now = time(NULL);
|
|
strftime(datestr, sizeof(datestr) - 1, "%Y-%m-%d", localtime(&now));
|
|
|
|
if (gethostname(hostname, sizeof(hostname) - 1))
|
|
sprintf(hostname, "OpenWrt");
|
|
|
|
printf("Status: 200 OK\r\n");
|
|
printf("Content-Type: application/x-targz\r\n");
|
|
printf("Content-Disposition: attachment; "
|
|
"filename=\"backup-%s-%s.tar.gz\"\r\n\r\n", hostname, datestr);
|
|
|
|
fflush(stdout);
|
|
|
|
do {
|
|
len = splice(fds[0], NULL, 1, NULL, READ_BLOCK, SPLICE_F_MORE);
|
|
} while (len > 0);
|
|
|
|
waitpid(pid, &status, 0);
|
|
|
|
close(fds[0]);
|
|
close(fds[1]);
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
|
|
static const char *
|
|
lookup_executable(const char *cmd)
|
|
{
|
|
size_t plen = 0, clen = strlen(cmd) + 1;
|
|
static char path[PATH_MAX];
|
|
char *search, *p;
|
|
struct stat s;
|
|
|
|
if (!stat(cmd, &s) && S_ISREG(s.st_mode))
|
|
return cmd;
|
|
|
|
search = getenv("PATH");
|
|
|
|
if (!search)
|
|
search = "/bin:/usr/bin:/sbin:/usr/sbin";
|
|
|
|
p = search;
|
|
|
|
do {
|
|
if (*p != ':' && *p != '\0')
|
|
continue;
|
|
|
|
plen = p - search;
|
|
|
|
if ((plen + clen) >= sizeof(path))
|
|
continue;
|
|
|
|
strncpy(path, search, plen);
|
|
sprintf(path + plen, "/%s", cmd);
|
|
|
|
if (!stat(path, &s) && S_ISREG(s.st_mode))
|
|
return path;
|
|
|
|
search = p + 1;
|
|
} while (*p++);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static char **
|
|
parse_command(const char *cmdline)
|
|
{
|
|
const char *p = cmdline, *s;
|
|
char **argv = NULL, *out;
|
|
size_t arglen = 0;
|
|
int argnum = 0;
|
|
bool esc;
|
|
|
|
while (isspace(*cmdline))
|
|
cmdline++;
|
|
|
|
for (p = cmdline, s = p, esc = false; p; p++) {
|
|
if (esc) {
|
|
esc = false;
|
|
}
|
|
else if (*p == '\\' && p[1] != 0) {
|
|
esc = true;
|
|
}
|
|
else if (isspace(*p) || *p == 0) {
|
|
if (p > s) {
|
|
argnum += 1;
|
|
arglen += sizeof(char *) + (p - s) + 1;
|
|
}
|
|
|
|
s = p + 1;
|
|
}
|
|
|
|
if (*p == 0)
|
|
break;
|
|
}
|
|
|
|
if (arglen == 0)
|
|
return NULL;
|
|
|
|
argv = calloc(1, arglen + sizeof(char *));
|
|
|
|
if (!argv)
|
|
return NULL;
|
|
|
|
out = (char *)argv + sizeof(char *) * (argnum + 1);
|
|
argv[0] = out;
|
|
|
|
for (p = cmdline, s = p, esc = false, argnum = 0; p; p++) {
|
|
if (esc) {
|
|
esc = false;
|
|
*out++ = *p;
|
|
}
|
|
else if (*p == '\\' && p[1] != 0) {
|
|
esc = true;
|
|
}
|
|
else if (isspace(*p) || *p == 0) {
|
|
if (p > s) {
|
|
*out++ = ' ';
|
|
argv[++argnum] = out;
|
|
}
|
|
|
|
s = p + 1;
|
|
}
|
|
else {
|
|
*out++ = *p;
|
|
}
|
|
|
|
if (*p == 0)
|
|
break;
|
|
}
|
|
|
|
argv[argnum] = NULL;
|
|
out[-1] = 0;
|
|
|
|
return argv;
|
|
}
|
|
|
|
static int
|
|
main_exec(int argc, char **argv)
|
|
{
|
|
char *fields[] = { "sessionid", NULL, "command", NULL, "filename", NULL, "mimetype", NULL };
|
|
int i, devnull, status, fds[2];
|
|
bool allowed = false;
|
|
ssize_t len = 0;
|
|
const char *exe;
|
|
char *p, **args;
|
|
pid_t pid;
|
|
|
|
postdecode(fields, 4);
|
|
|
|
if (!fields[1] || !session_access(fields[1], "cgi-io", "exec", "read"))
|
|
return failure(403, 0, "Exec permission denied");
|
|
|
|
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");
|
|
|
|
args = fields[3] ? parse_command(fields[3]) : NULL;
|
|
|
|
if (!args)
|
|
return failure(400, 0, "Invalid command parameter");
|
|
|
|
/* First check if we find an ACL match for the whole cmdline ... */
|
|
allowed = session_access(fields[1], "file", args[0], "exec");
|
|
|
|
/* Now split the command vector... */
|
|
for (i = 1; args[i]; i++)
|
|
args[i][-1] = 0;
|
|
|
|
/* Find executable... */
|
|
exe = lookup_executable(args[0]);
|
|
|
|
if (!exe) {
|
|
free(args);
|
|
return failure(404, 0, "Executable not found");
|
|
}
|
|
|
|
/* If there was no ACL match, check for a match on the executable */
|
|
if (!allowed && !session_access(fields[1], "file", exe, "exec")) {
|
|
free(args);
|
|
return failure(403, 0, "Access to command denied by ACL");
|
|
}
|
|
|
|
if (pipe(fds)) {
|
|
free(args);
|
|
return failure(500, errno, "Failed to spawn pipe");
|
|
}
|
|
|
|
switch ((pid = fork()))
|
|
{
|
|
case -1:
|
|
free(args);
|
|
close(fds[0]);
|
|
close(fds[1]);
|
|
return failure(500, errno, "Failed to fork process");
|
|
|
|
case 0:
|
|
devnull = open("/dev/null", O_RDWR);
|
|
|
|
if (devnull > -1) {
|
|
dup2(devnull, 0);
|
|
dup2(devnull, 2);
|
|
close(devnull);
|
|
}
|
|
else {
|
|
close(0);
|
|
close(2);
|
|
}
|
|
|
|
dup2(fds[1], 1);
|
|
close(fds[0]);
|
|
close(fds[1]);
|
|
|
|
if (chdir("/") < 0) {
|
|
free(args);
|
|
return failure(500, errno, "Failed chdir('/')");
|
|
}
|
|
|
|
if (execv(exe, args) < 0) {
|
|
free(args);
|
|
return failure(500, errno, "Failed execv(...)");
|
|
}
|
|
|
|
return -1;
|
|
|
|
default:
|
|
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("\r\n");
|
|
fflush(stdout);
|
|
|
|
do {
|
|
len = splice(fds[0], NULL, 1, NULL, READ_BLOCK, SPLICE_F_MORE);
|
|
} while (len > 0);
|
|
|
|
waitpid(pid, &status, 0);
|
|
|
|
close(fds[0]);
|
|
close(fds[1]);
|
|
free(args);
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
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);
|
|
else if (strstr(argv[0], "cgi-exec"))
|
|
return main_exec(argc, argv);
|
|
|
|
return -1;
|
|
}
|