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