From f7da091437b991fd860d1dac90af9fe111633c20 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Mon, 9 Jun 2014 22:27:51 +0100 Subject: [PATCH] ENH: Log unhandled exceptions to Fail2Ban log --- ChangeLog | 1 + fail2ban/helpers.py | 7 +++++++ fail2ban/server/jailthread.py | 13 +++++++++++++ fail2ban/server/server.py | 5 ++++- fail2ban/tests/servertestcase.py | 13 ++++++++++++- 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index ff70f07cc..a77438015 100644 --- a/ChangeLog +++ b/ChangeLog @@ -40,6 +40,7 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger * Match non "Bye Bye" disconnect messages for sshd locked account regex * Realign fail2ban log output with white space to improve readability. Does not affect SYSLOG output. + * Log unhandled exceptions ver. 0.9.0 (2014/03/14) - beta ---------- diff --git a/fail2ban/helpers.py b/fail2ban/helpers.py index c3e50336b..35c7ebc39 100644 --- a/fail2ban/helpers.py +++ b/fail2ban/helpers.py @@ -112,3 +112,10 @@ def getF2BLogger(name): """Get logging.Logger instance with Fail2Ban logger name convention """ return logging.getLogger("fail2ban.%s" % name.rpartition(".")[-1]) + +def fail2ban_excepthook(exctype, value, traceback): + """Except hook used to log unhandled exceptions to Fail2Ban log + """ + logging.getLogger("fail2ban").critical( + "Unhandled exception in Fail2Ban:", exc_info=True) + return sys.__excepthook__(exctype, value, traceback) diff --git a/fail2ban/server/jailthread.py b/fail2ban/server/jailthread.py index 7dcdb2d7e..4f640f8b4 100644 --- a/fail2ban/server/jailthread.py +++ b/fail2ban/server/jailthread.py @@ -24,9 +24,12 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" +import sys from threading import Thread from abc import abstractproperty, abstractmethod +from ..helpers import fail2ban_excepthook + class JailThread(Thread): """Abstract class for threading elements in Fail2Ban. @@ -53,6 +56,16 @@ class JailThread(Thread): ## The time the thread sleeps in the loop. self.sleeptime = 1 + # excepthook workaround for threads, derived from: + # http://bugs.python.org/issue1230540#msg91244 + run = self.run + def run_with_except_hook(*args, **kwargs): + try: + run(*args, **kwargs) + except: + fail2ban_excepthook(*sys.exc_info()) + self.run = run_with_except_hook + @abstractproperty def status(self): # pragma: no cover - abstract """Abstract - Should provide status information. diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 2f227517c..a6be18ac6 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -32,7 +32,7 @@ from .filter import FileFilter, JournalFilter from .transmitter import Transmitter from .asyncserver import AsyncServer, AsyncServerException from .. import version -from ..helpers import getF2BLogger +from ..helpers import getF2BLogger, fail2ban_excepthook # Gets the instance of the logger. logSys = getF2BLogger(__name__) @@ -70,6 +70,9 @@ class Server: signal.signal(signal.SIGTERM, self.__sigTERMhandler) signal.signal(signal.SIGINT, self.__sigTERMhandler) + # Ensure unhandled exceptions are logged + sys.excepthook = fail2ban_excepthook + # First set the mask to only allow access to owner os.umask(0077) if self.__daemon: # pragma: no cover diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 1dfe13ca3..6ce6e54fd 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -35,6 +35,8 @@ import logging from ..server.failregex import Regex, FailRegex, RegexException from ..server.server import Server from ..server.jail import Jail +from ..server.jailthread import JailThread +from .utils import LogCaptureTestCase from ..helpers import getF2BLogger try: @@ -797,10 +799,19 @@ class RegexTests(unittest.TestCase): self.assertTrue(fr.hasMatched()) self.assertRaises(RegexException, fr.getHost) -class LoggingTests(unittest.TestCase): +class _BadThread(JailThread): + def run(self): + int("cat") + +class LoggingTests(LogCaptureTestCase): def testGetF2BLogger(self): testLogSys = getF2BLogger("fail2ban.some.string.with.name") self.assertEqual(testLogSys.parent.name, "fail2ban") self.assertEqual(testLogSys.name, "fail2ban.name") + def testFail2BanExceptHook(self): + badThread = _BadThread() + badThread.start() + badThread.join() + self.assertTrue(self._is_logged("Unhandled exception"))