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 os
24import shlex
25import signal
26import socket
27import sys
28import time
29
30import threading
31from threading import Thread
32
33from ..version import version
34from .csocket import CSocket
35from .beautifier import Beautifier
36from .fail2bancmdline import Fail2banCmdLine, ServerExecutionException, ExitException, \
37	logSys, exit, output
38
39from ..server.utils import Utils
40
41PROMPT = "fail2ban> "
42
43
44def _thread_name():
45	return threading.current_thread().__class__.__name__
46
47def input_command(): # pragma: no cover
48	return input(PROMPT)
49
50##
51#
52# @todo This class needs cleanup.
53
54class Fail2banClient(Fail2banCmdLine, Thread):
55
56	def __init__(self):
57		Fail2banCmdLine.__init__(self)
58		Thread.__init__(self)
59		self._alive = True
60		self._server = None
61		self._beautifier = None
62
63	def dispInteractive(self):
64		output("Fail2Ban v" + version + " reads log file that contains password failure report")
65		output("and bans the corresponding IP addresses using firewall rules.")
66		output("")
67
68	def __sigTERMhandler(self, signum, frame): # pragma: no cover
69		# Print a new line because we probably come from wait
70		output("")
71		logSys.warning("Caught signal %d. Exiting" % signum)
72		exit(255)
73
74	def __ping(self, timeout=0.1):
75		return self.__processCmd([["ping"] + ([timeout] if timeout != -1 else [])],
76			False, timeout=timeout)
77
78	@property
79	def beautifier(self):
80		if self._beautifier:
81			return self._beautifier
82		self._beautifier = Beautifier()
83		return self._beautifier
84
85	def __processCmd(self, cmd, showRet=True, timeout=-1):
86		client = None
87		try:
88			beautifier = self.beautifier
89			streamRet = True
90			for c in cmd:
91				beautifier.setInputCmd(c)
92				try:
93					if not client:
94						client = CSocket(self._conf["socket"], timeout=timeout)
95					elif timeout != -1:
96						client.settimeout(timeout)
97					if self._conf["verbose"] > 2:
98						logSys.log(5, "CMD: %r", c)
99					ret = client.send(c)
100					if ret[0] == 0:
101						logSys.log(5, "OK : %r", ret[1])
102						if showRet or c[0] in ('echo', 'server-status'):
103							output(beautifier.beautify(ret[1]))
104					else:
105						logSys.error("NOK: %r", ret[1].args)
106						if showRet:
107							output(beautifier.beautifyError(ret[1]))
108						streamRet = False
109				except socket.error as e:
110					if showRet or self._conf["verbose"] > 1:
111						if showRet or c[0] != "ping":
112							self.__logSocketError(e, c[0] == "ping")
113						else:
114							logSys.log(5, " -- %s failed -- %r", c, e)
115					return False
116				except Exception as e: # pragma: no cover
117					if showRet or self._conf["verbose"] > 1:
118						if self._conf["verbose"] > 1:
119							logSys.exception(e)
120						else:
121							logSys.error(e)
122					return False
123		finally:
124			# prevent errors by close during shutdown (on exit command):
125			if client:
126				try :
127					client.close()
128				except Exception as e: # pragma: no cover
129					if showRet or self._conf["verbose"] > 1:
130						logSys.debug(e)
131			if showRet or c[0] in ('echo', 'server-status'):
132				sys.stdout.flush()
133		return streamRet
134
135	def __logSocketError(self, prevError="", errorOnly=False):
136		try:
137			if os.access(self._conf["socket"], os.F_OK): # pragma: no cover
138				# This doesn't check if path is a socket,
139				#  but socket.error should be raised
140				if os.access(self._conf["socket"], os.W_OK):
141					# Permissions look good, but socket.error was raised
142					if errorOnly:
143						logSys.error(prevError)
144					else:
145						logSys.error("%sUnable to contact server. Is it running?",
146							("[%s] " % prevError) if prevError else '')
147				else:
148					logSys.error("Permission denied to socket: %s,"
149								 " (you must be root)", self._conf["socket"])
150			else:
151				logSys.error("Failed to access socket path: %s."
152							 " Is fail2ban running?",
153							 self._conf["socket"])
154		except Exception as e: # pragma: no cover
155			logSys.error("Exception while checking socket access: %s",
156						 self._conf["socket"])
157			logSys.error(e)
158
159	##
160	def __prepareStartServer(self):
161		if self.__ping():
162			logSys.error("Server already running")
163			return None
164
165		# Read the config
166		ret, stream = self.readConfig()
167		# Do not continue if configuration is not 100% valid
168		if not ret:
169			return None
170
171		# Check already running
172		if not self._conf["force"] and os.path.exists(self._conf["socket"]):
173			logSys.error("Fail2ban seems to be in unexpected state (not running but the socket exists)")
174			return None
175
176		return [["server-stream", stream], ['server-status']]
177
178	##
179	def __startServer(self, background=True):
180		from .fail2banserver import Fail2banServer
181		stream = self.__prepareStartServer()
182		self._alive = True
183		if not stream:
184			return False
185		# Start the server or just initialize started one:
186		try:
187			if background:
188				# Start server daemon as fork of client process (or new process):
189				Fail2banServer.startServerAsync(self._conf)
190				# Send config stream to server:
191				if not self.__processStartStreamAfterWait(stream, False):
192					return False
193			else:
194				# In foreground mode we should make server/client communication in different threads:
195				th = Thread(target=Fail2banClient.__processStartStreamAfterWait, args=(self, stream, False))
196				th.daemon = True
197				th.start()
198				# Mark current (main) thread as daemon:
199				self.setDaemon(True)
200				# Start server direct here in main thread (not fork):
201				self._server = Fail2banServer.startServerDirect(self._conf, False)
202
203		except ExitException: # pragma: no cover
204			pass
205		except Exception as e: # pragma: no cover
206			output("")
207			logSys.error("Exception while starting server " + ("background" if background else "foreground"))
208			if self._conf["verbose"] > 1:
209				logSys.exception(e)
210			else:
211				logSys.error(e)
212			return False
213
214		return True
215
216	##
217	def configureServer(self, nonsync=True, phase=None):
218		# if asynchronous start this operation in the new thread:
219		if nonsync:
220			th = Thread(target=Fail2banClient.configureServer, args=(self, False, phase))
221			th.daemon = True
222			return th.start()
223		# prepare: read config, check configuration is valid, etc.:
224		if phase is not None:
225			phase['start'] = True
226			logSys.log(5, '  client phase %s', phase)
227		stream = self.__prepareStartServer()
228		if phase is not None:
229			phase['ready'] = phase['start'] = (True if stream else False)
230			logSys.log(5, '  client phase %s', phase)
231		if not stream:
232			return False
233		# wait a litle bit for phase "start-ready" before enter active waiting:
234		if phase is not None:
235			Utils.wait_for(lambda: phase.get('start-ready', None) is not None, 0.5, 0.001)
236			phase['configure'] = (True if stream else False)
237			logSys.log(5, '  client phase %s', phase)
238		# configure server with config stream:
239		ret = self.__processStartStreamAfterWait(stream, False)
240		if phase is not None:
241			phase['done'] = ret
242		return ret
243
244	##
245	# Process a command line.
246	#
247	# Process one command line and exit.
248	# @param cmd the command line
249
250	def __processCommand(self, cmd):
251		# wrap tuple to list (because could be modified here):
252		if not isinstance(cmd, list):
253			cmd = list(cmd)
254		# process:
255		if len(cmd) == 1 and cmd[0] == "start":
256
257			ret = self.__startServer(self._conf["background"])
258			if not ret:
259				return False
260			return ret
261
262		elif len(cmd) >= 1 and cmd[0] == "restart":
263			# if restart jail - re-operate via "reload --restart ...":
264			if len(cmd) > 1:
265				cmd[0:1] = ["reload", "--restart"]
266				return self.__processCommand(cmd)
267			# restart server:
268			if self._conf.get("interactive", False):
269				output('  ## stop ... ')
270			self.__processCommand(['stop'])
271			if not self.__waitOnServer(False): # pragma: no cover
272				logSys.error("Could not stop server")
273				return False
274			# in interactive mode reset config, to make full-reload if there something changed:
275			if self._conf.get("interactive", False):
276				output('  ## load configuration ... ')
277				self.resetConf()
278				ret = self.initCmdLine(self._argv)
279				if ret is not None:
280					return ret
281			if self._conf.get("interactive", False):
282				output('  ## start ... ')
283			return self.__processCommand(['start'])
284
285		elif len(cmd) >= 1 and cmd[0] == "reload":
286			# reload options:
287			opts = []
288			while len(cmd) >= 2:
289				if cmd[1] in ('--restart', "--unban", "--if-exists"):
290					opts.append(cmd[1])
291					del cmd[1]
292				else:
293					if len(cmd) > 2:
294						logSys.error("Unexpected argument(s) for reload: %r", cmd[1:])
295						return False
296					# stop options - jail name or --all
297					break
298			if self.__ping(timeout=-1):
299				if len(cmd) == 1 or cmd[1] == '--all':
300					jail = '--all'
301					ret, stream = self.readConfig()
302				else:
303					jail = cmd[1]
304					ret, stream = self.readConfig(jail)
305				# Do not continue if configuration is not 100% valid
306				if not ret:
307					return False
308				if self._conf.get("interactive", False):
309					output('  ## reload ... ')
310				# Reconfigure the server
311				return self.__processCmd([['reload', jail, opts, stream]], True)
312			else:
313				logSys.error("Could not find server")
314				return False
315
316		elif len(cmd) > 1 and cmd[0] == "ping":
317			return self.__processCmd([cmd], timeout=float(cmd[1]))
318
319		else:
320			return self.__processCmd([cmd])
321
322
323	def __processStartStreamAfterWait(self, *args):
324		try:
325			# Wait for the server to start
326			if not self.__waitOnServer(): # pragma: no cover
327				logSys.error("Could not find server, waiting failed")
328				return False
329				# Configure the server
330			self.__processCmd(*args)
331		except ServerExecutionException as e: # pragma: no cover
332			if self._conf["verbose"] > 1:
333				logSys.exception(e)
334			logSys.error("Could not start server. Maybe an old "
335						 "socket file is still present. Try to "
336						 "remove " + self._conf["socket"] + ". If "
337						 "you used fail2ban-client to start the "
338						 "server, adding the -x option will do it")
339			if self._server:
340				self._server.quit()
341			return False
342		return True
343
344	def __waitOnServer(self, alive=True, maxtime=None):
345		if maxtime is None:
346			maxtime = self._conf["timeout"]
347		# Wait for the server to start (the server has 30 seconds to answer ping)
348		starttime = time.time()
349		logSys.log(5, "__waitOnServer: %r", (alive, maxtime))
350		sltime = 0.0125 / 2
351		test = lambda: os.path.exists(self._conf["socket"]) and self.__ping(timeout=sltime)
352		with VisualWait(self._conf["verbose"]) as vis:
353			while self._alive:
354				runf = test()
355				if runf == alive:
356					return True
357				waittime = time.time() - starttime
358				logSys.log(5, "  wait-time: %s", waittime)
359				# Wonderful visual :)
360				if waittime > 1:
361					vis.heartbeat()
362				# f end time reached:
363				if waittime >= maxtime:
364					raise ServerExecutionException("Failed to start server")
365				# first 200ms faster:
366				sltime = min(sltime * 2, 0.5 if waittime > 0.2 else 0.1)
367				time.sleep(sltime)
368		return False
369
370	def start(self, argv):
371		# Install signal handlers
372		_prev_signals = {}
373		if _thread_name() == '_MainThread':
374			for s in (signal.SIGTERM, signal.SIGINT):
375				_prev_signals[s] = signal.getsignal(s)
376				signal.signal(s, self.__sigTERMhandler)
377		try:
378			# Command line options
379			if self._argv is None:
380				ret = self.initCmdLine(argv)
381				if ret is not None:
382					if ret:
383						return True
384					raise ServerExecutionException("Init of command line failed")
385
386			# Commands
387			args = self._args
388
389			# Interactive mode
390			if self._conf.get("interactive", False):
391				try:
392					import readline
393				except ImportError:
394					raise ServerExecutionException("Readline not available")
395				try:
396					ret = True
397					if len(args) > 0:
398						ret = self.__processCommand(args)
399					if ret:
400						readline.parse_and_bind("tab: complete")
401						self.dispInteractive()
402						while True:
403							cmd = input_command()
404							if cmd == "exit" or cmd == "quit":
405								# Exit
406								return True
407							if cmd == "help":
408								self.dispUsage()
409							elif not cmd == "":
410								try:
411									self.__processCommand(shlex.split(cmd))
412								except Exception as e: # pragma: no cover
413									if self._conf["verbose"] > 1:
414										logSys.exception(e)
415									else:
416										logSys.error(e)
417				except (EOFError, KeyboardInterrupt): # pragma: no cover
418					output("")
419					raise
420			# Single command mode
421			else:
422				if len(args) < 1:
423					self.dispUsage()
424					return False
425				return self.__processCommand(args)
426		except Exception as e:
427			if self._conf["verbose"] > 1:
428				logSys.exception(e)
429			else:
430				logSys.error(e)
431			return False
432		finally:
433			self._alive = False
434			for s, sh in _prev_signals.items():
435				signal.signal(s, sh)
436
437
438class _VisualWait:
439	"""Small progress indication (as "wonderful visual") during waiting process
440	"""
441	pos = 0
442	delta = 1
443	def __init__(self, maxpos=10):
444		self.maxpos = maxpos
445	def __enter__(self):
446		return self
447	def __exit__(self, *args):
448		if self.pos:
449			sys.stdout.write('\r'+(' '*(35+self.maxpos))+'\r')
450			sys.stdout.flush()
451	def heartbeat(self):
452		"""Show or step for progress indicator
453		"""
454		if not self.pos:
455			sys.stdout.write("\nINFO   [#" + (' '*self.maxpos) + "] Waiting on the server...\r\x1b[8C")
456		self.pos += self.delta
457		if self.delta > 0:
458			s = " #\x1b[1D" if self.pos > 1 else "# \x1b[2D"
459		else:
460			s = "\x1b[1D# \x1b[2D"
461		sys.stdout.write(s)
462		sys.stdout.flush()
463		if self.pos > self.maxpos:
464			self.delta = -1
465		elif self.pos < 2:
466			self.delta = 1
467class _NotVisualWait:
468	"""Mockup for invisible progress indication (not verbose)
469	"""
470	def __enter__(self):
471		return self
472	def __exit__(self, *args):
473		pass
474	def heartbeat(self):
475		pass
476
477def VisualWait(verbose, *args, **kwargs):
478	"""Wonderful visual progress indication (if verbose)
479	"""
480	return _VisualWait(*args, **kwargs) if verbose > 1 else _NotVisualWait()
481
482
483def exec_command_line(argv):
484	client = Fail2banClient()
485	# Exit with correct return value
486	if client.start(argv):
487		exit(0)
488	else:
489		exit(255)
490
491