normalize usage of preferred encoding (and decode any to string);

python 3.x compatibility (used uni_decode for string representation of stdout/stderr, unified test cases)
amend for #1542
pull/1557/head
sebres 2016-09-09 18:28:47 +02:00
parent e0347bb3a0
commit ebd864660a
10 changed files with 72 additions and 66 deletions

View File

@ -29,7 +29,6 @@ __copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halch
__license__ = "GPL" __license__ = "GPL"
import getopt import getopt
import locale
import logging import logging
import os import os
import shlex import shlex
@ -52,7 +51,7 @@ from .filterreader import FilterReader
from ..server.filter import Filter, FileContainer from ..server.filter import Filter, FileContainer
from ..server.failregex import RegexException from ..server.failregex import RegexException
from ..helpers import FormatterWithTraceBack, getLogger from ..helpers import FormatterWithTraceBack, getLogger, PREFER_ENC
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger("fail2ban") logSys = getLogger("fail2ban")
@ -239,7 +238,7 @@ class Fail2banRegex(object):
if opts.encoding: if opts.encoding:
self.encoding = opts.encoding self.encoding = opts.encoding
else: else:
self.encoding = locale.getpreferredencoding() self.encoding = PREFER_ENC
self.raw = True if opts.raw else False self.raw = True if opts.raw else False
def decode_line(self, line): def decode_line(self, line):

View File

@ -21,6 +21,7 @@ __author__ = "Cyril Jaquier, Arturo 'Buanzo' Busleiman, Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
import gc import gc
import locale
import logging import logging
import os import os
import re import re
@ -32,6 +33,9 @@ from threading import Lock
from .server.mytime import MyTime from .server.mytime import MyTime
PREFER_ENC = locale.getpreferredencoding()
def formatExceptionInfo(): def formatExceptionInfo():
""" Consistently format exception information """ """ Consistently format exception information """
cla, exc = sys.exc_info()[:2] cla, exc = sys.exc_info()[:2]
@ -144,6 +148,36 @@ def splitwords(s):
return filter(bool, map(str.strip, re.split('[ ,\n]+', s))) return filter(bool, map(str.strip, re.split('[ ,\n]+', s)))
#
# Following "uni_decode" function unified python independent any to string converting
#
# Typical example resp. work-case for understanding the coding/decoding issues:
#
# [isinstance('', str), isinstance(b'', str), isinstance(u'', str)]
# [True, True, False]; # -- python2
# [True, False, True]; # -- python3
#
if sys.version_info >= (3,):
def uni_decode(x, enc=PREFER_ENC, errors='strict'):
try:
if isinstance(x, bytes):
return x.decode(enc, errors)
return x
except (UnicodeDecodeError, UnicodeEncodeError): # pragma: no cover - unsure if reachable
if errors != 'strict':
raise
return uni_decode(x, enc, 'replace')
else:
def uni_decode(x, enc=PREFER_ENC, errors='strict'):
try:
if isinstance(x, unicode):
return x.encode(enc, errors)
return x
except (UnicodeDecodeError, UnicodeEncodeError): # pragma: no cover - unsure if reachable
if errors != 'strict':
raise
return uni_decode(x, enc, 'replace')
class BgService(object): class BgService(object):
"""Background servicing """Background servicing

View File

@ -22,7 +22,6 @@ __copyright__ = "Copyright (c) 2013 Steven Hiscocks"
__license__ = "GPL" __license__ = "GPL"
import json import json
import locale
import shutil import shutil
import sqlite3 import sqlite3
import sys import sys
@ -32,7 +31,7 @@ from threading import RLock
from .mytime import MyTime from .mytime import MyTime
from .ticket import FailTicket from .ticket import FailTicket
from ..helpers import getLogger from ..helpers import getLogger, PREFER_ENC
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
@ -41,7 +40,7 @@ if sys.version_info >= (3,):
def _json_dumps_safe(x): def _json_dumps_safe(x):
try: try:
x = json.dumps(x, ensure_ascii=False).encode( x = json.dumps(x, ensure_ascii=False).encode(
locale.getpreferredencoding(), 'replace') PREFER_ENC, 'replace')
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
logSys.error('json dumps failed: %s', e) logSys.error('json dumps failed: %s', e)
x = '{}' x = '{}'
@ -50,7 +49,7 @@ if sys.version_info >= (3,):
def _json_loads_safe(x): def _json_loads_safe(x):
try: try:
x = json.loads(x.decode( x = json.loads(x.decode(
locale.getpreferredencoding(), 'replace')) PREFER_ENC, 'replace'))
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
logSys.error('json loads failed: %s', e) logSys.error('json loads failed: %s', e)
x = {} x = {}
@ -62,14 +61,14 @@ else:
elif isinstance(x, list): elif isinstance(x, list):
return [_normalize(element) for element in x] return [_normalize(element) for element in x]
elif isinstance(x, unicode): elif isinstance(x, unicode):
return x.encode(locale.getpreferredencoding()) return x.encode(PREFER_ENC)
else: else:
return x return x
def _json_dumps_safe(x): def _json_dumps_safe(x):
try: try:
x = json.dumps(_normalize(x), ensure_ascii=False).decode( x = json.dumps(_normalize(x), ensure_ascii=False).decode(
locale.getpreferredencoding(), 'replace') PREFER_ENC, 'replace')
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
logSys.error('json dumps failed: %s', e) logSys.error('json dumps failed: %s', e)
x = '{}' x = '{}'
@ -78,7 +77,7 @@ else:
def _json_loads_safe(x): def _json_loads_safe(x):
try: try:
x = _normalize(json.loads(x.decode( x = _normalize(json.loads(x.decode(
locale.getpreferredencoding(), 'replace'))) PREFER_ENC, 'replace')))
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
logSys.error('json loads failed: %s', e) logSys.error('json loads failed: %s', e)
x = {} x = {}

