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