#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <syslog.h>
#include <getopt.h>
#include <pwd.h>
#include <selinux/selinux.h>
#include <semanage/handle.h>
#include <semanage/debug.h>
#include <semanage/booleans_policy.h>
#include <semanage/booleans_local.h>
#include <semanage/booleans_active.h>
#include <semanage/boolean_record.h>
#include <errno.h>

int permanent = 0;
int no_reload = 0;
int verbose = 0;

int setbool(char **list, size_t start, size_t end);

static __attribute__((__noreturn__)) void usage(void)
{
	fputs
	    ("\nUsage:  setsebool [ -NPV ] boolean value | bool1=val1 bool2=val2...\n\n",
	     stderr);
	exit(1);
}

int main(int argc, char **argv)
{
	size_t rc;
	int clflag;		/* holds codes for command line flags */
	if (argc < 2)
		usage();

	while (1) {
		clflag = getopt(argc, argv, "PNV");
		if (clflag == -1)
			break;

		switch (clflag) {
		case 'P':
			permanent = 1;
			break;
		case 'N':
			no_reload = 1;
			break;
		case 'V':
			verbose = 1;
			break;
		default:
			usage();
			break;
		}
	}

	if (argc - optind < 1) {
		fprintf(stderr, "Error: boolean name required\n");
		usage();
	}

	/* Check to see which way we are being called. If a '=' is passed,
	   we'll enforce the list syntax. If not we'll enforce the original
	   syntax for backward compatibility. */
	if (strchr(argv[optind], '=') == 0) {
		int len;
		char *bool_list[1];

		if ((argc - optind) != 2)
			usage();

		/* Add 1 for the '=' */
		len = strlen(argv[optind]) + strlen(argv[optind + 1]) + 2;
		bool_list[0] = (char *)malloc(len);
		if (bool_list[0] == NULL) {
			fputs("Out of memory - aborting\n", stderr);
			return 1;
		}
		snprintf(bool_list[0], len, "%s=%s", argv[optind],
			 argv[optind + 1]);
		rc = setbool(bool_list, 0, 1);
		free(bool_list[0]);
	} else
		rc = setbool(argv, optind, argc);

	return rc;
}

/* Apply temporal boolean changes to policy via libselinux */
static int selinux_set_boolean_list(size_t boolcnt,
				    SELboolean * boollist)
{

	if (security_set_boolean_list(boolcnt, boollist, 0)) {
		if (errno == ENOENT)
			fprintf(stderr, "Could not change active booleans: "
				"Invalid boolean\n");
		else if (errno) {
			if (getuid() == 0) {
				perror("Could not change active booleans");
			} else {
				perror("Could not change active booleans. Please try as root");
			}
		}

		return -1;
	}

	return 0;
}

