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 getopt 24import logging 25import os 26import sys 27 28from ..version import version, normVersion 29from ..protocol import printFormatted 30from ..helpers import getLogger, str2LogLevel, getVerbosityFormat, BrokenPipeError 31 32# Gets the instance of the logger. 33logSys = getLogger("fail2ban") 34 35def output(s): # pragma: no cover 36 try: 37 print(s) 38 except (BrokenPipeError, IOError) as e: # pragma: no cover 39 if e.errno != 32: # closed / broken pipe 40 raise 41 42# Config parameters required to start fail2ban which can be also set via command line (overwrite fail2ban.conf), 43CONFIG_PARAMS = ("socket", "pidfile", "logtarget", "loglevel", "syslogsocket") 44# Used to signal - we are in test cases (ex: prevents change logging params, log capturing, etc) 45PRODUCTION = True 46 47MAX_WAITTIME = 30 48 49 50class Fail2banCmdLine(): 51 52 def __init__(self): 53 self._argv = self._args = None 54 self._configurator = None 55 self.cleanConfOnly = False 56 self.resetConf() 57 58 def resetConf(self): 59 self._conf = { 60 "async": False, 61 "conf": "/usr/local/etc/fail2ban", 62 "force": False, 63 "background": True, 64 "verbose": 1, 65 "socket": None, 66 "pidfile": None, 67 "timeout": MAX_WAITTIME 68 } 69 70 @property 71 def configurator(self): 72 if self._configurator: 73 return self._configurator 74 # New configurator 75 from .configurator import Configurator 76 self._configurator = Configurator() 77 # Set the configuration path 78 self._configurator.setBaseDir(self._conf["conf"]) 79 return self._configurator 80 81 82 def applyMembers(self, obj): 83 for o in obj.__dict__: 84 self.__dict__[o] = obj.__dict__[o] 85 86 def dispVersion(self, short=False): 87 if not short: 88 output("Fail2Ban v" + version) 89 else: 90 output(normVersion()) 91 92 def dispUsage(self): 93 """ Prints Fail2Ban command line options and exits 94 """ 95 caller = os.path.basename(self._argv[0]) 96 output("Usage: "+caller+" [OPTIONS]" + (" <COMMAND>" if not caller.endswith('server') else "")) 97 output("") 98 output("Fail2Ban v" + version + " reads log file that contains password failure report") 99 output("and bans the corresponding IP addresses using firewall rules.") 100 output("") 101 output("Options:") 102 output(" -c, --conf <DIR> configuration directory") 103 output(" -s, --socket <FILE> socket path") 104 output(" -p, --pidfile <FILE> pidfile path") 105 output(" --pname <NAME> name of the process (main thread) to identify instance (default fail2ban-server)") 106 output(" --loglevel <LEVEL> logging level") 107 output(" --logtarget <TARGET> logging target, use file-name or stdout, stderr, syslog or sysout.") 108 output(" --syslogsocket auto|<FILE>") 109 output(" -d dump configuration. For debugging") 110 output(" --dp, --dump-pretty dump the configuration using more human readable representation") 111 output(" -t, --test test configuration (can be also specified with start parameters)") 112 output(" -i interactive mode") 113 output(" -v increase verbosity") 114 output(" -q decrease verbosity") 115 output(" -x force execution of the server (remove socket file)") 116 output(" -b start server in background (default)") 117 output(" -f start server in foreground") 118 output(" --async start server in async mode (for internal usage only, don't read configuration)") 119 output(" --timeout timeout to wait for the server (for internal usage only, don't read configuration)") 120 output(" --str2sec <STRING> convert time abbreviation format to seconds") 121 output(" -h, --help display this help message") 122 output(" -V, --version print the version (-V returns machine-readable short format)") 123 124 if not caller.endswith('server'): 125 output("") 126 output("Command:") 127 # Prints the protocol 128 printFormatted() 129 130 output("") 131 output("Report bugs to https://github.com/fail2ban/fail2ban/issues") 132 133 def __getCmdLineOptions(self, optList): 134 """ Gets the command line options 135 """ 136 for opt in optList: 137 o = opt[0] 138 if o in ("-c", "--conf"): 139 self._conf["conf"] = opt[1] 140 elif o in ("-s", "--socket"): 141 self._conf["socket"] = opt[1] 142 elif o in ("-p", "--pidfile"): 143 self._conf["pidfile"] = opt[1] 144 elif o in ("-d", "--dp", "--dump-pretty"): 145 self._conf["dump"] = True if o == "-d" else 2 146 elif o in ("-t", "--test"): 147 self.cleanConfOnly = True 148 self._conf["test"] = True 149 elif o == "-v": 150 self._conf["verbose"] += 1 151 elif o == "-q": 152 self._conf["verbose"] -= 1 153 elif o == "-x": 154 self._conf["force"] = True 155 elif o == "-i": 156 self._conf["interactive"] = True 157 elif o == "-b": 158 self._conf["background"] = True 159 elif o == "-f": 160 self._conf["background"] = False 161 elif o == "--async": 162 self._conf["async"] = True 163 elif o == "--timeout": 164 from ..server.mytime import MyTime 165 self._conf["timeout"] = MyTime.str2seconds(opt[1]) 166 elif o == "--str2sec": 167 from ..server.mytime import MyTime 168 output(MyTime.str2seconds(opt[1])) 169 return True 170 elif o in ("-h", "--help"): 171 self.dispUsage() 172 return True 173 elif o in ("-V", "--version"): 174 self.dispVersion(o == "-V") 175 return True 176 elif o.startswith("--"): # other long named params (see also resetConf) 177 self._conf[ o[2:] ] = opt[1] 178 return None 179 180 def initCmdLine(self, argv): 181 verbose = 1 182 try: 183 # First time? 184 initial = (self._argv is None) 185 186 # Command line options 187 self._argv = argv 188 logSys.info("Using start params %s", argv[1:]) 189 190 # Reads the command line options. 191 try: 192 cmdOpts = 'hc:s:p:xfbdtviqV' 193 cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'test', 'async', 194 'conf=', 'pidfile=', 'pname=', 'socket=', 195 'timeout=', 'str2sec=', 'help', 'version', 'dp', '--dump-pretty'] 196 optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts) 197 except getopt.GetoptError: 198 self.dispUsage() 199 return False 200 201 ret = self.__getCmdLineOptions(optList) 202 if ret is not None: 203 return ret 204 205 logSys.debug(" conf: %r, args: %r", self._conf, self._args) 206 207 if initial and PRODUCTION: # pragma: no cover - can't test 208 verbose = self._conf["verbose"] 209 if verbose <= 0: 210 logSys.setLevel(logging.ERROR) 211 elif verbose == 1: 212 logSys.setLevel(logging.WARNING) 213 elif verbose == 2: 214 logSys.setLevel(logging.INFO) 215 elif verbose == 3: 216 logSys.setLevel(logging.DEBUG) 217 else: 218 logSys.setLevel(logging.HEAVYDEBUG) 219 # Add the default logging handler to dump to stderr 220 logout = logging.StreamHandler(sys.stderr) 221 222 # Custom log format for the verbose run (-1, because default verbosity here is 1): 223 fmt = getVerbosityFormat(verbose-1) 224 formatter = logging.Formatter(fmt) 225 # tell the handler to use this format 226 logout.setFormatter(formatter) 227 logSys.addHandler(logout) 228 229 # Set expected parameters (like socket, pidfile, etc) from configuration, 230 # if those not yet specified, in which read configuration only if needed here: 231 conf = None 232 for o in CONFIG_PARAMS: 233 if self._conf.get(o, None) is None: 234 if not conf: 235 self.configurator.readEarly() 236 conf = self.configurator.getEarlyOptions() 237 if o in conf: 238 self._conf[o] = conf[o] 239 240 logSys.info("Using socket file %s", self._conf["socket"]) 241 242 # Check log-level before start (or transmit to server), to prevent error in background: 243 llev = str2LogLevel(self._conf["loglevel"]) 244 logSys.info("Using pid file %s, [%s] logging to %s", 245 self._conf["pidfile"], logging.getLevelName(llev), self._conf["logtarget"]) 246 247 readcfg = True 248 if self._conf.get("dump", False): 249 if readcfg: 250 ret, stream = self.readConfig() 251 readcfg = False 252 if stream is not None: 253 self.dumpConfig(stream, self._conf["dump"] == 2) 254 else: # pragma: no cover 255 output("ERROR: The configuration stream failed because of the invalid syntax.") 256 if not self._conf.get("test", False): 257 return ret 258 259 if self._conf.get("test", False): 260 if readcfg: 261 readcfg = False 262 ret, stream = self.readConfig() 263 if not ret: 264 raise ServerExecutionException("ERROR: test configuration failed") 265 # exit after test if no commands specified (test only): 266 if not len(self._args): 267 output("OK: configuration test is successful") 268 return ret 269 270 # Nothing to do here, process in client/server 271 return None 272 except ServerExecutionException: 273 raise 274 except Exception as e: 275 output("ERROR: %s" % (e,)) 276 if verbose > 2: 277 logSys.exception(e) 278 return False 279 280 def readConfig(self, jail=None): 281 # Read the configuration 282 # TODO: get away from stew of return codes and exception 283 # handling -- handle via exceptions 284 stream = None 285 try: 286 self.configurator.Reload() 287 self.configurator.readAll() 288 ret = self.configurator.getOptions(jail, self._conf, 289 ignoreWrong=not self.cleanConfOnly) 290 self.configurator.convertToProtocol( 291 allow_no_files=self._conf.get("dump", False)) 292 stream = self.configurator.getConfigStream() 293 except Exception as e: 294 logSys.error("Failed during configuration: %s" % e) 295 ret = False 296 return ret, stream 297 298 @staticmethod 299 def dumpConfig(cmd, pretty=False): 300 if pretty: 301 from pprint import pformat 302 def _output(s): 303 output(pformat(s, width=1000, indent=2)) 304 else: 305 _output = output 306 for c in cmd: 307 _output(c) 308 return True 309 310 # 311 # _exit is made to ease mocking out of the behaviour in tests, 312 # since method is also exposed in API via globally bound variable 313 @staticmethod 314 def _exit(code=0): 315 # implicit flush without to produce broken pipe error (32): 316 sys.stderr.close() 317 try: 318 sys.stdout.flush() 319 # exit: 320 if hasattr(sys, 'exit') and sys.exit: 321 sys.exit(code) 322 else: 323 os._exit(code) 324 except (BrokenPipeError, IOError) as e: # pragma: no cover 325 if e.errno != 32: # closed / broken pipe 326 raise 327 328 @staticmethod 329 def exit(code=0): 330 logSys.debug("Exit with code %s", code) 331 # because of possible buffered output in python, we should flush it before exit: 332 logging.shutdown() 333 # exit 334 Fail2banCmdLine._exit(code) 335 336 337# global exit handler: 338exit = Fail2banCmdLine.exit 339 340 341class ExitException(Exception): 342 pass 343 344 345class ServerExecutionException(Exception): 346 pass 347