/*
 * 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 <rpcd/plugin.h>

static struct blob_buf buf;

struct rpc_plugin_lookup_context {
	uint32_t id;
	char *name;
	bool found;
};

static void
rpc_plugin_lookup_plugin_cb(struct ubus_context *ctx,
                            struct ubus_object_data *obj, void *priv)
{
	struct rpc_plugin_lookup_context *c = priv;

	if (c->id == obj->id)
	{
		c->found = true;
		sprintf(c->name, "%s", obj->path);
	}
}

static bool
rpc_plugin_lookup_plugin(struct ubus_context *ctx, struct ubus_object *obj,
                         char *strptr)
{
	struct rpc_plugin_lookup_context c = { .id = obj->id, .name = strptr };

	if (ubus_lookup(ctx, NULL, rpc_plugin_lookup_plugin_cb, &c))
		return false;

	return c.found;
}

static void
rpc_plugin_json_array_to_blob(struct array_list *a, struct blob_buf *blob);

static void
rpc_plugin_json_object_to_blob(json_object *o, struct blob_buf *blob);

static void
rpc_plugin_json_element_to_blob(const char *name, json_object *val,
                                struct blob_buf *blob)
{
	void *c;
	int64_t n;

	switch (json_object_get_type(val)) {
	case json_type_object:
		c = blobmsg_open_table(blob, name);
		rpc_plugin_json_object_to_blob(val, blob);
		blobmsg_close_table(blob, c);
		break;

	case json_type_array:
		c = blobmsg_open_array(blob, name);
		rpc_plugin_json_array_to_blob(json_object_get_array(val), blob);
		blobmsg_close_array(blob, c);
		break;

	case json_type_string:
		blobmsg_add_string(blob, name, json_object_get_string(val));
		break;

	case json_type_boolean:
		blobmsg_add_u8(blob, name, json_object_get_boolean(val));
		break;

	case json_type_int:
		n = json_object_get_int64(val);
		if (n >= INT32_MIN && n <= INT32_MAX)
			blobmsg_add_u32(blob, name, n);
		else
			blobmsg_add_u64(blob, name, n);
		break;

	case json_type_double:
		blobmsg_add_double(blob, name, json_object_get_double(val));
		break;

	case json_type_null:
		blobmsg_add_field(blob, BLOBMSG_TYPE_UNSPEC, name, NULL, 0);
		break;
	}
}

static void
rpc_plugin_json_array_to_blob(struct array_list *a, struct blob_buf *blob)
{
	int i, len;

	for (i = 0, len = array_list_length(a); i < len; i++)
		rpc_plugin_json_element_to_blob(NULL, array_list_get_idx(a, i), blob);
}

static void
rpc_plugin_json_object_to_blob(json_object *o, struct blob_buf *blob)
{
	json_object_object_foreach(o, key, val)
		rpc_plugin_json_element_to_blob(key, val, blob);
}

struct call_context {
	char path[PATH_MAX];
	const char *argv[4];
	char *method;
	char *input;
	json_tokener *tok;
	json_object *obj;
	bool input_done;
	bool output_done;
};

static int
rpc_plugin_call_stdin_cb(struct ustream *s, void *priv)
{
	struct call_context *c = priv;

	if (!c->input_done)
	{
		ustream_write(s, c->input, strlen(c->input), false);
		c->input_done = true;
	}

	return 0;
}

static int
rpc_plugin_call_stdout_cb(struct blob_buf *blob, char *buf, int len, void *priv)
{
	struct call_context *c = priv;

	if (!c->output_done)
	{
		c->obj = json_tokener_parse_ex(c->tok, buf, len);

		if (json_tokener_get_error(c->tok) != json_tokener_continue)
			c->output_done = true;
	}

	return len;
}

static int
rpc_plugin_call_stderr_cb(struct blob_buf *blob, char *buf, int len, void *priv)
{
	return len;
}

static int
rpc_plugin_call_finish_cb(struct blob_buf *blob, int stat, void *priv)
{
	struct call_context *c = priv;
	int rv = UBUS_STATUS_INVALID_ARGUMENT;

	if (json_tokener_get_error(c->tok) == json_tokener_success)
	{
		if (c->obj)
		{
			if (json_object_get_type(c->obj) == json_type_object)
			{
				rpc_plugin_json_object_to_blob(c->obj, blob);
				rv = UBUS_STATUS_OK;
			}

			json_object_put(c->obj);
		}
		else
		{
			rv = UBUS_STATUS_NO_DATA;
		}
	}

	json_tokener_free(c->tok);

	free(c->input);

	return rv;
}

static int
rpc_plugin_call(struct ubus_context *ctx, struct ubus_object *obj,
                struct ubus_request_data *req, const char *method,
                struct blob_attr *msg)
{
	int rv = UBUS_STATUS_UNKNOWN_ERROR;
	struct call_context *c;
	char *plugin, *mptr;

	c = calloc_a(sizeof(*c), &mptr, strlen(method) + 1);

	if (!c)
		goto fail;

	c->method = strcpy(mptr, method);
	c->input = blobmsg_format_json(msg, true);
	c->tok = json_tokener_new();

	if (!c->input || !c->tok)
		goto fail;

	plugin = c->path + sprintf(c->path, "%s/", RPC_PLUGIN_DIRECTORY);

	if (!rpc_plugin_lookup_plugin(ctx, obj, plugin))
	{
		rv = UBUS_STATUS_NOT_FOUND;
		goto fail;
	}

	c->argv[0] = c->path;
	c->argv[1] = "call";
	c->argv[2] = c->method;

	rv = rpc_exec(c->argv, rpc_plugin_call_stdin_cb,
	              rpc_plugin_call_stdout_cb, rpc_plugin_call_stderr_cb,
	              rpc_plugin_call_finish_cb, c, ctx, req);

	if (rv == UBUS_STATUS_OK)
		return rv;

fail:
	if (c)
	{
		if (c->input)
			free(c->input);

		if (c->tok)
			json_tokener_free(c->tok);

		free(c);
	}

	return rv;
}

static bool
rpc_plugin_parse_signature(struct blob_attr *sig, struct ubus_method *method)
{
	int rem, n_attr;
	enum blobmsg_type type;
	struct blob_attr *attr;
	struct blobmsg_policy *policy = NULL;

	if (!sig || blobmsg_type(sig) != BLOBMSG_TYPE_TABLE)
		return false;

	n_attr = 0;

	blobmsg_for_each_attr(attr, sig, rem)
		n_attr++;

	if (n_attr)
	{
		policy = calloc(n_attr, sizeof(*policy));

		if (!policy)
			return false;

		n_attr = 0;

		blobmsg_for_each_attr(attr, sig, rem)
		{
			type = blobmsg_type(attr);

			if (type == BLOBMSG_TYPE_INT32)
			{
				switch (blobmsg_get_u32(attr))
				{
				case 8:
					type = BLOBMSG_TYPE_INT8;
					break;

				case 16:
					type = BLOBMSG_TYPE_INT16;
					break;

				case 64:
					type = BLOBMSG_TYPE_INT64;
					break;

				default:
					type = BLOBMSG_TYPE_INT32;
					break;
				}
			}

			policy[n_attr].name = strdup(blobmsg_name(attr));
			policy[n_attr].type = type;

			n_attr++;
		}
	}

	method->name = strdup(blobmsg_name(sig));
	method->handler = rpc_plugin_call;
	method->policy = policy;
	method->n_policy = n_attr;

	return true;
}

static struct ubus_object *
rpc_plugin_parse_exec(const char *name, int fd)
{
	int len, rem, n_method;
	struct blob_attr *cur;
	struct ubus_method *methods;
	struct ubus_object_type *obj_type;
	struct ubus_object *obj;
	char outbuf[1024];

	json_tokener *tok;
	json_object *jsobj;

	blob_buf_init(&buf, 0);

	tok = json_tokener_new();

	if (!tok)
		return NULL;

	while ((len = read(fd, outbuf, sizeof(outbuf))) > 0)
	{
		jsobj = json_tokener_parse_ex(tok, outbuf, len);

		if (json_tokener_get_error(tok) == json_tokener_continue)
			continue;

		if (json_tokener_get_error(tok) != json_tokener_success)
			break;

		if (jsobj)
		{
			if (json_object_get_type(jsobj) == json_type_object)
				blobmsg_add_object(&buf, jsobj);

			json_object_put(jsobj);
			break;
		}
	}

	json_tokener_free(tok);

	n_method = 0;

	blob_for_each_attr(cur, buf.head, rem)
		n_method++;

	if (!n_method)
		return NULL;

	methods = calloc(n_method, sizeof(*methods));

	if (!methods)
		return NULL;

	n_method = 0;

	blob_for_each_attr(cur, buf.head, rem)
	{
		if (!rpc_plugin_parse_signature(cur, &methods[n_method]))
			continue;

		n_method++;
	}

	obj = calloc(1, sizeof(*obj));

	if (!obj)
		return NULL;

	obj_type = calloc(1, sizeof(*obj_type));

	if (!obj_type) {
		free(obj);
		return NULL;
	}

	if (asprintf((char **)&obj_type->name, "rpcd-plugin-exec-%s", name) < 0) {
		free(obj);
		free(obj_type);
		return NULL;
	}

	obj_type->methods = methods;
	obj_type->n_methods = n_method;

	obj->name = strdup(name);
	obj->type = obj_type;
	obj->methods = methods;
	obj->n_methods = n_method;

	return obj;
}

static int
rpc_plugin_register_exec(struct ubus_context *ctx, const char *path)
{
	pid_t pid;
	int rv = UBUS_STATUS_NO_DATA, fd, fds[2];
	const char *name;
	struct ubus_object *plugin;

	name = strrchr(path, '/');

	if (!name)
		return UBUS_STATUS_INVALID_ARGUMENT;

	if (pipe(fds))
		return UBUS_STATUS_UNKNOWN_ERROR;

	switch ((pid = fork()))
	{
	case -1:
		return UBUS_STATUS_UNKNOWN_ERROR;

	case 0:
		fd = open("/dev/null", O_RDWR);

		if (fd > -1)
		{
			dup2(fd, 0);
			dup2(fd, 2);

			if (fd > 2)
				close(fd);
		}

		dup2(fds[1], 1);

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

		if (execl(path, path, "list", NULL))
			return UBUS_STATUS_UNKNOWN_ERROR;

	default:
		plugin = rpc_plugin_parse_exec(name + 1, fds[0]);

		if (!plugin)
			goto out;

		rv = ubus_add_object(ctx, plugin);

out:
		close(fds[0]);
		close(fds[1]);
		waitpid(pid, NULL, 0);

		return rv;
	}
}


static LIST_HEAD(plugins);

static const struct rpc_daemon_ops ops = {
	.session_access     = rpc_session_access,
	.session_create_cb  = rpc_session_create_cb,
	.session_destroy_cb = rpc_session_destroy_cb,
	.exec               = rpc_exec,
	.exec_timeout       = &rpc_exec_timeout,
};

static int
rpc_plugin_register_library(struct ubus_context *ctx, const char *path)
{
	struct rpc_plugin *p;
	void *dlh;

	dlh = dlopen(path, RTLD_LAZY | RTLD_LOCAL);

	if (!dlh) {
		fprintf(stderr, "Failed to load plugin %s: %s\n",
		        path, dlerror());

		return UBUS_STATUS_UNKNOWN_ERROR;
	}

	p = dlsym(dlh, "rpc_plugin");

	if (!p)
		return UBUS_STATUS_NOT_FOUND;

	list_add(&p->list, &plugins);

	return p->init(&ops, ctx);
}

int rpc_plugin_api_init(struct ubus_context *ctx)
{
	DIR *d;
	int rv = 0;
	struct stat s;
	struct dirent *e;
	char path[PATH_MAX];

	if ((d = opendir(RPC_PLUGIN_DIRECTORY)) != NULL)
	{
		while ((e = readdir(d)) != NULL)
		{
			snprintf(path, sizeof(path) - 1,
			         RPC_PLUGIN_DIRECTORY "/%s", e->d_name);

			if (stat(path, &s) || !S_ISREG(s.st_mode) || !(s.st_mode & S_IXUSR))
				continue;

			rv |= rpc_plugin_register_exec(ctx, path);
		}

		closedir(d);
	}

	if ((d = opendir(RPC_LIBRARY_DIRECTORY)) != NULL)
	{
		while ((e = readdir(d)) != NULL)
		{
			snprintf(path, sizeof(path) - 1,
			         RPC_LIBRARY_DIRECTORY "/%s", e->d_name);

			if (stat(path, &s) || !S_ISREG(s.st_mode))
				continue;

			rv |= rpc_plugin_register_library(ctx, path);
		}

		closedir(d);
	}

	return rv;
}
