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: Yaroslav Halchenko 21# Modified: Cyril Jaquier 22 23__author__ = 'Yaroslav Halchenko, Serg G. Brester (aka sebres)' 24__copyright__ = 'Copyright (c) 2007 Yaroslav Halchenko, 2015 Serg G. Brester (aka sebres)' 25__license__ = 'GPL' 26 27import os 28import re 29import sys 30from ..helpers import getLogger 31 32if sys.version_info >= (3,): # pragma: 2.x no cover 33 34 # SafeConfigParser deprecated from Python 3.2 (renamed to ConfigParser) 35 from configparser import ConfigParser as SafeConfigParser, BasicInterpolation, \ 36 InterpolationMissingOptionError, NoOptionError, NoSectionError 37 38 # And interpolation of __name__ was simply removed, thus we need to 39 # decorate default interpolator to handle it 40 class BasicInterpolationWithName(BasicInterpolation): 41 """Decorator to bring __name__ interpolation back. 42 43 Original handling of __name__ was removed because of 44 functional deficiencies: http://bugs.python.org/issue10489 45 46 commit v3.2a4-105-g61f2761 47 Author: Lukasz Langa <lukasz@langa.pl> 48 Date: Sun Nov 21 13:41:35 2010 +0000 49 50 Issue #10489: removed broken `__name__` support from configparser 51 52 But should be fine to reincarnate for our use case 53 """ 54 def _interpolate_some(self, parser, option, accum, rest, section, map, 55 *args, **kwargs): 56 if section and not (__name__ in map): 57 map = map.copy() # just to be safe 58 map['__name__'] = section 59 # try to wrap section options like %(section/option)s: 60 parser._map_section_options(section, option, rest, map) 61 return super(BasicInterpolationWithName, self)._interpolate_some( 62 parser, option, accum, rest, section, map, *args, **kwargs) 63 64else: # pragma: 3.x no cover 65 from configparser import SafeConfigParser, \ 66 InterpolationMissingOptionError, NoOptionError, NoSectionError 67 68 # Interpolate missing known/option as option from default section 69 SafeConfigParser._cp_interpolate_some = SafeConfigParser._interpolate_some 70 def _interpolate_some(self, option, accum, rest, section, map, *args, **kwargs): 71 # try to wrap section options like %(section/option)s: 72 self._map_section_options(section, option, rest, map) 73 return self._cp_interpolate_some(option, accum, rest, section, map, *args, **kwargs) 74 SafeConfigParser._interpolate_some = _interpolate_some 75 76def _expandConfFilesWithLocal(filenames): 77 """Expands config files with local extension. 78 """ 79 newFilenames = [] 80 for filename in filenames: 81 newFilenames.append(filename) 82 localname = os.path.splitext(filename)[0] + '.local' 83 if localname not in filenames and os.path.isfile(localname): 84 newFilenames.append(localname) 85 return newFilenames 86 87# Gets the instance of the logger. 88logSys = getLogger(__name__) 89logLevel = 7 90 91 92__all__ = ['SafeConfigParserWithIncludes'] 93 94 95class SafeConfigParserWithIncludes(SafeConfigParser): 96 """ 97 Class adds functionality to SafeConfigParser to handle included 98 other configuration files (or may be urls, whatever in the future) 99 100 File should have section [includes] and only 2 options implemented 101 are 'files_before' and 'files_after' where files are listed 1 per 102 line. 103 104 Example: 105 106[INCLUDES] 107before = 1.conf 108 3.conf 109 110after = 1.conf 111 112 It is a simple implementation, so just basic care is taken about 113 recursion. Includes preserve right order, ie new files are 114 inserted to the list of read configs before original, and their 115 includes correspondingly so the list should follow the leaves of 116 the tree. 117 118 I wasn't sure what would be the right way to implement generic (aka c++ 119 template) so we could base at any *configparser class... so I will 120 leave it for the future 121 122 """ 123 124 SECTION_NAME = "INCLUDES" 125 126 SECTION_OPTNAME_CRE = re.compile(r'^([\w\-]+)/([^\s>]+)$') 127 128 SECTION_OPTSUBST_CRE = re.compile(r'%\(([\w\-]+/([^\)]+))\)s') 129 130 CONDITIONAL_RE = re.compile(r"^(\w+)(\?.+)$") 131 132 if sys.version_info >= (3,2): 133 # overload constructor only for fancy new Python3's 134 def __init__(self, share_config=None, *args, **kwargs): 135 kwargs = kwargs.copy() 136 kwargs['interpolation'] = BasicInterpolationWithName() 137 kwargs['inline_comment_prefixes'] = ";" 138 super(SafeConfigParserWithIncludes, self).__init__( 139 *args, **kwargs) 140 self._cfg_share = share_config 141 142 else: 143 def __init__(self, share_config=None, *args, **kwargs): 144 SafeConfigParser.__init__(self, *args, **kwargs) 145 self._cfg_share = share_config 146 147 def get_ex(self, section, option, raw=False, vars={}): 148 """Get an option value for a given section. 149 150 In opposite to `get`, it differentiate session-related option name like `sec/opt`. 151 """ 152 sopt = None 153 # if option name contains section: 154 if '/' in option: 155 sopt = SafeConfigParserWithIncludes.SECTION_OPTNAME_CRE.search(option) 156 # try get value from named section/option: 157 if sopt: 158 sec = sopt.group(1) 159 opt = sopt.group(2) 160 seclwr = sec.lower() 161 if seclwr == 'known': 162 # try get value firstly from known options, hereafter from current section: 163 sopt = ('KNOWN/'+section, section) 164 else: 165 sopt = (sec,) if seclwr != 'default' else ("DEFAULT",) 166 for sec in sopt: 167 try: 168 v = self.get(sec, opt, raw=raw) 169 return v 170 except (NoSectionError, NoOptionError) as e: 171 pass 172 # get value of section/option using given section and vars (fallback): 173 v = self.get(section, option, raw=raw, vars=vars) 174 return v 175 176 def _map_section_options(self, section, option, rest, defaults): 177 """ 178 Interpolates values of the section options (name syntax `%(section/option)s`). 179 180 Fallback: try to wrap missing default options as "default/options" resp. "known/options" 181 """ 182 if '/' not in rest or '%(' not in rest: # pragma: no cover 183 return 0 184 rplcmnt = 0 185 soptrep = SafeConfigParserWithIncludes.SECTION_OPTSUBST_CRE.findall(rest) 186 if not soptrep: # pragma: no cover 187 return 0 188 for sopt, opt in soptrep: 189 if sopt not in defaults: 190 sec = sopt[:~len(opt)] 191 seclwr = sec.lower() 192 if seclwr != 'default': 193 usedef = 0 194 if seclwr == 'known': 195 # try get raw value from known options: 196 try: 197 v = self._sections['KNOWN/'+section][opt] 198 except KeyError: 199 # fallback to default: 200 usedef = 1 201 else: 202 # get raw value of opt in section: 203 try: 204 # if section not found - ignore: 205 try: 206 sec = self._sections[sec] 207 except KeyError: # pragma: no cover 208 continue 209 v = sec[opt] 210 except KeyError: # pragma: no cover 211 # fallback to default: 212 usedef = 1 213 else: 214 usedef = 1 215 if usedef: 216 try: 217 v = self._defaults[opt] 218 except KeyError: # pragma: no cover 219 continue 220 # replacement found: 221 rplcmnt = 1 222 try: # set it in map-vars (consider different python versions): 223 defaults[sopt] = v 224 except: 225 # try to set in first default map (corresponding vars): 226 try: 227 defaults._maps[0][sopt] = v 228 except: # pragma: no cover 229 # no way to update vars chain map - overwrite defaults: 230 self._defaults[sopt] = v 231 return rplcmnt 232 233 @property 234 def share_config(self): 235 return self._cfg_share 236 237 def _getSharedSCPWI(self, filename): 238 SCPWI = SafeConfigParserWithIncludes 239 # read single one, add to return list, use sharing if possible: 240 if self._cfg_share: 241 # cache/share each file as include (ex: filter.d/common could be included in each filter config): 242 hashv = 'inc:'+(filename if not isinstance(filename, list) else '\x01'.join(filename)) 243 cfg, i = self._cfg_share.get(hashv, (None, None)) 244 if cfg is None: 245 cfg = SCPWI(share_config=self._cfg_share) 246 i = cfg.read(filename, get_includes=False) 247 self._cfg_share[hashv] = (cfg, i) 248 elif logSys.getEffectiveLevel() <= logLevel: 249 logSys.log(logLevel, " Shared file: %s", filename) 250 else: 251 # don't have sharing: 252 cfg = SCPWI() 253 i = cfg.read(filename, get_includes=False) 254 return (cfg, i) 255 256 def _getIncludes(self, filenames, seen=[]): 257 if not isinstance(filenames, list): 258 filenames = [ filenames ] 259 filenames = _expandConfFilesWithLocal(filenames) 260 # retrieve or cache include paths: 261 if self._cfg_share: 262 # cache/share include list: 263 hashv = 'inc-path:'+('\x01'.join(filenames)) 264 fileNamesFull = self._cfg_share.get(hashv) 265 if fileNamesFull is None: 266 fileNamesFull = [] 267 for filename in filenames: 268 fileNamesFull += self.__getIncludesUncached(filename, seen) 269 self._cfg_share[hashv] = fileNamesFull 270 return fileNamesFull 271 # don't have sharing: 272 fileNamesFull = [] 273 for filename in filenames: 274 fileNamesFull += self.__getIncludesUncached(filename, seen) 275 return fileNamesFull 276 277 def __getIncludesUncached(self, resource, seen=[]): 278 """ 279 Given 1 config resource returns list of included files 280 (recursively) with the original one as well 281 Simple loops are taken care about 282 """ 283 SCPWI = SafeConfigParserWithIncludes 284 try: 285 parser, i = self._getSharedSCPWI(resource) 286 if not i: 287 return [] 288 except UnicodeDecodeError as e: 289 logSys.error("Error decoding config file '%s': %s" % (resource, e)) 290 return [] 291 292 resourceDir = os.path.dirname(resource) 293 294 newFiles = [ ('before', []), ('after', []) ] 295 if SCPWI.SECTION_NAME in parser.sections(): 296 for option_name, option_list in newFiles: 297 if option_name in parser.options(SCPWI.SECTION_NAME): 298 newResources = parser.get(SCPWI.SECTION_NAME, option_name) 299 for newResource in newResources.split('\n'): 300 if os.path.isabs(newResource): 301 r = newResource 302 else: 303 r = os.path.join(resourceDir, newResource) 304 if r in seen: 305 continue 306 s = seen + [resource] 307 option_list += self._getIncludes(r, s) 308 # combine lists 309 return newFiles[0][1] + [resource] + newFiles[1][1] 310 311 def get_defaults(self): 312 return self._defaults 313 314 def get_sections(self): 315 return self._sections 316 317 def options(self, section, withDefault=True): 318 """Return a list of option names for the given section name. 319 320 Parameter `withDefault` controls the include of names from section `[DEFAULT]` 321 """ 322 try: 323 opts = self._sections[section] 324 except KeyError: # pragma: no cover 325 raise NoSectionError(section) 326 if withDefault: 327 # mix it with defaults: 328 return set(opts.keys()) | set(self._defaults) 329 # only own option names: 330 return list(opts.keys()) 331 332 def read(self, filenames, get_includes=True): 333 if not isinstance(filenames, list): 334 filenames = [ filenames ] 335 # retrieve (and cache) includes: 336 fileNamesFull = [] 337 if get_includes: 338 fileNamesFull += self._getIncludes(filenames) 339 else: 340 fileNamesFull = filenames 341 342 if not fileNamesFull: 343 return [] 344 345 logSys.info(" Loading files: %s", fileNamesFull) 346 347 if get_includes or len(fileNamesFull) > 1: 348 # read multiple configs: 349 ret = [] 350 alld = self.get_defaults() 351 alls = self.get_sections() 352 for filename in fileNamesFull: 353 # read single one, add to return list, use sharing if possible: 354 cfg, i = self._getSharedSCPWI(filename) 355 if i: 356 ret += i 357 # merge defaults and all sections to self: 358 alld.update(cfg.get_defaults()) 359 for n, s in cfg.get_sections().items(): 360 # conditional sections 361 cond = SafeConfigParserWithIncludes.CONDITIONAL_RE.match(n) 362 if cond: 363 n, cond = cond.groups() 364 s = s.copy() 365 try: 366 del(s['__name__']) 367 except KeyError: 368 pass 369 for k in list(s.keys()): 370 v = s.pop(k) 371 s[k + cond] = v 372 s2 = alls.get(n) 373 if isinstance(s2, dict): 374 # save previous known values, for possible using in local interpolations later: 375 self.merge_section('KNOWN/'+n, 376 dict([i for i in iter(s2.items()) if i[0] in s]), '') 377 # merge section 378 s2.update(s) 379 else: 380 alls[n] = s.copy() 381 382 return ret 383 384 # read one config : 385 if logSys.getEffectiveLevel() <= logLevel: 386 logSys.log(logLevel, " Reading file: %s", fileNamesFull[0]) 387 # read file(s) : 388 if sys.version_info >= (3,2): # pragma: no cover 389 return SafeConfigParser.read(self, fileNamesFull, encoding='utf-8') 390 else: 391 return SafeConfigParser.read(self, fileNamesFull) 392 393 def merge_section(self, section, options, pref=None): 394 alls = self.get_sections() 395 try: 396 sec = alls[section] 397 except KeyError: 398 alls[section] = sec = dict() 399 if not pref: 400 sec.update(options) 401 return 402 sk = {} 403 for k, v in options.items(): 404 if not k.startswith(pref) and k != '__name__': 405 sk[pref+k] = v 406 sec.update(sk) 407 408