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__author__ = "Fail2Ban Developers"
20__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
21__license__ = "GPL"
22
23import getopt
24import logging
25import os
26import sys
27
28from ..version import version, normVersion
29from ..protocol import printFormatted
30from ..helpers import getLogger, str2LogLevel, getVerbosityFormat, BrokenPipeError
31
32# Gets the instance of the logger.
33logSys = getLogger("fail2ban")
34
35def output(s): # pragma: no cover
36	try:
37		print(s)
38	except (BrokenPipeError, IOError) as e: # pragma: no cover
39		if e.errno != 32: # closed / broken pipe
40			raise
41
42# Config parameters required to start fail2ban which can be also set via command line (overwrite fail2ban.conf),
43CONFIG_PARAMS = ("socket", "pidfile", "logtarget", "loglevel", "syslogsocket")
44# Used to signal - we are in test cases (ex: prevents change logging params, log capturing, etc)
45PRODUCTION = True
46
47MAX_WAITTIME = 30
48
49
50class Fail2banCmdLine():
51
52	def __init__(self):
53		self._argv = self._args = None
54		self._configurator = None
55		self.cleanConfOnly = False
56		self.resetConf()
57
58	def resetConf(self):
59		self._conf = {
60		  "async": False,
61			"conf": "/usr/local/etc/fail2ban",
62			"force": False,
63			"background": True,
64			"verbose": 1,
65			"socket": None,
66			"pidfile": None,
67			"timeout": MAX_WAITTIME
68		}
69
70	@property
71	def configurator(self):
72		if self._configurator:
73			return self._configurator
74		# New configurator
75		from .configurator import Configurator
76		self._configurator = Configurator()
77		# Set the configuration path
78		self._configurator.setBaseDir(self._conf["conf"])
79		return self._configurator
80
81
82	def applyMembers(self, obj):
83		for o in obj.__dict__:
84			self.__dict__[o] = obj.__dict__[o]
85
86	def dispVersion(self, short=False):
87		if not short:
88			output("Fail2Ban v" + version)
89		else:
90			output(normVersion())
91
92	def dispUsage(self):
93		""" Prints Fail2Ban command line options and exits
94		"""
95		caller = os.path.basename(self._argv[0])
96		output("Usage: "+caller+" [OPTIONS]" + (" <COMMAND>" if not caller.endswith('server') else ""))
97		output("")
98		output("Fail2Ban v" + version + " reads log file that contains password failure report")
99		output("and bans the corresponding IP addresses using firewall rules.")
100		output("")
101		output("Options:")
102		output("    -c, --conf <DIR>        configuration directory")
103		output("    -s, --socket <FILE>     socket path")
104		output("    -p, --pidfile <FILE>    pidfile path")
105		output("    --pname <NAME>          name of the process (main thread) to identify instance (default fail2ban-server)")
106		output("    --loglevel <LEVEL>      logging level")
107		output("    --logtarget <TARGET>    logging target, use file-name or stdout, stderr, syslog or sysout.")
108		output("    --syslogsocket auto|<FILE>")
109		output("    -d                      dump configuration. For debugging")
110		output("    --dp, --dump-pretty     dump the configuration using more human readable representation")
111		output("    -t, --test              test configuration (can be also specified with start parameters)")
112		output("    -i                      interactive mode")
113		output("    -v                      increase verbosity")
114		output("    -q                      decrease verbosity")
115		output("    -x                      force execution of the server (remove socket file)")
116		output("    -b                      start server in background (default)")
117		output("    -f                      start server in foreground")
118		output("    --async                 start server in async mode (for internal usage only, don't read configuration)")
119		output("    --timeout               timeout to wait for the server (for internal usage only, don't read configuration)")
120		output("    --str2sec <STRING>      convert time abbreviation format to seconds")
121		output("    -h, --help              display this help message")
122		output("    -V, --version           print the version (-V returns machine-readable short format)")
123
124		if not caller.endswith('server'):
125			output("")
126			output("Command:")
127			# Prints the protocol
128			printFormatted()
129
130		output("")
131		output("Report bugs to https://github.com/fail2ban/fail2ban/issues")
132
133	def __getCmdLineOptions(self, optList):
134		""" Gets the command line options
135		"""
136		for opt in optList:
137			o = opt[0]
138			if o in ("-c", "--conf"):
139				self._conf["conf"] = opt[1]
140			elif o in ("-s", "--socket"):
141				self._conf["socket"] = opt[1]
142			elif o in ("-p", "--pidfile"):
143				self._conf["pidfile"] = opt[1]
144			elif o in ("-d", "--dp", "--dump-pretty"):
145				self._conf["dump"] = True if o == "-d" else 2
146			elif o in ("-t", "--test"):
147				self.cleanConfOnly = True
148				self._conf["test"] = True
149			elif o == "-v":
150				self._conf["verbose"] += 1
151			elif o == "-q":
152				self._conf["verbose"] -= 1
153			elif o == "-x":
154				self._conf["force"] = True
155			elif o == "-i":
156				self._conf["interactive"] = True
157			elif o == "-b":
158				self._conf["background"] = True
159			elif o == "-f":
160				self._conf["background"] = False
161			elif o == "--async":
162				self._conf["async"] = True
163			elif o == "--timeout":
164				from ..server.mytime import MyTime
165				self._conf["timeout"] = MyTime.str2seconds(opt[1])
166			elif o == "--str2sec":
167				from ..server.mytime import MyTime
168				output(MyTime.str2seconds(opt[1]))
169				return True
170			elif o in ("-h", "--help"):
171				self.dispUsage()
172				return True
173			elif o in ("-V", "--version"):
174				self.dispVersion(o == "-V")
175				return True
176			elif o.startswith("--"): # other long named params (see also resetConf)
177				self._conf[ o[2:] ] = opt[1]
178		return None
179
180	def initCmdLine(self, argv):
181		verbose = 1
182		try:
183			# First time?
184			initial = (self._argv is None)
185
186			# Command line options
187			self._argv = argv
188			logSys.info("Using start params %s", argv[1:])
189
190			# Reads the command line options.
191			try:
192				cmdOpts = 'hc:s:p:xfbdtviqV'
193				cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'test', 'async',
194					'conf=', 'pidfile=', 'pname=', 'socket=',
195					'timeout=', 'str2sec=', 'help', 'version', 'dp', '--dump-pretty']
196				optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts)
197			except getopt.GetoptError:
198				self.dispUsage()
199				return False
200
201			ret = self.__getCmdLineOptions(optList)
202			if ret is not None:
203				return ret
204
205			logSys.debug("  conf: %r, args: %r", self._conf, self._args)
206
207			if initial and PRODUCTION: # pragma: no cover - can't test
208				verbose = self._conf["verbose"]
209				if verbose <= 0:
210					logSys.setLevel(logging.ERROR)
211				elif verbose == 1:
212					logSys.setLevel(logging.WARNING)
213				elif verbose == 2:
214					logSys.setLevel(logging.INFO)
215				elif verbose == 3:
216					logSys.setLevel(logging.DEBUG)
217				else:
218					logSys.setLevel(logging.HEAVYDEBUG)
219				# Add the default logging handler to dump to stderr
220				logout = logging.StreamHandler(sys.stderr)
221
222				# Custom log format for the verbose run (-1, because default verbosity here is 1):
223				fmt = getVerbosityFormat(verbose-1)
224				formatter = logging.Formatter(fmt)
225				# tell the handler to use this format
226				logout.setFormatter(formatter)
227				logSys.addHandler(logout)
228
229			# Set expected parameters (like socket, pidfile, etc) from configuration,
230			# if those not yet specified, in which read configuration only if needed here:
231			conf = None
232			for o in CONFIG_PARAMS:
233				if self._conf.get(o, None) is None:
234					if not conf:
235						self.configurator.readEarly()
236						conf = self.configurator.getEarlyOptions()
237					if o in conf:
238						self._conf[o] = conf[o]
239
240			logSys.info("Using socket file %s", self._conf["socket"])
241
242			# Check log-level before start (or transmit to server), to prevent error in background:
243			llev = str2LogLevel(self._conf["loglevel"])
244			logSys.info("Using pid file %s, [%s] logging to %s",
245				self._conf["pidfile"], logging.getLevelName(llev), self._conf["logtarget"])
246
247			readcfg = True
248			if self._conf.get("dump", False):
249				if readcfg:
250					ret, stream = self.readConfig()
251					readcfg = False
252				if stream is not None:
253					self.dumpConfig(stream, self._conf["dump"] == 2)
254				else: # pragma: no cover
255					output("ERROR: The configuration stream failed because of the invalid syntax.")
256				if not self._conf.get("test", False):
257					return ret
258
259			if self._conf.get("test", False):
260				if readcfg:
261					readcfg = False
262					ret, stream = self.readConfig()
263				if not ret:
264					raise ServerExecutionException("ERROR: test configuration failed")
265				# exit after test if no commands specified (test only):
266				if not len(self._args):
267					output("OK: configuration test is successful")
268					return ret
269
270			# Nothing to do here, process in client/server
271			return None
272		except ServerExecutionException:
273			raise
274		except Exception as e:
275			output("ERROR: %s" % (e,))
276			if verbose > 2:
277				logSys.exception(e)
278			return False
279
280	def readConfig(self, jail=None):
281		# Read the configuration
282		# TODO: get away from stew of return codes and exception
283		# handling -- handle via exceptions
284		stream = None
285		try:
286			self.configurator.Reload()
287			self.configurator.readAll()
288			ret = self.configurator.getOptions(jail, self._conf,
289				ignoreWrong=not self.cleanConfOnly)
290			self.configurator.convertToProtocol(
291				allow_no_files=self._conf.get("dump", False))
292			stream = self.configurator.getConfigStream()
293		except Exception as e:
294			logSys.error("Failed during configuration: %s" % e)
295			ret = False
296		return ret, stream
297
298	@staticmethod
299	def dumpConfig(cmd, pretty=False):
300		if pretty:
301			from pprint import pformat
302			def _output(s):
303				output(pformat(s, width=1000, indent=2))
304		else:
305			_output = output
306		for c in cmd:
307			_output(c)
308		return True
309
310	#
311	# _exit is made to ease mocking out of the behaviour in tests,
312	# since method is also exposed in API via globally bound variable
313	@staticmethod
314	def _exit(code=0):
315		# implicit flush without to produce broken pipe error (32):
316		sys.stderr.close()
317		try:
318			sys.stdout.flush()
319			# exit:
320			if hasattr(sys, 'exit') and sys.exit:
321				sys.exit(code)
322			else:
323				os._exit(code)
324		except (BrokenPipeError, IOError) as e: # pragma: no cover
325			if e.errno != 32: # closed / broken pipe
326				raise
327
328	@staticmethod
329	def exit(code=0):
330		logSys.debug("Exit with code %s", code)
331		# because of possible buffered output in python, we should flush it before exit:
332		logging.shutdown()
333		# exit
334		Fail2banCmdLine._exit(code)
335
336
337# global exit handler:
338exit = Fail2banCmdLine.exit
339
340
341class ExitException(Exception):
342	pass
343
344
345class ServerExecutionException(Exception):
346	pass
347