/*
 * helper.c: usermngr helper functions
 *
 * Copyright (C) 2024 IOPSYS Software Solutions AB. All rights reserved.
 *
 * Author: Suvendhu Hansa <suvendhu.hansa@iopsys.eu>
 *
 * See LICENSE file for license related information.
 */

#include <string.h>
#include <ctype.h>

#include <libubox/blobmsg.h>
#include <libubox/blobmsg_json.h>
#include <libubus.h>

#include "users.h"
#include "helper.h"

static struct uci_context *uci_ctx = NULL;

static int usermgr_commit_package(const char *package)
{
	struct uci_ptr ptr = {0};

	// cppcheck-suppress cert-EXP05-C
	if (uci_lookup_ptr(uci_ctx, &ptr, (char *)package, true) != UCI_OK) {
		return -1;
	}

	if (uci_commit(uci_ctx, &ptr.p, false) != UCI_OK) {
		return -1;
	}

	return 0;
}

int usermgr_uci_init(const char *conf_path)
{
	uci_ctx = uci_alloc_context();
	if (!uci_ctx)
		return -1;

	uci_set_confdir(uci_ctx, conf_path);

	return 0;
}

int usermgr_uci_fini(const char *package_name)
{
	/* Commit package */
	if (package_name)
		usermgr_commit_package(package_name);

	/* Freed uci context  */
	if (uci_ctx) {
		uci_free_context(uci_ctx);
		uci_ctx = NULL;
	}

	return 0;
}

static bool usermgr_uci_validate_section(const char *str)
{
	if (!*str)
		return false;

	for (; *str; str++) {
		unsigned char c = *str;

		if (isalnum(c) || c == '_')
			continue;

		return false;
	}

	return true;
}

static int usermgr_uci_init_ptr(struct uci_ptr *ptr, const char *package, const char *section, const char *option, const char *value)
{
	memset(ptr, 0, sizeof(struct uci_ptr));

	/* value */
	if (value)
		ptr->value = value;

	ptr->package = package;
	if (!ptr->package)
		goto error;

	ptr->section = section;
	if (!ptr->section) {
		ptr->target = UCI_TYPE_PACKAGE;
		goto lastval;
	}

	ptr->option = option;
	if (!ptr->option) {
		ptr->target = UCI_TYPE_SECTION;
		goto lastval;
	} else
		ptr->target = UCI_TYPE_OPTION;

lastval:
	if (ptr->section && !usermgr_uci_validate_section(ptr->section))
		ptr->flags |= UCI_LOOKUP_EXTENDED;

	return 0;

error:
	return -1;
}

struct uci_section *usermgr_uci_walk_section(const char *package, const char *section_type, struct uci_section *prev_section)
{
	struct uci_ptr ptr;
	struct uci_element *e;
	struct uci_section *next_section;

	if (section_type == NULL) {
		if (prev_section) {
			e = &prev_section->e;
			if (e->list.next == &prev_section->package->sections)
				return NULL;
			e = container_of(e->list.next, struct uci_element, list);
			next_section = uci_to_section(e);
			return next_section;
		} else {
			if (usermgr_uci_init_ptr(&ptr, package, NULL, NULL, NULL))
				return NULL;

			if (uci_lookup_ptr(uci_ctx, &ptr, NULL, true) != UCI_OK)
				return NULL;

			if (ptr.p->sections.next == &ptr.p->sections)
				return NULL;
			e = container_of(ptr.p->sections.next, struct uci_element, list);
			next_section = uci_to_section(e);

			return next_section;
		}
	} else {
		struct uci_list *ul = NULL, *shead = NULL;

		if (prev_section) {
			ul = &prev_section->e.list;
			shead = &prev_section->package->sections;
		} else {
			if (usermgr_uci_init_ptr(&ptr, package, NULL, NULL, NULL))
				return NULL;

			if (uci_lookup_ptr(uci_ctx, &ptr, NULL, true) != UCI_OK)
				return NULL;

			ul = &ptr.p->sections;
			shead = &ptr.p->sections;
		}
		while (ul->next != shead) {
			e = container_of(ul->next, struct uci_element, list);
			next_section = uci_to_section(e);
			if (strcmp(next_section->type, section_type) == 0)
				return next_section;
			ul = ul->next;
		}
		return NULL;
	}
	return NULL;
}

