pep8 and a few other cleanups

This commit is contained in:
Scott Kitterman
2018-03-10 02:45:35 -05:00
parent 6348bdcdc7
commit 70606ac58c
3 changed files with 361 additions and 316 deletions
+237 -215
View File
@@ -30,7 +30,7 @@ import os
import tempfile import tempfile
import StringIO import StringIO
import re import re
from Milter.utils import parse_addr,parseaddr from Milter.utils import parse_addr, parseaddr
import dkimpy_milter.config as config import dkimpy_milter.config as config
from dkimpy_milter.util import drop_privileges from dkimpy_milter.util import drop_privileges
from dkimpy_milter.util import setExceptHook from dkimpy_milter.util import setExceptHook
@@ -42,226 +42,246 @@ from dkimpy_milter.util import fold
__version__ = "0.9.5" __version__ = "0.9.5"
FWS = re.compile(r'\r?\n[ \t]+') FWS = re.compile(r'\r?\n[ \t]+')
class dkimMilter(Milter.Base): 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): def __init__(self):
self.mailfrom = None self.mailfrom = None
self.id = Milter.uniqueID() self.id = Milter.uniqueID()
# we don't want config used to change during a connection # we don't want config used to change during a connection
self.conf = milterconfig self.conf = milterconfig
self.privatersa = privateRSA self.privatersa = privateRSA
self.privateed25519 = privateEd25519 self.privateed25519 = privateEd25519
self.fp = None self.fp = None
@Milter.noreply @Milter.noreply
def connect(self,hostname,unused,hostaddr): def connect(self, hostname, unused, hostaddr):
self.internal_connection = False self.internal_connection = False
self.external_connection = False self.external_connection = False
self.hello_name = None self.hello_name = None
# sometimes people put extra space in sendmail config, so we strip # sometimes people put extra space in sendmail config, so we strip
self.receiver = self.getsymval('j').strip() self.receiver = self.getsymval('j').strip()
try: try:
self.AuthservID = milterconfig['AuthservID'] self.AuthservID = milterconfig['AuthservID']
except: except:
self.AuthservID = self.receiver self.AuthservID = self.receiver
if hostaddr and len(hostaddr) > 0: if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0] ipaddr = hostaddr[0]
if milterconfig['InternalHostsObj']: if milterconfig['InternalHostsObj']:
if milterconfig['InternalHostsObj'].match(ipaddr): if milterconfig['InternalHostsObj'].match(ipaddr):
self.internal_connection = True self.internal_connection = True
else: ipaddr = '' else:
self.connectip = ipaddr ipaddr = ''
if milterconfig.get('MacroList') and not self.internal_connection: self.connectip = ipaddr
macrolist = milterconfig.get('MacroList') if milterconfig.get('MacroList') and not self.internal_connection:
for macro in macrolist: macrolist = milterconfig.get('MacroList')
macroname = macro.split('|')[0] for macro in macrolist:
macroname = '{' + macroname + '}' macroname = macro.split('|')[0]
macroresult = self.getsymval(macroname) macroname = '{' + macroname + '}'
if (len(macro.split('|')) == 1 and macroresult) or macroresult in \ macroresult = self.getsymval(macroname)
macro.split('|')[1:]: if ((len(macro.split('|')) == 1 and macroresult) or macroresult
self.internal_connection = True in macro.split('|')[1:]):
if milterconfig.get('MacroListVerify'): self.internal_connection = True
macrolist = milterconfig.get('MacroListVerify') if milterconfig.get('MacroListVerify'):
for macro in macrolist: macrolist = milterconfig.get('MacroListVerify')
macroname = macro.split('|')[0] for macro in macrolist:
macroname = '{' + macroname + '}' macroname = macro.split('|')[0]
macroresult = self.getsymval(macroname) macroname = '{' + macroname + '}'
if (len(macro.split('|')) == 1 and macroresult) or macroresult in \ macroresult = self.getsymval(macroname)
macro.split('|')[1:]: if ((len(macro.split('|')) == 1 and macroresult) or macroresult
self.external_connection = True in macro.split('|')[1:]):
if self.internal_connection: self.external_connection = True
connecttype = 'INTERNAL' if self.internal_connection:
else: connecttype = 'INTERNAL'
connecttype = 'EXTERNAL' else:
if milterconfig.get('Syslog'): connecttype = 'EXTERNAL'
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':
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog("{0}: {1}".format(name,val)) syslog.syslog("connect from {0} at {1} {2}"
self.has_dkim += 1 .format(hostname, hostaddr, connecttype))
if lname == 'from': return Milter.CONTINUE
fname,self.author = parseaddr(val)
self.fdomain = self.author.split('@')[1] # 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'): if milterconfig.get('Syslog'):
syslog.syslog("{0}: {1}".format(name,val)) syslog.syslog("mail from: {0} {1}".format(f, str))
elif lname == 'authentication-results': self.fp = StringIO.StringIO()
self.arheaders.append(val) self.mailfrom = f
if self.fp: t = parse_addr(f)
self.fp.write("%s: %s\n" % (name,val)) if len(t) == 2:
return Milter.CONTINUE 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 @Milter.noreply
def eoh(self): def header(self, name, val):
if self.fp: lname = name.lower()
self.fp.write("\n") # terminate headers if lname == 'dkim-signature':
self.bodysize = 0 if milterconfig.get('Syslog'):
return Milter.CONTINUE 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 @Milter.noreply
def body(self,chunk): # copy body to temp file def eoh(self):
if self.fp: if self.fp:
self.fp.write(chunk) # IOError causes TEMPFAIL in milter self.fp.write("\n") # terminate headers
self.bodysize += len(chunk) self.bodysize = 0
return Milter.CONTINUE return Milter.CONTINUE
def eom(self): @Milter.noreply
if not self.fp: def body(self, chunk): # copy body to temp file
return Milter.ACCEPT # no message collected - so no eom processing if self.fp:
# Remove existing Authentication-Results headers for our authserv_id self.fp.write(chunk) # IOError causes TEMPFAIL in milter
for i,val in enumerate(self.arheaders,1): self.bodysize += len(chunk)
# FIXME: don't delete A-R headers from trusted MTAs return Milter.CONTINUE
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): def eom(self):
canon = milterconfig.get('Canonicalization') if not self.fp:
canonicalize = [] return Milter.ACCEPT # no message collected - so no eom processing
if len(canon.split('/')) == 2: # Remove existing Authentication-Results headers for our authserv_id
canonicalize.append(canon.split('/')[0]) for i, val in enumerate(self.arheaders, 1):
canonicalize.append(canon.split('/')[1]) # FIXME: don't delete A-R headers from trusted MTAs
else: try:
canonicalize.append(canon) ar = (authres.AuthenticationResultsHeader
canonicalize.append(canon) .parse_value(FWS.sub('', val)))
syslog.syslog('canonicalize: {0}'.format(canonicalize)) if ar.authserv_id == self.AuthservID:
try: self.chgheader('authentication-results', i, '')
if privateRSA: 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) d = dkim.DKIM(txt)
h = d.sign(milterconfig.get('Selector'), self.fdomain, privateRSA, try:
canonicalize=(canonicalize[0], canonicalize[1])) res = d.verify(idx=y)
name,val = h.split(': ',1) if res:
self.addheader(name,val.strip().replace('\r\n','\n'),0) self.dkim_comment = ('Good {0} bit {1} signature.'
if privateEd25519: .format(d.keysize,
d = dkim.DKIM(txt) d.signature_fields.get(b'a')))
h = d.sign(milterconfig.get('SelectorEd25519'), self.fdomain, privateEd25519, else:
canonicalize=(canonicalize[0], canonicalize[1]), signature_algorithm='ed25519-sha256') self.dkim_comment = ('Bad {0} bit {1} signature.'
name,val = h.split(': ',1) .format(d.keysize,
self.addheader(name,val.strip().replace('\r\n','\n'),0) d.signature_fields.get(b'a')))
except dkim.DKIMException as x: except dkim.DKIMException as x:
if milterconfig.get('Syslog'): self.dkim_comment = str(x)
syslog.syslog('DKIM: {0}'.format(x)) if milterconfig.get('Syslog'):
except Exception as x: syslog.syslog('DKIM: {0}'.format(x))
if milterconfig.get('Syslog'): except Exception as x:
syslog.syslog("sign_dkim: {0}".format(x)) self.dkim_comment = str(x)
raise 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 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
def main(): def main():
# Ugh, but there's no easy way around this. # Ugh, but there's no easy way around this.
@@ -272,13 +292,14 @@ def main():
privateEd25519 = False privateEd25519 = False
configFile = '/etc/dkimpy-milter.conf' configFile = '/etc/dkimpy-milter.conf'
if len(sys.argv) > 1: if len(sys.argv) > 1:
if sys.argv[1] in ( '-?', '--help', '-h' ): if sys.argv[1] in ('-?', '--help', '-h'):
print('usage: dkimpy-milter [<configfilename>]') print('usage: dkimpy-milter [<configfilename>]')
sys.exit(1) sys.exit(1)
configFile = sys.argv[1] configFile = sys.argv[1]
milterconfig = config._processConfigFile(filename = configFile) milterconfig = config._processConfigFile(filename=configFile)
if milterconfig.get('Syslog'): 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) syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, facility)
setExceptHook() setExceptHook()
pid = write_pid(milterconfig) pid = write_pid(milterconfig)
@@ -291,9 +312,10 @@ def main():
miltername = 'dkimpy-filter' miltername = 'dkimpy-filter'
socketname = milterconfig.get('Socket') socketname = milterconfig.get('Socket')
if milterconfig.get('Syslog'): 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() sys.stdout.flush()
Milter.runmilter(miltername,socketname,240) Milter.runmilter(miltername, socketname, 240)
own_socketfile(milterconfig) own_socketfile(milterconfig)
drop_privileges(milterconfig) drop_privileges(milterconfig)
+102 -86
View File
@@ -36,28 +36,27 @@ from dnsplug import Session
# default values # default values
defaultConfigData = { defaultConfigData = {
'Syslog' : 'yes', 'Syslog': 'yes',
'SyslogFacility' : 'mail', 'SyslogFacility': 'mail',
'UMask' : 007, 'UMask': 007,
'Mode' : 'sv', 'Mode': 'sv',
'Socket' : 'local:/var/run/dkimpy-milter/dkimpy-milter.sock', 'Socket': 'local:/var/run/dkimpy-milter/dkimpy-milter.sock',
'PidFile' : '/var/run/dkimpy-milter/dkimpy-milter.pid', 'PidFile': '/var/run/dkimpy-milter/dkimpy-milter.pid',
'UserID' : 'dkimpy-milter', 'UserID': 'dkimpy-milter',
'Canonicalization' : 'relaxed/simple', 'Canonicalization': 'relaxed/simple',
'InternalHosts' : '127.0.0.1', 'InternalHosts': '127.0.0.1',
'InternalHostsObj' : False, 'InternalHostsObj': False,
'DiagnosticDirectory' : '', 'DiagnosticDirectory': '',
'MacroList' : '', 'MacroList': '',
'MacroListVerify' : '' 'MacroListVerify': ''
} }
#################################
class ConfigException(Exception): class ConfigException(Exception):
'''Exception raised when there's a configuration file error.''' '''Exception raised when there's a configuration file error.'''
pass pass
#################################
class HostsDataset(object): class HostsDataset(object):
'''Hold a group of host related dataset objects''' '''Hold a group of host related dataset objects'''
@@ -86,34 +85,41 @@ class HostsDataset(object):
self.negative = True self.negative = True
try: try:
self.item = ipaddress.ip_address(unicode(self.item, "utf-8")) self.item = ipaddress.ip_address(unicode(self.item, "utf-8"))
if isinstance(self.item, ipaddress.IPv4Address): self.isipv4 = True if isinstance(self.item, ipaddress.IPv4Address):
elif isinstance(self.item, ipaddress.IPv6Address): self.isipv6 = True self.isipv4 = True
elif isinstance(self.item, ipaddress.IPv6Address):
self.isipv6 = True
except ValueError as e: except ValueError as e:
try: try:
self.item = ipaddress.ip_network(unicode(self.item, "utf-8"), strict=False) self.item = ipaddress.ip_network(unicode
if isinstance(self.item, ipaddress.IPv4Network): self.isipv4cidr = True (self.item, "utf-8"),
elif isinstance(self.item, ipaddress.IPv6Network): self.isipv6cidr = True strict=False)
if isinstance(self.item, ipaddress.IPv4Network):
self.isipv4cidr = True
elif isinstance(self.item, ipaddress.IPv6Network):
self.isipv6cidr = True
except ValueError as e2: except ValueError as e2:
if self.item[0] == '.' and len(self.item.split('.')) > 2: if self.item[0] == '.' and len(self.item.split('.')) > 2:
self.isdomain = True 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 self.ishostname = True
else: else:
raise ConfigException('Unknown dataset item: {0}'.format(item)) raise ConfigException('Unknown dataset item: {0}'
.format(item))
def match(self, connectip): def match(self, connectip):
'''Check if the connect IP is part of the dataset''' '''Check if the connect IP is part of the dataset'''
source = ipaddress.ip_address(unicode(connectip, "utf-8")) source = ipaddress.ip_address(unicode(connectip, "utf-8"))
for item in self.dataset: for item in self.dataset:
if item.isdomain or item.ishostname: 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: if result:
return(result) return(result)
elif item.isipv4 or item.isipv4cidr: elif item.isipv4 or item.isipv4cidr: # Then IPv4/6 addresses or
if isinstance(source, ipaddress.IPv4Address): # Then IPv4/6 addresses if isinstance(source, ipaddress.IPv4Address): # networks
return(self.match4(source)) # or networks depending return(self.match4(source)) # depending on the item type
elif item.isipv6 or item.isipv6cidr: # on the item type and elif item.isipv6 or item.isipv6cidr: # and connect type
if isinstance(source, ipaddress.IPv6Address): # connection type if isinstance(source, ipaddress.IPv6Address):
return(self.match6(source)) return(self.match6(source))
def matchname(self, source): def matchname(self, source):
@@ -127,7 +133,7 @@ class HostsDataset(object):
for item in self.dataset: for item in self.dataset:
if item.isdomain: if item.isdomain:
for ptr in ptrlist: 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:]: if item.item[1:] == ptr[-len(item.item)+1:]:
matchdomain = True matchdomain = True
negativedomain = item.negative negativedomain = item.negative
@@ -212,21 +218,16 @@ class HostsDataset(object):
match = False match = False
return(match) 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,
def _processConfigFile(filename = None, configdata = None, useSyslog = 1, useStderr=0):
useStderr = 0):
'''Load the specified config file, exit and log errors if it fails, '''Load the specified config file, exit and log errors if it fails,
otherwise return a config dictionary.''' otherwise return a config dictionary.'''
import config import config
if configdata == None: configdata = config.defaultConfigData if configdata is None:
if filename != None: configdata = config.defaultConfigData
if filename is not None:
try: try:
_readConfigFile(filename, configdata) _readConfigFile(filename, configdata)
except Exception, e: except Exception, e:
@@ -238,7 +239,7 @@ def _processConfigFile(filename = None, configdata = None, useSyslog = 1,
sys.exit(1) sys.exit(1)
return(configdata) return(configdata)
####################
def _find_boolean(item): def _find_boolean(item):
if type(item) == int: if type(item) == int:
item = str(item) item = str(item)
@@ -249,14 +250,15 @@ def _find_boolean(item):
else: else:
raise dkim.ParameterError() raise dkim.ParameterError()
return item return item
####################
def _calculate_authserv_id(as_id): def _calculate_authserv_id(as_id):
"""Determine AuthservID if needed""" """Determine AuthservID if needed"""
if as_id == 'HOSTNAME': if as_id == 'HOSTNAME':
as_id = socket.gethostname() as_id = socket.gethostname()
return as_id return as_id
####################
def _dataset_to_list(dataset): def _dataset_to_list(dataset):
"""Convert a dataset (as defined in dkimpymilter.8) and return a python """Convert a dataset (as defined in dkimpymilter.8) and return a python
list of values.""" list of values."""
@@ -292,80 +294,89 @@ def _dataset_to_list(dataset):
else: else:
return [dataset.strip().strip(',')] return [dataset.strip().strip(',')]
if dataset[-3:] == '.db' or dataset[:3] == 'db:': if dataset[-3:] == '.db' or dataset[:3] == 'db:':
# This is a Sleepycat (Oracle) DB dataset # This is a Sleepycat (Oracle) DB dataset
import whichdb # Will need rewriting someday for python3 import whichdb # Will need rewriting someday for python3
if dataset[-3:] == '.db': if dataset[-3:] == '.db':
dbname = dataset dbname = dataset
elif dataset[:3] == 'db:': elif dataset[:3] == 'db:':
dbname = dataset[3:] dbname = dataset[3:]
else: 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': 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 #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 '''Reads a configuration file from the specified path, merging it
with the configuration data specified in configData. Returns a with the configuration data specified in configData. Returns a
dictionary of name/value pairs based on configData and the values dictionary of name/value pairs based on configData and the values
read from path.''' read from path.'''
debugLevel = configGlobal.get('debugLevel', 0) debugLevel = configGlobal.get('debugLevel', 0)
if debugLevel >= 5: syslog.syslog('readConfigFile: Loading "%s"' % path) if debugLevel >= 5:
if configData == None: configData = {} syslog.syslog('readConfigFile: Loading "%s"' % path)
if configData is None:
configData = {}
nameConversion = { nameConversion = {
'AuthservID' : 'str', 'AuthservID': 'str',
'Syslog' : 'bool', 'Syslog': 'bool',
'SyslogFacility' : 'str', 'SyslogFacility': 'str',
'SyslogSuccess' : 'bool', 'SyslogSuccess': 'bool',
'UMask' : 'int', 'UMask': 'int',
'Mode' : 'str', 'Mode': 'str',
'Socket' : 'str', 'Socket': 'str',
'PidFile' : 'str', 'PidFile': 'str',
'UserID' : 'str', 'UserID': 'str',
'Domain' : 'dataset', 'Domain': 'dataset',
'KeyFile' : 'str', 'KeyFile': 'str',
'KeyFileEd25519' : 'str', 'KeyFileEd25519': 'str',
'Selector' : 'str', 'Selector': 'str',
'SelectorEd25519': 'str', 'SelectorEd25519': 'str',
'Canonicalization' : 'str', 'Canonicalization': 'str',
'InternalHosts' : 'dataset', 'InternalHosts': 'dataset',
'InternalHostsObj': 'bool', 'InternalHostsObj': 'bool',
'DiagnosticDirectory' : 'str', 'DiagnosticDirectory': 'str',
'MacroList' : 'dataset', 'MacroList': 'dataset',
'MacroListVerify' : 'dataset' 'MacroListVerify': 'dataset'
} }
# check to see if it's a file # check to see if it's a file
try: try:
mode = os.stat(path)[0] mode = os.stat(path)[0]
except OSError, e: 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) return(configData)
if not stat.S_ISREG(mode): 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) return(configData)
# load file # load file
fp = open(path, 'r') fp = open(path, 'r')
while 1: while 1:
line = fp.readline() line = fp.readline()
if not line: break if not line:
break
# parse line # parse line
line = line.split('#', 1)[0].strip() line = line.split('#', 1)[0].strip()
if not line: continue if not line:
continue
data = line.split() data = line.split()
if len(data) != 2: if len(data) != 2:
if len(data) == 1: if len(data) == 1:
if debugLevel >= 1: if debugLevel >= 1:
syslog.syslog('Configuration item "%s" not defined in file "%s"' syslog.syslog('Config item "%s" not defined in file "%s"'
% ( line, path )) % (line, path))
if len(data) == 1: if len(data) == 1:
name = data name = data
value = '' value = ''
@@ -377,12 +388,14 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
# check validity of name # check validity of name
conversion = nameConversion.get(name) conversion = nameConversion.get(name)
if conversion == None: if conversion is None:
syslog.syslog('ERROR: Unknown name "%s" in file "%s"' % ( name, path )) syslog.syslog('ERROR: Unknown name "%s" in file "%s"'
% (name, path))
continue continue
if debugLevel >= 5: syslog.syslog('readConfigFile: Found entry "%s=%s"' if debugLevel >= 5:
% ( name, value )) syslog.syslog('readConfigFile: Found entry "%s=%s"'
% (name, value))
if conversion == 'bool': if conversion == 'bool':
configData[name] = _find_boolean(value) configData[name] = _find_boolean(value)
elif conversion == 'str': elif conversion == 'str':
@@ -392,12 +405,15 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
elif conversion == 'dataset': elif conversion == 'dataset':
configData[name] = _dataset_to_list(value) configData[name] = _dataset_to_list(value)
else: else:
syslog.syslog(str('name: ' + name + ' value: ' + value + ' conversion: ' + conversion)) syslog.syslog(str('name: ' + name + ' value: ' + value +
' conversion: ' + conversion))
configData[name] = conversion(value) configData[name] = conversion(value)
fp.close() fp.close()
try: try:
configData['AuthservID'] = _calculate_authserv_id(configData['AuthservID']) configData['AuthservID'] = _calculate_authserv_id(configData
configData['InternalHostsObj'] = HostsDataset(configData['InternalHosts']) ['AuthservID'])
configData['InternalHostsObj'] = HostsDataset(configData
['InternalHosts'])
except: except:
pass pass
+19 -12
View File
@@ -16,6 +16,7 @@
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
def fold(header): def fold(header):
"""Fold a header line into multiple crlf-separated lines at column 72. """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 Borrowed from dkimpy and updated to only add \n instead of \r\n because
@@ -48,6 +49,7 @@ def fold(header):
header = header[j:] header = header[j:]
return pre + header return pre + header
def user_group(userid): def user_group(userid):
"""Return user and group from UserID""" """Return user and group from UserID"""
import grp import grp
@@ -63,13 +65,14 @@ def user_group(userid):
running_gid = grp.getgrnam(gidname).gr_gid running_gid = grp.getgrnam(gidname).gr_gid
return running_uid, running_gid return running_uid, running_gid
def drop_privileges(milterconfig): def drop_privileges(milterconfig):
import os import os
import syslog import syslog
if os.getuid() != 0: if os.getuid() != 0:
if milterconfig.get('Syslog'): 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 return
# Get user and group # Get user and group
@@ -85,9 +88,9 @@ def drop_privileges(milterconfig):
# Set umask # Set umask
old_umask = os.umask(milterconfig.get('UMask')) old_umask = os.umask(milterconfig.get('UMask'))
#################
class ExceptHook: class ExceptHook:
def __init__(self, useSyslog = 1, useStderr = 0): def __init__(self, useSyslog=1, useStderr=0):
self.useSyslog = useSyslog self.useSyslog = useSyslog
self.useStderr = useStderr self.useStderr = useStderr
@@ -103,12 +106,11 @@ class ExceptHook:
sys.stderr.write(line) sys.stderr.write(line)
####################
def setExceptHook(): def setExceptHook():
import sys import sys
sys.excepthook = ExceptHook(useSyslog = 1, useStderr = 1) sys.excepthook = ExceptHook(useSyslog=1, useStderr=1)
####################
def write_pid(milterconfig): def write_pid(milterconfig):
"""Write PID in pidfile. Will not overwrite an existing file.""" """Write PID in pidfile. Will not overwrite an existing file."""
import os import os
@@ -125,10 +127,11 @@ def write_pid(milterconfig):
os.chown(piddir, user, group) os.chown(piddir, user, group)
f = open(milterconfig.get('PidFile'), 'w') f = open(milterconfig.get('PidFile'), 'w')
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('Missing pid dir created: {0}'.format(piddir)) syslog.syslog('PID dir created: {0}'.format(piddir))
else: else:
if milterconfig.get('Syslog'): 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 raise
f.write(pid) f.write(pid)
f.close() f.close()
@@ -136,10 +139,13 @@ def write_pid(milterconfig):
os.chown(milterconfig.get('PidFile'), user, group) os.chown(milterconfig.get('PidFile'), user, group)
else: else:
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('Unable to write pidfle {0}. File exists.'.format(milterconfig.get('PidFile'))) syslog.syslog('Unable to write pidfle {0}. File exists.'
raise RuntimeError('Unable to write pidfle {0}. File exists.'.format(milterconfig.get('PidFile'))) .format(milterconfig.get('PidFile')))
raise RuntimeError('Unable to write pidfle {0}. File exists.'
.format(milterconfig.get('PidFile')))
return pid return pid
def own_socketfile(milterconfig): def own_socketfile(milterconfig):
"""If socket is Unix socket, chown to UserID before dropping privileges""" """If socket is Unix socket, chown to UserID before dropping privileges"""
import os import os
@@ -149,7 +155,7 @@ def own_socketfile(milterconfig):
if milterconfig.get('Socket')[:6] == "local:": if milterconfig.get('Socket')[:6] == "local:":
os.chown(milterconfig.get('Socket')[6:], user, group) os.chown(milterconfig.get('Socket')[6:], user, group)
####################
def read_keyfile(milterconfig, keytype): def read_keyfile(milterconfig, keytype):
"""Read private key from file.""" """Read private key from file."""
import syslog import syslog
@@ -162,7 +168,8 @@ def read_keyfile(milterconfig, keytype):
keylist = f.readlines() keylist = f.readlines()
except IOError as e: except IOError as e:
if milterconfig.get('Syslog'): 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 raise
f.close() f.close()
key = '' key = ''