rpcd/uci.c
Jo-Philipp Wich 92d0d75716 uci: use correct sort index when reordering sections
When reordering, the section indexes must be 0-based while the current
implementation incorrectly numbers starting with 1.

Fix this by start numbering ther sections with index 0.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
2018-04-28 18:23:59 +02:00

1617 lines
38 KiB
C

/*
* rpcd - UBUS RPC server
*
* Copyright (C) 2013-2014 Jo-Philipp Wich <jow@openwrt.org>
*
* 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 <libgen.h>
#include <glob.h>
#include <libubox/blobmsg.h>
#include <libubox/blobmsg_json.h>
#include <rpcd/uci.h>
#include <rpcd/exec.h>
#include <rpcd/session.h>
static struct blob_buf buf;
static struct uci_context *cursor;
static struct uloop_timeout apply_timer;
static struct ubus_context *apply_ctx;
char apply_sid[RPC_SID_LEN + 1];
enum {
RPC_G_CONFIG,
RPC_G_SECTION,
RPC_G_OPTION,
RPC_G_TYPE,
RPC_G_MATCH,
RPC_G_SESSION,
__RPC_G_MAX,
};
static const struct blobmsg_policy rpc_uci_get_policy[__RPC_G_MAX] = {
[RPC_G_CONFIG] = { .name = "config", .type = BLOBMSG_TYPE_STRING },
[RPC_G_SECTION] = { .name = "section", .type = BLOBMSG_TYPE_STRING },
[RPC_G_OPTION] = { .name = "option", .type = BLOBMSG_TYPE_STRING },
[RPC_G_TYPE] = { .name = "type", .type = BLOBMSG_TYPE_STRING },
[RPC_G_MATCH] = { .name = "match", .type = BLOBMSG_TYPE_TABLE },
[RPC_G_SESSION] = { .name = "ubus_rpc_session",
.type = BLOBMSG_TYPE_STRING },
};
enum {
RPC_A_CONFIG,
RPC_A_TYPE,
RPC_A_NAME,
RPC_A_VALUES,
RPC_A_SESSION,
__RPC_A_MAX,
};
static const struct blobmsg_policy rpc_uci_add_policy[__RPC_A_MAX] = {
[RPC_A_CONFIG] = { .name = "config", .type = BLOBMSG_TYPE_STRING },
[RPC_A_TYPE] = { .name = "type", .type = BLOBMSG_TYPE_STRING },
[RPC_A_NAME] = { .name = "name", .type = BLOBMSG_TYPE_STRING },
[RPC_A_VALUES] = { .name = "values", .type = BLOBMSG_TYPE_TABLE },
[RPC_A_SESSION] = { .name = "ubus_rpc_session",
.type = BLOBMSG_TYPE_STRING },
};
enum {
RPC_S_CONFIG,
RPC_S_SECTION,
RPC_S_TYPE,
RPC_S_MATCH,
RPC_S_VALUES,
RPC_S_SESSION,
__RPC_S_MAX,
};
static const struct blobmsg_policy rpc_uci_set_policy[__RPC_S_MAX] = {
[RPC_S_CONFIG] = { .name = "config", .type = BLOBMSG_TYPE_STRING },
[RPC_S_SECTION] = { .name = "section", .type = BLOBMSG_TYPE_STRING },
[RPC_S_TYPE] = { .name = "type", .type = BLOBMSG_TYPE_STRING },
[RPC_S_MATCH] = { .name = "match", .type = BLOBMSG_TYPE_TABLE },
[RPC_S_VALUES] = { .name = "values", .type = BLOBMSG_TYPE_TABLE },
[RPC_S_SESSION] = { .name = "ubus_rpc_session",
.type = BLOBMSG_TYPE_STRING },
};
enum {
RPC_D_CONFIG,
RPC_D_SECTION,
RPC_D_TYPE,
RPC_D_MATCH,
RPC_D_OPTION,
RPC_D_OPTIONS,
RPC_D_SESSION,
__RPC_D_MAX,
};
static const struct blobmsg_policy rpc_uci_delete_policy[__RPC_D_MAX] = {
[RPC_D_CONFIG] = { .name = "config", .type = BLOBMSG_TYPE_STRING },
[RPC_D_SECTION] = { .name = "section", .type = BLOBMSG_TYPE_STRING },
[RPC_D_TYPE] = { .name = "type", .type = BLOBMSG_TYPE_STRING },
[RPC_D_MATCH] = { .name = "match", .type = BLOBMSG_TYPE_TABLE },
[RPC_D_OPTION] = { .name = "option", .type = BLOBMSG_TYPE_STRING },
[RPC_D_OPTIONS] = { .name = "options", .type = BLOBMSG_TYPE_ARRAY },
[RPC_D_SESSION] = { .name = "ubus_rpc_session",
.type = BLOBMSG_TYPE_STRING },
};
enum {
RPC_R_CONFIG,
RPC_R_SECTION,
RPC_R_OPTION,
RPC_R_NAME,
RPC_R_SESSION,
__RPC_R_MAX,
};
static const struct blobmsg_policy rpc_uci_rename_policy[__RPC_R_MAX] = {
[RPC_R_CONFIG] = { .name = "config", .type = BLOBMSG_TYPE_STRING },
[RPC_R_SECTION] = { .name = "section", .type = BLOBMSG_TYPE_STRING },
[RPC_R_OPTION] = { .name = "option", .type = BLOBMSG_TYPE_STRING },
[RPC_R_NAME] = { .name = "name", .type = BLOBMSG_TYPE_STRING },
[RPC_R_SESSION] = { .name = "ubus_rpc_session",
.type = BLOBMSG_TYPE_STRING },
};
enum {
RPC_O_CONFIG,
RPC_O_SECTIONS,
RPC_O_SESSION,
__RPC_O_MAX,
};
static const struct blobmsg_policy rpc_uci_order_policy[__RPC_O_MAX] = {
[RPC_O_CONFIG] = { .name = "config", .type = BLOBMSG_TYPE_STRING },
[RPC_O_SECTIONS] = { .name = "sections", .type = BLOBMSG_TYPE_ARRAY },
[RPC_O_SESSION] = { .name = "ubus_rpc_session",
.type = BLOBMSG_TYPE_STRING },
};
enum {
RPC_C_CONFIG,
RPC_C_SESSION,
__RPC_C_MAX,
};
static const struct blobmsg_policy rpc_uci_config_policy[__RPC_C_MAX] = {
[RPC_C_CONFIG] = { .name = "config", .type = BLOBMSG_TYPE_STRING },
[RPC_C_SESSION] = { .name = "ubus_rpc_session",
.type = BLOBMSG_TYPE_STRING },
};
enum {
RPC_T_ROLLBACK,
RPC_T_TIMEOUT,
RPC_T_SESSION,
__RPC_T_MAX,
};
static const struct blobmsg_policy rpc_uci_apply_policy[__RPC_T_MAX] = {
[RPC_T_ROLLBACK] = { .name = "rollback", .type = BLOBMSG_TYPE_BOOL },
[RPC_T_TIMEOUT] = { .name = "timeout", .type = BLOBMSG_TYPE_INT32 },
[RPC_T_SESSION] = { .name = "ubus_rpc_session",
.type = BLOBMSG_TYPE_STRING },
};
enum {
RPC_B_SESSION,
__RPC_B_MAX,
};
static const struct blobmsg_policy rpc_uci_rollback_policy[__RPC_B_MAX] = {
[RPC_B_SESSION] = { .name = "ubus_rpc_session",
.type = BLOBMSG_TYPE_STRING },
};
/*
* Turn uci error state into ubus return code
*/
static int
rpc_uci_status(void)
{
switch (cursor->err)
{
case UCI_OK:
return UBUS_STATUS_OK;
case UCI_ERR_INVAL:
return UBUS_STATUS_INVALID_ARGUMENT;
case UCI_ERR_NOTFOUND:
return UBUS_STATUS_NOT_FOUND;
default:
return UBUS_STATUS_UNKNOWN_ERROR;
}
}
/*
* Clear all save directories from the uci cursor and append the given path
* as new save directory.
*/
static void
rpc_uci_replace_savedir(const char *path)
{
struct uci_element *e, *tmp;
uci_foreach_element_safe(&cursor->delta_path, tmp, e)
free(e);
cursor->delta_path.prev = &cursor->delta_path;
cursor->delta_path.next = &cursor->delta_path;
if (path)
uci_set_savedir(cursor, path);
}
/*
* Setup per-session delta save directory. If the passed "sid" blob attribute
* pointer is NULL then the precedure was not invoked through the ubus-rpc so
* we do not perform session isolation and use the default save directory.
*/
static void
rpc_uci_set_savedir(struct blob_attr *sid)
{
char path[PATH_MAX];
if (!sid)
{
rpc_uci_replace_savedir("/tmp/.uci");
return;
}
snprintf(path, sizeof(path) - 1,
RPC_UCI_SAVEDIR_PREFIX "%s", blobmsg_get_string(sid));
rpc_uci_replace_savedir(path);
}
/*
* Test read access to given config. If the passed "sid" blob attribute pointer
* is NULL then the precedure was not invoked through the ubus-rpc so we do not
* perform access control and always assume true.
*/
static bool
rpc_uci_read_access(struct blob_attr *sid, struct blob_attr *config)
{
rpc_uci_set_savedir(sid);
if (!sid)
return true;
return rpc_session_access(blobmsg_data(sid), "uci",
blobmsg_data(config), "read");
}
/*
* Test write access to given config. If the passed "sid" blob attribute pointer
* is NULL then the precedure was not invoked through the ubus-rpc so we do not
* perform access control and always assume true.
*/
static bool
rpc_uci_write_access(struct blob_attr *sid, struct blob_attr *config)
{
rpc_uci_set_savedir(sid);
if (!sid)
return true;
return rpc_session_access(blobmsg_data(sid), "uci",
blobmsg_data(config), "write");
}
/*
* Format applicable blob value as string and place a pointer to the string
* buffer in "p". Uses a static string buffer.
*/
static bool
rpc_uci_format_blob(struct blob_attr *v, const char **p)
{
static char buf[21];
*p = NULL;
switch (blobmsg_type(v))
{
case BLOBMSG_TYPE_STRING:
*p = blobmsg_data(v);
break;
case BLOBMSG_TYPE_INT64:
snprintf(buf, sizeof(buf), "%"PRIu64, blobmsg_get_u64(v));
*p = buf;
break;
case BLOBMSG_TYPE_INT32:
snprintf(buf, sizeof(buf), "%u", blobmsg_get_u32(v));
*p = buf;
break;
case BLOBMSG_TYPE_INT16:
snprintf(buf, sizeof(buf), "%u", blobmsg_get_u16(v));
*p = buf;
break;
case BLOBMSG_TYPE_INT8:
snprintf(buf, sizeof(buf), "%u", !!blobmsg_get_u8(v));
*p = buf;
break;
default:
break;
}
return !!*p;
}
/*
* Lookup the given uci_ptr and enable extended lookup format if the .section
* value of the uci_ptr looks like extended syntax. Uses an internal copy
* of the given uci_ptr to perform the lookup as failing extended section
* lookup operations in libuci will zero our the uci_ptr struct.
* Copies the internal uci_ptr back to given the uci_ptr on success.
*/
static int
rpc_uci_lookup(struct uci_ptr *ptr)
{
int rv;
struct uci_ptr lookup = *ptr;
if (!lookup.s && lookup.section && *lookup.section == '@')
lookup.flags |= UCI_LOOKUP_EXTENDED;
rv = uci_lookup_ptr(cursor, &lookup, NULL, true);
if (!rv)
*ptr = lookup;
return rv;
}
/*
* Checks whether the given uci_option object matches the given string value.
* 1) If the uci_option is of type list, check whether any of the list elements
* equals to the given string
* 2) If the uci_option is of type string, parse it into space separated tokens
* and check if any of the tokens equals to the given string.
* Returns true if a list element or token matched the given string.
*/
static bool
rpc_uci_match_option(struct uci_option *o, const char *cmp)
{
struct uci_element *e;
char *s, *p;
if (o->type == UCI_TYPE_LIST)
{
uci_foreach_element(&o->v.list, e)
if (e->name && !strcmp(e->name, cmp))
return true;
return false;
}
if (!o->v.string)
return false;
s = strdup(o->v.string);
if (!s)
return false;
for (p = strtok(s, " \t"); p; p = strtok(NULL, " \t"))
{
if (!strcmp(p, cmp))
{
free(s);
return true;
}
}
free(s);
return false;
}
/*
* Checks whether the given uci_section matches the type and value blob attrs.
* 1) Returns false if "type" is given and the section type does not match
* the value specified in the "type" string blob attribute, else continue.
* 2) Tests whether any key in the "matches" table blob attribute exists in
* the given uci_section and whether each value is contained in the
* corresponding uci option value (see rpc_uci_match_option()).
* 3) A missing or empty "matches" table blob attribute is always considered
* to be a match.
* Returns true if "type" matches or is NULL and "matches" matches or is NULL.
*/
static bool
rpc_uci_match_section(struct uci_section *s,
struct blob_attr *type, struct blob_attr *matches)
{
struct uci_element *e;
struct blob_attr *cur;
const char *cmp;
bool match = false;
bool empty = true;
int rem;
if (type && strcmp(s->type, blobmsg_data(type)))
return false;
if (!matches)
return true;
blobmsg_for_each_attr(cur, matches, rem)
{
if (!rpc_uci_format_blob(cur, &cmp))
continue;
uci_foreach_element(&s->options, e)
{
if (strcmp(e->name, blobmsg_name(cur)))
continue;
if (!rpc_uci_match_option(uci_to_option(e), cmp))
return false;
match = true;
}
empty = false;
}
return (empty || match);
}
/*
* Dump the given uci_option value into the global blobmsg buffer and use
* given "name" as key.
* 1) If the uci_option is of type list, put a table into the blob buffer and
* add each list item as string to it.
* 2) If the uci_option is of type string, put its value directly into the blob
* buffer.
*/
static void
rpc_uci_dump_option(struct uci_option *o, const char *name)
{
void *c;
struct uci_element *e;
switch (o->type)
{
case UCI_TYPE_STRING:
blobmsg_add_string(&buf, name, o->v.string);
break;
case UCI_TYPE_LIST:
c = blobmsg_open_array(&buf, name);
uci_foreach_element(&o->v.list, e)
blobmsg_add_string(&buf, NULL, e->name);
blobmsg_close_array(&buf, c);
break;
default:
break;
}
}
/*
* Dump the given uci_section object into the global blobmsg buffer and use
* given "name" as key.
* Puts a table into the blob buffer and puts each section option member value
* as value into the table using the option name as key.
* Adds three special keys ".anonymous", ".type" and ".name" which specify the
* corresponding section properties.
*/
static void
rpc_uci_dump_section(struct uci_section *s, const char *name, int index)
{
void *c;
struct uci_option *o;
struct uci_element *e;
c = blobmsg_open_table(&buf, name);
blobmsg_add_u8(&buf, ".anonymous", s->anonymous);
blobmsg_add_string(&buf, ".type", s->type);
blobmsg_add_string(&buf, ".name", s->e.name);
if (index >= 0)
blobmsg_add_u32(&buf, ".index", index);
uci_foreach_element(&s->options, e)
{
o = uci_to_option(e);
rpc_uci_dump_option(o, o->e.name);
}
blobmsg_close_table(&buf, c);
}
/*
* Dump the given uci_package object into the global blobmsg buffer and use
* given "name" as key.
* Puts a table into the blob buffer and puts each package section member as
* value into the table using the section name as key.
* Only dumps sections matching the given "type" and "matches", see explaination
* of rpc_uci_match_section() for details.
*/
static void
rpc_uci_dump_package(struct uci_package *p, const char *name,
struct blob_attr *type, struct blob_attr *matches)
{
void *c;
struct uci_element *e;
int i = -1;
c = blobmsg_open_table(&buf, name);
uci_foreach_element(&p->sections, e)
{
i++;
if (!rpc_uci_match_section(uci_to_section(e), type, matches))
continue;
rpc_uci_dump_section(uci_to_section(e), e->name, i);
}
blobmsg_close_table(&buf, c);
}
static int
rpc_uci_getcommon(struct ubus_context *ctx, struct ubus_request_data *req,
struct blob_attr *msg, bool use_state)
{
struct blob_attr *tb[__RPC_G_MAX];
struct uci_package *p = NULL;
struct uci_ptr ptr = { 0 };
blobmsg_parse(rpc_uci_get_policy, __RPC_G_MAX, tb,
blob_data(msg), blob_len(msg));
if (!tb[RPC_G_CONFIG])
return UBUS_STATUS_INVALID_ARGUMENT;
if (!rpc_uci_read_access(tb[RPC_G_SESSION], tb[RPC_G_CONFIG]))
return UBUS_STATUS_PERMISSION_DENIED;
ptr.package = blobmsg_data(tb[RPC_G_CONFIG]);
if (use_state)
uci_set_savedir(cursor, "/var/state");
if (uci_load(cursor, ptr.package, &p))
return rpc_uci_status();
if (tb[RPC_G_SECTION])
{
ptr.section = blobmsg_data(tb[RPC_G_SECTION]);
if (tb[RPC_G_OPTION])
ptr.option = blobmsg_data(tb[RPC_G_OPTION]);
}
if (rpc_uci_lookup(&ptr) || !(ptr.flags & UCI_LOOKUP_COMPLETE))
goto out;
blob_buf_init(&buf, 0);
switch (ptr.last->type)
{
case UCI_TYPE_PACKAGE:
rpc_uci_dump_package(ptr.p, "values", tb[RPC_G_TYPE], tb[RPC_G_MATCH]);
break;
case UCI_TYPE_SECTION:
rpc_uci_dump_section(ptr.s, "values", -1);
break;
case UCI_TYPE_OPTION:
rpc_uci_dump_option(ptr.o, "value");
break;
default:
break;
}
ubus_send_reply(ctx, req, buf.head);
out:
uci_unload(cursor, p);
return rpc_uci_status();
}
static int
rpc_uci_get(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
return rpc_uci_getcommon(ctx, req, msg, false);
}
static int
rpc_uci_state(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
return rpc_uci_getcommon(ctx, req, msg, true);
}
static int
rpc_uci_add(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
struct blob_attr *tb[__RPC_A_MAX];
struct blob_attr *cur, *elem;
struct uci_package *p = NULL;
struct uci_section *s;
struct uci_ptr ptr = { 0 };
int rem, rem2;
blobmsg_parse(rpc_uci_add_policy, __RPC_A_MAX, tb,
blob_data(msg), blob_len(msg));
if (!tb[RPC_A_CONFIG] || !tb[RPC_A_TYPE])
return UBUS_STATUS_INVALID_ARGUMENT;
if (!rpc_uci_write_access(tb[RPC_A_SESSION], tb[RPC_A_CONFIG]))
return UBUS_STATUS_PERMISSION_DENIED;
ptr.package = blobmsg_data(tb[RPC_A_CONFIG]);
if (uci_load(cursor, ptr.package, &p))
return rpc_uci_status();
/* add named section */
if (tb[RPC_A_NAME])
{
ptr.section = blobmsg_data(tb[RPC_A_NAME]);
ptr.value = blobmsg_data(tb[RPC_A_TYPE]);
ptr.option = NULL;
if (rpc_uci_lookup(&ptr) || uci_set(cursor, &ptr))
goto out;
}
/* add anon section */
else
{
if (uci_add_section(cursor, p, blobmsg_data(tb[RPC_A_TYPE]), &s) || !s)
goto out;
ptr.section = s->e.name;
}
if (tb[RPC_A_VALUES])
{
blobmsg_for_each_attr(cur, tb[RPC_A_VALUES], rem)
{
ptr.o = NULL;
ptr.option = blobmsg_name(cur);
if (rpc_uci_lookup(&ptr) || !ptr.s)
continue;
switch (blobmsg_type(cur))
{
case BLOBMSG_TYPE_ARRAY:
blobmsg_for_each_attr(elem, cur, rem2)
if (rpc_uci_format_blob(elem, &ptr.value))
uci_add_list(cursor, &ptr);
break;
default:
if (rpc_uci_format_blob(cur, &ptr.value))
uci_set(cursor, &ptr);
break;
}
}
}
uci_save(cursor, p);
blob_buf_init(&buf, 0);
blobmsg_add_string(&buf, "section", ptr.section);
ubus_send_reply(ctx, req, buf.head);
out:
uci_unload(cursor, p);
return rpc_uci_status();
}
/*
* Turn value from a blob attribute into uci set operation
* 1) if the blob is of type array, delete existing option (if any) and
* emit uci add_list operations for each element
* 2) if the blob is not an array but an option of type list exists,
* delete existing list and emit uci set operation for the blob value
* 3) in all other cases only emit a set operation if there is no existing
* option of if the existing options value differs from the blob value
*/
static void
rpc_uci_merge_set(struct blob_attr *opt, struct uci_ptr *ptr)
{
struct blob_attr *cur;
int rem;
ptr->o = NULL;
ptr->option = blobmsg_name(opt);
ptr->value = NULL;
if (rpc_uci_lookup(ptr) || !ptr->s)
return;
if (blobmsg_type(opt) == BLOBMSG_TYPE_ARRAY)
{
if (ptr->o)
uci_delete(cursor, ptr);
blobmsg_for_each_attr(cur, opt, rem)
if (rpc_uci_format_blob(cur, &ptr->value))
uci_add_list(cursor, ptr);
}
else if (ptr->o && ptr->o->type == UCI_TYPE_LIST)
{
uci_delete(cursor, ptr);
if (rpc_uci_format_blob(opt, &ptr->value))
uci_set(cursor, ptr);
}
else if (rpc_uci_format_blob(opt, &ptr->value))
{
if (!ptr->o || !ptr->o->v.string || strcmp(ptr->o->v.string, ptr->value))
uci_set(cursor, ptr);
}
}
static int
rpc_uci_set(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
struct blob_attr *tb[__RPC_S_MAX];
struct blob_attr *cur;
struct uci_package *p = NULL;
struct uci_element *e;
struct uci_ptr ptr = { 0 };
int rem;
blobmsg_parse(rpc_uci_set_policy, __RPC_S_MAX, tb,
blob_data(msg), blob_len(msg));
if (!tb[RPC_S_CONFIG] || !tb[RPC_S_VALUES] ||
(!tb[RPC_S_SECTION] && !tb[RPC_S_TYPE] && !tb[RPC_S_MATCH]))
return UBUS_STATUS_INVALID_ARGUMENT;
if (!rpc_uci_write_access(tb[RPC_S_SESSION], tb[RPC_S_CONFIG]))
return UBUS_STATUS_PERMISSION_DENIED;
ptr.package = blobmsg_data(tb[RPC_S_CONFIG]);
if (uci_load(cursor, ptr.package, &p))
return rpc_uci_status();
if (tb[RPC_S_SECTION])
{
ptr.section = blobmsg_data(tb[RPC_S_SECTION]);
blobmsg_for_each_attr(cur, tb[RPC_S_VALUES], rem)
rpc_uci_merge_set(cur, &ptr);
}
else
{
uci_foreach_element(&p->sections, e)
{
if (!rpc_uci_match_section(uci_to_section(e),
tb[RPC_S_TYPE], tb[RPC_S_MATCH]))
continue;
ptr.s = NULL;
ptr.section = e->name;
blobmsg_for_each_attr(cur, tb[RPC_S_VALUES], rem)
rpc_uci_merge_set(cur, &ptr);
}
}
uci_save(cursor, p);
uci_unload(cursor, p);
return rpc_uci_status();
}
/*
* Delete option or section from uci specified by given blob attribute pointer
* 1) if the blob is of type array, delete any option named after each element
* 2) if the blob is of type string, delete the option named after its value
* 3) if the blob is NULL, delete entire section
*/
static void
rpc_uci_merge_delete(struct blob_attr *opt, struct uci_ptr *ptr)
{
struct blob_attr *cur;
int rem;
if (rpc_uci_lookup(ptr) || !ptr->s)
return;
if (!opt)
{
ptr->o = NULL;
ptr->option = NULL;
uci_delete(cursor, ptr);
}
else if (blobmsg_type(opt) == BLOBMSG_TYPE_ARRAY)
{
blobmsg_for_each_attr(cur, opt, rem)
{
if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING)
continue;
ptr->o = NULL;
ptr->option = blobmsg_data(cur);
if (rpc_uci_lookup(ptr) || !ptr->o)
continue;
uci_delete(cursor, ptr);
}
}
else if (blobmsg_type(opt) == BLOBMSG_TYPE_STRING)
{
ptr->o = NULL;
ptr->option = blobmsg_data(opt);
if (rpc_uci_lookup(ptr) || !ptr->o)
return;
uci_delete(cursor, ptr);
}
}
static int
rpc_uci_delete(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
struct blob_attr *tb[__RPC_D_MAX];
struct uci_package *p = NULL;
struct uci_element *e, *tmp;
struct uci_ptr ptr = { 0 };
blobmsg_parse(rpc_uci_delete_policy, __RPC_D_MAX, tb,
blob_data(msg), blob_len(msg));
if (!tb[RPC_D_CONFIG] ||
(!tb[RPC_D_SECTION] && !tb[RPC_D_TYPE] && !tb[RPC_D_MATCH]))
return UBUS_STATUS_INVALID_ARGUMENT;
if (!rpc_uci_write_access(tb[RPC_D_SESSION], tb[RPC_D_CONFIG]))
return UBUS_STATUS_PERMISSION_DENIED;
ptr.package = blobmsg_data(tb[RPC_D_CONFIG]);
if (uci_load(cursor, ptr.package, &p))
return rpc_uci_status();
if (tb[RPC_D_SECTION])
{
ptr.section = blobmsg_data(tb[RPC_D_SECTION]);
if (tb[RPC_D_OPTIONS])
rpc_uci_merge_delete(tb[RPC_D_OPTIONS], &ptr);
else
rpc_uci_merge_delete(tb[RPC_D_OPTION], &ptr);
}
else
{
uci_foreach_element_safe(&p->sections, tmp, e)
{
if (!rpc_uci_match_section(uci_to_section(e),
tb[RPC_D_TYPE], tb[RPC_D_MATCH]))
continue;
ptr.s = NULL;
ptr.section = e->name;
if (tb[RPC_D_OPTIONS])
rpc_uci_merge_delete(tb[RPC_D_OPTIONS], &ptr);
else
rpc_uci_merge_delete(tb[RPC_D_OPTION], &ptr);
}
}
uci_save(cursor, p);
uci_unload(cursor, p);
return rpc_uci_status();
}
static int
rpc_uci_rename(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
struct blob_attr *tb[__RPC_R_MAX];
struct uci_package *p = NULL;
struct uci_ptr ptr = { 0 };
blobmsg_parse(rpc_uci_rename_policy, __RPC_R_MAX, tb,
blob_data(msg), blob_len(msg));
if (!tb[RPC_R_CONFIG] || !tb[RPC_R_SECTION] || !tb[RPC_R_NAME])
return UBUS_STATUS_INVALID_ARGUMENT;
if (!rpc_uci_write_access(tb[RPC_R_SESSION], tb[RPC_R_CONFIG]))
return UBUS_STATUS_PERMISSION_DENIED;
ptr.package = blobmsg_data(tb[RPC_R_CONFIG]);
ptr.section = blobmsg_data(tb[RPC_R_SECTION]);
ptr.value = blobmsg_data(tb[RPC_R_NAME]);
if (tb[RPC_R_OPTION])
ptr.option = blobmsg_data(tb[RPC_R_OPTION]);
if (uci_load(cursor, ptr.package, &p))
return rpc_uci_status();
if (uci_lookup_ptr(cursor, &ptr, NULL, true))
goto out;
if ((ptr.option && !ptr.o) || !ptr.s)
{
cursor->err = UCI_ERR_NOTFOUND;
goto out;
}
if (uci_rename(cursor, &ptr))
goto out;
uci_save(cursor, p);
out:
uci_unload(cursor, p);
return rpc_uci_status();
}
static int
rpc_uci_order(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
struct blob_attr *tb[__RPC_O_MAX];
struct blob_attr *cur;
struct uci_package *p = NULL;
struct uci_ptr ptr = { 0 };
int rem, i = 0;
blobmsg_parse(rpc_uci_order_policy, __RPC_O_MAX, tb,
blob_data(msg), blob_len(msg));
if (!tb[RPC_O_CONFIG] || !tb[RPC_O_SECTIONS])
return UBUS_STATUS_INVALID_ARGUMENT;
if (!rpc_uci_write_access(tb[RPC_O_SESSION], tb[RPC_O_CONFIG]))
return UBUS_STATUS_PERMISSION_DENIED;
ptr.package = blobmsg_data(tb[RPC_O_CONFIG]);
if (uci_load(cursor, ptr.package, &p))
return rpc_uci_status();
blobmsg_for_each_attr(cur, tb[RPC_O_SECTIONS], rem)
{
if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING)
continue;
ptr.s = NULL;
ptr.section = blobmsg_data(cur);
if (uci_lookup_ptr(cursor, &ptr, NULL, true) || !ptr.s)
continue;
uci_reorder_section(cursor, ptr.s, i++);
}
uci_save(cursor, p);
uci_unload(cursor, p);
return rpc_uci_status();
}
static void
rpc_uci_dump_change(struct uci_delta *d)
{
void *c;
const char *types[] = {
[UCI_CMD_REORDER] = "order",
[UCI_CMD_REMOVE] = "remove",
[UCI_CMD_RENAME] = "rename",
[UCI_CMD_ADD] = "add",
[UCI_CMD_LIST_ADD] = "list-add",
[UCI_CMD_LIST_DEL] = "list-del",
[UCI_CMD_CHANGE] = "set",
};
if (!d->section)
return;
c = blobmsg_open_array(&buf, NULL);
blobmsg_add_string(&buf, NULL, types[d->cmd]);
blobmsg_add_string(&buf, NULL, d->section);
if (d->e.name)
blobmsg_add_string(&buf, NULL, d->e.name);
if (d->value)
{
if (d->cmd == UCI_CMD_REORDER)
blobmsg_add_u32(&buf, NULL, strtoul(d->value, NULL, 10));
else
blobmsg_add_string(&buf, NULL, d->value);
}
blobmsg_close_array(&buf, c);
}
static int
rpc_uci_changes(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
struct blob_attr *tb[__RPC_C_MAX];
struct uci_package *p = NULL;
struct uci_element *e;
char **configs;
void *c, *d;
int i;
blobmsg_parse(rpc_uci_config_policy, __RPC_C_MAX, tb,
blob_data(msg), blob_len(msg));
if (tb[RPC_C_CONFIG])
{
if (!rpc_uci_read_access(tb[RPC_C_SESSION], tb[RPC_C_CONFIG]))
return UBUS_STATUS_PERMISSION_DENIED;
if (uci_load(cursor, blobmsg_data(tb[RPC_C_CONFIG]), &p))
return rpc_uci_status();
blob_buf_init(&buf, 0);
c = blobmsg_open_array(&buf, "changes");
uci_foreach_element(&p->saved_delta, e)
rpc_uci_dump_change(uci_to_delta(e));
blobmsg_close_array(&buf, c);
uci_unload(cursor, p);
ubus_send_reply(ctx, req, buf.head);
return rpc_uci_status();
}
rpc_uci_set_savedir(tb[RPC_C_SESSION]);
if (uci_list_configs(cursor, &configs))
return rpc_uci_status();
blob_buf_init(&buf, 0);
c = blobmsg_open_table(&buf, "changes");
for (i = 0; configs[i]; i++)
{
if (tb[RPC_C_SESSION] &&
!rpc_session_access(blobmsg_data(tb[RPC_C_SESSION]), "uci",
configs[i], "read"))
continue;
if (uci_load(cursor, configs[i], &p))
continue;
if (!uci_list_empty(&p->saved_delta))
{
d = blobmsg_open_array(&buf, configs[i]);
uci_foreach_element(&p->saved_delta, e)
rpc_uci_dump_change(uci_to_delta(e));
blobmsg_close_array(&buf, d);
}
uci_unload(cursor, p);
}
blobmsg_close_table(&buf, c);
ubus_send_reply(ctx, req, buf.head);
return 0;
}
static void
rpc_uci_trigger_event(struct ubus_context *ctx, const char *config)
{
char *pkg = strdup(config);
static struct blob_buf b;
uint32_t id;
if (!ubus_lookup_id(ctx, "service", &id)) {
void *c;
blob_buf_init(&b, 0);
blobmsg_add_string(&b, "type", "config.change");
c = blobmsg_open_table(&b, "data");
blobmsg_add_string(&b, "package", pkg);
blobmsg_close_table(&b, c);
ubus_invoke(ctx, id, "event", b.head, NULL, 0, 1000);
}
free(pkg);
}
static int
rpc_uci_revert_commit(struct ubus_context *ctx, struct blob_attr *msg, bool commit)
{
struct blob_attr *tb[__RPC_C_MAX];
struct uci_package *p = NULL;
struct uci_ptr ptr = { 0 };
if (apply_sid[0])
return UBUS_STATUS_PERMISSION_DENIED;
blobmsg_parse(rpc_uci_config_policy, __RPC_C_MAX, tb,
blob_data(msg), blob_len(msg));
if (!tb[RPC_C_CONFIG])
return UBUS_STATUS_INVALID_ARGUMENT;
if (!rpc_uci_write_access(tb[RPC_C_SESSION], tb[RPC_C_CONFIG]))
return UBUS_STATUS_PERMISSION_DENIED;
ptr.package = blobmsg_data(tb[RPC_C_CONFIG]);
if (commit)
{
if (!uci_load(cursor, ptr.package, &p))
{
uci_commit(cursor, &p, false);
uci_unload(cursor, p);
rpc_uci_trigger_event(ctx, blobmsg_get_string(tb[RPC_C_CONFIG]));
}
}
else
{
if (!uci_lookup_ptr(cursor, &ptr, NULL, true) && ptr.p)
{
uci_revert(cursor, &ptr);
uci_unload(cursor, ptr.p);
}
}
return rpc_uci_status();
}
static int
rpc_uci_revert(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
return rpc_uci_revert_commit(ctx, msg, false);
}
static int
rpc_uci_commit(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
return rpc_uci_revert_commit(ctx, msg, true);
}
static int
rpc_uci_configs(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
char **configs;
void *c;
int i;
if (uci_list_configs(cursor, &configs))
goto out;
blob_buf_init(&buf, 0);
c = blobmsg_open_array(&buf, "configs");
for (i = 0; configs[i]; i++)
blobmsg_add_string(&buf, NULL, configs[i]);
blobmsg_close_array(&buf, c);
ubus_send_reply(ctx, req, buf.head);
out:
return rpc_uci_status();
}
/*
* Remove given delta save directory (if any).
*/
static void
rpc_uci_purge_dir(const char *path)
{
DIR *d;
struct stat s;
struct dirent *e;
char file[PATH_MAX];
if (stat(path, &s) || !S_ISDIR(s.st_mode))
return;
if ((d = opendir(path)) != NULL)
{
while ((e = readdir(d)) != NULL)
{
snprintf(file, sizeof(file) - 1, "%s/%s", path, e->d_name);
if (stat(file, &s) || !S_ISREG(s.st_mode))
continue;
unlink(file);
}
closedir(d);
rmdir(path);
}
}
static int
rpc_uci_apply_config(struct ubus_context *ctx, char *config)
{
struct uci_package *p = NULL;
if (!uci_load(cursor, config, &p)) {
uci_commit(cursor, &p, false);
uci_unload(cursor, p);
}
rpc_uci_trigger_event(ctx, config);
return 0;
}
static void
rpc_uci_copy_file(const char *src, const char *target, const char *file)
{
char tmp[256];
FILE *in, *out;
snprintf(tmp, sizeof(tmp), "%s%s", src, file);
in = fopen(tmp, "rb");
snprintf(tmp, sizeof(tmp), "%s%s", target, file);
out = fopen(tmp, "wb+");
if (in && out)
while (!feof(in)) {
int len = fread(tmp, 1, sizeof(tmp), in);
if(len > 0)
fwrite(tmp, 1, len, out);
}
if(in)
fclose(in);
if(out)
fclose(out);
}
static int
rpc_uci_apply_access(const char *sid, glob_t *gl)
{
struct stat s;
int i, c = 0;
if (gl->gl_pathc < 3)
return UBUS_STATUS_NO_DATA;
for (i = 0; i < gl->gl_pathc; i++) {
char *config = basename(gl->gl_pathv[i]);
if (*config == '.')
continue;
if (stat(gl->gl_pathv[i], &s) || !s.st_size)
continue;
if (!rpc_session_access(sid, "uci", config, "write"))
return UBUS_STATUS_PERMISSION_DENIED;
c++;
}
if (!c)
return UBUS_STATUS_NO_DATA;
return 0;
}
static void
rpc_uci_do_rollback(struct ubus_context *ctx, glob_t *gl)
{
int i, deny;
char tmp[PATH_MAX];
/* Test apply permission to see if the initiator session still exists.
* If it does, restore the delta files as well, else just restore the
* main configuration files. */
deny = apply_sid[0]
? rpc_uci_apply_access(apply_sid, gl) : UBUS_STATUS_NOT_FOUND;
if (!deny) {
snprintf(tmp, sizeof(tmp), RPC_UCI_SAVEDIR_PREFIX "%s/", apply_sid);
mkdir(tmp, 0700);
}
/* avoid merging unrelated uci changes when restoring old configs */
rpc_uci_replace_savedir("/dev/null");
for (i = 0; i < gl->gl_pathc; i++) {
char *config = basename(gl->gl_pathv[i]);
if (*config == '.')
continue;
rpc_uci_copy_file(RPC_SNAPSHOT_FILES, RPC_UCI_DIR, config);
rpc_uci_apply_config(ctx, config);
if (deny)
continue;
rpc_uci_copy_file(RPC_SNAPSHOT_DELTA, tmp, config);
}
rpc_uci_purge_dir(RPC_SNAPSHOT_FILES);
rpc_uci_purge_dir(RPC_SNAPSHOT_DELTA);
uloop_timeout_cancel(&apply_timer);
memset(apply_sid, 0, sizeof(apply_sid));
apply_ctx = NULL;
}
static void
rpc_uci_apply_timeout(struct uloop_timeout *t)
{
glob_t gl;
char tmp[PATH_MAX];
snprintf(tmp, sizeof(tmp), "%s/*", RPC_SNAPSHOT_FILES);
if (glob(tmp, GLOB_PERIOD, NULL, &gl) < 0)
return;
rpc_uci_do_rollback(apply_ctx, &gl);
globfree(&gl);
}
static int
rpc_uci_apply(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
struct blob_attr *tb[__RPC_T_MAX];
int timeout = RPC_APPLY_TIMEOUT;
char tmp[PATH_MAX];
bool rollback = false;
int ret, i;
char *sid;
glob_t gl;
blobmsg_parse(rpc_uci_apply_policy, __RPC_T_MAX, tb,
blob_data(msg), blob_len(msg));
if (tb[RPC_T_ROLLBACK])
rollback = blobmsg_get_bool(tb[RPC_T_ROLLBACK]);
if (apply_sid[0] && rollback)
return UBUS_STATUS_PERMISSION_DENIED;
if (!tb[RPC_T_SESSION])
return UBUS_STATUS_INVALID_ARGUMENT;
sid = blobmsg_data(tb[RPC_T_SESSION]);
if (tb[RPC_T_TIMEOUT])
timeout = blobmsg_get_u32(tb[RPC_T_TIMEOUT]);
rpc_uci_purge_dir(RPC_SNAPSHOT_FILES);
rpc_uci_purge_dir(RPC_SNAPSHOT_DELTA);
if (!apply_sid[0]) {
rpc_uci_set_savedir(tb[RPC_T_SESSION]);
mkdir(RPC_SNAPSHOT_FILES, 0700);
mkdir(RPC_SNAPSHOT_DELTA, 0700);
snprintf(tmp, sizeof(tmp), RPC_UCI_SAVEDIR_PREFIX "%s/*", sid);
if (glob(tmp, GLOB_PERIOD, NULL, &gl) < 0)
return UBUS_STATUS_NOT_FOUND;
snprintf(tmp, sizeof(tmp), RPC_UCI_SAVEDIR_PREFIX "%s/", sid);
ret = rpc_uci_apply_access(sid, &gl);
if (ret) {
globfree(&gl);
return ret;
}
/* copy SID early because rpc_uci_apply_config() will clobber buf */
if (rollback)
strncpy(apply_sid, sid, RPC_SID_LEN);
for (i = 0; i < gl.gl_pathc; i++) {
char *config = basename(gl.gl_pathv[i]);
struct stat s;
if (*config == '.')
continue;
if (stat(gl.gl_pathv[i], &s) || !s.st_size)
continue;
rpc_uci_copy_file(RPC_UCI_DIR, RPC_SNAPSHOT_FILES, config);
rpc_uci_copy_file(tmp, RPC_SNAPSHOT_DELTA, config);
rpc_uci_apply_config(ctx, config);
}
globfree(&gl);
if (rollback) {
apply_timer.cb = rpc_uci_apply_timeout;
uloop_timeout_set(&apply_timer, timeout * 1000);
apply_ctx = ctx;
}
}
return 0;
}
static int
rpc_uci_confirm(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
struct blob_attr *tb[__RPC_B_MAX];
char *sid;
blobmsg_parse(rpc_uci_rollback_policy, __RPC_B_MAX, tb,
blob_data(msg), blob_len(msg));
if (!tb[RPC_B_SESSION])
return UBUS_STATUS_INVALID_ARGUMENT;
sid = blobmsg_data(tb[RPC_B_SESSION]);
if (!apply_sid[0])
return UBUS_STATUS_NO_DATA;
if (strcmp(apply_sid, sid))
return UBUS_STATUS_PERMISSION_DENIED;
rpc_uci_purge_dir(RPC_SNAPSHOT_FILES);
rpc_uci_purge_dir(RPC_SNAPSHOT_DELTA);
uloop_timeout_cancel(&apply_timer);
memset(apply_sid, 0, sizeof(apply_sid));
apply_ctx = NULL;
return 0;
}
static int
rpc_uci_rollback(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
struct blob_attr *tb[__RPC_B_MAX];
char tmp[PATH_MAX];
glob_t gl;
char *sid;
blobmsg_parse(rpc_uci_rollback_policy, __RPC_B_MAX, tb,
blob_data(msg), blob_len(msg));
if (!apply_sid[0])
return UBUS_STATUS_NO_DATA;
if (!tb[RPC_B_SESSION])
return UBUS_STATUS_INVALID_ARGUMENT;
sid = blobmsg_data(tb[RPC_B_SESSION]);
if (strcmp(apply_sid, sid))
return UBUS_STATUS_PERMISSION_DENIED;
snprintf(tmp, sizeof(tmp), "%s/*", RPC_SNAPSHOT_FILES);
if (glob(tmp, GLOB_PERIOD, NULL, &gl) < 0)
return UBUS_STATUS_NOT_FOUND;
rpc_uci_do_rollback(ctx, &gl);
globfree(&gl);
return 0;
}
static int
rpc_uci_reload(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
char * const cmd[2] = { "/sbin/reload_config", NULL };
if (!fork()) {
/* wait for the RPC call to complete */
sleep(2);
return execv(cmd[0], cmd);
}
return 0;
}
/*
* Session destroy callback to purge associated delta directory.
*/
static void
rpc_uci_purge_savedir_cb(struct rpc_session *ses, void *priv)
{
char path[PATH_MAX];
snprintf(path, sizeof(path) - 1, RPC_UCI_SAVEDIR_PREFIX "%s", ses->id);
rpc_uci_purge_dir(path);
}
/*
* Removes all delta directories which match the RPC_UCI_SAVEDIR_PREFIX.
* This is used to clean up garbage when starting rpcd.
*/
void rpc_uci_purge_savedirs(void)
{
int i;
glob_t gl;
if (!glob(RPC_UCI_SAVEDIR_PREFIX "*", 0, NULL, &gl))
{
for (i = 0; i < gl.gl_pathc; i++)
rpc_uci_purge_dir(gl.gl_pathv[i]);
globfree(&gl);
}
}
int rpc_uci_api_init(struct ubus_context *ctx)
{
static const struct ubus_method uci_methods[] = {
{ .name = "configs", .handler = rpc_uci_configs },
UBUS_METHOD("get", rpc_uci_get, rpc_uci_get_policy),
UBUS_METHOD("state", rpc_uci_state, rpc_uci_get_policy),
UBUS_METHOD("add", rpc_uci_add, rpc_uci_add_policy),
UBUS_METHOD("set", rpc_uci_set, rpc_uci_set_policy),
UBUS_METHOD("delete", rpc_uci_delete, rpc_uci_delete_policy),
UBUS_METHOD("rename", rpc_uci_rename, rpc_uci_rename_policy),
UBUS_METHOD("order", rpc_uci_order, rpc_uci_order_policy),
UBUS_METHOD("changes", rpc_uci_changes, rpc_uci_config_policy),
UBUS_METHOD("revert", rpc_uci_revert, rpc_uci_config_policy),
UBUS_METHOD("commit", rpc_uci_commit, rpc_uci_config_policy),
UBUS_METHOD("apply", rpc_uci_apply, rpc_uci_apply_policy),
UBUS_METHOD("confirm", rpc_uci_confirm, rpc_uci_rollback_policy),
UBUS_METHOD("rollback", rpc_uci_rollback, rpc_uci_rollback_policy),
UBUS_METHOD_NOARG("reload_config", rpc_uci_reload),
};
static struct ubus_object_type uci_type =
UBUS_OBJECT_TYPE("luci-rpc-uci", uci_methods);
static struct ubus_object obj = {
.name = "uci",
.type = &uci_type,
.methods = uci_methods,
.n_methods = ARRAY_SIZE(uci_methods),
};
static struct rpc_session_cb cb = {
.cb = rpc_uci_purge_savedir_cb
};
cursor = uci_alloc_context();
if (!cursor)
return UBUS_STATUS_UNKNOWN_ERROR;
rpc_session_destroy_cb(&cb);
return ubus_add_object(ctx, &obj);
}