static struct uci_element *usermgr_uci_lookup_list(struct uci_list *list, const char *name)
{
	struct uci_element *e;

	uci_foreach_element(list, e) {
		if (!strcmp(e->name, name))
			return e;
	}

	return NULL;
}

static int usermgr_uci_lookup_ptr_by_section(struct uci_ptr *ptr, struct uci_section *section, const char *option, const char *value)
{
	struct uci_element *e;
	memset(ptr, 0, sizeof(struct uci_ptr));

	ptr->package = section->package->e.name;
	ptr->section = section->e.name;
	ptr->option = option;
	ptr->value = value;
	ptr->flags |= UCI_LOOKUP_DONE;

	ptr->p = section->package;
	ptr->s = section;

	if (ptr->option) {
		e = usermgr_uci_lookup_list(&ptr->s->options, ptr->option);
		if (!e)
			return UCI_OK;
		ptr->o = uci_to_option(e);
		ptr->last = e;
		ptr->target = UCI_TYPE_OPTION;
	} else {
		ptr->last = &ptr->s->e;
		ptr->target = UCI_TYPE_SECTION;
	}

	ptr->flags |= UCI_LOOKUP_COMPLETE;

	return UCI_OK;
}

char *usermgr_uci_get_value_by_section(struct uci_section *section, const char *option)
{
	struct uci_ptr ptr;

	if (usermgr_uci_lookup_ptr_by_section(&ptr, section, option, NULL) != UCI_OK)
		return "";

	if (!ptr.o)
		return "";

	if (ptr.o->v.string)
		return ptr.o->v.string;
	else
		return "";
}

char *usermgr_uci_set_value_by_section(struct uci_section *section, const char *option, const char *value)
{
	struct uci_ptr ptr;

	if (section == NULL)
		return "";

	if (usermgr_uci_lookup_ptr_by_section(&ptr, section, option, value) != UCI_OK)
		return "";

	if (uci_set(uci_ctx, &ptr) != UCI_OK)
		return "";

	if (uci_save(uci_ctx, ptr.p) != UCI_OK)
		return "";

	if (ptr.o && ptr.o->v.string)
		return ptr.o->v.string;

	return "";
}

struct uci_section *usermgr_uci_add_section(const char *package, const char *section_type)
{
	struct uci_ptr ptr;
	struct uci_section *s = NULL;

	if (usermgr_uci_init_ptr(&ptr, package, NULL, NULL, NULL)) {
		return NULL;
	}

	if (uci_lookup_ptr(uci_ctx, &ptr, NULL, true) != UCI_OK) {
		return NULL;
	}

	if (uci_add_section(uci_ctx, ptr.p, section_type, &s) != UCI_OK) {
		return NULL;
	}

	if (uci_save(uci_ctx, ptr.p) != UCI_OK)
		return NULL;

	return s;
}

int usermgr_uci_delete_by_section(struct uci_section *section, const char *option, const char *value)
{
	struct uci_ptr ptr = {0};

	if (section == NULL)
		return -1;

	if (usermgr_uci_lookup_ptr_by_section(&ptr, section, option, value) != UCI_OK)
		return -1;

	if (uci_delete(uci_ctx, &ptr) != UCI_OK)
		return -1;

	if (uci_save(uci_ctx, ptr.p) != UCI_OK)
		return -1;

	return 0;
}

bool check_section_exist_by_option(const char *conf_path, const char *package, const char *section_type, const char *option, const char *val_check)
{
	struct uci_section *s = NULL;
	bool ret = false;

	if (conf_path == NULL || package == NULL || section_type == NULL || option == NULL || val_check == NULL)
		return ret;

	usermgr_uci_init(conf_path);

	usermgr_uci_foreach_section(package, section_type, s) {
		const char *val = usermgr_uci_get_value_by_section(s, option);

		if (strcmp(val, val_check) == 0) {
			ret = true;
			break;
		}
	}

	usermgr_uci_fini(NULL);
	return ret;
}

