393 lines
13 KiB
Python
393 lines
13 KiB
Python
# emacs: -*- mode: python; coding: utf-8; py-indent-offset: 4; indent-tabs-mode: t -*-
|
|
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
|
|
|
# This file is part of Fail2Ban.
|
|
#
|
|
# Fail2Ban is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Fail2Ban is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Fail2Ban; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
import re
|
|
import time
|
|
import calendar
|
|
import datetime
|
|
from _strptime import LocaleTime, TimeRE, _calc_julian_from_U_or_W
|
|
|
|
from .mytime import MyTime
|
|
|
|
locale_time = LocaleTime()
|
|
|
|
TZ_ABBR_RE = r"[A-Z](?:[A-Z]{2,4})?"
|
|
FIXED_OFFSET_TZ_RE = re.compile(r"(%s)?([+-][01]\d(?::?\d{2})?)?$" % (TZ_ABBR_RE,))
|
|
|
|
timeRE = TimeRE()
|
|
|
|
# %k - one- or two-digit number giving the hour of the day (0-23) on a 24-hour clock,
|
|
# (corresponds %H, but allows space if not zero-padded).
|
|
# %l - one- or two-digit number giving the hour of the day (12-11) on a 12-hour clock,
|
|
# (corresponds %I, but allows space if not zero-padded).
|
|
timeRE['k'] = r" ?(?P<H>[0-2]?\d)"
|
|
timeRE['l'] = r" ?(?P<I>1?\d)"
|
|
|
|
# TODO: because python currently does not support mixing of case-sensitive with case-insensitive matching,
|
|
# check how TZ (in uppercase) can be combined with %a/%b etc. (that are currently case-insensitive),
|
|
# to avoid invalid date-time recognition in strings like '11-Aug-2013 03:36:11.372 error ...'
|
|
# with wrong TZ "error", which is at least not backwards compatible.
|
|
# Hence %z currently match literal Z|UTC|GMT only (and offset-based), and %Exz - all zone abbreviations.
|
|
timeRE['Z'] = r"(?P<Z>Z|[A-Z]{3,5})"
|
|
timeRE['z'] = r"(?P<z>Z|UTC|GMT|[+-][01]\d(?::?\d{2})?)"
|
|
|
|
# Note: this extended tokens supported zone abbreviations, but it can parse 1 or 3-5 char(s) in lowercase,
|
|
# see todo above. Don't use them in default date-patterns (if not anchored, few precise resp. optional).
|
|
timeRE['ExZ'] = r"(?P<Z>%s)" % (TZ_ABBR_RE,)
|
|
timeRE['Exz'] = r"(?P<z>(?:%s)?[+-][01]\d(?::?\d{2})?|%s)" % (TZ_ABBR_RE, TZ_ABBR_RE)
|
|
|
|
# overwrite default patterns, since they can be non-optimal:
|
|
timeRE['d'] = r"(?P<d>[1-2]\d|[0 ]?[1-9]|3[0-1])"
|
|
timeRE['m'] = r"(?P<m>0?[1-9]|1[0-2])"
|
|
timeRE['Y'] = r"(?P<Y>\d{4})"
|
|
timeRE['H'] = r"(?P<H>[0-1]?\d|2[0-3])"
|
|
timeRE['M'] = r"(?P<M>[0-5]?\d)"
|
|
timeRE['S'] = r"(?P<S>[0-5]?\d|6[0-1])"
|
|
|
|
# Extend built-in TimeRE with some exact patterns
|
|
# exact two-digit patterns:
|
|
timeRE['Exd'] = r"(?P<d>[1-2]\d|0[1-9]|3[0-1])"
|
|
timeRE['Exm'] = r"(?P<m>0[1-9]|1[0-2])"
|
|
timeRE['ExH'] = r"(?P<H>[0-1]\d|2[0-3])"
|
|
timeRE['Exk'] = r" ?(?P<H>[0-1]?\d|2[0-3])"
|
|
timeRE['Exl'] = r" ?(?P<I>1[0-2]|\d)"
|
|
timeRE['ExM'] = r"(?P<M>[0-5]\d)"
|
|
timeRE['ExS'] = r"(?P<S>[0-5]\d|6[0-1])"
|
|
|
|
def _updateTimeRE():
|
|
def _getYearCentRE(cent=(0,3), distance=3, now=(MyTime.now(), MyTime.alternateNow)):
|
|
""" Build century regex for last year and the next years (distance).
|
|
|
|
Thereby respect possible run in the test-cases (alternate date used there)
|
|
"""
|
|
cent = lambda year, f=cent[0], t=cent[1]: str(year)[f:t]
|
|
def grp(exprset):
|
|
c = None
|
|
if len(exprset) > 1:
|
|
for i in exprset:
|
|
if c is None or i[0:-1] == c:
|
|
c = i[0:-1]
|
|
else:
|
|
c = None
|
|
break
|
|
if not c:
|
|
for i in exprset:
|
|
if c is None or i[0] == c:
|
|
c = i[0]
|
|
else:
|
|
c = None
|
|
break
|
|
if c:
|
|
return "%s%s" % (c, grp([i[len(c):] for i in exprset]))
|
|
return ("(?:%s)" % "|".join(exprset) if len(exprset[0]) > 1 else "[%s]" % "".join(exprset)) \
|
|
if len(exprset) > 1 else "".join(exprset)
|
|
exprset = set( cent(now[0].year + i) for i in (-1, distance) )
|
|
if len(now) > 1 and now[1]:
|
|
exprset |= set( cent(now[1].year + i) for i in range(-1, now[0].year-now[1].year+1, distance) )
|
|
return grp(sorted(list(exprset)))
|
|
|
|
# more precise year patterns, within same century of last year and
|
|
# the next 3 years (for possible long uptime of fail2ban); thereby
|
|
# consider possible run in the test-cases (alternate date used there),
|
|
# so accept years: 20xx (from test-date or 2001 up to current century)
|
|
timeRE['ExY'] = r"(?P<Y>%s\d)" % _getYearCentRE(cent=(0,3), distance=3,
|
|
now=(datetime.datetime.now(), datetime.datetime.fromtimestamp(
|
|
min(MyTime.alternateNowTime or 978393600, 978393600))
|
|
)
|
|
)
|
|
timeRE['Exy'] = r"(?P<y>\d{2})"
|
|
|
|
_updateTimeRE()
|
|
|
|
def getTimePatternRE():
|
|
keys = list(timeRE.keys())
|
|
patt = (r"%%(%%|%s|[%s])" % (
|
|
"|".join([k for k in keys if len(k) > 1]),
|
|
"".join([k for k in keys if len(k) == 1]),
|
|
))
|
|
names = {
|
|
'a': "DAY", 'A': "DAYNAME", 'b': "MON", 'B': "MONTH", 'd': "Day",
|
|
'H': "24hour", 'I': "12hour", 'j': "Yearday", 'm': "Month",
|
|
'M': "Minute", 'p': "AMPM", 'S': "Second", 'U': "Yearweek",
|
|
'w': "Weekday", 'W': "Yearweek", 'y': 'Year2', 'Y': "Year", '%': "%",
|
|
'z': "Zone offset", 'f': "Microseconds", 'Z': "Zone name",
|
|
}
|
|
for key in set(keys) - set(names): # may not have them all...
|
|
if key.startswith('Ex'):
|
|
kn = names.get(key[2:])
|
|
if kn:
|
|
names[key] = "Ex" + kn
|
|
continue
|
|
names[key] = "%%%s" % key
|
|
return (patt, names)
|
|
|
|
|
|
def validateTimeZone(tz):
|
|
"""Validate a timezone and convert it to offset if it can (offset-based TZ).
|
|
|
|
For now this accepts the UTC[+-]hhmm format (UTC has aliases GMT/Z and optional).
|
|
Additionally it accepts all zone abbreviations mentioned below in TZ_STR.
|
|
Note that currently this zone abbreviations are offset-based and used fixed
|
|
offset without automatically DST-switch (if CET used then no automatically CEST-switch).
|
|
|
|
In the future, it may be extended for named time zones (such as Europe/Paris)
|
|
present on the system, if a suitable tz library is present (pytz).
|
|
"""
|
|
if tz is None:
|
|
return None
|
|
m = FIXED_OFFSET_TZ_RE.match(tz)
|
|
if m is None:
|
|
raise ValueError("Unknown or unsupported time zone: %r" % tz)
|
|
tz = m.groups()
|
|
return zone2offset(tz, 0)
|
|
|
|
def zone2offset(tz, dt):
|
|
"""Return the proper offset, in minutes according to given timezone at a given time.
|
|
|
|
Parameters
|
|
----------
|
|
tz: symbolic timezone or offset (for now only TZA?([+-]hh:?mm?)? is supported,
|
|
as value are accepted:
|
|
int offset;
|
|
string in form like 'CET+0100' or 'UTC' or '-0400';
|
|
tuple (or list) in form (zone name, zone offset);
|
|
dt: datetime instance for offset computation (currently unused)
|
|
"""
|
|
if isinstance(tz, int):
|
|
return tz
|
|
if isinstance(tz, str):
|
|
return validateTimeZone(tz)
|
|
tz, tzo = tz
|
|
if tzo is None or tzo == '': # without offset
|
|
return TZ_ABBR_OFFS[tz]
|
|
if len(tzo) <= 3: # short tzo (hh only)
|
|
# [+-]hh --> [+-]hh*60
|
|
return TZ_ABBR_OFFS[tz] + int(tzo)*60
|
|
if tzo[3] != ':':
|
|
# [+-]hhmm --> [+-]1 * (hh*60 + mm)
|
|
return TZ_ABBR_OFFS[tz] + (-1 if tzo[0] == '-' else 1) * (int(tzo[1:3])*60 + int(tzo[3:5]))
|
|
else:
|
|
# [+-]hh:mm --> [+-]1 * (hh*60 + mm)
|
|
return TZ_ABBR_OFFS[tz] + (-1 if tzo[0] == '-' else 1) * (int(tzo[1:3])*60 + int(tzo[4:6]))
|
|
|
|
def reGroupDictStrptime(found_dict, msec=False, default_tz=None):
|
|
"""Return time from dictionary of strptime fields
|
|
|
|
This is tweaked from python built-in _strptime.
|
|
|
|
Parameters
|
|
----------
|
|
found_dict : dict
|
|
Dictionary where keys represent the strptime fields, and values the
|
|
respective value.
|
|
default_tz : default timezone to apply if nothing relevant is in found_dict
|
|
(may be a non-fixed one in the future)
|
|
Returns
|
|
-------
|
|
float
|
|
Unix time stamp.
|
|
"""
|
|
|
|
now = \
|
|
year = month = day = tzoffset = \
|
|
weekday = julian = week_of_year = None
|
|
hour = minute = second = fraction = 0
|
|
for key, val in found_dict.items():
|
|
if val is None: continue
|
|
# Directives not explicitly handled below:
|
|
# c, x, X
|
|
# handled by making out of other directives
|
|
# U, W
|
|
# worthless without day of the week
|
|
if key == 'y':
|
|
year = int(val)
|
|
# Fail2ban year should be always in the current century (>= 2000)
|
|
if year <= 2000:
|
|
year += 2000
|
|
elif key == 'Y':
|
|
year = int(val)
|
|
elif key == 'm':
|
|
month = int(val)
|
|
elif key == 'B':
|
|
month = locale_time.f_month.index(val.lower())
|
|
elif key == 'b':
|
|
month = locale_time.a_month.index(val.lower())
|
|
elif key == 'd':
|
|
day = int(val)
|
|
elif key == 'H':
|
|
hour = int(val)
|
|
elif key == 'I':
|
|
hour = int(val)
|
|
ampm = found_dict.get('p', '').lower()
|
|
# If there was no AM/PM indicator, we'll treat this like AM
|
|
if ampm in ('', locale_time.am_pm[0]):
|
|
# We're in AM so the hour is correct unless we're
|
|
# looking at 12 midnight.
|
|
# 12 midnight == 12 AM == hour 0
|
|
if hour == 12:
|
|
hour = 0
|
|
elif ampm == locale_time.am_pm[1]:
|
|
# We're in PM so we need to add 12 to the hour unless
|
|
# we're looking at 12 noon.
|
|
# 12 noon == 12 PM == hour 12
|
|
if hour != 12:
|
|
hour += 12
|
|
elif key == 'M':
|
|
minute = int(val)
|
|
elif key == 'S':
|
|
second = int(val)
|
|
elif key == 'f':
|
|
if msec: # pragma: no cover - currently unused
|
|
s = val
|
|
# Pad to always return microseconds.
|
|
s += "0" * (6 - len(s))
|
|
fraction = int(s)
|
|
elif key == 'A':
|
|
weekday = locale_time.f_weekday.index(val.lower())
|
|
elif key == 'a':
|
|
weekday = locale_time.a_weekday.index(val.lower())
|
|
elif key == 'w':
|
|
weekday = int(val) - 1
|
|
if weekday < 0: weekday = 6
|
|
elif key == 'j':
|
|
julian = int(val)
|
|
elif key in ('U', 'W'):
|
|
week_of_year = int(val)
|
|
# U starts week on Sunday, W - on Monday
|
|
week_of_year_start = 6 if key == 'U' else 0
|
|
elif key in ('z', 'Z'):
|
|
z = val
|
|
if z in ("Z", "UTC", "GMT"):
|
|
tzoffset = 0
|
|
else:
|
|
tzoffset = zone2offset(z, 0); # currently offset-based only
|
|
|
|
# Fail2Ban will assume it's this year
|
|
assume_year = False
|
|
if year is None:
|
|
if not now: now = MyTime.now()
|
|
year = now.year
|
|
assume_year = True
|
|
if month is None or day is None:
|
|
# If we know the week of the year and what day of that week, we can figure
|
|
# out the Julian day of the year.
|
|
if julian is None and week_of_year is not None and weekday is not None:
|
|
julian = _calc_julian_from_U_or_W(year, week_of_year, weekday,
|
|
(week_of_year_start == 0))
|
|
# Cannot pre-calculate datetime.datetime() since can change in Julian
|
|
# calculation and thus could have different value for the day of the week
|
|
# calculation.
|
|
if julian is not None:
|
|
datetime_result = datetime.datetime.fromordinal((julian - 1) + datetime.datetime(year, 1, 1).toordinal())
|
|
year = datetime_result.year
|
|
month = datetime_result.month
|
|
day = datetime_result.day
|
|
|
|
# Fail2Ban assume today
|
|
assume_today = False
|
|
if month is None and day is None:
|
|
if not now: now = MyTime.now()
|
|
month = now.month
|
|
day = now.day
|
|
assume_today = True
|
|
|
|
# Actually create date
|
|
date_result = datetime.datetime(
|
|
year, month, day, hour, minute, second, fraction)
|
|
# Correct timezone if not supplied in the log linge
|
|
if tzoffset is None and default_tz is not None:
|
|
tzoffset = zone2offset(default_tz, date_result)
|
|
# Add timezone info
|
|
if tzoffset is not None:
|
|
date_result -= datetime.timedelta(seconds=tzoffset * 60)
|
|
|
|
if assume_today:
|
|
if not now: now = MyTime.now()
|
|
if date_result > now:
|
|
# Rollover at midnight, could mean it's yesterday...
|
|
date_result -= datetime.timedelta(days=1)
|
|
if assume_year:
|
|
if not now: now = MyTime.now()
|
|
if date_result > now + datetime.timedelta(days=1): # ignore by timezone issues (+24h)
|
|
# assume last year - also reset month and day as it's not yesterday...
|
|
date_result = date_result.replace(
|
|
year=year-1, month=month, day=day)
|
|
|
|
# make time:
|
|
if tzoffset is not None:
|
|
tm = calendar.timegm(date_result.utctimetuple())
|
|
else:
|
|
tm = time.mktime(date_result.timetuple())
|
|
if msec: # pragma: no cover - currently unused
|
|
tm += fraction/1000000.0
|
|
return tm
|
|
|
|
|
|
TZ_ABBR_OFFS = {'':0, None:0}
|
|
TZ_STR = '''
|
|
-12 Y
|
|
-11 X NUT SST
|
|
-10 W CKT HAST HST TAHT TKT
|
|
-9 V AKST GAMT GIT HADT HNY
|
|
-8 U AKDT CIST HAY HNP PST PT
|
|
-7 T HAP HNR MST PDT
|
|
-6 S CST EAST GALT HAR HNC MDT
|
|
-5 R CDT COT EASST ECT EST ET HAC HNE PET
|
|
-4 Q AST BOT CLT COST EDT FKT GYT HAE HNA PYT
|
|
-3 P ADT ART BRT CLST FKST GFT HAA PMST PYST SRT UYT WGT
|
|
-2 O BRST FNT PMDT UYST WGST
|
|
-1 N AZOT CVT EGT
|
|
0 Z EGST GMT UTC WET WT
|
|
1 A CET DFT WAT WEDT WEST
|
|
2 B CAT CEDT CEST EET SAST WAST
|
|
3 C EAT EEDT EEST IDT MSK
|
|
4 D AMT AZT GET GST KUYT MSD MUT RET SAMT SCT
|
|
5 E AMST AQTT AZST HMT MAWT MVT PKT TFT TJT TMT UZT YEKT
|
|
6 F ALMT BIOT BTT IOT KGT NOVT OMST YEKST
|
|
7 G CXT DAVT HOVT ICT KRAT NOVST OMSST THA WIB
|
|
8 H ACT AWST BDT BNT CAST HKT IRKT KRAST MYT PHT SGT ULAT WITA WST
|
|
9 I AWDT IRKST JST KST PWT TLT WDT WIT YAKT
|
|
10 K AEST ChST PGT VLAT YAKST YAPT
|
|
11 L AEDT LHDT MAGT NCT PONT SBT VLAST VUT
|
|
12 M ANAST ANAT FJT GILT MAGST MHT NZST PETST PETT TVT WFT
|
|
13 FJST NZDT
|
|
11.5 NFT
|
|
10.5 ACDT LHST
|
|
9.5 ACST
|
|
6.5 CCT MMT
|
|
5.75 NPT
|
|
5.5 SLT
|
|
4.5 AFT IRDT
|
|
3.5 IRST
|
|
-2.5 HAT NDT
|
|
-3.5 HNT NST NT
|
|
-4.5 HLV VET
|
|
-9.5 MART MIT
|
|
'''
|
|
|
|
def _init_TZ_ABBR():
|
|
"""Initialized TZ_ABBR_OFFS dictionary (TZ -> offset in minutes)"""
|
|
for tzline in map(str.split, TZ_STR.split('\n')):
|
|
if not len(tzline): continue
|
|
tzoffset = int(float(tzline[0]) * 60)
|
|
for tz in tzline[1:]:
|
|
TZ_ABBR_OFFS[tz] = tzoffset
|
|
|
|
_init_TZ_ABBR()
|