/*
 *  ConnMan VPN daemon utils
 *
 *  Copyright (C) 2020  Jolla Ltd. All rights reserved.
 *  Copyright (C) 2020  Open Mobile Platform LLC.
 *  Contact: jussi.laakkonen@jolla.com
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>

#include <glib/gstdio.h>

#include <connman/log.h>

#include "vpn.h"

static bool is_string_digits(const char *str)
{
	int i;

	if (!str || !*str)
		return false;

	for (i = 0; str[i]; i++) {
		if (!g_ascii_isdigit(str[i]))
			return false;
	}

	return true;
}

static uid_t get_str_id(const char *username)
{
	if (!username || !*username)
		return 0;

	return (uid_t)g_ascii_strtoull(username, NULL, 10);
}

struct passwd *vpn_util_get_passwd(const char *username)
{
	struct passwd *pwd;
	uid_t uid;

	if (!username || !*username)
		return NULL;

	if (is_string_digits(username)) {
		uid = get_str_id(username);
		pwd = getpwuid(uid);
	} else {
		pwd = getpwnam(username);
	}

	return pwd;
}

struct group *vpn_util_get_group(const char *groupname)
{
	struct group *grp;
	gid_t gid;

	if (!groupname || !*groupname)
		return NULL;

	if (is_string_digits(groupname)) {
		gid = get_str_id(groupname);
		grp = getgrgid(gid);
	} else {
		grp = getgrnam(groupname);
	}

	return grp;
}

/*
 * These prefixes are used for checking if the requested path for
 * vpn_util_create_path() is acceptable. Allow only prefixes meant for run-time
 * or temporary use to limit the access to any system resources.
 *
 * VPN core and plugins would need to create only temporary dirs for the
 * run-time use. The requested dirs can be created for a specific user when
 * running a VPN plugin as a different user and thus, user specific run dir is
 * allowed and limitation to access any other system dir is restricted.
 */
static const char *allowed_prefixes[] = { RUNSTATEDIR "/connman-vpn/",
					RUNSTATEDIR "/user/", "/tmp/", NULL };

static int is_path_allowed(const char *path)
{
	int err = -EPERM;
	int i;

	if (!path || !*path || !g_path_is_absolute(path))
		return -EINVAL;

	if (g_strrstr(path, "..") || g_strrstr(path, "./"))
		return -EPERM;

	for (i = 0; allowed_prefixes[i]; i++) {
		if (g_str_has_prefix(path, allowed_prefixes[i])) {
			const char *suffix = path +
						strlen(allowed_prefixes[i]);

			/*
			 * Don't allow plain prefixes, an additional dir must
			 * be included after the prexix in the requested path.
			 */
			if (suffix && *suffix != G_DIR_SEPARATOR &&
						g_strrstr(suffix,
							G_DIR_SEPARATOR_S)) {
				DBG("allowed %s, has suffix %s", path, suffix);
				err = 0;
			}

			break;
		}
	}

	return err;
}

int vpn_util_create_path(const char *path, uid_t uid, gid_t grp, int mode)
{
	mode_t old_umask;
	char *dir_p;
	int err;

	err = is_path_allowed(path);
	if (err)
		return err;

	dir_p = g_path_get_dirname(path);
	if (!dir_p)
		return -ENOMEM;

	err = g_unlink(dir_p);
	if (err)
		err = -errno;

	switch (err) {
	case 0:
		/* Removed */
	case -ENOENT:
		/* Did not exist */
		break;
	case -EACCES:
		/*
		 * Cannot get write access to the containing directory, check
		 * if the path exists.
		 */
		if (!g_file_test(dir_p, G_FILE_TEST_EXISTS))
			goto out;

		/* If the dir does not exist new one cannot be created */
		if (!g_file_test(dir_p, G_FILE_TEST_IS_DIR))
			goto out;

		/* Do a chmod as the dir exists */
		/* fallthrough */
	case -EISDIR:
		/* Exists as dir, just chmod and change owner */
		err = g_chmod(dir_p, mode);
		if (err) {
			connman_warn("chmod %s failed, err %d", dir_p, err);
			err = -errno;
		}

		goto chown;
	default:
		/* Any other error that is not handled here */
		connman_warn("remove %s failed, err %d", dir_p, err);
		goto out;
	}

	/* Set dir creation mask to correspond to the mode */
	old_umask = umask(~mode & 0777);

	DBG("mkdir %s", dir_p);
	err = g_mkdir_with_parents(dir_p, mode);

	umask(old_umask);

	if (err) {
		connman_warn("mkdir %s failed, err %d", dir_p, err);
		err = -errno;
		goto out;
	}

chown:
	if (uid && grp) {
		err = chown(dir_p, uid, grp);
		if (err) {
			err = -errno;
			connman_warn("chown %s failed for %d/%d, err %d",
							dir_p, uid, grp, err);
		}
	}

out:
	g_free(dir_p);

	return err;
}