struct uci_section *get_section_from_config(const char *conf_path, const char *package, const char *section_type, const char *sec_name)
{
	struct uci_section *s = NULL;

	if (conf_path == NULL || package == NULL || section_type == NULL || sec_name == NULL)
		return s;

	usermgr_uci_init(conf_path);

	usermgr_uci_foreach_section(package, section_type, s) {
		if (strcmp(s->e.name, sec_name) == 0) {
			break;
		}
	}

	usermgr_uci_fini(NULL);
	return s;
}

void strip_newline(char *str)
{
	if (str == NULL)
		return;

	char* end_str = str + strlen(str) - 1;

	while (str <= end_str  && *end_str == '\n') {
		*end_str = '\0';
		--end_str ;
	}
}

static void get_shell_path(const char *shell, char **path, bool hidden)
{
	char line[2048] = {0};

	if (shell == NULL || path == NULL)
		return;

	// cppcheck-suppress cert-MSC24-C
	FILE *fp = fopen("/etc/shells", "r");
	if (fp == NULL)
		return;

	while (fgets(line, sizeof(line), fp)) {
		strip_newline(line);
		char *tmp = strrchr(line, '/');
		if (tmp == NULL)
			tmp = line;
		else
			tmp = tmp + 1;

		if (strcmp(shell, tmp) == 0) {
			bool sh_enabled = false;
			struct uci_section *s = NULL;

			if (hidden == true) {
				*path = strdup(line);
				break;
			}

			usermgr_uci_foreach_section("users", "shell", s) {
				const char *val = usermgr_uci_get_value_by_section(s, "name");

				if (strcmp(val, shell) == 0) {
					val = usermgr_uci_get_value_by_section(s, "enabled");
					if (strlen(val) == 0)
						sh_enabled = true;
					else
						sh_enabled = dmuci_string_to_boolean(val);
					break;
				}
			}

			if (sh_enabled == true) {
				*path = strdup(line);
			}

			break;
		}
	}

	fclose(fp);
	return;
}

static bool valid_group(char *grp)
{
	char line[2048] = {0};
	bool ret = false;

	if (grp == NULL || strlen(grp) == 0)
		return ret;

	// cppcheck-suppress cert-MSC24-C
	FILE *fp = fopen("/etc/group", "r");
	if (fp == NULL)
		return ret;

	int len = strlen(grp) + 2;
	char tmp[len];

	snprintf(tmp, sizeof(tmp), "%s:", grp);

	while (fgets(line, sizeof(line), fp)) {
		if (strncmp(line, tmp, len-1) == 0) {
			ret = true;
			break;
		}
	}

	fclose(fp);
	return ret;
}

bool user_exist(char *user_name)
{
	char line[2048] = {0};
	bool ret = false;

	if (user_name == NULL || strlen(user_name) == 0)
		return ret;

	// cppcheck-suppress cert-MSC24-C
	FILE *fp = fopen("/etc/passwd", "r");
	if (fp == NULL)
		return ret;

	int len = strlen(user_name) + 2;
	char tmp[len];

	snprintf(tmp, sizeof(tmp), "%s:", user_name);

	while (fgets(line, sizeof(line), fp)) {
		if (strncmp(line, tmp, len-1) == 0) {
			ret = true;
			break;
		}
	}

	fclose(fp);
	return ret;
}

static void get_text_password(struct ubus_request *req, int type __attribute__((unused)), struct blob_attr *msg)
{
	char **text = (char **)req->priv;
	const struct blobmsg_policy p[1] = { {"value", BLOBMSG_TYPE_STRING} };
	struct blob_attr *tb[1] = {0};

	if (msg == NULL)
		return;

	blobmsg_parse(p, 1, tb, blobmsg_data(msg), blobmsg_len(msg));

	if (!tb[0])
		return;

	char *val = blobmsg_get_string(tb[0]);
	if (val == NULL || strlen(val) == 0)
		return;

	*text = strdup(val);
	return;
}