View File

@ -24,7 +24,6 @@ __license__ = "GPL"
import codecs import codecs
import datetime import datetime
import fcntl import fcntl
import locale
import logging import logging
import os import os
import re import re
@ -40,7 +39,7 @@ from .datetemplate import DatePatternRegex, DateEpoch, DateTai64n
from .mytime import MyTime from .mytime import MyTime
from .failregex import FailRegex, Regex, RegexException from .failregex import FailRegex, Regex, RegexException
from .action import CommandAction from .action import CommandAction
from ..helpers import getLogger from ..helpers import getLogger, PREFER_ENC
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
@ -87,7 +86,7 @@ class Filter(JailThread):
## External command ## External command
self.__ignoreCommand = False self.__ignoreCommand = False
## Default or preferred encoding (to decode bytes from file or journal): ## Default or preferred encoding (to decode bytes from file or journal):
self.__encoding = locale.getpreferredencoding() self.__encoding = PREFER_ENC
## Error counter (protected, so can be used in filter implementations) ## Error counter (protected, so can be used in filter implementations)
## if it reached 100 (at once), run-cycle will go idle ## if it reached 100 (at once), run-cycle will go idle
self._errors = 0 self._errors = 0
@ -329,7 +328,7 @@ class Filter(JailThread):
def setLogEncoding(self, encoding): def setLogEncoding(self, encoding):
if encoding.lower() == "auto": if encoding.lower() == "auto":
encoding = locale.getpreferredencoding() encoding = PREFER_ENC
codecs.lookup(encoding) # Raise LookupError if invalid codec codecs.lookup(encoding) # Raise LookupError if invalid codec
self.__encoding = encoding self.__encoding = encoding
logSys.info(" encoding: %s" % encoding) logSys.info(" encoding: %s" % encoding)
@ -452,29 +451,6 @@ class Filter(JailThread):
return False return False
if sys.version_info >= (3,):
@staticmethod
def uni_decode(x, enc, errors='strict'):
try:
if isinstance(x, bytes):
return x.decode(enc, errors)
return x
except (UnicodeDecodeError, UnicodeEncodeError): # pragma: no cover - unsure if reachable
if errors != 'strict':
raise
return uni_decode(x, enc, 'replace')
else:
@staticmethod
def uni_decode(x, enc, errors='strict'):
try:
if isinstance(x, unicode):
return x.encode(enc, errors)
return x
except (UnicodeDecodeError, UnicodeEncodeError): # pragma: no cover - unsure if reachable
if errors != 'strict':
raise
return uni_decode(x, enc, 'replace')
def processLine(self, line, date=None, returnRawHost=False, def processLine(self, line, date=None, returnRawHost=False,
checkAllRegex=False, checkFindTime=False): checkAllRegex=False, checkFindTime=False):
"""Split the time portion from log msg and return findFailures on them """Split the time portion from log msg and return findFailures on them

View File

