/* strongSwan IPsec starter
 * Copyright (C) 2001-2002 Mathieu Lafon - Arkoon Network Security
 *
 * 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; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#define _GNU_SOURCE

#include <sys/select.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <syslog.h>
#include <unistd.h>
#include <sys/time.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <grp.h>
#include <pthread.h>

#include <library.h>
#include <utils/backtrace.h>
#include <threading/thread.h>
#include <utils/debug.h>

#include "confread.h"
#include "files.h"
#include "starterstroke.h"
#include "invokecharon.h"
#include "cmp.h"

#ifndef LOG_AUTHPRIV
#define LOG_AUTHPRIV LOG_AUTH
#endif

#define CHARON_RESTART_DELAY 5

static const char* cmd_default = IPSEC_DIR "/charon";
static const char* pid_file_default = IPSEC_PIDDIR "/charon.pid";
static const char* starter_pid_file_default = IPSEC_PIDDIR "/starter.pid";

char *daemon_name = NULL;
char *cmd = NULL;
char *pid_file = NULL;
char *starter_pid_file = NULL;

static char *config_file = NULL;

/* logging */
static bool log_to_stderr = TRUE;
static bool log_to_syslog = TRUE;
static level_t current_loglevel = 1;

/**
 * logging function for scepclient
 */
static void starter_dbg(debug_t group, level_t level, char *fmt, ...)
{
	char buffer[8192];
	char *current = buffer, *next;
	va_list args;

	if (level <= current_loglevel)
	{
		if (log_to_stderr)
		{
			va_start(args, fmt);
			vfprintf(stderr, fmt, args);
			va_end(args);
			fprintf(stderr, "\n");
		}
		if (log_to_syslog)
		{
			/* write in memory buffer first */
			va_start(args, fmt);
			vsnprintf(buffer, sizeof(buffer), fmt, args);
			va_end(args);

			/* do a syslog with every line */
			while (current)
			{
				next = strchr(current, '\n');
				if (next)
				{
					*(next++) = '\0';
				}
				syslog(LOG_INFO, "%s\n", current);
				current = next;
			}
		}
	}
}

/**
 * Initialize logging to stderr/syslog
 */
static void init_log(const char *program)
{
	dbg = starter_dbg;

	if (log_to_stderr)
	{
		setbuf(stderr, NULL);
	}
	if (log_to_syslog)
	{
		openlog(program, LOG_CONS | LOG_NDELAY | LOG_PID, LOG_AUTHPRIV);
	}
}

/**
 * Deinitialize logging to syslog
 */
static void close_log()
{
	if (log_to_syslog)
	{
		closelog();
	}
}

/**
 * Return codes defined by Linux Standard Base Core Specification 3.1
 * in section 20.2. Init Script Actions
 */
#define LSB_RC_SUCCESS               0   /* success                          */
#define LSB_RC_FAILURE               1   /* generic or unspecified error     */
#define LSB_RC_INVALID_ARGUMENT      2   /* invalid or excess argument(s)    */
#define LSB_RC_NOT_IMPLEMENTED       3   /* unimplemented feature (reload)   */
#define LSB_RC_NOT_ALLOWED           4   /* user had insufficient privilege  */
#define LSB_RC_NOT_INSTALLED         5   /* program is not installed         */
#define LSB_RC_NOT_CONFIGURED        6   /* program is not configured        */
#define LSB_RC_NOT_RUNNING           7   /* program is not running           */

#define FLAG_ACTION_START_PLUTO   0x01
#define FLAG_ACTION_UPDATE        0x02
#define FLAG_ACTION_RELOAD        0x04
#define FLAG_ACTION_QUIT          0x08
#define FLAG_ACTION_LISTEN        0x10
#define FLAG_ACTION_START_CHARON  0x20

static unsigned int _action_ = 0;

/**
 * Handle signals in the main thread
 */