static char *decrypt_password(const char *password)
{
	uint32_t id = 0;
	char *text = NULL;
	struct blob_buf b = {0};

	if (password == NULL)
		return text;

	struct ubus_context *ctx = ubus_connect(NULL);
	if (ctx == NULL)
		return text;

	memset(&b, 0, sizeof(struct blob_buf));
	blob_buf_init(&b, 0);
	blobmsg_add_string(&b, "data", password);

	if (!ubus_lookup_id(ctx, "bbf.secure", &id)) {
		ubus_invoke(ctx, id, "decode", b.head, get_text_password, &text, 5000);
	}

	blob_buf_free(&b);
	ubus_free(ctx);

	return text;
}

int add_user(const char *user_name)
{
	char cmd[256] = {0};
	FILE *pp = NULL;

	if (user_name == NULL || strlen(user_name) == 0)
		return -1;

	snprintf(cmd, sizeof(cmd), "adduser -D -H -s /bin/false %s", user_name);
	pp = popen(cmd, "r"); // flawfinder: ignore
	if (pp != NULL) {
		pclose(pp);
		return 0;
	}

	return -1;
}

int del_user(const char *user_name)
{
	char cmd[256] = {0};
	FILE *pp = NULL;

	if (user_name == NULL || strlen(user_name) == 0)
		return -1;

	snprintf(cmd, sizeof(cmd), "userdel -f %s", user_name);
	pp = popen(cmd, "r"); // flawfinder: ignore
	if (pp != NULL) {
		pclose(pp);
		return 0;
	}

	return -1;
}

int enable_user(const char *user_name)
{
	char cmd[256] = {0};
	FILE *pp = NULL;

	if (user_name == NULL || strlen(user_name) == 0)
		return -1;

	snprintf(cmd, sizeof(cmd), "usermod -U %s", user_name);
	pp = popen(cmd, "r"); // flawfinder: ignore
	if (pp == NULL)
		return -1;

	pclose(pp);
	return 0;
}

int disable_user(const char *user_name)
{
	char cmd[256] = {0};
	FILE *pp = NULL;

	if (user_name == NULL || strlen(user_name) == 0)
		return -1;

	snprintf(cmd, sizeof(cmd), "usermod -L %s", user_name);
	pp = popen(cmd, "r"); // flawfinder: ignore
	if (pp == NULL)
		return -1;

	pclose(pp);
	return 0;
}

int change_username(const char *old_name, const char *new_name)
{
	char cmd[256] = {0};
	FILE *pp = NULL;

	if (old_name == NULL || strlen(old_name) == 0 || new_name == NULL || strlen(new_name) == 0)
		return -1;

	snprintf(cmd, sizeof(cmd), "usermod -b -l %s %s", new_name, old_name);
	pp = popen(cmd, "r"); // flawfinder: ignore
	if (pp == NULL)
		return -1;

	pclose(pp);
	return 0;
}

int change_password(const char *user_name, const char *password, bool is_encrypted)
{
	FILE *pp = NULL;
	char creden[512] = {0};


	if (user_name == NULL || strlen(user_name) == 0 || password == NULL || strlen(password) == 0)
		return -1;

	if (is_encrypted == true) {
		char *text_pass = decrypt_password(password);
		if (text_pass == NULL)
			return -1;

		snprintf(creden, sizeof(creden), "%s:%s\n", user_name, text_pass);
		FREE(text_pass);
	} else {
		snprintf(creden, sizeof(creden), "%s:%s\n", user_name, password);
	}

	if (is_encrypted == false) {
		pp = popen("chpasswd -e", "w"); // flawfinder: ignore
	} else {
		pp = popen("chpasswd", "w"); // flawfinder: ignore
	}

	if (pp == NULL)
		return -1;

	if (fputs(creden, pp) == EOF) {
		pclose(pp);
		return -1;
	}

	int status = pclose(pp);
	if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
		return 0;
	}

	return -1;
}

