# -*- coding: utf-8 -*-
#
#  king_phisher/server/__main__.py
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are
#  met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above
#    copyright notice, this list of conditions and the following disclaimer
#    in the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of the project nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
#  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
#  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
#  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#  pylint: disable=too-many-locals

import argparse
import functools
import logging
import os
import signal
import sys
import threading

from king_phisher import startup
from king_phisher import color
from king_phisher import constants
from king_phisher import errors
from king_phisher import find
from king_phisher import utilities
from king_phisher import version
from king_phisher.server import build
from king_phisher.server import configuration
from king_phisher.server import fs_utilities
from king_phisher.server import plugins
from king_phisher.server import pylibc

logger = logging.getLogger('KingPhisher.Server.CLI')

def sig_handler(server, name, number, frame):
	signal.signal(signal.SIGINT, signal.SIG_IGN)
	signal.signal(signal.SIGTERM, signal.SIG_IGN)
	logger.info("received signal {0}, shutting down the server".format(name))
	threading.Thread(target=server.shutdown).start()

def build_and_run(arguments, config, plugin_manager, log_file=None):
	# fork into the background
	should_fork = True
	if arguments.foreground:
		should_fork = False
	elif config.has_option('server.fork'):
		should_fork = bool(config.get('server.fork'))
	if should_fork:
		if os.fork():
			return sys.exit(os.EX_OK)
		os.setsid()

	try:
		king_phisher_server = build.server_from_config(config, plugin_manager=plugin_manager)
	except errors.KingPhisherDatabaseAuthenticationError:
		logger.critical('failed to authenticate to the database, this usually means the password is incorrect and needs to be updated')
		return os.EX_SOFTWARE
	except errors.KingPhisherError as error:
		logger.critical('server failed to build with error: ' + error.message)
		return os.EX_SOFTWARE

	server_pid = os.getpid()
	logger.info("server running in process: {0} main tid: 0x{1:x}".format(server_pid, threading.current_thread().ident))

	if should_fork and config.has_option('server.pid_file'):
		pid_file = open(config.get('server.pid_file'), 'w')
		pid_file.write(str(server_pid))
		pid_file.close()

	if config.has_option('server.setuid_username'):
		setuid_username = config.get('server.setuid_username')
		try:
			passwd = pylibc.getpwnam(setuid_username)
		except KeyError:
			logger.critical('an invalid username was specified as \'server.setuid_username\'')
			king_phisher_server.shutdown()
			return os.EX_NOUSER

		if log_file is not None:
			fs_utilities.chown(log_file, user=passwd.pw_uid, group=passwd.pw_gid, recursive=False)
		data_path = config.get_if_exists('server.letsencrypt.data_path')
		if data_path and config.get_if_exists('server.letsencrypt.chown_data_path', True):
			if os.path.isdir(data_path):
				fs_utilities.chown(data_path, user=passwd.pw_uid, group=passwd.pw_gid, recursive=True)
			else:
				logger.warning('can not chown the letsencrypt data directory (directory not found)')

		os.setgroups(pylibc.getgrouplist(setuid_username))
		os.setresgid(passwd.pw_gid, passwd.pw_gid, passwd.pw_gid)
		os.setresuid(passwd.pw_uid, passwd.pw_uid, passwd.pw_uid)
		logger.info("dropped privileges to the {} account (uid: {}, gid: {})".format(setuid_username, passwd.pw_uid, passwd.pw_gid))
	else:
		logger.warning('running with root privileges is dangerous, drop them by configuring \'server.setuid_username\'')
	os.umask(0o077)

	db_engine_url = king_phisher_server.database_engine.url
	if db_engine_url.drivername == 'sqlite':
		logger.warning('sqlite is no longer fully supported, see https://github.com/securestate/king-phisher/wiki/Database#sqlite for more details')
		database_dir = os.path.dirname(db_engine_url.database)
		if not os.access(database_dir, os.W_OK):
			logger.critical('sqlite requires write permissions to the folder containing the database')
			king_phisher_server.shutdown()
			return os.EX_NOPERM

	signal.signal(signal.SIGHUP, functools.partial(sig_handler, king_phisher_server, 'SIGHUP'))
	signal.signal(signal.SIGINT, functools.partial(sig_handler, king_phisher_server, 'SIGINT'))
	signal.signal(signal.SIGTERM, functools.partial(sig_handler, king_phisher_server, 'SIGTERM'))

	try:
		king_phisher_server.serve_forever(fork=False)
	except KeyboardInterrupt:
		pass
	king_phisher_server.shutdown()
	return os.EX_OK

