|
@@ -0,0 +1,363 @@
|
|
|
+// 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;
|
|
|
+ unsigned int start;
|
|
|
+ unsigned 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)
|
|
|
+ blobmsg_add_u16(c->buf, "start", c->entry.start);
|
|
|
+ if (c->entry.stop)
|
|
|
+ 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));
|
|
|
+
|
|
|
+ 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 && !c->entry.stop && 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);
|
|
|
+
|
|
|
+ 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);
|
|
|
+}
|