/* Authors: Karl MacMillan <kmacmillan@tresys.com>
 *          Joshua Brindle <jbrindle@tresys.com>
 *          Jason Tang <jtang@tresys.com>
 *
 * Copyright (C) 2004-2005 Tresys Technology, LLC
 *      This program is free software; you can redistribute it and/or
 *      modify it under the terms of the GNU General Public License as
 *      published by the Free Software Foundation, version 2.
 */

#include <fcntl.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <libgen.h>
#include <limits.h>

#include <sepol/cil/cil.h>
#include <semanage/modules.h>

enum client_modes {
	NO_MODE, INSTALL_M, REMOVE_M, EXTRACT_M, CIL_M, HLL_M,
	LIST_M, RELOAD, PRIORITY_M, ENABLE_M, DISABLE_M
};
/* list of modes in which one ought to commit afterwards */
static const int do_commit[] = {
	0, 1, 1, 0, 0, 0,
	0, 0, 0, 1, 1,
};

struct command {
	enum client_modes mode;
	char *arg;
};
static struct command *commands = NULL;
static int num_commands = 0;

/* options given on command line */
static int verbose;
static int reload;
static int no_reload;
static int build;
static int check_ext_changes;
static int disable_dontaudit;
static int preserve_tunables;
static int ignore_module_cache;
static uint16_t priority;
static int priority_set = 0;

static semanage_handle_t *sh = NULL;
static char *store;
static char *store_root;
int extract_cil = 0;
static int checksum = 0;

extern char *optarg;
extern int optind;

static void cleanup(void)
{
	while (--num_commands >= 0) {
		free(commands[num_commands].arg);
	}
	free(commands);
}

/* Signal handlers. */
static void handle_signal(int sig_num)
{
	if (sig_num == SIGINT || sig_num == SIGQUIT || sig_num == SIGTERM) {
		/* catch these signals, and then drop them */
	}
}

static void set_store(char *storename)
{
	/* For now this only supports a store name, later on this 
	 * should support an address for a remote connection */

	if ((store = strdup(storename)) == NULL) {
		fprintf(stderr, "Out of memory!\n");
		goto bad;
	}

	return;

      bad:
	cleanup();
	exit(1);
}

static void set_store_root(char *path)
{
	if ((store_root = strdup(path)) == NULL) {
		fprintf(stderr, "Out of memory!\n");
		goto bad;
	}

	return;

      bad:
	cleanup();
	exit(1);
}

/* Establish signal handlers for the process. */
static void create_signal_handlers(void)
{
	if (signal(SIGINT, handle_signal) == SIG_ERR ||
	    signal(SIGQUIT, handle_signal) == SIG_ERR ||
	    signal(SIGTERM, handle_signal) == SIG_ERR) {
		fprintf(stderr, "Could not set up signal handler.\n");
		exit(255);
	}
}

static void usage(char *progname)
{
	printf("usage:  %s [option]... MODE...\n", progname);
	printf("Manage SELinux policy modules.\n");
	printf("MODES:\n");
	printf("  -R, --reload		    reload policy\n");
	printf("  -B, --build		    build and reload policy\n");
	printf("  -D,--disable_dontaudit    Remove dontaudits from policy\n");
	printf("  -i,--install=MODULE_PKG   install a new module\n");
	printf("  -r,--remove=MODULE_NAME   remove existing module at desired priority\n");
	printf("  -l[KIND],--list-modules[=KIND]  display list of installed modules\n");
	printf("     KIND:  standard  list highest priority, enabled modules\n");
	printf("            full      list all modules\n");
	printf("  -X,--priority=PRIORITY    set priority for following operations (1-999)\n");
	printf("  -e,--enable=MODULE_NAME   enable module\n");
	printf("  -d,--disable=MODULE_NAME  disable module\n");
	printf("  -E,--extract=MODULE_NAME  extract module\n");
	printf("Options:\n");
	printf("  -s,--store	   name of the store to operate on\n");
	printf("  -N,-n,--noreload do not reload policy after commit\n");
	printf("  -h,--help        print this message and quit\n");
	printf("  -v,--verbose     be verbose\n");
	printf("  -P,--preserve_tunables	Preserve tunables in policy\n");
	printf("  -C,--ignore-module-cache	Rebuild CIL modules compiled from HLL files\n");
	printf("  -p,--path        use an alternate path for the policy root\n");
	printf("  -S,--store-path  use an alternate path for the policy store root\n");
	printf("  -c, --cil extract module as cil. This only affects module extraction.\n");
	printf("  -H, --hll extract module as hll. This only affects module extraction.\n");
	printf("  -m, --checksum   print module checksum (SHA256).\n");
	printf("      --refresh    like --build, but reuses existing linked policy if no\n"
	       "                   changes to module files are detected (via checksum)\n");
	printf("Deprecated options:\n");
	printf("  -b,--base	   same as --install\n");
	printf("  --rebuild-if-modules-changed\n"
	       "                   same as --refresh\n");
}