int configure_user_shell(const char *user_name, const char *shell, bool hidden)
{
	char cmd[256] = {0};
	FILE *pp = NULL;

	if (user_name == NULL || strlen(user_name) == 0)
		return -1;

	// Only override the shell to false for non-hidden users
	if (hidden == false) {
		snprintf(cmd, sizeof(cmd), "usermod -s /bin/false %s", user_name);
		pp = popen(cmd, "r"); // flawfinder: ignore
		if (pp == NULL)
			return -1;

		pclose(pp);
	}

	if (shell == NULL || strlen(shell) == 0 || strcmp(shell, "false") == 0)
		return 0;

	char *path = NULL;

	get_shell_path(shell, &path, hidden);
	if (path == NULL || strlen(path) == 0) {
		FREE(path);
		return -1;
	}

	snprintf(cmd, sizeof(cmd), "usermod -s %s %s", path, user_name);
	FREE(path);

	pp = popen(cmd, "r"); // flawfinder: ignore
	if (pp == NULL)
		return -1;

	pclose(pp);

	return 0;
}

int configure_user_group(const char *user_name, const char *group)
{
	char cmd[256] = {0};
	FILE *pp = NULL;
	char buff[2048] = {0};
	char *tok = NULL, *ptr = NULL;

	if (user_name == NULL || strlen(user_name) == 0)
		return -1;

	snprintf(cmd, sizeof(cmd), "usermod -G \"\" %s", user_name);
	pp = popen(cmd, "r"); // flawfinder: ignore
	if (pp == NULL)
		return -1;

	pclose(pp);

	if (group == NULL || strlen(group) == 0)
		return 0;

	snprintf(buff, sizeof(buff), "%s", group);

	for (tok = strtok_r(buff, ",", &ptr); tok != NULL; tok = strtok_r(NULL, ",", &ptr)) {
		char *grp = NULL;

		if (strncmp(tok, GROUP_PREFIX, strlen(GROUP_PREFIX)) == 0)
			grp = tok + strlen(GROUP_PREFIX);
		else
			grp = tok;

		if (!valid_group(grp))
			continue;

		snprintf(cmd, sizeof(cmd), "usermod -aG %s %s", grp, user_name);
		pp = popen(cmd, "r"); // flawfinder: ignore
		if (pp == NULL)
			return -1;

		pclose(pp);
	}

	return 0;
}


int add_group(const char *group_name)
{
	char cmd[256] = {0};
	FILE *pp = NULL;
	const char *grp = NULL;

	if (group_name == NULL || strlen(group_name) == 0)
		return -1;

	if (strncmp(group_name, GROUP_PREFIX, strlen(GROUP_PREFIX)) == 0)
		grp = group_name + strlen(GROUP_PREFIX);
	else
		grp = group_name;

	snprintf(cmd, sizeof(cmd), "groupadd -f %s", grp);
	pp = popen(cmd, "r"); // flawfinder: ignore
	if (pp != NULL) {
		pclose(pp);
		return 0;
	}

	return -1;
}

int del_group(const char *group_name)
{
	char cmd[256] = {0};
	FILE *pp = NULL;
	const char *grp = NULL;

	if (group_name == NULL || strlen(group_name) == 0)
		return -1;

	if (strncmp(group_name, GROUP_PREFIX, strlen(GROUP_PREFIX)) == 0)
		grp = group_name + strlen(GROUP_PREFIX);
	else
		grp = group_name;

	snprintf(cmd, sizeof(cmd), "groupdel -f %s", grp);
	pp = popen(cmd, "r"); // flawfinder: ignore
	if (pp != NULL) {
		pclose(pp);
		return 0;
	}

	return -1;
}

int validate_password_quality(const char *password, const char *min, const char *max)
{
	FILE *pp = NULL;
	char input[128] = {0};
	char cmd[128] = {0};

	if (password == NULL || strlen(password) == 0)
		return -1;

	snprintf(cmd, sizeof(cmd), "pwqcheck -1 min=%s max=%s match=0 passphrase=0", min, max);
	pp = popen(cmd, "w"); // flawfinder: ignore
	if (pp == NULL)
		return -1;

	snprintf(input, sizeof(input), "%s\n", password);
	if (fputs(input, pp) == EOF) {
		pclose(pp);
		return -1;
	}

	int status = pclose(pp);
	if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
		return 0;
	}

	return -1;
}

