// SPDX-License-Identifier: ISC OR MIT
/*
 * rpcd - UBUS RPC server
 *
 * Copyright (C) 2020 Rafał Miłecki <rafal@milecki.pl>
 */

#include <dirent.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <sys/stat.h>
#include <sys/wait.h>

#include <libubox/blobmsg.h>
#include <libubox/ulog.h>
#include <libubox/uloop.h>
#include <libubus.h>

#include <rpcd/rc.h>

#define RC_LIST_EXEC_TIMEOUT_MS			3000

enum {
	RC_INIT_NAME,
	RC_INIT_ACTION,
	__RC_INIT_MAX
};

static const struct blobmsg_policy rc_init_policy[] = {
	[RC_INIT_NAME] = { "name", BLOBMSG_TYPE_STRING },
	[RC_INIT_ACTION] = { "action", BLOBMSG_TYPE_STRING },
};

struct rc_list_context {
	struct uloop_process process;
	struct uloop_timeout timeout;
	struct ubus_context *ctx;
	struct ubus_request_data req;
	struct blob_buf *buf;
	DIR *dir;

	/* Info about currently processed init.d entry */
	struct {
		char path[PATH_MAX];
		const char *d_name;
		int start;
		int stop;
		bool enabled;
		bool running;
	} entry;
};

static void rc_list_readdir(struct rc_list_context *c);

/**
 * rc_check_script - check if script is safe to execute as root
 *
 * Check if it's owned by root and if only root can modify it.
 */
static int rc_check_script(const char *path)
{
	struct stat s;

	if (stat(path, &s))
		return UBUS_STATUS_NOT_FOUND;

	if (s.st_uid != 0 || s.st_gid != 0 || !(s.st_mode & S_IXUSR) || (s.st_mode & S_IWOTH))
		return UBUS_STATUS_PERMISSION_DENIED;

	return UBUS_STATUS_OK;
}

static void rc_list_add_table(struct rc_list_context *c)
{
	void *e;

	e = blobmsg_open_table(c->buf, c->entry.d_name);

	if (c->entry.start >= 0)
		blobmsg_add_u16(c->buf, "start", c->entry.start);
	if (c->entry.stop >= 0)
		blobmsg_add_u16(c->buf, "stop", c->entry.stop);
	blobmsg_add_u8(c->buf, "enabled", c->entry.enabled);
	blobmsg_add_u8(c->buf, "running", c->entry.running);

	blobmsg_close_table(c->buf, e);
}

static void rpc_list_exec_timeout_cb(struct uloop_timeout *t)
{
	struct rc_list_context *c = container_of(t, struct rc_list_context, timeout);

	ULOG_WARN("Timeout waiting for %s\n", c->entry.path);

	uloop_process_delete(&c->process);
	kill(c->process.pid, SIGKILL);

	rc_list_readdir(c);
}

/**
 * rc_exec - execute a file and call callback on complete
 */
static int rc_list_exec(struct rc_list_context *c, const char *action, uloop_process_handler cb)
{
	pid_t pid;
	int err;
	int fd;

	pid = fork();
	switch (pid) {
	case -1:
		return -errno;
	case 0:
		/* Set stdin, stdout & stderr to /dev/null */
		fd = open("/dev/null", O_RDWR);
		if (fd >= 0) {
			dup2(fd, 0);
			dup2(fd, 1);
			dup2(fd, 2);
			if (fd > 2)
				close(fd);
		}

		uloop_end();

		execl(c->entry.path, c->entry.path, action, NULL);
		exit(errno);
	default:
		c->process.pid = pid;
		c->process.cb = cb;

		err = uloop_process_add(&c->process);
		if (err)
			return err;

		c->timeout.cb = rpc_list_exec_timeout_cb;
		err = uloop_timeout_set(&c->timeout, RC_LIST_EXEC_TIMEOUT_MS);
		if (err) {
			uloop_process_delete(&c->process);
			return err;
		}

		return 0;
	}
}

static void rc_list_exec_running_cb(struct uloop_process *p, int stat)
{
	struct rc_list_context *c = container_of(p, struct rc_list_context, process);

	uloop_timeout_cancel(&c->timeout);

	c->entry.running = !stat;
	rc_list_add_table(c);

	rc_list_readdir(c);
}

static void rc_list_readdir(struct rc_list_context *c)
{
	struct dirent *e;
	FILE *fp;

	e = readdir(c->dir);
	if (!e) {
		closedir(c->dir);
		ubus_send_reply(c->ctx, &c->req, c->buf->head);
		ubus_complete_deferred_request(c->ctx, &c->req, UBUS_STATUS_OK);
		return;
	}

	if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
		goto next;

	memset(&c->entry, 0, sizeof(c->entry));
	c->entry.start = -1;
	c->entry.stop = -1;

	snprintf(c->entry.path, sizeof(c->entry.path), "/etc/init.d/%s", e->d_name);
	if (rc_check_script(c->entry.path))
		goto next;

	c->entry.d_name = e->d_name;

	fp = fopen(c->entry.path, "r");
	if (fp) {
		struct stat s;
		char path[PATH_MAX];
		char line[32];
		bool beginning;

		beginning = true;
		while (c->entry.start < 0 && c->entry.stop < 0 && fgets(line, sizeof(line), fp)) {
			if (beginning) {
				if (!strncmp(line, "START=", 6)) {
					c->entry.start = strtoul(line + 6, NULL, 0);
				} else if (!strncmp(line, "STOP=", 5)) {
					c->entry.stop = strtoul(line + 5, NULL, 0);
				}
			}

			beginning = !!strchr(line, '\n');
		}
		fclose(fp);

		if (c->entry.start >= 0) {
			snprintf(path, sizeof(path), "/etc/rc.d/S%02d%s", c->entry.start, c->entry.d_name);
			if (!stat(path, &s) && (s.st_mode & S_IXUSR))
				c->entry.enabled = true;
		}
	}

	if (rc_list_exec(c, "running", rc_list_exec_running_cb))
		goto next;

	return;
next:
	rc_list_readdir(c);
}