/* Sets the global mode variable to new_mode, but only if no other
 * mode has been given. */
static void set_mode(enum client_modes new_mode, char *arg)
{
	struct command *c;
	char *s;
	if ((c = realloc(commands, sizeof(*c) * (num_commands + 1))) == NULL) {
		fprintf(stderr, "Out of memory!\n");
		cleanup();
		exit(1);
	}
	commands = c;
	commands[num_commands].mode = new_mode;
	commands[num_commands].arg = NULL;
	num_commands++;
	if (arg != NULL) {
		if ((s = strdup(arg)) == NULL) {
			fprintf(stderr, "Out of memory!\n");
			cleanup();
			exit(1);
		}
		commands[num_commands - 1].arg = s;
	}
}

/* Parse command line and set global options. */
static void parse_command_line(int argc, char **argv)
{
	static struct option opts[] = {
		{"rebuild-if-modules-changed", 0, NULL, '\0'},
		{"refresh", 0, NULL, '\0'},
		{"store", required_argument, NULL, 's'},
		{"base", required_argument, NULL, 'b'},
		{"help", 0, NULL, 'h'},
		{"install", required_argument, NULL, 'i'},
		{"extract", required_argument, NULL, 'E'},
		{"cil", 0, NULL, 'c'},
		{"hll", 0, NULL, 'H'},
		{"list-modules", optional_argument, NULL, 'l'},
		{"verbose", 0, NULL, 'v'},
		{"remove", required_argument, NULL, 'r'},
		{"upgrade", required_argument, NULL, 'u'},
		{"reload", 0, NULL, 'R'},
		{"noreload", 0, NULL, 'n'},
		{"build", 0, NULL, 'B'},
		{"disable_dontaudit", 0, NULL, 'D'},
		{"preserve_tunables", 0, NULL, 'P'},
		{"ignore-module-cache", 0, NULL, 'C'},
		{"priority", required_argument, NULL, 'X'},
		{"enable", required_argument, NULL, 'e'},
		{"disable", required_argument, NULL, 'd'},
		{"path", required_argument, NULL, 'p'},
		{"store-path", required_argument, NULL, 'S'},
		{"checksum", 0, NULL, 'm'},
		{NULL, 0, NULL, 0}
	};
	int extract_selected = 0;
	int cil_hll_set = 0;
	int i, longind;
	verbose = 0;
	reload = 0;
	no_reload = 0;
	check_ext_changes = 0;
	priority = 400;
	while ((i =
		getopt_long(argc, argv, "s:b:hi:l::vr:u:RnNBDCPX:e:d:p:S:E:cHm",
			    opts, &longind)) != -1) {
		switch (i) {
		case '\0':
			switch(longind) {
			case 0: /* --rebuild-if-modules-changed */
				fprintf(stderr, "The --rebuild-if-modules-changed option is deprecated. Use --refresh instead.\n");
				/* fallthrough */
			case 1: /* --refresh */
				check_ext_changes = 1;
				break;
			default:
				usage(argv[0]);
				exit(1);
			}
			break;
		case 'b':
			fprintf(stderr, "The --base option is deprecated. Use --install instead.\n");
			set_mode(INSTALL_M, optarg);
			break;
		case 'h':
			usage(argv[0]);
			exit(0);
		case 'i':
			set_mode(INSTALL_M, optarg);
			break;
		case 'E':
			set_mode(EXTRACT_M, optarg);
			extract_selected = 1;
			break;
		case 'c':
			set_mode(CIL_M, NULL);
			cil_hll_set = 1;
			break;
		case 'H':
			set_mode(HLL_M, NULL);
			cil_hll_set = 1;
			break;
		case 'l':
			set_mode(LIST_M, optarg);
			break;
		case 'v':
			verbose++;
			break;
		case 'r':
			set_mode(REMOVE_M, optarg);
			break;
		case 'u':
			fprintf(stderr, "The --upgrade option is deprecated. Use --install instead.\n");
			set_mode(INSTALL_M, optarg);
			break;
		case 's':
			set_store(optarg);
			break;
		case 'p':
			semanage_set_root(optarg);
			break;
		case 'S':
			set_store_root(optarg);
			break;
		case 'R':
			reload = 1;
			break;
		case 'n':
			no_reload = 1;
			break;
		case 'N':
			no_reload = 1;
			break;
		case 'B':
			build = 1;
			break;
		case 'D':
			disable_dontaudit = 1;
			break;
		case 'P':
			preserve_tunables = 1;
			break;
		case 'C':
			ignore_module_cache = 1;
			break;
		case 'X':
			set_mode(PRIORITY_M, optarg);
			break;
		case 'e':
			set_mode(ENABLE_M, optarg);
			break;
		case 'd':
			set_mode(DISABLE_M, optarg);
			break;
		case 'm':
			checksum = 1;
			break;
		case '?':
		default:{
				usage(argv[0]);
				exit(1);
			}
		}
	}
	if ((build || reload || check_ext_changes) && num_commands) {
		fprintf(stderr,
			"build or reload should not be used with other commands\n");
		usage(argv[0]);
		exit(1);
	}
	if (num_commands == 0 && reload == 0 && build == 0 && check_ext_changes == 0) {
		fprintf(stderr, "At least one mode must be specified.\n");
		usage(argv[0]);
		exit(1);
	}
	if (extract_selected == 0 && cil_hll_set == 1) {
		fprintf(stderr, "--cil and --hll require a module to export with the --extract option.\n");
		usage(argv[0]);
		exit(1);
	}

	if (optind < argc) {
		int mode = commands ? (int) commands[num_commands - 1].mode : -1;
		/* if -i/u/r/E was the last command treat any remaining
		 * arguments as args. Will allow 'semodule -i *.pp' to
		 * work as expected.
		 */

		switch (mode) {
			case INSTALL_M:
			case REMOVE_M:
			case EXTRACT_M:
			case ENABLE_M:
			case DISABLE_M:
				while (optind < argc)
					set_mode(mode, argv[optind++]);
				break;
			default:
				fprintf(stderr, "unknown additional arguments:\n");
				while (optind < argc)
					fprintf(stderr, " %s", argv[optind++]);
				fprintf(stderr, "\n\n");
				usage(argv[0]);
				exit(1);
		}
	}
}

