diff --git a/dkimpy_milter/__init__.py b/dkimpy_milter/__init__.py index 1eb95a6..b8a9073 100644 --- a/dkimpy_milter/__init__.py +++ b/dkimpy_milter/__init__.py @@ -30,7 +30,7 @@ import os import tempfile import StringIO import re -from Milter.utils import parse_addr,parseaddr +from Milter.utils import parse_addr, parseaddr import dkimpy_milter.config as config from dkimpy_milter.util import drop_privileges from dkimpy_milter.util import setExceptHook @@ -41,227 +41,247 @@ from dkimpy_milter.util import fold __version__ = "0.9.5" FWS = re.compile(r'\r?\n[ \t]+') - + + class dkimMilter(Milter.Base): - "Milter to check and sign DKIM. Each connection gets its own instance." + "Milter to check and sign DKIM. Each connection gets its own instance." - def __init__(self): - self.mailfrom = None - self.id = Milter.uniqueID() - # we don't want config used to change during a connection - self.conf = milterconfig - self.privatersa = privateRSA - self.privateed25519 = privateEd25519 - self.fp = None + def __init__(self): + self.mailfrom = None + self.id = Milter.uniqueID() + # we don't want config used to change during a connection + self.conf = milterconfig + self.privatersa = privateRSA + self.privateed25519 = privateEd25519 + self.fp = None - @Milter.noreply - def connect(self,hostname,unused,hostaddr): - self.internal_connection = False - self.external_connection = False - self.hello_name = None - # sometimes people put extra space in sendmail config, so we strip - self.receiver = self.getsymval('j').strip() - try: - self.AuthservID = milterconfig['AuthservID'] - except: - self.AuthservID = self.receiver - if hostaddr and len(hostaddr) > 0: - ipaddr = hostaddr[0] - if milterconfig['InternalHostsObj']: - if milterconfig['InternalHostsObj'].match(ipaddr): - self.internal_connection = True - else: ipaddr = '' - self.connectip = ipaddr - if milterconfig.get('MacroList') and not self.internal_connection: - macrolist = milterconfig.get('MacroList') - for macro in macrolist: - macroname = macro.split('|')[0] - macroname = '{' + macroname + '}' - macroresult = self.getsymval(macroname) - if (len(macro.split('|')) == 1 and macroresult) or macroresult in \ - macro.split('|')[1:]: - self.internal_connection = True - if milterconfig.get('MacroListVerify'): - macrolist = milterconfig.get('MacroListVerify') - for macro in macrolist: - macroname = macro.split('|')[0] - macroname = '{' + macroname + '}' - macroresult = self.getsymval(macroname) - if (len(macro.split('|')) == 1 and macroresult) or macroresult in \ - macro.split('|')[1:]: - self.external_connection = True - if self.internal_connection: - connecttype = 'INTERNAL' - else: - connecttype = 'EXTERNAL' - if milterconfig.get('Syslog'): - syslog.syslog("connect from {0} at {1} {2}".format(hostname,hostaddr,connecttype)) - return Milter.CONTINUE - - # multiple messages can be received on a single connection - # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start - # of each message. - @Milter.noreply - def envfrom(self,f,*str): - if milterconfig.get('Syslog'): - syslog.syslog("mail from: {0} {1}".format(f,str)) - self.fp = StringIO.StringIO() - self.mailfrom = f - t = parse_addr(f) - if len(t) == 2: t[1] = t[1].lower() - self.canon_from = '@'.join(t) - self.user = self.getsymval('{auth_authen}') - self.has_dkim = 0 - self.author = None - self.arheaders = [] - self.arresults = [] - return Milter.CONTINUE - - @Milter.noreply - def header(self,name,val): - lname = name.lower() - if lname == 'dkim-signature': + @Milter.noreply + def connect(self, hostname, unused, hostaddr): + self.internal_connection = False + self.external_connection = False + self.hello_name = None + # sometimes people put extra space in sendmail config, so we strip + self.receiver = self.getsymval('j').strip() + try: + self.AuthservID = milterconfig['AuthservID'] + except: + self.AuthservID = self.receiver + if hostaddr and len(hostaddr) > 0: + ipaddr = hostaddr[0] + if milterconfig['InternalHostsObj']: + if milterconfig['InternalHostsObj'].match(ipaddr): + self.internal_connection = True + else: + ipaddr = '' + self.connectip = ipaddr + if milterconfig.get('MacroList') and not self.internal_connection: + macrolist = milterconfig.get('MacroList') + for macro in macrolist: + macroname = macro.split('|')[0] + macroname = '{' + macroname + '}' + macroresult = self.getsymval(macroname) + if ((len(macro.split('|')) == 1 and macroresult) or macroresult + in macro.split('|')[1:]): + self.internal_connection = True + if milterconfig.get('MacroListVerify'): + macrolist = milterconfig.get('MacroListVerify') + for macro in macrolist: + macroname = macro.split('|')[0] + macroname = '{' + macroname + '}' + macroresult = self.getsymval(macroname) + if ((len(macro.split('|')) == 1 and macroresult) or macroresult + in macro.split('|')[1:]): + self.external_connection = True + if self.internal_connection: + connecttype = 'INTERNAL' + else: + connecttype = 'EXTERNAL' if milterconfig.get('Syslog'): - syslog.syslog("{0}: {1}".format(name,val)) - self.has_dkim += 1 - if lname == 'from': - fname,self.author = parseaddr(val) - self.fdomain = self.author.split('@')[1] + syslog.syslog("connect from {0} at {1} {2}" + .format(hostname, hostaddr, connecttype)) + return Milter.CONTINUE + + # multiple messages can be received on a single connection + # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start + # of each message. + @Milter.noreply + def envfrom(self, f, *str): if milterconfig.get('Syslog'): - syslog.syslog("{0}: {1}".format(name,val)) - elif lname == 'authentication-results': - self.arheaders.append(val) - if self.fp: - self.fp.write("%s: %s\n" % (name,val)) - return Milter.CONTINUE + syslog.syslog("mail from: {0} {1}".format(f, str)) + self.fp = StringIO.StringIO() + self.mailfrom = f + t = parse_addr(f) + if len(t) == 2: + t[1] = t[1].lower() + self.canon_from = '@'.join(t) + self.has_dkim = 0 + self.author = None + self.arheaders = [] + self.arresults = [] + return Milter.CONTINUE - @Milter.noreply - def eoh(self): - if self.fp: - self.fp.write("\n") # terminate headers - self.bodysize = 0 - return Milter.CONTINUE + @Milter.noreply + def header(self, name, val): + lname = name.lower() + if lname == 'dkim-signature': + if milterconfig.get('Syslog'): + syslog.syslog("{0}: {1}".format(name, val)) + self.has_dkim += 1 + if lname == 'from': + fname, self.author = parseaddr(val) + self.fdomain = self.author.split('@')[1] + if milterconfig.get('Syslog'): + syslog.syslog("{0}: {1}".format(name, val)) + elif lname == 'authentication-results': + self.arheaders.append(val) + if self.fp: + self.fp.write("%s: %s\n" % (name, val)) + return Milter.CONTINUE - @Milter.noreply - def body(self,chunk): # copy body to temp file - if self.fp: - self.fp.write(chunk) # IOError causes TEMPFAIL in milter - self.bodysize += len(chunk) - return Milter.CONTINUE + @Milter.noreply + def eoh(self): + if self.fp: + self.fp.write("\n") # terminate headers + self.bodysize = 0 + return Milter.CONTINUE - def eom(self): - if not self.fp: - return Milter.ACCEPT # no message collected - so no eom processing - # Remove existing Authentication-Results headers for our authserv_id - for i,val in enumerate(self.arheaders,1): - # FIXME: don't delete A-R headers from trusted MTAs - try: - ar = authres.AuthenticationResultsHeader.parse_value(FWS.sub('',val)) - if ar.authserv_id == self.AuthservID: - self.chgheader('authentication-results',i,'') - if milterconfig.get('Syslog'): - syslog.syslog('REMOVE: {0}'.format(val)) - except: - # Don't error out on unparseable AR header fiels - pass - # Check or sign DKIM - self.fp.seek(0) - if milterconfig.get('Domain'): - domain = milterconfig.get('Domain') - else: - domain = '' - if (self.fdomain in domain) and not milterconfig.get('Mode') == 'v' and not self.external_connection: - txt = self.fp.read() - self.sign_dkim(txt) - if (self.has_dkim) and (not self.internal_connection) and (milterconfig.get('Mode') == 'v' or milterconfig.get('Mode') == 'sv'): - txt = self.fp.read() - self.check_dkim(txt) - if self.arresults: - h = authres.AuthenticationResultsHeader(authserv_id = self.AuthservID, - results=self.arresults) - h = fold(str(h)) - if milterconfig.get('Syslog'): - syslog.syslog(str(h)) - name,val = str(h).split(': ',1) - self.addheader(name,val,0) - return Milter.CONTINUE + @Milter.noreply + def body(self, chunk): # copy body to temp file + if self.fp: + self.fp.write(chunk) # IOError causes TEMPFAIL in milter + self.bodysize += len(chunk) + return Milter.CONTINUE - def sign_dkim(self,txt): - canon = milterconfig.get('Canonicalization') - canonicalize = [] - if len(canon.split('/')) == 2: - canonicalize.append(canon.split('/')[0]) - canonicalize.append(canon.split('/')[1]) - else: - canonicalize.append(canon) - canonicalize.append(canon) - syslog.syslog('canonicalize: {0}'.format(canonicalize)) - try: - if privateRSA: + def eom(self): + if not self.fp: + return Milter.ACCEPT # no message collected - so no eom processing + # Remove existing Authentication-Results headers for our authserv_id + for i, val in enumerate(self.arheaders, 1): + # FIXME: don't delete A-R headers from trusted MTAs + try: + ar = (authres.AuthenticationResultsHeader + .parse_value(FWS.sub('', val))) + if ar.authserv_id == self.AuthservID: + self.chgheader('authentication-results', i, '') + if milterconfig.get('Syslog'): + syslog.syslog('REMOVE: {0}'.format(val)) + except: + # Don't error out on unparseable AR header fiels + pass + # Check or sign DKIM + self.fp.seek(0) + if milterconfig.get('Domain'): + domain = milterconfig.get('Domain') + else: + domain = '' + if ((self.fdomain in domain) and not milterconfig.get('Mode') == 'v' + and not self.external_connection): + txt = self.fp.read() + self.sign_dkim(txt) + if ((self.has_dkim) and (not self.internal_connection) and + (milterconfig.get('Mode') == 'v' or + milterconfig.get('Mode') == 'sv')): + txt = self.fp.read() + self.check_dkim(txt) + if self.arresults: + h = authres.AuthenticationResultsHeader(authserv_id= + self.AuthservID, + results=self.arresults) + h = fold(str(h)) + if milterconfig.get('Syslog'): + syslog.syslog(str(h)) + name, val = str(h).split(': ', 1) + self.addheader(name, val, 0) + return Milter.CONTINUE + + def sign_dkim(self, txt): + canon = milterconfig.get('Canonicalization') + canonicalize = [] + if len(canon.split('/')) == 2: + canonicalize.append(canon.split('/')[0]) + canonicalize.append(canon.split('/')[1]) + else: + canonicalize.append(canon) + canonicalize.append(canon) + syslog.syslog('canonicalize: {0}'.format(canonicalize)) + try: + if privateRSA: + d = dkim.DKIM(txt) + h = d.sign(milterconfig.get('Selector'), self.fdomain, + privateRSA, canonicalize=(canonicalize[0], + canonicalize[1])) + name, val = h.split(': ', 1) + self.addheader(name, val.strip().replace('\r\n', '\n'), 0) + if privateEd25519: + d = dkim.DKIM(txt) + h = d.sign(milterconfig.get('SelectorEd25519'), self.fdomain, + privateEd25519, canonicalize=(canonicalize[0], + canonicalize[1]), + signature_algorithm='ed25519-sha256') + name, val = h.split(': ', 1) + self.addheader(name, val.strip().replace('\r\n', '\n'), 0) + except dkim.DKIMException as x: + if milterconfig.get('Syslog'): + syslog.syslog('DKIM: {0}'.format(x)) + except Exception as x: + if milterconfig.get('Syslog'): + syslog.syslog("sign_dkim: {0}".format(x)) + raise + + def check_dkim(self, txt): + res = False + for y in range(self.has_dkim): # Verify _ALL_ the signatures d = dkim.DKIM(txt) - h = d.sign(milterconfig.get('Selector'), self.fdomain, privateRSA, - canonicalize=(canonicalize[0], canonicalize[1])) - name,val = h.split(': ',1) - self.addheader(name,val.strip().replace('\r\n','\n'),0) - if privateEd25519: - d = dkim.DKIM(txt) - h = d.sign(milterconfig.get('SelectorEd25519'), self.fdomain, privateEd25519, - canonicalize=(canonicalize[0], canonicalize[1]), signature_algorithm='ed25519-sha256') - name,val = h.split(': ',1) - self.addheader(name,val.strip().replace('\r\n','\n'),0) - except dkim.DKIMException as x: - if milterconfig.get('Syslog'): - syslog.syslog('DKIM: {0}'.format(x)) - except Exception as x: - if milterconfig.get('Syslog'): - syslog.syslog("sign_dkim: {0}".format(x)) - raise - - def check_dkim(self,txt): - res = False - for y in range(self.has_dkim): # Verify _ALL_ the signatures - d = dkim.DKIM(txt) - try: - res = d.verify(idx=y) - if res: - self.dkim_comment = 'Good {0} bit {1} signature.'.format(d.keysize, d.signature_fields.get(b'a')) - else: - self.dkim_comment = 'Bad {0} bit {1} signature.'.format(d.keysize, d.signature_fields.get(b'a')) - except dkim.DKIMException as x: - self.dkim_comment = str(x) - if milterconfig.get('Syslog'): - syslog.syslog('DKIM: {0}'.format(x)) - except Exception as x: - self.dkim_comment = str(x) - if milterconfig.get('Syslog'): - syslog.syslog("check_dkim: {0}".format(x)) - self.header_i = d.signature_fields.get(b'i') - self.header_d = d.signature_fields.get(b'd') - self.header_a = d.signature_fields.get(b'a') - if res: - if milterconfig.get('Syslog'): - syslog.syslog('DKIM: Pass ({0})'.format(d.domain)) - self.dkim_domain = d.domain - else: - if milterconfig.get('DiagnosticDirectory'): - fd,fname = tempfile.mkstemp(".dkim") - with os.fdopen(fd,"w+b") as fp: - fp.write(txt) - if milterconfig.get('Syslog'): - syslog.syslog('DKIM: Fail (saved as {0})'.format(fname)) - else: - syslog.syslog('DKIM: Fail ({0})'.format(d.domain)) - if res: - result = 'pass' - else: - result = 'fail' - res = False - self.arresults.append( - authres.DKIMAuthenticationResult(result=result, - header_i = self.header_i, header_d = self.header_d, header_a = self.header_a, - result_comment = self.dkim_comment) - ) - return + try: + res = d.verify(idx=y) + if res: + self.dkim_comment = ('Good {0} bit {1} signature.' + .format(d.keysize, + d.signature_fields.get(b'a'))) + else: + self.dkim_comment = ('Bad {0} bit {1} signature.' + .format(d.keysize, + d.signature_fields.get(b'a'))) + except dkim.DKIMException as x: + self.dkim_comment = str(x) + if milterconfig.get('Syslog'): + syslog.syslog('DKIM: {0}'.format(x)) + except Exception as x: + self.dkim_comment = str(x) + if milterconfig.get('Syslog'): + syslog.syslog("check_dkim: {0}".format(x)) + self.header_i = d.signature_fields.get(b'i') + self.header_d = d.signature_fields.get(b'd') + self.header_a = d.signature_fields.get(b'a') + if res: + if milterconfig.get('Syslog'): + syslog.syslog('DKIM: Pass ({0})'.format(d.domain)) + self.dkim_domain = d.domain + else: + if milterconfig.get('DiagnosticDirectory'): + fd, fname = tempfile.mkstemp(".dkim") + with os.fdopen(fd, "w+b") as fp: + fp.write(txt) + if milterconfig.get('Syslog'): + syslog.syslog('DKIM: Fail (saved as {0})' + .format(fname)) + else: + syslog.syslog('DKIM: Fail ({0})'.format(d.domain)) + if res: + result = 'pass' + else: + result = 'fail' + res = False + self.arresults.append( + authres.DKIMAuthenticationResult(result=result, + header_i=self.header_i, + header_d=self.header_d, + header_a=self.header_a, + result_comment= + self.dkim_comment) + ) + return + def main(): # Ugh, but there's no easy way around this. @@ -272,13 +292,14 @@ def main(): privateEd25519 = False configFile = '/etc/dkimpy-milter.conf' if len(sys.argv) > 1: - if sys.argv[1] in ( '-?', '--help', '-h' ): + if sys.argv[1] in ('-?', '--help', '-h'): print('usage: dkimpy-milter []') sys.exit(1) configFile = sys.argv[1] - milterconfig = config._processConfigFile(filename = configFile) + milterconfig = config._processConfigFile(filename=configFile) if milterconfig.get('Syslog'): - facility = eval("syslog.LOG_{0}".format(milterconfig.get('SyslogFacility').upper())) + facility = eval("syslog.LOG_{0}" + .format(milterconfig.get('SyslogFacility').upper())) syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, facility) setExceptHook() pid = write_pid(milterconfig) @@ -291,9 +312,10 @@ def main(): miltername = 'dkimpy-filter' socketname = milterconfig.get('Socket') if milterconfig.get('Syslog'): - syslog.syslog('dkimpy-milter started:{0} user:{1}'.format(pid,milterconfig.get('UserID'))) + syslog.syslog('dkimpy-milter started:{0} user:{1}' + .format(pid, milterconfig.get('UserID'))) sys.stdout.flush() - Milter.runmilter(miltername,socketname,240) + Milter.runmilter(miltername, socketname, 240) own_socketfile(milterconfig) drop_privileges(milterconfig) diff --git a/dkimpy_milter/config.py b/dkimpy_milter/config.py index 5fb9b6e..0d1ff1a 100644 --- a/dkimpy_milter/config.py +++ b/dkimpy_milter/config.py @@ -12,7 +12,7 @@ # 2.0 license - 100% GPL ''' This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License version 2 as published + it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, @@ -36,28 +36,27 @@ from dnsplug import Session # default values defaultConfigData = { - 'Syslog' : 'yes', - 'SyslogFacility' : 'mail', - 'UMask' : 007, - 'Mode' : 'sv', - 'Socket' : 'local:/var/run/dkimpy-milter/dkimpy-milter.sock', - 'PidFile' : '/var/run/dkimpy-milter/dkimpy-milter.pid', - 'UserID' : 'dkimpy-milter', - 'Canonicalization' : 'relaxed/simple', - 'InternalHosts' : '127.0.0.1', - 'InternalHostsObj' : False, - 'DiagnosticDirectory' : '', - 'MacroList' : '', - 'MacroListVerify' : '' - } + 'Syslog': 'yes', + 'SyslogFacility': 'mail', + 'UMask': 007, + 'Mode': 'sv', + 'Socket': 'local:/var/run/dkimpy-milter/dkimpy-milter.sock', + 'PidFile': '/var/run/dkimpy-milter/dkimpy-milter.pid', + 'UserID': 'dkimpy-milter', + 'Canonicalization': 'relaxed/simple', + 'InternalHosts': '127.0.0.1', + 'InternalHostsObj': False, + 'DiagnosticDirectory': '', + 'MacroList': '', + 'MacroListVerify': '' + } -################################# class ConfigException(Exception): '''Exception raised when there's a configuration file error.''' pass -################################# + class HostsDataset(object): '''Hold a group of host related dataset objects''' @@ -86,34 +85,41 @@ class HostsDataset(object): self.negative = True try: self.item = ipaddress.ip_address(unicode(self.item, "utf-8")) - if isinstance(self.item, ipaddress.IPv4Address): self.isipv4 = True - elif isinstance(self.item, ipaddress.IPv6Address): self.isipv6 = True + if isinstance(self.item, ipaddress.IPv4Address): + self.isipv4 = True + elif isinstance(self.item, ipaddress.IPv6Address): + self.isipv6 = True except ValueError as e: try: - self.item = ipaddress.ip_network(unicode(self.item, "utf-8"), strict=False) - if isinstance(self.item, ipaddress.IPv4Network): self.isipv4cidr = True - elif isinstance(self.item, ipaddress.IPv6Network): self.isipv6cidr = True + self.item = ipaddress.ip_network(unicode + (self.item, "utf-8"), + strict=False) + if isinstance(self.item, ipaddress.IPv4Network): + self.isipv4cidr = True + elif isinstance(self.item, ipaddress.IPv6Network): + self.isipv6cidr = True except ValueError as e2: if self.item[0] == '.' and len(self.item.split('.')) > 2: self.isdomain = True - elif len(self.item.split('.')) > 1: # It has a '.' in it + elif len(self.item.split('.')) > 1: # It has a '.' in it self.ishostname = True else: - raise ConfigException('Unknown dataset item: {0}'.format(item)) + raise ConfigException('Unknown dataset item: {0}' + .format(item)) def match(self, connectip): '''Check if the connect IP is part of the dataset''' source = ipaddress.ip_address(unicode(connectip, "utf-8")) for item in self.dataset: if item.isdomain or item.ishostname: - result = self.matchname(source) # Match host/domain names first + result = self.matchname(source) # Match host/domains first if result: return(result) - elif item.isipv4 or item.isipv4cidr: - if isinstance(source, ipaddress.IPv4Address): # Then IPv4/6 addresses - return(self.match4(source)) # or networks depending - elif item.isipv6 or item.isipv6cidr: # on the item type and - if isinstance(source, ipaddress.IPv6Address): # connection type + elif item.isipv4 or item.isipv4cidr: # Then IPv4/6 addresses or + if isinstance(source, ipaddress.IPv4Address): # networks + return(self.match4(source)) # depending on the item type + elif item.isipv6 or item.isipv6cidr: # and connect type + if isinstance(source, ipaddress.IPv6Address): return(self.match6(source)) def matchname(self, source): @@ -127,7 +133,7 @@ class HostsDataset(object): for item in self.dataset: if item.isdomain: for ptr in ptrlist: - # Strip the leading '.' off the domain name so exact match works. + # Strip the leading '.' off the domain name for exact match if item.item[1:] == ptr[-len(item.item)+1:]: matchdomain = True negativedomain = item.negative @@ -212,21 +218,16 @@ class HostsDataset(object): match = False return(match) - def dump(self): - for item in self.dataset: - print 'name: {0} ip4: {1} cidr4: {2} ip6: {3} cidr6: {4} host: {5} domain: {6} negat: {7} type: {8}'.format(item.item, - item.isipv4, item.isipv4cidr, item.isipv6, item.isipv6cidr, item.ishostname, item.isdomain, - item.negative, type(item.item)) -#################################################################### -def _processConfigFile(filename = None, configdata = None, useSyslog = 1, - useStderr = 0): +def _processConfigFile(filename=None, configdata=None, useSyslog=1, + useStderr=0): '''Load the specified config file, exit and log errors if it fails, otherwise return a config dictionary.''' import config - if configdata == None: configdata = config.defaultConfigData - if filename != None: + if configdata is None: + configdata = config.defaultConfigData + if filename is not None: try: _readConfigFile(filename, configdata) except Exception, e: @@ -238,7 +239,7 @@ def _processConfigFile(filename = None, configdata = None, useSyslog = 1, sys.exit(1) return(configdata) -#################### + def _find_boolean(item): if type(item) == int: item = str(item) @@ -249,14 +250,15 @@ def _find_boolean(item): else: raise dkim.ParameterError() return item -#################### + + def _calculate_authserv_id(as_id): """Determine AuthservID if needed""" if as_id == 'HOSTNAME': as_id = socket.gethostname() return as_id -#################### + def _dataset_to_list(dataset): """Convert a dataset (as defined in dkimpymilter.8) and return a python list of values.""" @@ -292,80 +294,89 @@ def _dataset_to_list(dataset): else: return [dataset.strip().strip(',')] if dataset[-3:] == '.db' or dataset[:3] == 'db:': - # This is a Sleepycat (Oracle) DB dataset - import whichdb # Will need rewriting someday for python3 + # This is a Sleepycat (Oracle) DB dataset + import whichdb # Will need rewriting someday for python3 if dataset[-3:] == '.db': dbname = dataset elif dataset[:3] == 'db:': dbname = dataset[3:] else: - raise dkim.ParameterError('Unimplmented dataset type: {0}'.format(type(dataset))) + raise dkim.ParameterError('Unimplmented dataset type: {0}' + .format(type(dataset))) if whichdb.whichdb(dbname) != 'dbhash': - raise dkim.ParameterError('Unimplmented dataset type: {0}'.format(type(dataset))) + raise dkim.ParameterError('Unimplmented dataset type: {0}' + .format(type(dataset))) #TODO replace this with code to use db maps - raise dkim.ParameterError('Unsupported dataset db dataset not yet used: {0}'.format(type(dataset))) + raise dkim.ParameterError('Unsupported dataset db datase: {0}' + .format(type(dataset))) - raise dkim.ParameterError('Unimplmented dataset type: {0}'.format(type(dataset))) + raise dkim.ParameterError('Unimplmented dataset type: {0}' + .format(type(dataset))) -############################################################### -commentRx = re.compile(r'^(.*)#.*$') -def _readConfigFile(path, configData = None, configGlobal = {}): + +def _readConfigFile(path, configData=None, configGlobal={}): '''Reads a configuration file from the specified path, merging it with the configuration data specified in configData. Returns a dictionary of name/value pairs based on configData and the values read from path.''' debugLevel = configGlobal.get('debugLevel', 0) - if debugLevel >= 5: syslog.syslog('readConfigFile: Loading "%s"' % path) - if configData == None: configData = {} + if debugLevel >= 5: + syslog.syslog('readConfigFile: Loading "%s"' % path) + if configData is None: + configData = {} nameConversion = { - 'AuthservID' : 'str', - 'Syslog' : 'bool', - 'SyslogFacility' : 'str', - 'SyslogSuccess' : 'bool', - 'UMask' : 'int', - 'Mode' : 'str', - 'Socket' : 'str', - 'PidFile' : 'str', - 'UserID' : 'str', - 'Domain' : 'dataset', - 'KeyFile' : 'str', - 'KeyFileEd25519' : 'str', - 'Selector' : 'str', + 'AuthservID': 'str', + 'Syslog': 'bool', + 'SyslogFacility': 'str', + 'SyslogSuccess': 'bool', + 'UMask': 'int', + 'Mode': 'str', + 'Socket': 'str', + 'PidFile': 'str', + 'UserID': 'str', + 'Domain': 'dataset', + 'KeyFile': 'str', + 'KeyFileEd25519': 'str', + 'Selector': 'str', 'SelectorEd25519': 'str', - 'Canonicalization' : 'str', - 'InternalHosts' : 'dataset', + 'Canonicalization': 'str', + 'InternalHosts': 'dataset', 'InternalHostsObj': 'bool', - 'DiagnosticDirectory' : 'str', - 'MacroList' : 'dataset', - 'MacroListVerify' : 'dataset' - } + 'DiagnosticDirectory': 'str', + 'MacroList': 'dataset', + 'MacroListVerify': 'dataset' + } # check to see if it's a file try: mode = os.stat(path)[0] except OSError, e: - syslog.syslog(syslog.LOG_ERR,'ERROR stating "%s": %s' % ( path, e.strerror )) + syslog.syslog(syslog.LOG_ERR, 'ERROR stating "%s": %s' + % (path, e.strerror)) return(configData) if not stat.S_ISREG(mode): - syslog.syslog(syslog.LOG_ERR,'ERROR: is not a file: "%s", mode=%s' % ( path, oct(mode) )) + syslog.syslog(syslog.LOG_ERR, 'ERROR: is not a file: "%s", mode=%s' + % (path, oct(mode))) return(configData) # load file fp = open(path, 'r') while 1: line = fp.readline() - if not line: break + if not line: + break # parse line line = line.split('#', 1)[0].strip() - if not line: continue + if not line: + continue data = line.split() if len(data) != 2: if len(data) == 1: if debugLevel >= 1: - syslog.syslog('Configuration item "%s" not defined in file "%s"' - % ( line, path )) + syslog.syslog('Config item "%s" not defined in file "%s"' + % (line, path)) if len(data) == 1: name = data value = '' @@ -377,12 +388,14 @@ def _readConfigFile(path, configData = None, configGlobal = {}): # check validity of name conversion = nameConversion.get(name) - if conversion == None: - syslog.syslog('ERROR: Unknown name "%s" in file "%s"' % ( name, path )) + if conversion is None: + syslog.syslog('ERROR: Unknown name "%s" in file "%s"' + % (name, path)) continue - if debugLevel >= 5: syslog.syslog('readConfigFile: Found entry "%s=%s"' - % ( name, value )) + if debugLevel >= 5: + syslog.syslog('readConfigFile: Found entry "%s=%s"' + % (name, value)) if conversion == 'bool': configData[name] = _find_boolean(value) elif conversion == 'str': @@ -392,12 +405,15 @@ def _readConfigFile(path, configData = None, configGlobal = {}): elif conversion == 'dataset': configData[name] = _dataset_to_list(value) else: - syslog.syslog(str('name: ' + name + ' value: ' + value + ' conversion: ' + conversion)) + syslog.syslog(str('name: ' + name + ' value: ' + value + + ' conversion: ' + conversion)) configData[name] = conversion(value) fp.close() try: - configData['AuthservID'] = _calculate_authserv_id(configData['AuthservID']) - configData['InternalHostsObj'] = HostsDataset(configData['InternalHosts']) + configData['AuthservID'] = _calculate_authserv_id(configData + ['AuthservID']) + configData['InternalHostsObj'] = HostsDataset(configData + ['InternalHosts']) except: pass diff --git a/dkimpy_milter/util.py b/dkimpy_milter/util.py index 060eb8e..5d3f69d 100644 --- a/dkimpy_milter/util.py +++ b/dkimpy_milter/util.py @@ -16,6 +16,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + def fold(header): """Fold a header line into multiple crlf-separated lines at column 72. Borrowed from dkimpy and updated to only add \n instead of \r\n because @@ -48,6 +49,7 @@ def fold(header): header = header[j:] return pre + header + def user_group(userid): """Return user and group from UserID""" import grp @@ -63,13 +65,14 @@ def user_group(userid): running_gid = grp.getgrnam(gidname).gr_gid return running_uid, running_gid + def drop_privileges(milterconfig): import os import syslog if os.getuid() != 0: if milterconfig.get('Syslog'): - syslog.syslog('drop_privileges: Not running as root. Cannot drop permissions.') + syslog.syslog('drop_privileges: Not root. No action taken.') return # Get user and group @@ -85,9 +88,9 @@ def drop_privileges(milterconfig): # Set umask old_umask = os.umask(milterconfig.get('UMask')) -################# + class ExceptHook: - def __init__(self, useSyslog = 1, useStderr = 0): + def __init__(self, useSyslog=1, useStderr=0): self.useSyslog = useSyslog self.useStderr = useStderr @@ -103,12 +106,11 @@ class ExceptHook: sys.stderr.write(line) -#################### def setExceptHook(): import sys - sys.excepthook = ExceptHook(useSyslog = 1, useStderr = 1) + sys.excepthook = ExceptHook(useSyslog=1, useStderr=1) + -#################### def write_pid(milterconfig): """Write PID in pidfile. Will not overwrite an existing file.""" import os @@ -125,10 +127,11 @@ def write_pid(milterconfig): os.chown(piddir, user, group) f = open(milterconfig.get('PidFile'), 'w') if milterconfig.get('Syslog'): - syslog.syslog('Missing pid dir created: {0}'.format(piddir)) + syslog.syslog('PID dir created: {0}'.format(piddir)) else: if milterconfig.get('Syslog'): - syslog.syslog('Unable to write pidfle {0}. IOError: {1}'.format(milterconfig.get('PidFile'), e)) + syslog.syslog('Unable to write pidfle {0}. IOError: {1}' + .format(milterconfig.get('PidFile'), e)) raise f.write(pid) f.close() @@ -136,10 +139,13 @@ def write_pid(milterconfig): os.chown(milterconfig.get('PidFile'), user, group) else: if milterconfig.get('Syslog'): - syslog.syslog('Unable to write pidfle {0}. File exists.'.format(milterconfig.get('PidFile'))) - raise RuntimeError('Unable to write pidfle {0}. File exists.'.format(milterconfig.get('PidFile'))) + syslog.syslog('Unable to write pidfle {0}. File exists.' + .format(milterconfig.get('PidFile'))) + raise RuntimeError('Unable to write pidfle {0}. File exists.' + .format(milterconfig.get('PidFile'))) return pid + def own_socketfile(milterconfig): """If socket is Unix socket, chown to UserID before dropping privileges""" import os @@ -149,7 +155,7 @@ def own_socketfile(milterconfig): if milterconfig.get('Socket')[:6] == "local:": os.chown(milterconfig.get('Socket')[6:], user, group) -#################### + def read_keyfile(milterconfig, keytype): """Read private key from file.""" import syslog @@ -162,7 +168,8 @@ def read_keyfile(milterconfig, keytype): keylist = f.readlines() except IOError as e: if milterconfig.get('Syslog'): - syslog.syslog('Unable to read keyfile {0}. IOError: {1}'.format(keyfile, e)) + syslog.syslog('Unable to read keyfile {0}. IOError: {1}' + .format(keyfile, e)) raise f.close() key = ''