@ -34,7 +34,7 @@ from .failmanager import FailManagerEmpty
from .filter import JournalFilter, Filter from .filter import JournalFilter, Filter
from .mytime import MyTime from .mytime import MyTime
from .utils import Utils from .utils import Utils
from ..helpers import getLogger, logging, splitwords from ..helpers import getLogger, logging, splitwords, uni_decode
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
@ -174,10 +174,6 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
def getJournalMatch(self): def getJournalMatch(self):
return self.__matches return self.__matches
def uni_decode(self, x):
v = Filter.uni_decode(x, self.getLogEncoding())
return v
## ##
# Format journal log entry into syslog style # Format journal log entry into syslog style
# #
@ -186,16 +182,16 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
def formatJournalEntry(self, logentry): def formatJournalEntry(self, logentry):
# Be sure, all argument of line tuple should have the same type: # Be sure, all argument of line tuple should have the same type:
uni_decode = self.uni_decode enc = self.getLogEncoding()
logelements = [] logelements = []
v = logentry.get('_HOSTNAME') v = logentry.get('_HOSTNAME')
if v: if v:
logelements.append(uni_decode(v)) logelements.append(uni_decode(v, enc))
v = logentry.get('SYSLOG_IDENTIFIER') v = logentry.get('SYSLOG_IDENTIFIER')
if not v: if not v:
v = logentry.get('_COMM') v = logentry.get('_COMM')
if v: if v:
logelements.append(uni_decode(v)) logelements.append(uni_decode(v, enc))
v = logentry.get('SYSLOG_PID') v = logentry.get('SYSLOG_PID')
if not v: if not v:
v = logentry.get('_PID') v = logentry.get('_PID')
@ -210,9 +206,9 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
logelements.append("[%12.6f]" % monotonic.total_seconds()) logelements.append("[%12.6f]" % monotonic.total_seconds())
msg = logentry.get('MESSAGE','') msg = logentry.get('MESSAGE','')
if isinstance(msg, list): if isinstance(msg, list):
logelements.append(" ".join(uni_decode(v) for v in msg)) logelements.append(" ".join(uni_decode(v, enc) for v in msg))
else: else:
logelements.append(uni_decode(msg)) logelements.append(uni_decode(msg, enc))
logline = " ".join(logelements) logline = " ".join(logelements)

View File

@ -21,8 +21,14 @@ __author__ = "Serg G. Brester (sebres) and Fail2Ban Contributors"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Yaroslav Halchenko, 2012-2015 Serg G. Brester" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Yaroslav Halchenko, 2012-2015 Serg G. Brester"
__license__ = "GPL" __license__ = "GPL"
import logging, os, fcntl, subprocess, time, signal import fcntl
from ..helpers import getLogger import logging
import os
import signal
import subprocess
import sys
import time
from ..helpers import getLogger, uni_decode
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
@ -179,7 +185,7 @@ class Utils():
if stdout is not None and stdout != '' and std_level >= logSys.getEffectiveLevel(): if stdout is not None and stdout != '' and std_level >= logSys.getEffectiveLevel():
logSys.log(std_level, "%s -- stdout:", realCmd) logSys.log(std_level, "%s -- stdout:", realCmd)
for l in stdout.splitlines(): for l in stdout.splitlines():
logSys.log(std_level, " -- stdout: %r", l) logSys.log(std_level, " -- stdout: %r", uni_decode(l))
popen.stdout.close() popen.stdout.close()
if popen.stderr: if popen.stderr:
try: try:
@ -191,7 +197,7 @@ class Utils():
if stderr is not None and stderr != '' and std_level >= logSys.getEffectiveLevel(): if stderr is not None and stderr != '' and std_level >= logSys.getEffectiveLevel():
logSys.log(std_level, "%s -- stderr:", realCmd) logSys.log(std_level, "%s -- stderr:", realCmd)
for l in stderr.splitlines(): for l in stderr.splitlines():
logSys.log(std_level, " -- stderr: %r", l) logSys.log(std_level, " -- stderr: %r", uni_decode(l))
popen.stderr.close() popen.stderr.close()
success = False success = False

View File

