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 20# Author: Cyril Jaquier 21# 22 23__author__ = "Cyril Jaquier" 24__copyright__ = "Copyright (c) 2004 Cyril Jaquier" 25__license__ = "GPL" 26 27import glob 28import json 29import os.path 30import re 31 32from .configreader import ConfigReaderUnshared, ConfigReader 33from .filterreader import FilterReader 34from .actionreader import ActionReader 35from ..version import version 36from ..helpers import getLogger, extractOptions, splitWithOptions, splitwords 37 38# Gets the instance of the logger. 39logSys = getLogger(__name__) 40 41 42class JailReader(ConfigReader): 43 44 def __init__(self, name, force_enable=False, **kwargs): 45 ConfigReader.__init__(self, **kwargs) 46 self.__name = name 47 self.__filter = None 48 self.__force_enable = force_enable 49 self.__actions = list() 50 self.__opts = None 51 52 @property 53 def options(self): 54 return self.__opts 55 56 def setName(self, value): 57 self.__name = value 58 59 def getName(self): 60 return self.__name 61 62 def read(self): 63 out = ConfigReader.read(self, "jail") 64 # Before returning -- verify that requested section 65 # exists at all 66 if not (self.__name in self.sections()): 67 raise ValueError("Jail %r was not found among available" 68 % self.__name) 69 return out 70 71 def isEnabled(self): 72 return self.__force_enable or ( 73 self.__opts and self.__opts.get("enabled", False)) 74 75 @staticmethod 76 def _glob(path): 77 """Given a path for glob return list of files to be passed to server. 78 79 Dangling symlinks are warned about and not returned 80 """ 81 pathList = [] 82 for p in glob.glob(path): 83 if os.path.exists(p): 84 pathList.append(p) 85 else: 86 logSys.warning("File %s is a dangling link, thus cannot be monitored" % p) 87 return pathList 88 89 _configOpts1st = { 90 "enabled": ["bool", False], 91 "backend": ["string", "auto"], 92 "filter": ["string", ""] 93 } 94 _configOpts = { 95 "enabled": ["bool", False], 96 "backend": ["string", "auto"], 97 "maxretry": ["int", None], 98 "maxmatches": ["int", None], 99 "findtime": ["string", None], 100 "bantime": ["string", None], 101 "bantime.increment": ["bool", None], 102 "bantime.factor": ["string", None], 103 "bantime.formula": ["string", None], 104 "bantime.multipliers": ["string", None], 105 "bantime.maxtime": ["string", None], 106 "bantime.rndtime": ["string", None], 107 "bantime.overalljails": ["bool", None], 108 "ignorecommand": ["string", None], 109 "ignoreself": ["bool", None], 110 "ignoreip": ["string", None], 111 "ignorecache": ["string", None], 112 "filter": ["string", ""], 113 "logtimezone": ["string", None], 114 "logencoding": ["string", None], 115 "logpath": ["string", None], 116 "action": ["string", ""] 117 } 118 _configOpts.update(FilterReader._configOpts) 119 120 _ignoreOpts = set(['action', 'filter', 'enabled'] + list(FilterReader._configOpts.keys())) 121 122 def getOptions(self): 123 124 # Before interpolation (substitution) add static options always available as default: 125 self.merge_defaults({ 126 "fail2ban_version": version 127 }) 128 129 try: 130 131 # Read first options only needed for merge defaults ('known/...' from filter): 132 self.__opts = ConfigReader.getOptions(self, self.__name, self._configOpts1st, 133 shouldExist=True) 134 if not self.__opts: # pragma: no cover 135 raise JailDefError("Init jail options failed") 136 137 if not self.isEnabled(): 138 return True 139 140 # Read filter 141 flt = self.__opts["filter"] 142 if flt: 143 filterName, filterOpt = extractOptions(flt) 144 if not filterName: 145 raise JailDefError("Invalid filter definition %r" % flt) 146 self.__filter = FilterReader( 147 filterName, self.__name, filterOpt, 148 share_config=self.share_config, basedir=self.getBaseDir()) 149 ret = self.__filter.read() 150 if not ret: 151 raise JailDefError("Unable to read the filter %r" % filterName) 152 # set backend-related options (logtype): 153 self.__filter.applyAutoOptions(self.__opts.get('backend', '')) 154 # merge options from filter as 'known/...' (all options unfiltered): 155 self.__filter.getOptions(self.__opts, all=True) 156 ConfigReader.merge_section(self, self.__name, self.__filter.getCombined(), 'known/') 157 else: 158 self.__filter = None 159 logSys.warning("No filter set for jail %s" % self.__name) 160 161 # Read second all options (so variables like %(known/param) can be interpolated): 162 self.__opts = ConfigReader.getOptions(self, self.__name, self._configOpts) 163 if not self.__opts: # pragma: no cover 164 raise JailDefError("Read jail options failed") 165 166 # cumulate filter options again (ignore given in jail): 167 if self.__filter: 168 self.__filter.getOptions(self.__opts) 169 170 # Read action 171 for act in splitWithOptions(self.__opts["action"]): 172 try: 173 act = act.strip() 174 if not act: # skip empty actions 175 continue 176 # join with previous line if needed (consider possible new-line): 177 actName, actOpt = extractOptions(act) 178 prevln = '' 179 if not actName: 180 raise JailDefError("Invalid action definition %r" % act) 181 if actName.endswith(".py"): 182 self.__actions.append([ 183 "set", 184 self.__name, 185 "addaction", 186 actOpt.pop("actname", os.path.splitext(actName)[0]), 187 os.path.join( 188 self.getBaseDir(), "action.d", actName), 189 json.dumps(actOpt), 190 ]) 191 else: 192 action = ActionReader( 193 actName, self.__name, actOpt, 194 share_config=self.share_config, basedir=self.getBaseDir()) 195 ret = action.read() 196 if ret: 197 action.getOptions(self.__opts) 198 self.__actions.append(action) 199 else: 200 raise JailDefError("Unable to read action %r" % actName) 201 except JailDefError: 202 raise 203 except Exception as e: 204 logSys.debug("Caught exception: %s", e, exc_info=True) 205 raise ValueError("Error in action definition %r: %r" % (act, e)) 206 if not len(self.__actions): 207 logSys.warning("No actions were defined for %s" % self.__name) 208 209 except JailDefError as e: 210 e = str(e) 211 logSys.error(e) 212 if not self.__opts: 213 self.__opts = dict() 214 self.__opts['config-error'] = e 215 return False 216 return True 217 218 def convert(self, allow_no_files=False): 219 """Convert read before __opts to the commands stream 220 221 Parameters 222 ---------- 223 allow_missing : bool 224 Either to allow log files to be missing entirely. Primarily is 225 used for testing 226 """ 227 228 stream = [] 229 stream2 = [] 230 e = self.__opts.get('config-error') 231 if e: 232 stream.extend([['config-error', "Jail '%s' skipped, because of wrong configuration: %s" % (self.__name, e)]]) 233 return stream 234 # fill jail with filter options, using filter (only not overriden in jail): 235 if self.__filter: 236 stream.extend(self.__filter.convert()) 237 # and using options from jail: 238 FilterReader._fillStream(stream, self.__opts, self.__name) 239 for opt, value in self.__opts.items(): 240 if opt == "logpath": 241 if self.__opts.get('backend', '').startswith("systemd"): continue 242 found_files = 0 243 for path in value.split("\n"): 244 path = path.rsplit(" ", 1) 245 path, tail = path if len(path) > 1 else (path[0], "head") 246 pathList = JailReader._glob(path) 247 if len(pathList) == 0: 248 logSys.notice("No file(s) found for glob %s" % path) 249 for p in pathList: 250 found_files += 1 251 # logpath after all log-related data (backend, date-pattern, etc) 252 stream2.append( 253 ["set", self.__name, "addlogpath", p, tail]) 254 if not found_files: 255 msg = "Have not found any log file for %s jail" % self.__name 256 if not allow_no_files: 257 raise ValueError(msg) 258 logSys.warning(msg) 259 elif opt == "backend": 260 backend = value 261 elif opt == "ignoreip": 262 stream.append(["set", self.__name, "addignoreip"] + splitwords(value)) 263 elif opt not in JailReader._ignoreOpts: 264 stream.append(["set", self.__name, opt, value]) 265 # consider options order (after other options): 266 if stream2: stream += stream2 267 for action in self.__actions: 268 if isinstance(action, (ConfigReaderUnshared, ConfigReader)): 269 stream.extend(action.convert()) 270 else: 271 stream.append(action) 272 stream.insert(0, ["add", self.__name, backend]) 273 return stream 274 275class JailDefError(Exception): 276 pass 277