summaryrefslogtreecommitdiff
path: root/fail2ban/client/fail2banserver.py
blob: eee78d5f6e7cfd33ca72d3458727981a6f538016 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# 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__ = "Fail2Ban Developers"
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
__license__ = "GPL"

import os
import sys

from .fail2bancmdline import Fail2banCmdLine, ServerExecutionException, \
	logSys, PRODUCTION, exit

SERVER = "fail2ban-server"

##
# \mainpage Fail2Ban
#
# \section Introduction
#
class Fail2banServer(Fail2banCmdLine):

	# def __init__(self):
	# 	Fail2banCmdLine.__init__(self)

	##
	# Start Fail2Ban server in main thread without fork (direct, it can fork itself in Server if daemon=True).
	#
	# Start the Fail2ban server in background/foreground (daemon mode or not).

	@staticmethod
	def startServerDirect(conf, daemon=True, setServer=None):
		logSys.debug("  direct starting of server in %s, deamon: %s", os.getpid(), daemon)
		from ..server.server import Server
		server = None
		try:
			# Start it in foreground (current thread, not new process),
			# server object will internally fork self if daemon is True
			server = Server(daemon)
			# notify caller - set server handle:
			if setServer:
				setServer(server)
			# run:
			server.start(conf["socket"],
							conf["pidfile"], conf["force"],
							conf=conf)
		except Exception as e: # pragma: no cover
			try:
				if server:
					server.quit()
			except Exception as e2:
				if conf["verbose"] > 1:
					logSys.exception(e2)
			raise
		finally:
			# notify waiting thread server ready resp. done (background execution, error case, etc):
			if conf.get('onstart'):
				conf['onstart']()

		return server

	##
	# Start Fail2Ban server.
	#
	# Start the Fail2ban server in daemon mode (background, start from client).

	@staticmethod
	def startServerAsync(conf):
		# Forks the current process, don't fork if async specified (ex: test cases)
		pid = 0
		frk = not conf["async"] and PRODUCTION
		if frk: # pragma: no cover
			pid = os.fork()
		logSys.debug("  async starting of server in %s, fork: %s - %s", os.getpid(), frk, pid)
		if pid == 0:
			args = list()
			args.append(SERVER)
			# Start async (don't read config) and in background as requested.
			args.append("--async")
			args.append("-b")
			# Set the socket path.
			args.append("-s")
			args.append(conf["socket"])
			# Set the pidfile
			args.append("-p")
			args.append(conf["pidfile"])
			# Force the execution if needed.
			if conf["force"]:
				args.append("-x")
			if conf["verbose"] > 1:
				args.append("-" + "v"*(conf["verbose"]-1))
			# Logging parameters:
			for o in ('loglevel', 'logtarget', 'syslogsocket'):
				args.append("--"+o)
				args.append(conf[o])
			try:
				# Directory of client (to try the first start from current or the same directory as client, and from relative bin):
				exe = Fail2banServer.getServerPath()
				if not frk:
					# Wrapr args to use the same python version in client/server (important for multi-python systems):
					args[0] = exe
					exe = sys.executable
					args[0:0] = [exe]
				logSys.debug("Starting %r with args %r", exe, args)
				if frk: # pragma: no cover
					os.execv(exe, args)
				else:
					# use P_WAIT instead of P_NOWAIT (to prevent defunct-zomby process), it startet as daemon, so parent exit fast after fork):
					ret = os.spawnv(os.P_WAIT, exe, args)
					if ret != 0: # pragma: no cover
						raise OSError(ret, "Unknown error by executing server %r with %r" % (args[1], exe))
			except OSError as e: # pragma: no cover
				if not frk: #not PRODUCTION:
					raise
				# Use the PATH env.
				logSys.warning("Initial start attempt failed (%s). Starting %r with the same args", e, SERVER)
				if frk: # pragma: no cover
					os.execvp(SERVER, args)

	@staticmethod
	def getServerPath():
		startdir = sys.path[0]
		exe = os.path.abspath(os.path.join(startdir, SERVER))
		if not os.path.isfile(exe): # may be unresolved in test-cases, so get relative starter (client):
			startdir = os.path.dirname(sys.argv[0])
			exe = os.path.abspath(os.path.join(startdir, SERVER))
			if not os.path.isfile(exe): # may be unresolved in test-cases, so try to get relative bin-directory:
				startdir = os.path.dirname(os.path.abspath(__file__))
				startdir = os.path.join(os.path.dirname(os.path.dirname(startdir)), "bin")
				exe = os.path.abspath(os.path.join(startdir, SERVER))
		return exe

	def _Fail2banClient(self):
		from .fail2banclient import Fail2banClient
		cli = Fail2banClient()
		cli.applyMembers(self)
		return cli

	def start(self, argv):
		server = None
		try:
			# Command line options
			ret = self.initCmdLine(argv)
			if ret is not None:
				return ret

			# Commands
			args = self._args

			cli = None
			# Just start:
			if len(args) == 1 and args[0] == 'start' and not self._conf.get("interactive", False):
				pass
			else:
				# If client mode - whole processing over client:
				if len(args) or self._conf.get("interactive", False):
					cli = self._Fail2banClient()
					return cli.start(argv)

			# Start the server, corresponding options:
			#   background = True, if should be new process running in background, otherwise start in
			#     foreground process will be forked in daemonize, inside of Server module.
			#   nonsync = True, normally internal call only, if started from client, so configures
			#     the server via asynchronous thread.
			background = self._conf["background"]
			nonsync = self._conf.get("async", False)

			# If was started not from the client:
			if not nonsync:
				# Load requirements on demand (we need utils only when asynchronous handling):
				from ..server.utils import Utils
				# Start new thread with client to read configuration and
				# transfer it to the server:
				cli = self._Fail2banClient()
				cli._conf = self._conf
				phase = dict()
				logSys.debug('Configure via async client thread')
				cli.configureServer(phase=phase)

			# Start server, daemonize it, etc.
			pid = os.getpid()
			server = Fail2banServer.startServerDirect(self._conf, background,
				 cli._set_server if cli else None)
			# If forked - just exit other processes
			if pid != os.getpid(): # pragma: no cover
				os._exit(0)
			if cli:
				cli._server = server

			# wait for client answer "done":
			if not nonsync and cli:
				Utils.wait_for(lambda: phase.get('done', None) is not None, self._conf["timeout"], 0.001)
				if not phase.get('done', False):
					if server: # pragma: no cover
						server.quit()
					exit(255)
				if background:
					logSys.debug('Starting server done')

		except Exception as e:
			if self._conf["verbose"] > 1:
				logSys.exception(e)
			else:
				logSys.error(e)
			if server: # pragma: no cover
				server.quit()
			exit(255)

		return True

	@staticmethod
	def exit(code=0): # pragma: no cover
		if code != 0:
			logSys.error("Could not start %s", SERVER)
		exit(code)

def exec_command_line(argv):
	server = Fail2banServer()
	if server.start(argv):
		exit(0)
	else:
		exit(255)