static void signal_handler(int signal)
{
	switch (signal)
	{
		case SIGCHLD:
		{
			int status, exit_status = 0;
			pid_t pid;
			char *name = NULL;

			while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
			{
				if (pid == starter_charon_pid())
				{
					if (asprintf(&name, " (%s)", daemon_name) < 0)
					{
						 name = NULL;
					}
				}
				if (WIFSIGNALED(status))
				{
					DBG2(DBG_APP, "child %d%s has been killed by sig %d\n",
						 pid, name?name:"", WTERMSIG(status));
				}
				else if (WIFSTOPPED(status))
				{
					DBG2(DBG_APP, "child %d%s has been stopped by sig %d\n",
						 pid, name?name:"", WSTOPSIG(status));
				}
				else if (WIFEXITED(status))
				{
					exit_status =  WEXITSTATUS(status);
					if (exit_status >= SS_RC_FIRST && exit_status <= SS_RC_LAST)
					{
						_action_ =  FLAG_ACTION_QUIT;
					}
					DBG2(DBG_APP, "child %d%s has quit (exit code %d)\n",
						 pid, name?name:"", exit_status);
				}
				else
				{
					DBG2(DBG_APP, "child %d%s has quit", pid, name?name:"");
				}
				if (pid == starter_charon_pid())
				{
					starter_charon_sigchild(pid, exit_status);
				}
			}

			if (name)
			{
				free(name);
			}
		}
		break;

		case SIGALRM:
			_action_ |= FLAG_ACTION_START_CHARON;
			break;

		case SIGHUP:
			_action_ |= FLAG_ACTION_UPDATE;
			break;

		case SIGTERM:
		case SIGQUIT:
		case SIGINT:
			_action_ |= FLAG_ACTION_QUIT;
			break;

		case SIGUSR1:
			_action_ |= FLAG_ACTION_RELOAD;
			_action_ |= FLAG_ACTION_UPDATE;
			break;

		default:
			DBG1(DBG_APP, "fsig(): unknown signal %d -- investigate", signal);
			break;
	}
}

/**
 * Handle fatal signals raised by threads
 */
static void fatal_signal_handler(int signal)
{
	backtrace_t *backtrace;

	DBG1(DBG_APP, "thread %u received %d", thread_current_id(), signal);
	backtrace = backtrace_create(2);
	backtrace->log(backtrace, stderr, TRUE);
	backtrace->destroy(backtrace);

	DBG1(DBG_APP, "killing ourself, received critical signal");
	abort();
}

static bool check_pid(char *file)
{
	struct stat stb;
	FILE *pidfile;

	if (stat(file, &stb) == 0)
	{
		pidfile = fopen(file, "r");
		if (pidfile)
		{
			char buf[64];
			pid_t pid = 0;
			memset(buf, 0, sizeof(buf));
			if (fread(buf, 1, sizeof(buf), pidfile))
			{
				buf[sizeof(buf) - 1] = '\0';
				pid = atoi(buf);
			}
			fclose(pidfile);
			if (pid && pid != getpid() && kill(pid, 0) == 0)
			{	/* such a process is running */
				return TRUE;
			}
		}
		DBG1(DBG_APP, "removing pidfile '%s', process not running", file);
		unlink(file);
	}
	return FALSE;
}

/* Set daemon name and adjust command and pid filenames accordingly */
static bool set_daemon_name()
{
	if (!daemon_name)
	{
		daemon_name = "charon";
	}

	if (asprintf(&cmd, IPSEC_DIR"/%s", daemon_name) < 0)
	{
		 cmd = (char*)cmd_default;
	}

	if (asprintf(&pid_file, IPSEC_PIDDIR"/%s.pid", daemon_name) < 0)
	{
		 pid_file = (char*)pid_file_default;
	}

	if (asprintf(&starter_pid_file, IPSEC_PIDDIR"/starter.%s.pid",
				 daemon_name) < 0)
	{
		 starter_pid_file = (char*)starter_pid_file_default;
	}

	return TRUE;
}

static void cleanup()
{
	if (cmd != cmd_default)
	{
		free(cmd);
	}

	if (pid_file != pid_file_default)
	{
		free(pid_file);
	}

	if (starter_pid_file != starter_pid_file_default)
	{
		free(starter_pid_file);
	}
}

static void usage(char *name)
{
	fprintf(stderr, "Usage: starter [--nofork] [--auto-update <sec>]\n"
			"               [--debug|--debug-more|--debug-all|--nolog]\n"
			"               [--attach-gdb] [--daemon <name>]\n"
			"               [--conf <path to ipsec.conf>]\n");
	exit(LSB_RC_INVALID_ARGUMENT);
}

