# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
# vi: set ft=python sts=4 ts=4 sw=4 noet :

# This file is part of Fail2Ban.
#
# Fail2Ban 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.
#
# Fail2Ban 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.
#
# You should have received a copy of the GNU General Public License
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

# Author: Cyril Jaquier
# 

__author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"

import time
import json

from ..helpers import getLogger, logging
from .. import version

# Gets the instance of the logger.
logSys = getLogger(__name__)


class Transmitter:
	
	##
	# Constructor.
	#
	# @param The server reference
	
	def __init__(self, server):
		self.__server = server
		self.__quiet = 0
		
	##
	# Proceeds a command.
	#
	# Proceeds an incoming command.
	# @param command The incoming command
	
	def proceed(self, command):
		# Deserialize object
		logSys.log(5, "Command: %r", command)
		try:
			ret = self.__commandHandler(command)
			ack = 0, ret
		except Exception as e:
			logSys.error("Command %r has failed. Received %r",
						command, e, 
						exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
			ack = 1, e
		return ack
	
	##
	# Handle an command.
	#
	# 
	
	def __commandHandler(self, command):
		name = command[0]
		if name == "ping":
			return "pong"
		elif name == "add":
			name = command[1]
			if name == "--all":
				raise Exception("Reserved name %r" % (name,))
			try:
				backend = command[2]
			except IndexError:
				backend = "auto"
			self.__server.addJail(name, backend)
			return name
		elif name == "multi-set":
			return self.__commandSet(command[1:], True)
		elif name == "set":
			return self.__commandSet(command[1:])
		elif name == "start":
			name = command[1]
			self.__server.startJail(name)
			return None
		elif name == "stop":
			if len(command) == 1:
				self.__server.quit()
			elif command[1] == "--all":
				self.__server.stopAllJail()
			else:
				name = command[1]
				self.__server.stopJail(name)
			return None
		elif name == "reload":
			opts = command[1:3]
			self.__quiet = 1
			try:
				self.__server.reloadJails(*opts, begin=True)
				for cmd in command[3]:
					self.__commandHandler(cmd)
			finally:
				self.__quiet = 0
				self.__server.reloadJails(*opts, begin=False)
			return 'OK'
		elif name == "unban" and len(command) >= 2:
			# unban in all jails:
			value = command[1:]
			# if all ips:
			if len(value) == 1 and value[0] == "--all":
				return self.__server.setUnbanIP()
			return self.__server.setUnbanIP(None, value)
		elif name == "banned":
			# check IP is banned in all jails:
			return self.__server.banned(None, command[1:])
		elif name == "echo":
			return command[1:]
		elif name == "server-status":
			logSys.debug("Status: ready")
			return "Server ready"
		elif name == "server-stream":
			self.__quiet = 1
			try:
				for cmd in command[1]:
					self.__commandHandler(cmd)
			finally:
				self.__quiet = 0
			return None
		elif name == "sleep":
			value = command[1]
			time.sleep(float(value))
			return None
		elif name == "flushlogs":
			return self.__server.flushLogs()
		elif name == "get":
			return self.__commandGet(command[1:])
		elif name == "status":
			return self.status(command[1:])
		elif name in ("stats", "statistic", "statistics"):
			return self.__server.status("--all", "stats")
		elif name == "version":
			return version.version
		elif name == "config-error":
			logSys.error(command[1])
			return None
		raise Exception("Invalid command")
	
	def __commandSet(self, command, multiple=False):
		name = command[0]
		# Logging
		if name == "loglevel":
			value = command[1]
			self.__server.setLogLevel(value)
			if self.__quiet: return
			return self.__server.getLogLevel()
		elif name == "logtarget":
			value = command[1]
			if self.__server.setLogTarget(value):
				if self.__quiet: return
				return self.__server.getLogTarget()
			else:
				raise Exception("Failed to change log target")
		elif name == "syslogsocket":
			value = command[1]
			if self.__server.setSyslogSocket(value):
				if self.__quiet: return
				return self.__server.getSyslogSocket()
			else:
				raise Exception("Failed to change syslog socket")
		elif name == "allowipv6":
			value = command[1]
			self.__server.setIPv6IsAllowed(value)
			if self.__quiet: return
			return value
		#Thread
		elif name == "thread":
			value = command[1]
			return self.__server.setThreadOptions(value)
		#Database
		elif name == "dbfile":
			self.__server.setDatabase(command[1])
			db = self.__server.getDatabase()
			if db is None:
				return None
			else:
				if self.__quiet: return
				return db.filename
		elif name == "dbmaxmatches":
			db = self.__server.getDatabase()
			if db is None:
				logSys.log(logging.MSG, "dbmaxmatches setting was not in effect since no db yet")
				return None
			else:
				db.maxMatches = int(command[1])
				if self.__quiet: return
				return db.maxMatches
		elif name == "dbpurgeage":
			db = self.__server.getDatabase()
			if db is None:
				logSys.log(logging.MSG, "dbpurgeage setting was not in effect since no db yet")
				return None
			else:
				db.purgeage = command[1]
				if self.__quiet: return
				return db.purgeage
		# Jail
		elif command[1] == "idle":
			if command[2] == "on":
				self.__server.setIdleJail(name, True)
			elif command[2] == "off":
				self.__server.setIdleJail(name, False)
			else:
				raise Exception("Invalid idle option, must be 'on' or 'off'")
			if self.__quiet: return
			return self.__server.getIdleJail(name)
		# Filter
		elif command[1] == "ignoreself":
			value = command[2]
			self.__server.setIgnoreSelf(name, value)
			if self.__quiet: return
			return self.__server.getIgnoreSelf(name)
		elif command[1] == "addignoreip":
			for value in command[2:]:
				self.__server.addIgnoreIP(name, value)
			if self.__quiet: return
			return self.__server.getIgnoreIP(name)
		elif command[1] == "delignoreip":
			value = command[2]
			self.__server.delIgnoreIP(name, value)
			if self.__quiet: return
			return self.__server.getIgnoreIP(name)
		elif command[1] == "ignorecommand":
			value = command[2]
			self.__server.setIgnoreCommand(name, value)
			if self.__quiet: return
			return self.__server.getIgnoreCommand(name)
		elif command[1] == "ignorecache":
			value = command[2]
			self.__server.setIgnoreCache(name, value)
			if self.__quiet: return
			return self.__server.getIgnoreCache(name)
		elif command[1] == "addlogpath":
			value = command[2]
			tail = False
			if len(command) == 4:
				if command[3].lower()  == "tail":
					tail = True
				elif command[3].lower() != "head":
					raise ValueError("File option must be 'head' or 'tail'")
			elif len(command) > 4:
				raise ValueError("Only one file can be added at a time")
			self.__server.addLogPath(name, value, tail)
			if self.__quiet: return
			return self.__server.getLogPath(name)
		elif command[1] == "dellogpath":
			value = command[2]
			self.__server.delLogPath(name, value)
			if self.__quiet: return
			return self.__server.getLogPath(name)
		elif command[1] == "logencoding":
			value = command[2]
			self.__server.setLogEncoding(name, value)
			if self.__quiet: return
			return self.__server.getLogEncoding(name)
		elif command[1] == "addjournalmatch": # pragma: systemd no cover
			value = command[2:]
			self.__server.addJournalMatch(name, value)
			if self.__quiet: return
			return self.__server.getJournalMatch(name)
		elif command[1] == "deljournalmatch": # pragma: systemd no cover
			value = command[2:]
			self.__server.delJournalMatch(name, value)
			if self.__quiet: return
			return self.__server.getJournalMatch(name)
		elif command[1] == "prefregex":
			value = command[2]
			self.__server.setPrefRegex(name, value)
			if self.__quiet: return
			v = self.__server.getPrefRegex(name)
			return v.getRegex() if v else ""
		elif command[1] == "addfailregex":
			value = command[2]
			self.__server.addFailRegex(name, value, multiple=multiple)
			if multiple:
				return True
			if self.__quiet: return
			return self.__server.getFailRegex(name)
		elif command[1] == "delfailregex":
			value = int(command[2])
			self.__server.delFailRegex(name, value)
			if self.__quiet: return
			return self.__server.getFailRegex(name)
		elif command[1] == "addignoreregex":
			value = command[2]
			self.__server.addIgnoreRegex(name, value, multiple=multiple)
			if multiple:
				return True
			if self.__quiet: return
			return self.__server.getIgnoreRegex(name)
		elif command[1] == "delignoreregex":
			value = int(command[2])
			self.__server.delIgnoreRegex(name, value)
			if self.__quiet: return
			return self.__server.getIgnoreRegex(name)
		elif command[1] == "usedns":
			value = command[2]
			self.__server.setUseDns(name, value)
			if self.__quiet: return
			return self.__server.getUseDns(name)
		elif command[1] == "findtime":
			value = command[2]
			self.__server.setFindTime(name, value)
			if self.__quiet: return
			return self.__server.getFindTime(name)
		elif command[1] == "datepattern":
			value = command[2]
			self.__server.setDatePattern(name, value)
			if self.__quiet: return
			return self.__server.getDatePattern(name)
		elif command[1] == "logtimezone":
			value = command[2]
			self.__server.setLogTimeZone(name, value)
			if self.__quiet: return
			return self.__server.getLogTimeZone(name)
		elif command[1] == "maxmatches":
			value = command[2]
			self.__server.setMaxMatches(name, int(value))
			if self.__quiet: return
			return self.__server.getMaxMatches(name)
		elif command[1] == "maxretry":
			value = command[2]
			self.__server.setMaxRetry(name, int(value))
			if self.__quiet: return
			return self.__server.getMaxRetry(name)
		elif command[1] == "maxlines":
			value = command[2]
			self.__server.setMaxLines(name, int(value))
			if self.__quiet: return
			return self.__server.getMaxLines(name)
		# command
		elif command[1] == "bantime":
			value = command[2]
			self.__server.setBanTime(name, value)
			if self.__quiet: return
			return self.__server.getBanTime(name)
		elif command[1] == "attempt":
			value = command[2:]
			if self.__quiet: return
			return self.__server.addAttemptIP(name, *value)
		elif command[1].startswith("bantime."):
			value = command[2]
			opt = command[1][len("bantime."):]
			self.__server.setBanTimeExtra(name, opt, value)
			if self.__quiet: return
			return self.__server.getBanTimeExtra(name, opt)
		elif command[1] == "banip":
			value = command[2:]
			return self.__server.setBanIP(name,value)
		elif command[1] == "unbanip":
			ifexists = True
			if command[2] != "--report-absent":
				value = command[2:]
			else:
				ifexists = False
				value = command[3:]
			return self.__server.setUnbanIP(name, value, ifexists=ifexists)
		elif command[1] == "addaction":
			args = [command[2]]
			if len(command) > 3:
				args.extend([command[3], json.loads(command[4])])
			self.__server.addAction(name, *args)
			if self.__quiet: return
			return args[0]
		elif command[1] == "delaction":
			value = command[2]
			self.__server.delAction(name, value)
			return None
		elif command[1] == "action":
			actionname = command[2]
			action = self.__server.getAction(name, actionname)
			if multiple:
				for cmd in command[3]:
					logSys.log(5, "  %r", cmd)
					actionkey = cmd[0]
					if callable(getattr(action, actionkey, None)):
						actionvalue = json.loads(cmd[1]) if len(cmd)>1 else {}
						getattr(action, actionkey)(**actionvalue)
					else:
						actionvalue = cmd[1]
						setattr(action, actionkey, actionvalue)
				return True
			else:
				actionkey = command[3]
				if callable(getattr(action, actionkey, None)):
					actionvalue = json.loads(command[4]) if len(command)>4 else {}
					if self.__quiet: return
					return getattr(action, actionkey)(**actionvalue)
				else:
					actionvalue = command[4]
					setattr(action, actionkey, actionvalue)
					if self.__quiet: return
					return getattr(action, actionkey)
		raise Exception("Invalid command %r (no set action or not yet implemented)" % (command[1],))
	
	def __commandGet(self, command):
		name = command[0]
		# Logging
		if name == "loglevel":
			return self.__server.getLogLevel()
		elif name == "logtarget":
			return self.__server.getLogTarget()
		elif name == "syslogsocket":
			return self.__server.getSyslogSocket()
		#Thread
		elif name == "thread":
			return self.__server.getThreadOptions()
		#Database
		elif name == "dbfile":
			db = self.__server.getDatabase()
			if db is None:
				return None
			else:
				return db.filename
		elif name == "dbmaxmatches":
			db = self.__server.getDatabase()
			if db is None:
				return None
			else:
				return db.maxMatches
		elif name == "dbpurgeage":
			db = self.__server.getDatabase()
			if db is None:
				return None
			else:
				return db.purgeage
		# Jail, Filter
		elif command[1] == "banned":
			# check IP is banned in all jails:
			return self.__server.banned(name, command[2:])
		elif command[1] == "logpath":
			return self.__server.getLogPath(name)
		elif command[1] == "logencoding":
			return self.__server.getLogEncoding(name)
		elif command[1] == "journalmatch": # pragma: systemd no cover
			return self.__server.getJournalMatch(name)
		elif command[1] == "ignoreself":
			return self.__server.getIgnoreSelf(name)
		elif command[1] == "ignoreip":
			return self.__server.getIgnoreIP(name)
		elif command[1] == "ignorecommand":
			return self.__server.getIgnoreCommand(name)
		elif command[1] == "ignorecache":
			return self.__server.getIgnoreCache(name)
		elif command[1] == "prefregex":
			v = self.__server.getPrefRegex(name)
			return v.getRegex() if v else ""
		elif command[1] == "failregex":
			return self.__server.getFailRegex(name)
		elif command[1] == "ignoreregex":
			return self.__server.getIgnoreRegex(name)
		elif command[1] == "usedns":
			return self.__server.getUseDns(name)
		elif command[1] == "findtime":
			return self.__server.getFindTime(name)
		elif command[1] == "datepattern":
			return self.__server.getDatePattern(name)
		elif command[1] == "logtimezone":
			return self.__server.getLogTimeZone(name)
		elif command[1] == "maxmatches":
			return self.__server.getMaxMatches(name)
		elif command[1] == "maxretry":
			return self.__server.getMaxRetry(name)
		elif command[1] == "maxlines":
			return self.__server.getMaxLines(name)
		# Action
		elif command[1] == "bantime":
			return self.__server.getBanTime(name)
		elif command[1] == "banip":
			return self.__server.getBanList(name,
				withTime=len(command) > 2 and command[2] == "--with-time")
		elif command[1].startswith("bantime."):
			opt = command[1][len("bantime."):]
			return self.__server.getBanTimeExtra(name, opt)
		elif command[1] == "actions":
			return list(self.__server.getActions(name).keys())
		elif command[1] == "action":
			actionname = command[2]
			actionvalue = command[3]
			action = self.__server.getAction(name, actionname)
			return getattr(action, actionvalue)
		elif command[1] == "actionproperties":
			actionname = command[2]
			action = self.__server.getAction(name, actionname)
			return [
				key for key in dir(action)
				if not key.startswith("_") and
					not callable(getattr(action, key))]
		elif command[1] == "actionmethods":
			actionname = command[2]
			action = self.__server.getAction(name, actionname)
			return [
				key for key in dir(action)
				if not key.startswith("_") and callable(getattr(action, key))]
		raise Exception("Invalid command (no get action or not yet implemented)")
	
	def status(self, command):
		if len(command) == 0:
			return self.__server.status()
		elif len(command) >= 1 and len(command) <= 2:
			name = command[0]
			flavor = command[1] if len(command) == 2 else "basic"
			if name == "--all":
				return self.__server.status("--all", flavor)
			return self.__server.statusJail(name, flavor=flavor)
		raise Exception("Invalid command (no status)")
