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 sys
25
26from .fail2bancmdline import Fail2banCmdLine, ServerExecutionException, \
27	logSys, PRODUCTION, exit
28
29SERVER = "fail2ban-server"
30
31##
32# \mainpage Fail2Ban
33#
34# \section Introduction
35#
36class Fail2banServer(Fail2banCmdLine):
37
38	# def __init__(self):
39	# 	Fail2banCmdLine.__init__(self)
40
41	##
42	# Start Fail2Ban server in main thread without fork (direct, it can fork itself in Server if daemon=True).
43	#
44	# Start the Fail2ban server in background/foreground (daemon mode or not).
45
46	@staticmethod
47	def startServerDirect(conf, daemon=True):
48		logSys.debug("  direct starting of server in %s, deamon: %s", os.getpid(), daemon)
49		from ..server.server import Server
50		server = None
51		try:
52			# Start it in foreground (current thread, not new process),
53			# server object will internally fork self if daemon is True
54			server = Server(daemon)
55			server.start(conf["socket"],
56							conf["pidfile"], conf["force"],
57							conf=conf)
58		except Exception as e: # pragma: no cover
59			try:
60				if server:
61					server.quit()
62			except Exception as e2:
63				if conf["verbose"] > 1:
64					logSys.exception(e2)
65			raise
66
67		return server
68
69	##
70	# Start Fail2Ban server.
71	#
72	# Start the Fail2ban server in daemon mode (background, start from client).
73
74	@staticmethod
75	def startServerAsync(conf):
76		# Forks the current process, don't fork if async specified (ex: test cases)
77		pid = 0
78		frk = not conf["async"] and PRODUCTION
79		if frk: # pragma: no cover
80			pid = os.fork()
81		logSys.debug("  async starting of server in %s, fork: %s - %s", os.getpid(), frk, pid)
82		if pid == 0:
83			args = list()
84			args.append(SERVER)
85			# Start async (don't read config) and in background as requested.
86			args.append("--async")
87			args.append("-b")
88			# Set the socket path.
89			args.append("-s")
90			args.append(conf["socket"])
91			# Set the pidfile
92			args.append("-p")
93			args.append(conf["pidfile"])
94			# Force the execution if needed.
95			if conf["force"]:
96				args.append("-x")
97			if conf["verbose"] > 1:
98				args.append("-" + "v"*(conf["verbose"]-1))
99			# Logging parameters:
100			for o in ('loglevel', 'logtarget', 'syslogsocket'):
101				args.append("--"+o)
102				args.append(conf[o])
103			try:
104				# Directory of client (to try the first start from current or the same directory as client, and from relative bin):
105				exe = Fail2banServer.getServerPath()
106				if not frk:
107					# Wrapr args to use the same python version in client/server (important for multi-python systems):
108					args[0] = exe
109					exe = sys.executable
110					args[0:0] = [exe]
111				logSys.debug("Starting %r with args %r", exe, args)
112				if frk: # pragma: no cover
113					os.execv(exe, args)
114				else:
115					# use P_WAIT instead of P_NOWAIT (to prevent defunct-zomby process), it startet as daemon, so parent exit fast after fork):
116					ret = os.spawnv(os.P_WAIT, exe, args)
117					if ret != 0: # pragma: no cover
118						raise OSError(ret, "Unknown error by executing server %r with %r" % (args[1], exe))
119			except OSError as e: # pragma: no cover
120				if not frk: #not PRODUCTION:
121					raise
122				# Use the PATH env.
123				logSys.warning("Initial start attempt failed (%s). Starting %r with the same args", e, SERVER)
124				if frk: # pragma: no cover
125					os.execvp(SERVER, args)
126
127	@staticmethod
128	def getServerPath():
129		startdir = sys.path[0]
130		exe = os.path.abspath(os.path.join(startdir, SERVER))
131		if not os.path.isfile(exe): # may be unresolved in test-cases, so get relative starter (client):
132			startdir = os.path.dirname(sys.argv[0])
133			exe = os.path.abspath(os.path.join(startdir, SERVER))
134			if not os.path.isfile(exe): # may be unresolved in test-cases, so try to get relative bin-directory:
135				startdir = os.path.dirname(os.path.abspath(__file__))
136				startdir = os.path.join(os.path.dirname(os.path.dirname(startdir)), "bin")
137				exe = os.path.abspath(os.path.join(startdir, SERVER))
138		return exe
139
140	def _Fail2banClient(self):
141		from .fail2banclient import Fail2banClient
142		cli = Fail2banClient()
143		cli.applyMembers(self)
144		return cli
145
146	def start(self, argv):
147		server = None
148		try:
149			# Command line options
150			ret = self.initCmdLine(argv)
151			if ret is not None:
152				return ret
153
154			# Commands
155			args = self._args
156
157			cli = None
158			# Just start:
159			if len(args) == 1 and args[0] == 'start' and not self._conf.get("interactive", False):
160				pass
161			else:
162				# If client mode - whole processing over client:
163				if len(args) or self._conf.get("interactive", False):
164					cli = self._Fail2banClient()
165					return cli.start(argv)
166
167			# Start the server, corresponding options:
168			#   background = True, if should be new process running in background, otherwise start in
169			#     foreground process will be forked in daemonize, inside of Server module.
170			#   nonsync = True, normally internal call only, if started from client, so configures
171			#     the server via asynchronous thread.
172			background = self._conf["background"]
173			nonsync = self._conf.get("async", False)
174
175			# If was started not from the client:
176			if not nonsync:
177				# Load requirements on demand (we need utils only when asynchronous handling):
178				from ..server.utils import Utils
179				# Start new thread with client to read configuration and
180				# transfer it to the server:
181				cli = self._Fail2banClient()
182				phase = dict()
183				logSys.debug('Configure via async client thread')
184				cli.configureServer(phase=phase)
185				# wait, do not continue if configuration is not 100% valid:
186				Utils.wait_for(lambda: phase.get('ready', None) is not None, self._conf["timeout"], 0.001)
187				logSys.log(5, '  server phase %s', phase)
188				if not phase.get('start', False):
189					raise ServerExecutionException('Async configuration of server failed')
190				# event for server ready flag:
191				def _server_ready():
192					phase['start-ready'] = True
193					logSys.log(5, '  server phase %s', phase)
194				# notify waiting thread if server really ready
195				self._conf['onstart'] = _server_ready
196
197			# Start server, daemonize it, etc.
198			pid = os.getpid()
199			server = Fail2banServer.startServerDirect(self._conf, background)
200			# notify waiting thread server ready resp. done (background execution, error case, etc):
201			if not nonsync:
202				_server_ready()
203			# If forked - just exit other processes
204			if pid != os.getpid(): # pragma: no cover
205				os._exit(0)
206			if cli:
207				cli._server = server
208
209			# wait for client answer "done":
210			if not nonsync and cli:
211				Utils.wait_for(lambda: phase.get('done', None) is not None, self._conf["timeout"], 0.001)
212				if not phase.get('done', False):
213					if server: # pragma: no cover
214						server.quit()
215					exit(255)
216				if background:
217					logSys.debug('Starting server done')
218
219		except Exception as e:
220			if self._conf["verbose"] > 1:
221				logSys.exception(e)
222			else:
223				logSys.error(e)
224			if server: # pragma: no cover
225				server.quit()
226			exit(255)
227
228		return True
229
230	@staticmethod
231	def exit(code=0): # pragma: no cover
232		if code != 0:
233			logSys.error("Could not start %s", SERVER)
234		exit(code)
235
236def exec_command_line(argv):
237	server = Fail2banServer()
238	if server.start(argv):
239		exit(0)
240	else:
241		exit(255)
242