/**
 * rc_list - allocate listing context and start reading directory
 */
static int rc_list(struct ubus_context *ctx, struct ubus_object *obj,
		   struct ubus_request_data *req, const char *method,
		   struct blob_attr *msg)
{
	static struct blob_buf buf;
	struct rc_list_context *c;

	blob_buf_init(&buf, 0);

	c = calloc(1, sizeof(*c));
	if (!c)
		return UBUS_STATUS_UNKNOWN_ERROR;

	c->ctx = ctx;
	c->buf = &buf;
	c->dir = opendir("/etc/init.d");
	if (!c->dir) {
		free(c);
		return UBUS_STATUS_UNKNOWN_ERROR;
	}

	ubus_defer_request(ctx, req, &c->req);

	rc_list_readdir(c);

	return 0; /* Deferred */
}

struct rc_init_context {
	struct uloop_process process;
	struct ubus_context *ctx;
	struct ubus_request_data req;
};

static void rc_init_cb(struct uloop_process *p, int stat)
{
	struct rc_init_context *c = container_of(p, struct rc_init_context, process);

	ubus_complete_deferred_request(c->ctx, &c->req, UBUS_STATUS_OK);

	free(c);
}

static int rc_init(struct ubus_context *ctx, struct ubus_object *obj,
		   struct ubus_request_data *req, const char *method,
		   struct blob_attr *msg)
{
	struct blob_attr *tb[__RC_INIT_MAX];
	struct rc_init_context *c;
	char path[PATH_MAX];
	const char *action;
	const char *name;
	const char *chr;
	pid_t pid;
	int err;
	int fd;

	blobmsg_parse(rc_init_policy, __RC_INIT_MAX, tb, blobmsg_data(msg), blobmsg_data_len(msg));

	if (!tb[RC_INIT_NAME] || !tb[RC_INIT_ACTION])
		return UBUS_STATUS_INVALID_ARGUMENT;

	name = blobmsg_get_string(tb[RC_INIT_NAME]);

	/* Validate script name */
	for (chr = name; (chr = strchr(chr, '.')); chr++) {
		if (*(chr + 1) == '.')
			return UBUS_STATUS_INVALID_ARGUMENT;
	}
	if (strchr(name, '/'))
		return UBUS_STATUS_INVALID_ARGUMENT;

	snprintf(path, sizeof(path), "/etc/init.d/%s", name);

	/* Validate script privileges */
	err = rc_check_script(path);
	if (err)
		return err;

	action = blobmsg_get_string(tb[RC_INIT_ACTION]);
	if (strcmp(action, "disable") &&
	    strcmp(action, "enable") &&
	    strcmp(action, "stop") &&
	    strcmp(action, "start") &&
	    strcmp(action, "restart") &&
	    strcmp(action, "reload"))
		return UBUS_STATUS_INVALID_ARGUMENT;

	c = calloc(1, sizeof(*c));
	if (!c)
		return UBUS_STATUS_UNKNOWN_ERROR;

	pid = fork();
	switch (pid) {
	case -1:
		free(c);
		return UBUS_STATUS_UNKNOWN_ERROR;
	case 0:
		/* Set stdin, stdout & stderr to /dev/null */
		fd = open("/dev/null", O_RDWR);
		if (fd >= 0) {
			dup2(fd, 0);
			dup2(fd, 1);
			dup2(fd, 2);
			if (fd > 2)
				close(fd);
		}

		uloop_end();

		execl(path, path, action, NULL);
		exit(errno);
	default:
		c->ctx = ctx;
		c->process.pid = pid;
		c->process.cb = rc_init_cb;
		uloop_process_add(&c->process);

		ubus_defer_request(ctx, req, &c->req);

		return 0; /* Deferred */
	}
}

int rpc_rc_api_init(struct ubus_context *ctx)
{
	static const struct ubus_method rc_methods[] = {
		UBUS_METHOD_NOARG("list", rc_list),
		UBUS_METHOD("init", rc_init, rc_init_policy),
	};

	static struct ubus_object_type rc_type =
		UBUS_OBJECT_TYPE("rc", rc_methods);

	static struct ubus_object obj = {
		.name = "rc",
		.type = &rc_type,
		.methods = rc_methods,
		.n_methods = ARRAY_SIZE(rc_methods),
	};

	return ubus_add_object(ctx, &obj);
}