def _ex_config_logging(arguments, config, console_handler):
	"""
	If a setting is configured improperly, this will terminate execution via
	:py:func:`sys.exit`.

	:return: The path to a log file if one is in use.
	:rtype: str
	"""
	default_log_level = min(
		getattr(logging, (arguments.loglvl or constants.DEFAULT_LOG_LEVEL)),
		getattr(logging, config.get_if_exists('logging.level', 'critical').upper())
	)
	log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'FATAL')
	file_path = None
	if config.has_option('logging.file'):
		options = config.get('logging.file')
		for _ in range(1):
			default_format = '%(asctime)s %(name)-50s %(levelname)-8s %(message)s'
			if isinstance(options, dict):   # new style
				if not options.get('enabled', True):
					break
				if 'path' not in options:
					color.print_error('logging.file is missing required key \'path\'')
					sys.exit(os.EX_CONFIG)
				if 'level' not in options:
					color.print_error('logging.file is missing required key \'level\'')
					sys.exit(os.EX_CONFIG)
				file_path = options['path']
				formatter = logging.Formatter(options.get('format', default_format))
				if not options['level'].upper() in log_levels:
					color.print_error('logging.file.level is invalid, must be one of: ' + ', '.join(log_levels))
					sys.exit(os.EX_CONFIG)
				log_level = getattr(logging, options['level'].upper())
				root = options.get('root', '')
			elif isinstance(options, str):  # old style
				file_path = options
				formatter = logging.Formatter(default_format)
				log_level = default_log_level
				root = ''
			else:
				break
			file_handler = logging.FileHandler(file_path)
			file_handler.setFormatter(formatter)
			logging.getLogger(root).addHandler(file_handler)
			file_handler.setLevel(log_level)

	if config.has_option('logging.console'):
		options = config.get('logging.console')
		for _ in range(1):
			if isinstance(options, dict):   # new style
				if not options.get('enabled', True):
					break
				if 'format' in options:
					console_handler.setFormatter(color.ColoredLogFormatter(options['format']))

				if arguments.loglvl is None and 'level' in options:
					log_level = str(options.get('level', '')).upper()
					if log_level not in log_levels:
						color.print_error('logging.console.level is invalid, must be one of: ' + ', '.join(log_levels))
						sys.exit(os.EX_CONFIG)
					console_handler.setLevel(getattr(logging, log_level))
			elif isinstance(options, str):  # old style
				console_handler.setLevel(default_log_level)
	return file_path

def main():
	parser = argparse.ArgumentParser(prog='KingPhisherServer', description='King Phisher Server', conflict_handler='resolve')
	utilities.argp_add_args(parser)
	startup.argp_add_server(parser)
	arguments = parser.parse_args()

	# basic runtime checks
	if sys.version_info < (3, 4):
		color.print_error('the python version is too old (minimum required is 3.4)')
		return 0

	console_log_handler = utilities.configure_stream_logger(arguments.logger, arguments.loglvl)
	del parser

	# configure environment variables and load the config
	find.init_data_path('server')
	if not os.path.exists(arguments.config_file):
		color.print_error('invalid configuration file')
		color.print_error('the specified path does not exist')
		return os.EX_NOINPUT
	if not os.path.isfile(arguments.config_file):
		color.print_error('invalid configuration file')
		color.print_error('the specified path is not a file')
		return os.EX_NOINPUT
	if not os.access(arguments.config_file, os.R_OK):
		color.print_error('invalid configuration file')
		color.print_error('the specified path can not be read')
		return os.EX_NOPERM
	config = configuration.ex_load_config(arguments.config_file)
	if arguments.verify_config:
		color.print_good('configuration verification passed')
		color.print_good('all required settings are present')
		return os.EX_OK
	if config.has_option('server.data_path'):
		find.data_path_append(config.get('server.data_path'))

	if os.getuid():
		color.print_error('the server must be started as root, configure the')
		color.print_error('\'server.setuid_username\' option in the config file to drop privileges')
		return os.EX_NOPERM

	# setup logging based on the configuration
	if config.has_section('logging'):
		log_file = _ex_config_logging(arguments, config, console_log_handler)
	logger.debug("king phisher version: {0} python version: {1}.{2}.{3}".format(version.version, sys.version_info[0], sys.version_info[1], sys.version_info[2]))

	# initialize the plugin manager
	try:
		plugin_manager = plugins.ServerPluginManager(config)
	except errors.KingPhisherError as error:
		if isinstance(error, errors.KingPhisherPluginError):
			color.print_error("plugin error: {0} ({1})".format(error.plugin_name, error.message))
		else:
			color.print_error(error.message)
		return os.EX_SOFTWARE

	status_code = build_and_run(arguments, config, plugin_manager, log_file)
	plugin_manager.shutdown()
	logging.shutdown()
	return status_code

if __name__ == '__main__':
	sys.exit(main())