diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bfc286..26e011e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,7 @@ INCLUDE_DIRECTORIES(${ubus_include_dir}) FIND_PATH(ubox_include_dir libubox/blobmsg_json.h) INCLUDE_DIRECTORIES(${ubox_include_dir}) -ADD_EXECUTABLE(rpcd main.c exec.c session.c uci.c plugin.c) +ADD_EXECUTABLE(rpcd main.c exec.c session.c uci.c rc.c plugin.c) TARGET_LINK_LIBRARIES(rpcd ${ubox} ${ubus} ${uci} ${blobmsg_json} ${json} ${crypt} dl) SET(PLUGINS "") diff --git a/include/rpcd/rc.h b/include/rpcd/rc.h new file mode 100644 index 0000000..ca00f56 --- /dev/null +++ b/include/rpcd/rc.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: ISC OR MIT +#ifndef __RPCD_RC_H +#define __RPCD_RC_H + +int rpc_rc_api_init(struct ubus_context *ctx); + +#endif diff --git a/main.c b/main.c index 9a177cf..d77a814 100644 --- a/main.c +++ b/main.c @@ -25,10 +25,11 @@ #include #include +#include +#include +#include #include #include -#include -#include static struct ubus_context *ctx; static bool respawn = false; @@ -113,6 +114,7 @@ int main(int argc, char **argv) rpc_session_api_init(ctx); rpc_uci_api_init(ctx); + rpc_rc_api_init(ctx); rpc_plugin_api_init(ctx); hangup = getenv("RPC_HANGUP"); diff --git a/rc.c b/rc.c new file mode 100644 index 0000000..b4787d5 --- /dev/null +++ b/rc.c @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: ISC OR MIT +/* + * rpcd - UBUS RPC server + * + * Copyright (C) 2020 Rafał Miłecki + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#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); +}