@ -399,11 +399,11 @@ class CommandActionTest(LogCaptureTestCase):
def testCaptureStdOutErr(self): def testCaptureStdOutErr(self):
CommandAction.executeCmd('echo "How now brown cow"') CommandAction.executeCmd('echo "How now brown cow"')
self.assertLogged("stdout: 'How now brown cow'\n", "stdout: b'How now brown cow'\n") self.assertLogged("stdout: 'How now brown cow'\n")
CommandAction.executeCmd( CommandAction.executeCmd(
'echo "The rain in Spain stays mainly in the plain" 1>&2') 'echo "The rain in Spain stays mainly in the plain" 1>&2')
self.assertLogged( self.assertLogged(
"stderr: 'The rain in Spain stays mainly in the plain'\n", "stderr: b'The rain in Spain stays mainly in the plain'\n") "stderr: 'The rain in Spain stays mainly in the plain'\n")
def testCallingMap(self): def testCallingMap(self):
mymap = CallingMap(callme=lambda: str(10), error=lambda: int('a'), mymap = CallingMap(callme=lambda: str(10), error=lambda: int('a'),

View File

@ -38,11 +38,11 @@ except ImportError:
from ..server.jail import Jail from ..server.jail import Jail
from ..server.filterpoll import FilterPoll from ..server.filterpoll import FilterPoll
from ..server.filter import Filter, FileFilter, FileContainer, locale from ..server.filter import Filter, FileFilter, FileContainer
from ..server.failmanager import FailManagerEmpty from ..server.failmanager import FailManagerEmpty
from ..server.ipdns import DNSUtils, IPAddr from ..server.ipdns import DNSUtils, IPAddr
from ..server.mytime import MyTime from ..server.mytime import MyTime
from ..server.utils import Utils from ..server.utils import Utils, uni_decode
from .utils import setUpMyTime, tearDownMyTime, mtimesleep, LogCaptureTestCase from .utils import setUpMyTime, tearDownMyTime, mtimesleep, LogCaptureTestCase
from .dummyjail import DummyJail from .dummyjail import DummyJail
@ -311,8 +311,7 @@ class BasicFilter(unittest.TestCase):
b'Fail for "g\xc3\xb6ran" from 192.0.2.1' b'Fail for "g\xc3\xb6ran" from 192.0.2.1'
): ):
# join should work if all arguments have the same type: # join should work if all arguments have the same type:
enc = locale.getpreferredencoding() "".join([uni_decode(v) for v in (a1, a2, a3)])
"".join([Filter.uni_decode(v, enc) for v in (a1, a2, a3)])
class IgnoreIP(LogCaptureTestCase): class IgnoreIP(LogCaptureTestCase):

View File

@ -35,7 +35,7 @@ from StringIO import StringIO
from utils import LogCaptureTestCase, logSys as DefLogSys from utils import LogCaptureTestCase, logSys as DefLogSys
from ..helpers import formatExceptionInfo, mbasename, TraceBack, FormatterWithTraceBack, getLogger from ..helpers import formatExceptionInfo, mbasename, TraceBack, FormatterWithTraceBack, getLogger, uni_decode
from ..helpers import splitwords from ..helpers import splitwords
from ..server.datedetector import DateDetector from ..server.datedetector import DateDetector
from ..server.datetemplate import DatePatternRegex from ..server.datetemplate import DatePatternRegex
@ -74,16 +74,14 @@ class HelpersTest(unittest.TestCase):
if sys.version_info >= (2,7): if sys.version_info >= (2,7):
def _sh_call(cmd): def _sh_call(cmd):
import subprocess, locale import subprocess
ret = subprocess.check_output(cmd, shell=True) ret = subprocess.check_output(cmd, shell=True)
if sys.version_info >= (3,): return uni_decode(ret).rstrip()
ret = ret.decode(locale.getpreferredencoding(), 'replace')
return str(ret).rstrip()
else: else:
def _sh_call(cmd): def _sh_call(cmd):
import subprocess import subprocess
ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout.read() ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout.read()
return str(ret).rstrip() return uni_decode(ret).rstrip()
def _getSysPythonVersion(): def _getSysPythonVersion():
return _sh_call("fail2ban-python -c 'import sys; print(tuple(sys.version_info))'") return _sh_call("fail2ban-python -c 'import sys; print(tuple(sys.version_info))'")

View File

@ -28,7 +28,6 @@ import unittest
import time import time
import tempfile import tempfile
import os import os
import locale
import sys import sys
import platform import platform
@ -40,7 +39,7 @@ from ..server.jail import Jail
from ..server.jailthread import JailThread from ..server.jailthread import JailThread
from ..server.utils import Utils from ..server.utils import Utils
from .utils import LogCaptureTestCase from .utils import LogCaptureTestCase
from ..helpers import getLogger from ..helpers import getLogger, PREFER_ENC
from .. import version from .. import version
try: try:
@ -354,7 +353,7 @@ class Transmitter(TransmitterBase):
def testJailLogEncoding(self): def testJailLogEncoding(self):
self.setGetTest("logencoding", "UTF-8", jail=self.jailName) self.setGetTest("logencoding", "UTF-8", jail=self.jailName)
self.setGetTest("logencoding", "ascii", jail=self.jailName) self.setGetTest("logencoding", "ascii", jail=self.jailName)
self.setGetTest("logencoding", "auto", locale.getpreferredencoding(), self.setGetTest("logencoding", "auto", PREFER_ENC,
jail=self.jailName) jail=self.jailName)
self.setGetTestNOK("logencoding", "Monkey", jail=self.jailName) self.setGetTestNOK("logencoding", "Monkey", jail=self.jailName)