/*
 * 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.
 */

#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 <libubus.h>
#include <libubox/blobmsg.h>

#include "multipart_parser.h"


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 *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", "cgi-io");
	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 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));
		read(fds[0], chksum, sumlen);
		waitpid(pid, NULL, 0);
		close(fds[0]);
		close(fds[1]);
	}

	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 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 e, const char *message)
{
	printf("Status: 500 Internal Server failure\r\n");
	printf("Content-Type: text/plain\r\n\r\n");
	printf("%s", message);

	if (e)
		printf(": %s", strerror(e));

	return -1;
}

static int
filecopy(void)
{
	int len;
	char buf[4096];

	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)
{
	int 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");

		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)
{
	switch (st.parttype)
	{
	case PART_SESSIONID:
		st.sessionid = datadup(data, len);
		break;

	case PART_FILENAME:
		st.filename = datadup(data, len);
		break;

	case PART_FILEMODE:
		st.filemode = strtoul(data, NULL, 8);
		break;

	case PART_FILEDATA:
		if (write(st.tempfd, data, len) != len)
		{
			close(st.tempfd);
			return response(false, "I/O failure while writing temporary file");
		}

		if (!st.filedata)
			st.filedata = !!len;

		break;

	default:
		break;
	}

	return 0;
}

static int
data_end_cb(multipart_parser *p)
{
	if (st.parttype == PART_SESSIONID)
	{
		if (!session_access(st.sessionid, "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;
	char buf[4096];
	multipart_parser *p;

	p = init_parser();

	if (!p)
	{
		errno = EINVAL;
		return response(false, "Invalid request");
	}

	while ((len = read(0, buf, sizeof(buf))) > 0)
	{
		rem = multipart_parser_execute(p, buf, len);

		if (rem < len)
			break;
	}

	multipart_parser_free(p);

	/* read remaining post data */
	while ((len = read(0, buf, sizeof(buf))) > 0);

	return 0;
}

static int
main_backup(int argc, char **argv)
{
	pid_t pid;
	time_t now;
	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 (pipe(fds))
		return failure(errno, "Failed to spawn pipe");

	switch ((pid = fork()))
	{
	case -1:
		return failure(errno, "Failed to fork process");

	case 0:
		dup2(fds[1], 1);

		close(0);
		close(2);
		close(fds[0]);
		close(fds[1]);

		chdir("/");

		execl("/sbin/sysupgrade", "/sbin/sysupgrade",
		      "--create-backup", "-", NULL);

		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));

		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);

		do {
			waitpid(pid, &status, 0);

			while ((len = read(fds[0], buf, sizeof(buf))) > 0) {
				fwrite(buf, len, 1, stdout);
				fflush(stdout);
			}

		} while (!WIFEXITED(status));

		close(fds[0]);
		close(fds[1]);

		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-backup"))
		return main_backup(argc, argv);

	return -1;
}