/* Get module checksum */
static char *hash_module_data(const char *module_name, const int prio) {
	semanage_module_key_t *modkey = NULL;
	char *hash_str = NULL;
	void *hash = NULL;
	size_t hash_len = 0;
	int result;

	result = semanage_module_key_create(sh, &modkey);
	if (result != 0) {
		goto cleanup;
	}

	result = semanage_module_key_set_name(sh, modkey, module_name);
	if (result != 0) {
		goto cleanup;
	}

	result = semanage_module_key_set_priority(sh, modkey, prio);
	if (result != 0) {
		goto cleanup;
	}

	result = semanage_module_compute_checksum(sh, modkey, 1, &hash_str,
						  &hash_len);
	if (result != 0) {
		goto cleanup;
	}

cleanup:
	free(hash);
	semanage_module_key_destroy(sh, modkey);
	free(modkey);
	return hash_str;
}

int main(int argc, char *argv[])
{
	int i, commit = 0;
	int result;
	int status = EXIT_FAILURE;
	const char *genhomedirconargv[] = { "genhomedircon", "-B", "-n" };
	create_signal_handlers();
	if (strcmp(basename(argv[0]), "genhomedircon") == 0) {
		argc = 3;
		argv = (char **)genhomedirconargv;
	}
	parse_command_line(argc, argv);

	cil_set_log_level(CIL_ERR + verbose);

	if (build || check_ext_changes)
		commit = 1;

	sh = semanage_handle_create();
	if (!sh) {
		fprintf(stderr, "%s:  Could not create semanage handle\n",
			argv[0]);
		goto cleanup_nohandle;
	}

	if (store) {
		/* Set the store we want to connect to, before connecting.
		 * this will always set a direct connection now, an additional
		 * option will need to be used later to specify a policy server 
		 * location */
		semanage_select_store(sh, store, SEMANAGE_CON_DIRECT);
	}

	if (store_root) {
		semanage_set_store_root(sh, store_root);
	}

	/* create store if necessary, for bootstrapping */
	semanage_set_create_store(sh, 1);

	if ((result = semanage_connect(sh)) < 0) {
		fprintf(stderr, "%s:  Could not connect to policy handler\n",
			argv[0]);
		goto cleanup;
	}

	if (reload) {
		if ((result = semanage_reload_policy(sh)) < 0) {
			fprintf(stderr, "%s:  Could not reload policy\n",
				argv[0]);
			goto cleanup;
		}
	}

	if (build || check_ext_changes) {
		if ((result = semanage_begin_transaction(sh)) < 0) {
			fprintf(stderr, "%s:  Could not begin transaction:  %s\n",
				argv[0], errno ? strerror(errno) : "");
			goto cleanup;
		}
	}

	if ((result = semanage_set_default_priority(sh, priority)) != 0) {
		fprintf(stderr,
			"%s: Invalid priority %d (needs to be between 1 and 999)\n",
			argv[0],
			priority);
		goto cleanup;
	}

	for (i = 0; i < num_commands; i++) {
		enum client_modes mode = commands[i].mode;
		char *mode_arg = commands[i].arg;

		switch (mode) {
		case INSTALL_M:{
				if (verbose) {
					printf
					    ("Attempting to install module '%s':\n",
					     mode_arg);
				}
				result =
				    semanage_module_install_file(sh, mode_arg);
				break;
			}
		case EXTRACT_M:{
				semanage_module_info_t *extract_info = NULL;
				semanage_module_key_t *modkey = NULL;
				uint16_t curr_priority;
				void *data = NULL;
				size_t data_len = 0;
				char output_path[PATH_MAX];
				const char *output_name = NULL;
				const char *lang_ext = NULL;
				int rlen;
				FILE *output_fd = NULL;

				result = semanage_module_key_create(sh, &modkey);
				if (result != 0) {
					goto cleanup_extract;
				}

				result = semanage_module_key_set_name(sh, modkey, mode_arg);
				if (result != 0) {
					goto cleanup_extract;
				}

				if (priority_set == 0) {
					result = semanage_module_get_module_info(sh, modkey, &extract_info);
					if (result != 0) {
						goto cleanup_extract;
					}

					semanage_module_info_get_priority(sh, extract_info, &curr_priority);
					printf("Extracting at highest existing priority '%d'.\n", curr_priority);
					priority = curr_priority;
				}

				result  = semanage_module_key_set_priority(sh, modkey, priority);
				if (result != 0) {
					goto cleanup_extract;
				}

				if (verbose) {
					printf
						("Attempting to extract module '%s':\n",
							mode_arg);
				}
				result = semanage_module_extract(sh, modkey, extract_cil, &data, &data_len, &extract_info);
				if (result != 0) {
					goto cleanup_extract;
				}

				if (extract_cil) {
					lang_ext = "cil";
				} else {
					result = semanage_module_info_get_lang_ext(sh, extract_info, &lang_ext);
					if (result != 0) {
						goto cleanup_extract;
					}
				}

				result = semanage_module_info_get_name(sh, extract_info, &output_name);
				if (result != 0) {
					goto cleanup_extract;
				}

				rlen = snprintf(output_path, PATH_MAX, "%s.%s", output_name, lang_ext);
				if (rlen < 0 || rlen >= PATH_MAX) {
					fprintf(stderr, "%s: Failed to generate output path.\n", argv[0]);
					result = -1;
					goto cleanup_extract;
				}

				output_fd = fopen(output_path, "wx");
				if (output_fd == NULL) {
					if (errno == EEXIST)
						fprintf(stderr, "%s: %s is already extracted with extension %s.\n", argv[0], mode_arg, lang_ext);
					else
						fprintf(stderr, "%s: Unable to open %s:  %s\n", argv[0], output_path, strerror(errno));
					result = -1;
					goto cleanup_extract;
				}

				if (fwrite(data, 1, data_len, output_fd) < data_len) {
					fprintf(stderr, "%s: Unable to write to %s\n", argv[0], output_path);
					result = -1;
					goto cleanup_extract;
				}
cleanup_extract:
				if (output_fd != NULL) {
					fclose(output_fd);
				}
				if (data_len > 0) {
					munmap(data, data_len);
				}
				semanage_module_info_destroy(sh, extract_info);
				free(extract_info);
				semanage_module_key_destroy(sh, modkey);
				free(modkey);
				break;
			}
		case CIL_M:
				extract_cil = 1;
				break;
		case HLL_M:
				extract_cil = 0;
				break;
		case REMOVE_M:{
				if (verbose) {
					printf
					    ("Attempting to remove module '%s':\n",
					     mode_arg);
				}
				result = semanage_module_remove(sh, mode_arg);
				if ( result == -2 ) { 
					continue;
				}
				break;
			}
		case LIST_M:{
				semanage_module_info_t *modinfos = NULL;
				int modinfos_len = 0;
				semanage_module_info_t *m = NULL;
				int j = 0;
				char *module_checksum = NULL;
				uint16_t pri = 0;

				if (verbose) {
					printf
					    ("Attempting to list active modules:\n");
				}

				if (mode_arg == NULL || strcmp(mode_arg, "standard") == 0) {
					result = semanage_module_list(sh,
								      &modinfos,
								      &modinfos_len);
					if (result < 0) goto cleanup_list;

					if (modinfos_len == 0) {
						printf("No modules.\n");
					}

					const char *name = NULL;

					for (j = 0; j < modinfos_len; j++) {
						m = semanage_module_list_nth(modinfos, j);

						result = semanage_module_info_get_name(sh, m, &name);
						if (result != 0) goto cleanup_list;

						result = semanage_module_info_get_priority(sh, m, &pri);
						if (result != 0) goto cleanup_list;

						printf("%s", name);
						if (checksum) {
							module_checksum = hash_module_data(name, pri);
							if (module_checksum) {
								printf(" %s", module_checksum);
								free(module_checksum);
							}
						}
						printf("\n");
					}
				}
				else if (strcmp(mode_arg, "full") == 0) {
					/* get the modules */
					result = semanage_module_list_all(sh,
									  &modinfos,
									  &modinfos_len);
					if (result != 0) goto cleanup_list;

					if (modinfos_len == 0) {
						printf("No modules.\n");
					}

					/* calculate column widths */
					size_t column[5] = { 0, 0, 0, 0, 0 };

					/* fixed width columns */
					column[0] = sizeof("000") - 1;
					column[3] = sizeof("disabled") - 1;

					result = semanage_module_compute_checksum(sh, NULL, 0, NULL,
										  &column[4]);
					if (result != 0) goto cleanup_list;

					/* variable width columns */
					const char *tmp = NULL;
					size_t size;
					for (j = 0; j < modinfos_len; j++) {
						m = semanage_module_list_nth(modinfos, j);

						result = semanage_module_info_get_name(sh, m, &tmp);
						if (result != 0) goto cleanup_list;

						size = strlen(tmp);
						if (size > column[1]) column[1] = size;

						result = semanage_module_info_get_lang_ext(sh, m, &tmp);
						if (result != 0) goto cleanup_list;

						size = strlen(tmp);
						if (size > column[2]) column[2] = size;
					}

					/* print out each module */
					for (j = 0; j < modinfos_len; j++) {
						const char *name = NULL;
						int enabled = 0;
						const char *lang_ext = NULL;

						m = semanage_module_list_nth(modinfos, j);

						result = semanage_module_info_get_priority(sh, m, &pri);
						if (result != 0) goto cleanup_list;

						result = semanage_module_info_get_name(sh, m, &name);
						if (result != 0) goto cleanup_list;

						result = semanage_module_info_get_enabled(sh, m, &enabled);
						if (result != 0) goto cleanup_list;

						result = semanage_module_info_get_lang_ext(sh, m, &lang_ext);
						if (result != 0) goto cleanup_list;

						printf("%0*u %-*s %-*s %-*s",
							(int)column[0], pri,
							(int)column[1], name,
							(int)column[2], lang_ext,
							(int)column[3], enabled ? "" : "disabled");
						if (checksum) {
							module_checksum = hash_module_data(name, pri);
							if (module_checksum) {
								printf(" %-*s", (int)column[4], module_checksum);
								free(module_checksum);
							}
						}
						printf("\n");

					}
				}
				else {
					result = -1;
				}

cleanup_list:
				for (j = 0; j < modinfos_len; j++) {
					m = semanage_module_list_nth(modinfos, j);
					semanage_module_info_destroy(sh, m);
				}

				free(modinfos);

				break;
			}
		case PRIORITY_M:{
				char *endptr = NULL;
				priority = (uint16_t)strtoul(mode_arg, &endptr, 10);
				priority_set = 1;

				if ((result = semanage_set_default_priority(sh, priority)) != 0) {
					fprintf(stderr,
						"%s: Invalid priority %d (needs to be between 1 and 999)\n",
						argv[0],
						priority);
					goto cleanup;
				}

				break;
			}
		case ENABLE_M:{
				if (verbose) {
					printf
					    ("Attempting to enable module '%s':\n",
					     mode_arg);
				}

				semanage_module_key_t *modkey = NULL;

				result = semanage_module_key_create(sh, &modkey);
				if (result != 0) goto cleanup_enable;

				result = semanage_module_key_set_name(sh, modkey, mode_arg);
				if (result != 0) goto cleanup_enable;

				result = semanage_module_set_enabled(sh, modkey, 1);
				if (result != 0) goto cleanup_enable;

cleanup_enable:
				semanage_module_key_destroy(sh, modkey);
				free(modkey);

				break;
			}
		case DISABLE_M:{
				if (verbose) {
					printf
					    ("Attempting to disable module '%s':\n",
					     mode_arg);
				}

				semanage_module_key_t *modkey = NULL;

				result = semanage_module_key_create(sh, &modkey);
				if (result != 0) goto cleanup_disable;

				result = semanage_module_key_set_name(sh, modkey, mode_arg);
				if (result != 0) goto cleanup_disable;

				result = semanage_module_set_enabled(sh, modkey, 0);
				if (result != 0) goto cleanup_disable;

cleanup_disable:
				semanage_module_key_destroy(sh, modkey);
				free(modkey);

				break;
			}
		default:{
				fprintf(stderr,
					"%s:  Unknown mode specified.\n",
					argv[0]);
				usage(argv[0]);
				goto cleanup;
			}
		}
		commit += do_commit[mode];
		if (result < 0) {
			fprintf(stderr, "%s:  Failed on %s!\n", argv[0],
				mode_arg ? : "list");
			goto cleanup;
		} else if (verbose) {
			printf("Ok: return value of %d.\n", result);
		}
	}

	if (commit) {
		if (verbose)
			printf("Committing changes:\n");
		if (no_reload)
			semanage_set_reload(sh, 0);
		if (build)
			semanage_set_rebuild(sh, 1);
		if (check_ext_changes)
			semanage_set_check_ext_changes(sh, 1);
		if (disable_dontaudit)
			semanage_set_disable_dontaudit(sh, 1);
		else if (build)
			semanage_set_disable_dontaudit(sh, 0);
		if (preserve_tunables)
			semanage_set_preserve_tunables(sh, 1);
		if (ignore_module_cache)
			semanage_set_ignore_module_cache(sh, 1);

		result = semanage_commit(sh);
	}

	if (result < 0) {
		fprintf(stderr, "%s:  Failed!\n", argv[0]);
		goto cleanup;
	} else if (commit && verbose) {
		printf("Ok: transaction number %d.\n", result);
	}

	if (semanage_disconnect(sh) < 0) {
		fprintf(stderr, "%s:  Error disconnecting\n", argv[0]);
		goto cleanup;
	}
	status = EXIT_SUCCESS;

      cleanup:
	if (semanage_is_connected(sh)) {
		if (semanage_disconnect(sh) < 0) {
			fprintf(stderr, "%s:  Error disconnecting\n", argv[0]);
		}
	}
	semanage_handle_destroy(sh);

      cleanup_nohandle:
	cleanup();
	exit(status);
}