/* Apply permanent boolean changes to policy via libsemanage */
static int semanage_set_boolean_list(size_t boolcnt,
				     SELboolean * boollist)
{

	size_t j;
	semanage_handle_t *handle = NULL;
	semanage_bool_t *boolean = NULL;
	semanage_bool_key_t *bool_key = NULL;
	int managed;
	int result;
	int enabled = is_selinux_enabled();

	handle = semanage_handle_create();
	if (handle == NULL) {
		fprintf(stderr, "Could not create semanage library handle\n");
		goto err;
	}

	if (!verbose) {
		semanage_msg_set_callback(handle, NULL, NULL);
	}

	managed = semanage_is_managed(handle);
	if (managed < 0) {
		fprintf(stderr,
			"Error when checking whether policy is managed\n");
		goto err;

	} else if (managed == 0) {
		if (getuid() == 0) {
			fprintf(stderr,
				"Cannot set persistent booleans without managed policy.\n");
		} else {
			fprintf(stderr,
				"Cannot set persistent booleans, please try as root.\n");
		}
		goto err;
	}

	if (semanage_connect(handle) < 0)
		goto err;

	if (semanage_begin_transaction(handle) < 0)
		goto err;

	for (j = 0; j < boolcnt; j++) {

		if (semanage_bool_create(handle, &boolean) < 0)
			goto err;

		if (semanage_bool_set_name(handle, boolean, boollist[j].name) <
		    0)
			goto err;

		semanage_bool_set_value(boolean, boollist[j].value);

		if (semanage_bool_key_extract(handle, boolean, &bool_key) < 0)
			goto err;

		semanage_bool_exists(handle, bool_key, &result);
		if (!result) {
			semanage_bool_exists_local(handle, bool_key, &result);
			if (!result) {
				fprintf(stderr, "Boolean %s is not defined\n", boollist[j].name);
				goto err;
			}
		}

		if (semanage_bool_modify_local(handle, bool_key,
						  boolean) < 0)
			goto err;

		if (enabled && semanage_bool_set_active(handle, bool_key, boolean) < 0) {
			fprintf(stderr, "Failed to change boolean %s: %m\n",
				boollist[j].name);
			goto err;
		}
		semanage_bool_key_free(bool_key);
		semanage_bool_free(boolean);
		bool_key = NULL;
		boolean = NULL;
	}

	if (no_reload)
		semanage_set_reload(handle, 0);
	if (semanage_commit(handle) < 0) {
		fprintf(stderr, "Failed to commit changes to booleans: %m\n");
		goto err;
	}

	semanage_disconnect(handle);
	semanage_handle_destroy(handle);
	return 0;

      err:
	semanage_bool_key_free(bool_key);
	semanage_bool_free(boolean);
	semanage_handle_destroy(handle);
	return -1;
}

/* Given an array of strings in the form "boolname=value", a start index,
   and a finish index...walk the list and set the bool. */
int setbool(char **list, size_t start, size_t end)
{
	char *name, *value_ptr;
	int j = 0, value;
	size_t i = start;
	size_t boolcnt = end - start;
	struct passwd *pwd;
	SELboolean *vallist = calloc(boolcnt, sizeof(SELboolean));
	if (!vallist)
		goto omem;

	while (i < end) {
		name = list[i];
		value_ptr = strchr(list[i], '=');
		if (value_ptr == NULL) {
			fprintf(stderr,
				"setsebool: '=' not found in boolean expression %s\n",
				list[i]);
			goto err;
		}
		*value_ptr = '\0';
		value_ptr++;
		if (strcmp(value_ptr, "1") == 0 ||
		    strcasecmp(value_ptr, "true") == 0 ||
		    strcasecmp(value_ptr, "on") == 0)
			value = 1;
		else if (strcmp(value_ptr, "0") == 0 ||
			 strcasecmp(value_ptr, "false") == 0 ||
			 strcasecmp(value_ptr, "off") == 0)
			value = 0;
		else {
			fprintf(stderr, "setsebool: illegal value "
				"%s for boolean %s\n", value_ptr, name);
			goto err;
		}

		vallist[j].value = value;
		vallist[j].name = strdup(name);
		if (!vallist[j].name)
			goto omem;
		i++;
		j++;

		/* Now put it back */
		value_ptr--;
		*value_ptr = '=';
	}

	if (permanent) {
		if (semanage_set_boolean_list(boolcnt, vallist) < 0)
			goto err;
	} else {
		if (selinux_set_boolean_list(boolcnt, vallist) < 0)
			goto err;
	}

	/* Now log what was done */
	pwd = getpwuid(getuid());
	i = start;
	while (i < end) {
		name = list[i];
		value_ptr = strchr(name, '=');
		*value_ptr = '\0';
		value_ptr++;
		if (pwd && pwd->pw_name)
			syslog(LOG_NOTICE,
			       "The %s policy boolean was changed to %s by %s",
			       name, value_ptr, pwd->pw_name);
		else
			syslog(LOG_NOTICE,
			       "The %s policy boolean was changed to %s by uid:%d",
			       name, value_ptr, getuid());
		i++;
	}

	for (i = 0; i < boolcnt; i++)
		free(vallist[i].name);
	free(vallist);
	return 0;

      omem:
	fprintf(stderr, "setsebool: out of memory");

      err:
	if (vallist) {
		for (i = 0; i < boolcnt; i++)
			free(vallist[i].name);
		free(vallist);
	}
	return -1;
}
