1# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
2# vi: set ft=python sts=4 ts=4 sw=4 noet :
3
4# This file is part of Fail2Ban.
5#
6# Fail2Ban is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# Fail2Ban is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Fail2Ban; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19
20# Author: Cyril Jaquier
21#
22
23__author__ = "Cyril Jaquier"
24__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
25__license__ = "GPL"
26
27import glob
28import json
29import os.path
30import re
31
32from .configreader import ConfigReaderUnshared, ConfigReader
33from .filterreader import FilterReader
34from .actionreader import ActionReader
35from ..version import version
36from ..helpers import getLogger, extractOptions, splitWithOptions, splitwords
37
38# Gets the instance of the logger.
39logSys = getLogger(__name__)
40
41
42class JailReader(ConfigReader):
43
44	def __init__(self, name, force_enable=False, **kwargs):
45		ConfigReader.__init__(self, **kwargs)
46		self.__name = name
47		self.__filter = None
48		self.__force_enable = force_enable
49		self.__actions = list()
50		self.__opts = None
51
52	@property
53	def options(self):
54		return self.__opts
55
56	def setName(self, value):
57		self.__name = value
58
59	def getName(self):
60		return self.__name
61
62	def read(self):
63		out = ConfigReader.read(self, "jail")
64		# Before returning -- verify that requested section
65		# exists at all
66		if not (self.__name in self.sections()):
67			raise ValueError("Jail %r was not found among available"
68							 % self.__name)
69		return out
70
71	def isEnabled(self):
72		return self.__force_enable or (
73			self.__opts and self.__opts.get("enabled", False))
74
75	@staticmethod
76	def _glob(path):
77		"""Given a path for glob return list of files to be passed to server.
78
79		Dangling symlinks are warned about and not returned
80		"""
81		pathList = []
82		for p in glob.glob(path):
83			if os.path.exists(p):
84				pathList.append(p)
85			else:
86				logSys.warning("File %s is a dangling link, thus cannot be monitored" % p)
87		return pathList
88
89	_configOpts1st = {
90		"enabled": ["bool", False],
91		"backend": ["string", "auto"],
92		"filter": ["string", ""]
93	}
94	_configOpts = {
95		"enabled": ["bool", False],
96		"backend": ["string", "auto"],
97		"maxretry": ["int", None],
98		"maxmatches": ["int", None],
99		"findtime": ["string", None],
100		"bantime": ["string", None],
101		"bantime.increment": ["bool", None],
102		"bantime.factor": ["string", None],
103		"bantime.formula": ["string", None],
104		"bantime.multipliers": ["string", None],
105		"bantime.maxtime": ["string", None],
106		"bantime.rndtime": ["string", None],
107		"bantime.overalljails": ["bool", None],
108		"ignorecommand": ["string", None],
109		"ignoreself": ["bool", None],
110		"ignoreip": ["string", None],
111		"ignorecache": ["string", None],
112		"filter": ["string", ""],
113		"logtimezone": ["string", None],
114		"logencoding": ["string", None],
115		"logpath": ["string", None],
116		"action": ["string", ""]
117	}
118	_configOpts.update(FilterReader._configOpts)
119
120	_ignoreOpts = set(['action', 'filter', 'enabled'] + list(FilterReader._configOpts.keys()))
121
122	def getOptions(self):
123
124		# Before interpolation (substitution) add static options always available as default:
125		self.merge_defaults({
126			"fail2ban_version": version
127		})
128
129		try:
130
131			# Read first options only needed for merge defaults ('known/...' from filter):
132			self.__opts = ConfigReader.getOptions(self, self.__name, self._configOpts1st,
133				shouldExist=True)
134			if not self.__opts: # pragma: no cover
135				raise JailDefError("Init jail options failed")
136
137			if not self.isEnabled():
138				return True
139
140			# Read filter
141			flt = self.__opts["filter"]
142			if flt:
143				filterName, filterOpt = extractOptions(flt)
144				if not filterName:
145					raise JailDefError("Invalid filter definition %r" % flt)
146				self.__filter = FilterReader(
147					filterName, self.__name, filterOpt,
148					share_config=self.share_config, basedir=self.getBaseDir())
149				ret = self.__filter.read()
150				if not ret:
151					raise JailDefError("Unable to read the filter %r" % filterName)
152				# set backend-related options (logtype):
153				self.__filter.applyAutoOptions(self.__opts.get('backend', ''))
154				# merge options from filter as 'known/...' (all options unfiltered):
155				self.__filter.getOptions(self.__opts, all=True)
156				ConfigReader.merge_section(self, self.__name, self.__filter.getCombined(), 'known/')
157			else:
158				self.__filter = None
159				logSys.warning("No filter set for jail %s" % self.__name)
160
161			# Read second all options (so variables like %(known/param) can be interpolated):
162			self.__opts = ConfigReader.getOptions(self, self.__name, self._configOpts)
163			if not self.__opts: # pragma: no cover
164				raise JailDefError("Read jail options failed")
165
166			# cumulate filter options again (ignore given in jail):
167			if self.__filter:
168				self.__filter.getOptions(self.__opts)
169
170			# Read action
171			for act in splitWithOptions(self.__opts["action"]):
172				try:
173					act = act.strip()
174					if not act:			  # skip empty actions
175						continue
176					# join with previous line if needed (consider possible new-line):
177					actName, actOpt = extractOptions(act)
178					prevln = ''
179					if not actName:
180						raise JailDefError("Invalid action definition %r" % act)
181					if actName.endswith(".py"):
182						self.__actions.append([
183							"set",
184							self.__name,
185							"addaction",
186							actOpt.pop("actname", os.path.splitext(actName)[0]),
187							os.path.join(
188								self.getBaseDir(), "action.d", actName),
189							json.dumps(actOpt),
190							])
191					else:
192						action = ActionReader(
193							actName, self.__name, actOpt,
194							share_config=self.share_config, basedir=self.getBaseDir())
195						ret = action.read()
196						if ret:
197							action.getOptions(self.__opts)
198							self.__actions.append(action)
199						else:
200							raise JailDefError("Unable to read action %r" % actName)
201				except JailDefError:
202					raise
203				except Exception as e:
204					logSys.debug("Caught exception: %s", e, exc_info=True)
205					raise ValueError("Error in action definition %r: %r" % (act, e))
206			if not len(self.__actions):
207				logSys.warning("No actions were defined for %s" % self.__name)
208
209		except JailDefError as e:
210			e = str(e)
211			logSys.error(e)
212			if not self.__opts:
213				self.__opts = dict()
214			self.__opts['config-error'] = e
215			return False
216		return True
217
218	def convert(self, allow_no_files=False):
219		"""Convert read before __opts to the commands stream
220
221		Parameters
222		----------
223		allow_missing : bool
224		  Either to allow log files to be missing entirely.  Primarily is
225		  used for testing
226		 """
227
228		stream = []
229		stream2 = []
230		e = self.__opts.get('config-error')
231		if e:
232			stream.extend([['config-error', "Jail '%s' skipped, because of wrong configuration: %s" % (self.__name, e)]])
233			return stream
234		# fill jail with filter options, using filter (only not overriden in jail):
235		if self.__filter:
236			stream.extend(self.__filter.convert())
237		# and using options from jail:
238		FilterReader._fillStream(stream, self.__opts, self.__name)
239		for opt, value in self.__opts.items():
240			if opt == "logpath":
241				if self.__opts.get('backend', '').startswith("systemd"): continue
242				found_files = 0
243				for path in value.split("\n"):
244					path = path.rsplit(" ", 1)
245					path, tail = path if len(path) > 1 else (path[0], "head")
246					pathList = JailReader._glob(path)
247					if len(pathList) == 0:
248						logSys.notice("No file(s) found for glob %s" % path)
249					for p in pathList:
250						found_files += 1
251						# logpath after all log-related data (backend, date-pattern, etc)
252						stream2.append(
253							["set", self.__name, "addlogpath", p, tail])
254				if not found_files:
255					msg = "Have not found any log file for %s jail" % self.__name
256					if not allow_no_files:
257						raise ValueError(msg)
258					logSys.warning(msg)
259			elif opt == "backend":
260				backend = value
261			elif opt == "ignoreip":
262				stream.append(["set", self.__name, "addignoreip"] + splitwords(value))
263			elif opt not in JailReader._ignoreOpts:
264				stream.append(["set", self.__name, opt, value])
265		# consider options order (after other options):
266		if stream2: stream += stream2
267		for action in self.__actions:
268			if isinstance(action, (ConfigReaderUnshared, ConfigReader)):
269				stream.extend(action.convert())
270			else:
271				stream.append(action)
272		stream.insert(0, ["add", self.__name, backend])
273		return stream
274
275class JailDefError(Exception):
276	pass
277