int main (int argc, char **argv)
{
	starter_config_t *cfg = NULL;
	starter_config_t *new_cfg;
	starter_conn_t *conn, *conn2;
	starter_ca_t *ca, *ca2;

	struct sigaction action;
	struct stat stb;

	int i;
	int id = 1;
	struct timespec ts;
	unsigned long auto_update = 0;
	time_t last_reload;
	bool no_fork = FALSE;
	bool attach_gdb = FALSE;
	bool load_warning = FALSE;
	bool conftest = FALSE;

	library_init(NULL, "starter");
	atexit(library_deinit);

	/* parse command line */
	for (i = 1; i < argc; i++)
	{
		if (streq(argv[i], "--debug"))
		{
			current_loglevel = 2;
		}
		else if (streq(argv[i], "--debug-more"))
		{
			current_loglevel = 3;
		}
		else if (streq(argv[i], "--debug-all"))
		{
			current_loglevel = 4;
		}
		else if (streq(argv[i], "--nolog"))
		{
			current_loglevel = 0;
		}
		else if (streq(argv[i], "--nofork"))
		{
			no_fork = TRUE;
		}
		else if (streq(argv[i], "--attach-gdb"))
		{
			no_fork = TRUE;
			attach_gdb = TRUE;
		}
		else if (streq(argv[i], "--auto-update") && i+1 < argc)
		{
			auto_update = atoi(argv[++i]);
			if (!auto_update)
				usage(argv[0]);
		}
		else if (streq(argv[i], "--daemon") && i+1 < argc)
		{
			daemon_name = argv[++i];
		}
		else if (streq(argv[i], "--conf") && i+1 < argc)
		{
			config_file = argv[++i];
		}
		else if (streq(argv[i], "--conftest"))
		{
			conftest = TRUE;
		}
		else
		{
			usage(argv[0]);
		}
	}

	if (!set_daemon_name())
	{
		DBG1(DBG_APP, "unable to set daemon name");
		exit(LSB_RC_FAILURE);
	}
	if (!config_file)
	{
		config_file = lib->settings->get_str(lib->settings,
											 "starter.config_file", CONFIG_FILE);
	}

	init_log("ipsec_starter");

	if (conftest)
	{
		int status = LSB_RC_SUCCESS;

		cfg = confread_load(config_file);
		if (cfg == NULL || cfg->err > 0)
		{
			DBG1(DBG_APP, "config invalid!");
			status = LSB_RC_INVALID_ARGUMENT;
		}
		else
		{
			DBG1(DBG_APP, "config OK");
		}
		if (cfg)
		{
			confread_free(cfg);
		}
		cleanup();
		exit(status);
	}

	if (stat(cmd, &stb) != 0)
	{
		DBG1(DBG_APP, "IKE daemon '%s' not found", cmd);
		cleanup();
		exit(LSB_RC_FAILURE);
	}

	DBG1(DBG_APP, "Starting %sSwan "VERSION" IPsec [starter]...",
		lib->settings->get_bool(lib->settings,
			"charon.i_dont_care_about_security_and_use_aggressive_mode_psk",
				FALSE) ? "weak" : "strong");

#ifdef LOAD_WARNING
	load_warning = TRUE;
#endif

	if (lib->settings->get_bool(lib->settings, "starter.load_warning", load_warning))
	{
		if (lib->settings->get_str(lib->settings, "charon.load", NULL))
		{
			DBG1(DBG_APP, "!! Your strongswan.conf contains manual plugin load options for charon.");
			DBG1(DBG_APP, "!! This is recommended for experts only, see");
			DBG1(DBG_APP, "!! http://wiki.strongswan.org/projects/strongswan/wiki/PluginLoad");
		}
	}

#ifndef STARTER_ALLOW_NON_ROOT
	/* verify that we can start */
	if (getuid() != 0)
	{
		DBG1(DBG_APP, "permission denied (must be superuser)");
		cleanup();
		exit(LSB_RC_NOT_ALLOWED);
	}
#endif

	if (check_pid(pid_file))
	{
		DBG1(DBG_APP, "%s is already running (%s exists) -- skipping daemon start",
			 daemon_name, pid_file);
	}
	else
	{
		_action_ |= FLAG_ACTION_START_CHARON;
	}
	if (stat(DEV_RANDOM, &stb) != 0)
	{
		DBG1(DBG_APP, "unable to start strongSwan IPsec -- no %s!", DEV_RANDOM);
		cleanup();
		exit(LSB_RC_FAILURE);
	}

	if (stat(DEV_URANDOM, &stb)!= 0)
	{
		DBG1(DBG_APP, "unable to start strongSwan IPsec -- no %s!", DEV_URANDOM);
		cleanup();
		exit(LSB_RC_FAILURE);
	}

	cfg = confread_load(config_file);
	if (cfg == NULL || cfg->err > 0)
	{
		DBG1(DBG_APP, "unable to start strongSwan -- fatal errors in config");
		if (cfg)
		{
			confread_free(cfg);
		}
		cleanup();
		exit(LSB_RC_INVALID_ARGUMENT);
	}

	last_reload = time_monotonic(NULL);

	if (check_pid(starter_pid_file))
	{
		DBG1(DBG_APP, "starter is already running (%s exists) -- no fork done",
			 starter_pid_file);
		confread_free(cfg);
		cleanup();
		exit(LSB_RC_SUCCESS);
	}

	/* fork if we're not debugging stuff */
	if (!no_fork)
	{
		log_to_stderr = FALSE;

		switch (fork())
		{
			case 0:
			{
				int fnull;

				close_log();

				fnull = open("/dev/null", O_RDWR);
				if (fnull >= 0)
				{
					dup2(fnull, STDIN_FILENO);
					dup2(fnull, STDOUT_FILENO);
					dup2(fnull, STDERR_FILENO);
					close(fnull);
				}

				setsid();
				init_log("ipsec_starter");
			}
			break;
			case -1:
				DBG1(DBG_APP, "can't fork: %s", strerror(errno));
				break;
			default:
				confread_free(cfg);
				cleanup();
				exit(LSB_RC_SUCCESS);
		}
	}

	/* save pid file in /var/run/starter[.daemon_name].pid */
	{
		FILE *fd = fopen(starter_pid_file, "w");

		if (fd)
		{
			fprintf(fd, "%u\n", getpid());
			fclose(fd);
		}
	}

	/* we handle these signals only in pselect() */
	memset(&action, 0, sizeof(action));
	sigemptyset(&action.sa_mask);
	sigaddset(&action.sa_mask, SIGHUP);
	sigaddset(&action.sa_mask, SIGINT);
	sigaddset(&action.sa_mask, SIGTERM);
	sigaddset(&action.sa_mask, SIGQUIT);
	sigaddset(&action.sa_mask, SIGALRM);
	sigaddset(&action.sa_mask, SIGUSR1);
	pthread_sigmask(SIG_SETMASK, &action.sa_mask, NULL);

	/* install a handler for fatal signals */
	action.sa_handler = fatal_signal_handler;
	sigaction(SIGSEGV, &action, NULL);
	sigaction(SIGILL, &action, NULL);
	sigaction(SIGBUS, &action, NULL);
	action.sa_handler = SIG_IGN;
	sigaction(SIGPIPE, &action, NULL);

	/* install main signal handler */
	action.sa_handler = signal_handler;
	sigaction(SIGHUP, &action, NULL);
	sigaction(SIGINT, &action, NULL);
	sigaction(SIGTERM, &action, NULL);
	sigaction(SIGQUIT, &action, NULL);
	sigaction(SIGALRM, &action, NULL);
	sigaction(SIGUSR1, &action, NULL);
	/* this is not blocked above as we want to receive it asynchronously */
	sigaction(SIGCHLD, &action, NULL);

	/* empty mask for pselect() call below */
	sigemptyset(&action.sa_mask);

	for (;;)
	{
		/*
		 * Stop charon (if started) and exit
		 */
		if (_action_ & FLAG_ACTION_QUIT)
		{
			if (starter_charon_pid())
			{
				starter_stop_charon();
			}
			confread_free(cfg);
			unlink(starter_pid_file);
			cleanup();
			DBG1(DBG_APP, "ipsec starter stopped");
			close_log();
			exit(LSB_RC_SUCCESS);
		}

		/*
		 * Delete all connections. Will be added below
		 */
		if (_action_ & FLAG_ACTION_RELOAD)
		{
			_action_ &= ~FLAG_ACTION_RELOAD;
			if (starter_charon_pid())
			{
				for (conn = cfg->conn_first; conn; conn = conn->next)
				{
					if (conn->state == STATE_ADDED)
					{
						if (starter_charon_pid())
						{
							if (conn->startup == STARTUP_ROUTE)
							{
								starter_stroke_unroute_conn(conn);
							}
							starter_stroke_del_conn(conn);
						}
						conn->state = STATE_TO_ADD;
					}
				}
				for (ca = cfg->ca_first; ca; ca = ca->next)
				{
					if (ca->state == STATE_ADDED)
					{
						if (starter_charon_pid())
						{
							starter_stroke_del_ca(ca);
						}
						ca->state = STATE_TO_ADD;
					}
				}
			}
		}

		/*
		 * Update configuration
		 */
		if (_action_ & FLAG_ACTION_UPDATE)
		{
			_action_ &= ~FLAG_ACTION_UPDATE;
			DBG2(DBG_APP, "Reloading config...");
			new_cfg = confread_load(config_file);

			if (new_cfg && (new_cfg->err == 0))
			{
				/* Switch to new config. New conn will be loaded below */

				/* Look for new connections that are already loaded */
				for (conn = cfg->conn_first; conn; conn = conn->next)
				{
					if (conn->state == STATE_ADDED)
					{
						for (conn2 = new_cfg->conn_first; conn2; conn2 = conn2->next)
						{
							if (conn2->state == STATE_TO_ADD && starter_cmp_conn(conn, conn2))
							{
								conn->state = STATE_REPLACED;
								conn2->state = STATE_ADDED;
								conn2->id = conn->id;
								break;
							}
						}
					}
				}

				/* Remove conn sections that have become unused */
				for (conn = cfg->conn_first; conn; conn = conn->next)
				{
					if (conn->state == STATE_ADDED)
					{
						if (starter_charon_pid())
						{
							if (conn->startup == STARTUP_ROUTE)
							{
								starter_stroke_unroute_conn(conn);
							}
							starter_stroke_del_conn(conn);
						}
					}
				}

				/* Look for new ca sections that are already loaded */
				for (ca = cfg->ca_first; ca; ca = ca->next)
				{
					if (ca->state == STATE_ADDED)
					{
						for (ca2 = new_cfg->ca_first; ca2; ca2 = ca2->next)
						{
							if (ca2->state == STATE_TO_ADD && starter_cmp_ca(ca, ca2))
							{
								ca->state = STATE_REPLACED;
								ca2->state = STATE_ADDED;
								break;
							}
						}
					}
				}

				/* Remove ca sections that have become unused */
				for (ca = cfg->ca_first; ca; ca = ca->next)
				{
					if (ca->state == STATE_ADDED)
					{
						if (starter_charon_pid())
						{
							starter_stroke_del_ca(ca);
						}
					}
				}
				confread_free(cfg);
				cfg = new_cfg;
			}
			else
			{
				DBG1(DBG_APP, "can't reload config file due to errors -- keeping old one");
				if (new_cfg)
				{
					confread_free(new_cfg);
				}
			}
			last_reload = time_monotonic(NULL);
		}

		/*
		 * Start daemon
		 */
		if (_action_ & FLAG_ACTION_START_CHARON)
		{
			_action_ &= ~FLAG_ACTION_START_CHARON;
			if (!starter_charon_pid())
			{
				DBG2(DBG_APP, "Attempting to start %s...", daemon_name);
				if (starter_start_charon(cfg, no_fork, attach_gdb))
				{
					/* schedule next try */
					alarm(CHARON_RESTART_DELAY);
				}
				starter_stroke_configure(cfg);
			}

			for (ca = cfg->ca_first; ca; ca = ca->next)
			{
				if (ca->state == STATE_ADDED)
				{
					ca->state = STATE_TO_ADD;
				}
			}

			for (conn = cfg->conn_first; conn; conn = conn->next)
			{
				if (conn->state == STATE_ADDED)
				{
					conn->state = STATE_TO_ADD;
				}
			}
		}

		/*
		 * Add stale conn and ca sections
		 */
		if (starter_charon_pid())
		{
			for (ca = cfg->ca_first; ca; ca = ca->next)
			{
				if (ca->state == STATE_TO_ADD)
				{
					if (starter_charon_pid())
					{
						starter_stroke_add_ca(ca);
					}
					ca->state = STATE_ADDED;
				}
			}

			for (conn = cfg->conn_first; conn; conn = conn->next)
			{
				if (conn->state == STATE_TO_ADD)
				{
					if (conn->id == 0)
					{
						/* affect new unique id */
						conn->id = id++;
					}
					if (starter_charon_pid())
					{
						starter_stroke_add_conn(cfg, conn);
					}
					conn->state = STATE_ADDED;

					if (conn->startup == STARTUP_START)
					{
						if (starter_charon_pid())
						{
							starter_stroke_initiate_conn(conn);
						}
					}
					else if (conn->startup == STARTUP_ROUTE)
					{
						if (starter_charon_pid())
						{
							starter_stroke_route_conn(conn);
						}
					}
				}
			}
		}

		/*
		 * If auto_update activated, when to stop select
		 */
		if (auto_update)
		{
			time_t now = time_monotonic(NULL);

			ts.tv_sec = (now < last_reload + auto_update) ?
						(last_reload + auto_update - now) : 0;
			ts.tv_nsec = 0;
		}

		/*
		 * Wait for something to happen
		 */
		if (!_action_ &&
			pselect(0, NULL, NULL, NULL, auto_update ? &ts : NULL,
					&action.sa_mask) == 0)
		{
			/* timeout -> auto_update */
			_action_ |= FLAG_ACTION_UPDATE;
		}
	}
	exit(LSB_RC_SUCCESS);
}
