Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d8c309da9 | |||
| 4d5961e4d5 | |||
| 59448e8e57 | |||
| 695de0db14 | |||
| dfd6fa68c3 | |||
| 86eb152f93 | |||
| 126966e110 | |||
| 5d8d47cd52 | |||
| 1843ca6244 | |||
| f9358d594c | |||
| a8aa422b03 | |||
| 9836f2c9c2 | |||
| 70606ac58c | |||
| 6348bdcdc7 | |||
| fd39384e78 | |||
| 924c96d555 | |||
| efeabd19d3 | |||
| a9b8a44bfc | |||
| daaa6aada7 | |||
| e795db7c69 |
@@ -1,3 +1,22 @@
|
||||
0.9.5.1 2018-03-10
|
||||
- Add conf file location to systemd unit file
|
||||
- Fix setup.py install locations so they are installed correctly
|
||||
|
||||
0.9.5 2018-03-10
|
||||
- Beta 1 (updated Alpha -> Beta warning in README and trove classifiers)
|
||||
- Added support for MacroList option
|
||||
- Added support for MacroListVerify option
|
||||
- Added example in README to show use of MacroList* to separate inbound and
|
||||
outbound mail streams
|
||||
- Added support for SyslogSuccess option (both signing and verifying)
|
||||
- Rationalized logging to be much less verbose unless SyslogSuccess or
|
||||
debugLevel are set - default is generally start/stop/errors only
|
||||
- Fixed install_requires so either dnspython (preferred if neither is
|
||||
installed) or PyDNS satisfies the install requirements
|
||||
- Updated Authentication Results result comment not to mention key size for
|
||||
ed25519 signatures, since it's irrelevant
|
||||
- Enhanced signature verification logging to provide more useful information
|
||||
|
||||
0.9.4 2018-03-09
|
||||
- Create PID directory if it is missing
|
||||
- Fix crash when verifying if domain for signing was not set
|
||||
|
||||
@@ -86,10 +86,42 @@ submission inet n - - - - smtpd
|
||||
|
||||
These need to match the Socket value for each dkimpy-milter instance.
|
||||
|
||||
Care is required to segregate outbound mail to be signed and inbound mail to
|
||||
be verified. The above example uses two instances of dkimpy-milter to do
|
||||
this. There are many possible ways. Here is another example using milter
|
||||
macros to keep the mail streams segregated:
|
||||
|
||||
Postfix master.cf:
|
||||
|
||||
smtp inet n - - - - smtpd
|
||||
...
|
||||
-o smtpd_milters=inet:localhost:8891
|
||||
-o milter_macro_daemon_name=VERIFYING
|
||||
...
|
||||
|
||||
submission inet n - - - - smtpd
|
||||
-o syslog_name=postfix/submission
|
||||
-o smtpd_tls_security_level=encrypt
|
||||
-o smtpd_sasl_auth_enable=yes
|
||||
...
|
||||
-o milter_macro_daemon_name=ORIGINATING
|
||||
-o smtpd_milters=inet:localhost:8891
|
||||
...
|
||||
|
||||
Dkimpy-milter.conf:
|
||||
|
||||
...
|
||||
Mode sv
|
||||
MacroList dameon_name|ORIGINATING
|
||||
MacroListVerify daemon_name|VERIFYING
|
||||
...
|
||||
|
||||
|
||||
The python DKIM library, dkimpy, requires the entire message being signed or
|
||||
verified to be in memory, so dkimpy-milter does not write messages out to a temp
|
||||
file. This may impact performance on low-memory systems.
|
||||
|
||||
WARNING: This is an alpha grade release to support interoperability testing with
|
||||
Ed25519 signatures and basic functionality. It is known to be incomplete and
|
||||
not suitable for general use.
|
||||
This is an beta grade release to support interoperability testing with Ed25519
|
||||
signatures sufficient functionality for basic use. The documented
|
||||
functionality has been implemented and at least partially tested. It is free
|
||||
of known major defects, but is not fully tested in a variety of environments.
|
||||
|
||||
@@ -34,15 +34,14 @@ DiagnosticDirectory implemented verified
|
||||
InternalHosts implemented verified
|
||||
|
||||
0.9.5 (Beta)
|
||||
|
||||
|
||||
SyslogSuccess
|
||||
MacroList implemented verified
|
||||
MacroListVerify implemented verified
|
||||
SyslogSuccess implemented verified
|
||||
|
||||
1.0.0
|
||||
Convert dkim-milter-python config
|
||||
No additional features planned
|
||||
|
||||
Plannedataset type support:
|
||||
Plannedataset type support (if needed):
|
||||
db:/.db
|
||||
mdb:
|
||||
|
||||
@@ -52,7 +51,6 @@ AlwaysAddARHeader
|
||||
ChangeRootDirectory
|
||||
ClockDrift (requires dkimpy change)
|
||||
DNSTimeout (requires dkmpy change)
|
||||
MacroList
|
||||
MilterDebug
|
||||
MinimumKeyBits
|
||||
PeerList
|
||||
|
||||
+108
-56
@@ -25,15 +25,12 @@ import sys
|
||||
import syslog
|
||||
import Milter
|
||||
import dkim
|
||||
from dkim.dnsplug import get_txt
|
||||
from dkim.util import parse_tag_value
|
||||
import authres
|
||||
import os
|
||||
import tempfile
|
||||
import StringIO
|
||||
import re
|
||||
from Milter.config import MilterConfigParser
|
||||
from Milter.utils import iniplist,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
|
||||
@@ -42,9 +39,10 @@ from dkimpy_milter.util import read_keyfile
|
||||
from dkimpy_milter.util import own_socketfile
|
||||
from dkimpy_milter.util import fold
|
||||
|
||||
__version__ = "0.9.4"
|
||||
__version__ = "0.9.5.1"
|
||||
FWS = re.compile(r'\r?\n[ \t]+')
|
||||
|
||||
|
||||
class dkimMilter(Milter.Base):
|
||||
"Milter to check and sign DKIM. Each connection gets its own instance."
|
||||
|
||||
@@ -60,6 +58,7 @@ class dkimMilter(Milter.Base):
|
||||
@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()
|
||||
@@ -69,17 +68,37 @@ class dkimMilter(Milter.Base):
|
||||
self.AuthservID = self.receiver
|
||||
if hostaddr and len(hostaddr) > 0:
|
||||
ipaddr = hostaddr[0]
|
||||
if milterconfig['InternalHostsObj']:
|
||||
if milterconfig['InternalHostsObj'].match(ipaddr):
|
||||
if milterconfig['IntHosts']:
|
||||
if milterconfig['IntHosts'].match(ipaddr):
|
||||
self.internal_connection = True
|
||||
else: ipaddr = ''
|
||||
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))
|
||||
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 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
|
||||
@@ -87,47 +106,33 @@ class dkimMilter(Milter.Base):
|
||||
# of each message.
|
||||
@Milter.noreply
|
||||
def envfrom(self, f, *str):
|
||||
if milterconfig.get('Syslog'):
|
||||
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 2:
|
||||
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()
|
||||
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 = []
|
||||
'''if self.user:
|
||||
# Very simple SMTP AUTH policy by default:
|
||||
# any successful authentication is considered INTERNAL
|
||||
self.internal_connection = True
|
||||
auth_type = self.getsymval('{auth_type}')
|
||||
ssl_bits = self.getsymval('{cipher_bits}')
|
||||
if milterconfig.get('Syslog'):
|
||||
syslog.syslog(
|
||||
"SMTP AUTH:",self.user,"sslbits =",ssl_bits, auth_type,
|
||||
"ssf =",self.getsymval('{auth_ssf}'), "INTERNAL"
|
||||
)
|
||||
# Detailed authorization policy is configured in the access file below.
|
||||
self.arresults.append(
|
||||
authres.SMTPAUTHAuthenticationResult(result = 'pass',
|
||||
result_comment = auth_type+' sslbits='+ssl_bits, smtp_auth = self.user)
|
||||
)'''
|
||||
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') and
|
||||
milterconfig.get('debugLevel') >= 1):
|
||||
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'):
|
||||
if (milterconfig.get('Syslog') and
|
||||
milterconfig.get('debugLevel') >= 1):
|
||||
syslog.syslog("{0}: {1}".format(name, val))
|
||||
elif lname == 'authentication-results':
|
||||
self.arheaders.append(val)
|
||||
@@ -156,10 +161,12 @@ class dkimMilter(Milter.Base):
|
||||
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))
|
||||
ar = (authres.AuthenticationResultsHeader
|
||||
.parse_value(FWS.sub('', val)))
|
||||
if ar.authserv_id == self.AuthservID:
|
||||
self.chgheader('authentication-results', i, '')
|
||||
if milterconfig.get('Syslog'):
|
||||
if (milterconfig.get('Syslog') and
|
||||
milterconfig.get('debugLevel') >= 1):
|
||||
syslog.syslog('REMOVE: {0}'.format(val))
|
||||
except:
|
||||
# Don't error out on unparseable AR header fiels
|
||||
@@ -170,20 +177,22 @@ class dkimMilter(Milter.Base):
|
||||
domain = milterconfig.get('Domain')
|
||||
else:
|
||||
domain = ''
|
||||
if (self.fdomain in domain) and (not milterconfig.get('Mode') == 'v'):
|
||||
if ((self.fdomain in domain) and not milterconfig.get('Mode') == 'v'
|
||||
and not self.external_connection):
|
||||
txt = self.fp.read()
|
||||
self.sign_dkim(txt)
|
||||
result = None
|
||||
if (self.has_dkim) and (not self.internal_connection) and (milterconfig.get('Mode') == 'v' or milterconfig.get('Mode') == 'sv'):
|
||||
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)
|
||||
else:
|
||||
result = 'none'
|
||||
if self.arresults:
|
||||
h = authres.AuthenticationResultsHeader(authserv_id = self.AuthservID,
|
||||
h = authres.AuthenticationResultsHeader(authserv_id=
|
||||
self.AuthservID,
|
||||
results=self.arresults)
|
||||
h = fold(str(h))
|
||||
if milterconfig.get('Syslog'):
|
||||
if (milterconfig.get('Syslog') and
|
||||
milterconfig.get('debugLevel') >= 2):
|
||||
syslog.syslog(str(h))
|
||||
name, val = str(h).split(': ', 1)
|
||||
self.addheader(name, val, 0)
|
||||
@@ -198,20 +207,41 @@ class dkimMilter(Milter.Base):
|
||||
else:
|
||||
canonicalize.append(canon)
|
||||
canonicalize.append(canon)
|
||||
if (milterconfig.get('Syslog') and
|
||||
milterconfig.get('debugLevel') >= 1):
|
||||
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]))
|
||||
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 (milterconfig.get('Syslog') and
|
||||
(milterconfig.get('SyslogSuccess')
|
||||
or milterconfig.get('debugLevel') >= 1)):
|
||||
syslog.syslog('{0}: {1} DKIM-Signature field added (s={2} '
|
||||
'd={3})'.format(self.getsymval('i'),
|
||||
d.signature_fields.get(b'a'),
|
||||
d.signature_fields.get(b's'),
|
||||
d.domain))
|
||||
if privateEd25519:
|
||||
d = dkim.DKIM(txt)
|
||||
h = d.sign(milterconfig.get('SelectorEd25519'), self.fdomain, privateEd25519,
|
||||
canonicalize=(canonicalize[0], canonicalize[1]), signature_algorithm='ed25519-sha256')
|
||||
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)
|
||||
if (milterconfig.get('Syslog') and
|
||||
(milterconfig.get('SyslogSuccess')
|
||||
or milterconfig.get('debugLevel') >= 1)):
|
||||
syslog.syslog('{0}: {1} DKIM-Signature field added (s={2} '
|
||||
'd={3})'.format(self.getsymval('i'),
|
||||
d.signature_fields.get(b'a'),
|
||||
d.signature_fields.get(b's'),
|
||||
d.domain))
|
||||
except dkim.DKIMException as x:
|
||||
if milterconfig.get('Syslog'):
|
||||
syslog.syslog('DKIM: {0}'.format(x))
|
||||
@@ -222,15 +252,24 @@ class dkimMilter(Milter.Base):
|
||||
|
||||
def check_dkim(self, txt):
|
||||
res = False
|
||||
conf = self.conf
|
||||
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'))
|
||||
if d.signature_fields.get(b'a') == 'ed25519-sha256':
|
||||
self.dkim_comment = ('Good {0} signature'
|
||||
.format(d.signature_fields
|
||||
.get(b'a')))
|
||||
else:
|
||||
self.dkim_comment = 'Bad {0} bit {1} signature.'.format(d.keysize, d.signature_fields.get(b'a'))
|
||||
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'):
|
||||
@@ -243,16 +282,23 @@ class dkimMilter(Milter.Base):
|
||||
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))
|
||||
if (milterconfig.get('Syslog') and
|
||||
(milterconfig.get('SyslogSuccess') or
|
||||
milterconfig.get('debugLevel') >= 1)):
|
||||
syslog.syslog('{0}: {1} DKIM signature verified (s={2} '
|
||||
'd={3})'.format(self.getsymval('i'),
|
||||
d.signature_fields.get(b'a'),
|
||||
d.signature_fields.get(b's'),
|
||||
d.domain))
|
||||
self.dkim_domain = d.domain
|
||||
else:
|
||||
if milterconfig.get['DiagnosticDirectory']:
|
||||
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))
|
||||
syslog.syslog('DKIM: Fail (saved as {0})'
|
||||
.format(fname))
|
||||
else:
|
||||
syslog.syslog('DKIM: Fail ({0})'.format(d.domain))
|
||||
if res:
|
||||
@@ -262,11 +308,15 @@ class dkimMilter(Milter.Base):
|
||||
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)
|
||||
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.
|
||||
global milterconfig
|
||||
@@ -274,7 +324,7 @@ def main():
|
||||
global privateEd25519
|
||||
privateRSA = False
|
||||
privateEd25519 = False
|
||||
configFile = '/etc/dkimpy-milter.conf'
|
||||
configFile = '/usr/loca/etc/dkimpy-milter.conf'
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] in ('-?', '--help', '-h'):
|
||||
print('usage: dkimpy-milter [<configfilename>]')
|
||||
@@ -282,7 +332,8 @@ def main():
|
||||
configFile = sys.argv[1]
|
||||
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)
|
||||
@@ -295,7 +346,8 @@ 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)
|
||||
own_socketfile(milterconfig)
|
||||
|
||||
+70
-52
@@ -27,8 +27,6 @@
|
||||
import syslog
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import urllib
|
||||
import stat
|
||||
import dkim
|
||||
import socket
|
||||
@@ -46,17 +44,19 @@ defaultConfigData = {
|
||||
'UserID': 'dkimpy-milter',
|
||||
'Canonicalization': 'relaxed/simple',
|
||||
'InternalHosts': '127.0.0.1',
|
||||
'InternalHostsObj' : False,
|
||||
'DiagnosticDirectory' : ''
|
||||
'IntHosts': False,
|
||||
'DiagnosticDirectory': '',
|
||||
'MacroList': '',
|
||||
'MacroListVerify': '',
|
||||
'debugLevel': 0 # Undocumented config item for developer use
|
||||
}
|
||||
|
||||
|
||||
#################################
|
||||
class ConfigException(Exception):
|
||||
'''Exception raised when there's a configuration file error.'''
|
||||
pass
|
||||
|
||||
#################################
|
||||
|
||||
class HostsDataset(object):
|
||||
'''Hold a group of host related dataset objects'''
|
||||
|
||||
@@ -85,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
|
||||
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):
|
||||
@@ -126,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
|
||||
@@ -211,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):
|
||||
'''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:
|
||||
@@ -237,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)
|
||||
@@ -248,14 +250,15 @@ def _find_boolean(item):
|
||||
else:
|
||||
raise dkim.ParameterError()
|
||||
return item
|
||||
####################
|
||||
def _calculate_authserv_id(as_id):
|
||||
|
||||
|
||||
def _make_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."""
|
||||
@@ -298,16 +301,19 @@ def _dataset_to_list(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={}):
|
||||
'''Reads a configuration file from the specified path, merging it
|
||||
with the configuration data specified in configData. Returns a
|
||||
@@ -315,8 +321,10 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
|
||||
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',
|
||||
@@ -334,34 +342,41 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
|
||||
'SelectorEd25519': 'str',
|
||||
'Canonicalization': 'str',
|
||||
'InternalHosts': 'dataset',
|
||||
'InternalHostsObj': 'bool',
|
||||
'DiagnosticDirectory' : 'str'
|
||||
'IntHosts': 'bool',
|
||||
'DiagnosticDirectory': 'str',
|
||||
'MacroList': 'dataset',
|
||||
'MacroListVerify': 'dataset',
|
||||
'debugLevel': 'int'
|
||||
}
|
||||
|
||||
# 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"'
|
||||
syslog.syslog('Config item "%s" not defined in file "%s"'
|
||||
% (line, path))
|
||||
if len(data) == 1:
|
||||
name = data
|
||||
@@ -374,11 +389,13 @@ 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"'
|
||||
if debugLevel >= 5:
|
||||
syslog.syslog('readConfigFile: Found entry "%s=%s"'
|
||||
% (name, value))
|
||||
if conversion == 'bool':
|
||||
configData[name] = _find_boolean(value)
|
||||
@@ -389,12 +406,13 @@ 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'] = _make_authserv_id(configData['AuthservID'])
|
||||
configData['IntHosts'] = HostsDataset(configData['InternalHosts'])
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
+17
-11
@@ -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
|
||||
@@ -46,9 +47,9 @@ def fold(header):
|
||||
j = i + 1
|
||||
pre += header[:j] + b"\n "
|
||||
header = header[j:]
|
||||
namelen = 0
|
||||
return pre + header
|
||||
|
||||
|
||||
def user_group(userid):
|
||||
"""Return user and group from UserID"""
|
||||
import grp
|
||||
@@ -64,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
|
||||
@@ -86,7 +88,7 @@ def drop_privileges(milterconfig):
|
||||
# Set umask
|
||||
old_umask = os.umask(milterconfig.get('UMask'))
|
||||
|
||||
#################
|
||||
|
||||
class ExceptHook:
|
||||
def __init__(self, useSyslog=1, useStderr=0):
|
||||
self.useSyslog = useSyslog
|
||||
@@ -104,12 +106,11 @@ class ExceptHook:
|
||||
sys.stderr.write(line)
|
||||
|
||||
|
||||
####################
|
||||
def setExceptHook():
|
||||
import sys
|
||||
sys.excepthook = ExceptHook(useSyslog=1, useStderr=1)
|
||||
|
||||
####################
|
||||
|
||||
def write_pid(milterconfig):
|
||||
"""Write PID in pidfile. Will not overwrite an existing file."""
|
||||
import os
|
||||
@@ -126,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()
|
||||
@@ -137,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
|
||||
@@ -150,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
|
||||
@@ -163,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 = ''
|
||||
|
||||
@@ -255,13 +255,50 @@ all messages. Ignored if a
|
||||
is defined. [KeyTable NOT IMPLEMENTED]
|
||||
|
||||
.TP
|
||||
.I KeyFileEd25119 (string)
|
||||
.I KeyFileEd25519 (string)
|
||||
Gives the location of a Ed25519 private key to be used for Ed25519 signing
|
||||
all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key
|
||||
generation (as used in dkimpy). Ignored if a
|
||||
.I KeyTableEd25519
|
||||
is defined. [KeyTableEd25519 NOT IMPLEMENTED]
|
||||
|
||||
.TP
|
||||
.I MacroList (dataset)
|
||||
Defines a set of MTA-provided
|
||||
.I macros
|
||||
that should be checked to see if the sender has been determined to be a
|
||||
local user and therefore whether or not the message should be signed. If
|
||||
a
|
||||
.I value
|
||||
is specified matching a macro name in the data set, the value of the macro
|
||||
must match a value specified (matching is case-sensitive), otherwise the
|
||||
macro must be defined but may contain any value. The set is empty by
|
||||
default, meaning macros are not considered when making the sign-verify
|
||||
decision. The general format of the value is
|
||||
.I value1[|value2[|...]];
|
||||
if one or more value is defined then the macro must be set to one of the
|
||||
listed values, otherwise the macro must be set but can contain any
|
||||
value.
|
||||
|
||||
In order for the macro and its value to be available to the filter for
|
||||
checking, the MTA must send it during the protocol exchange. This is either
|
||||
accomplished via manual configuration of the MTA to send the desired macros
|
||||
or, for MTA/filter combinations that support the feature, the filter can
|
||||
request those macros that are of interest. The latter is a feature negotiated
|
||||
at the time the filter receives a connection from the MTA and its availability
|
||||
depends upon the version of milter used to compile the filter and the version
|
||||
of the MTA making the connection.
|
||||
|
||||
.TP
|
||||
.I MacroListVerify (dataset)
|
||||
Defines a set of MTA-provided
|
||||
.I macros
|
||||
that should be checked to see if the sender has been determined to be an
|
||||
external source and therefore whether or not the message should be signed.
|
||||
Entries in this data set follow the same form as those of the
|
||||
.I MacroList
|
||||
option above. [this option is not inhereted from OpenDKIM]
|
||||
|
||||
.TP
|
||||
.I Mode (string)
|
||||
Selects operating modes. The string is a concatenation of characters that
|
||||
|
||||
@@ -22,6 +22,13 @@ import dkimpy_milter
|
||||
|
||||
description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail."
|
||||
|
||||
kw = {} # Work-around for lack of 'or' requires in setuptools.
|
||||
try:
|
||||
import DNS
|
||||
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'PyDNS']
|
||||
except ImportError: # If PyDNS is not installed, prefer dnspython
|
||||
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dnspython']
|
||||
|
||||
setup(
|
||||
name='dkimpy-milter',
|
||||
version=dkimpy_milter.__version__,
|
||||
@@ -31,7 +38,7 @@ setup(
|
||||
description=description,
|
||||
download_url = "https://pypi.python.org/pypi/dkimpy-milter",
|
||||
classifiers= [
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: No Input/Output (Daemon)',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: GNU General Public License (GPL)',
|
||||
@@ -52,9 +59,9 @@ setup(
|
||||
data_files=[(os.path.join('share', 'man', 'man5'),
|
||||
['man/dkimpy-milter.conf.5']), (os.path.join('share', 'man', 'man8'),
|
||||
['man/dkimpy-milter.8']), ('etc', ['etc/dkimpy-milter.conf']),
|
||||
(os.path.join('/lib', 'systemd', 'system'),
|
||||
['system/dkimpy-milter.service']),(os.path.join('/etc', 'init.d'),
|
||||
(os.path.join('lib', 'systemd', 'system'),
|
||||
['system/dkimpy-milter.service']),(os.path.join('etc', 'init.d'),
|
||||
['system/dkimpy-milter'])],
|
||||
install_requires = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dns'],
|
||||
zip_safe = False,
|
||||
**kw
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ After=syslog.target network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
PIDFile=/var/run/dkimpy-milter/dkimpy-milter.pid
|
||||
ExecStart=/usr/local/bin/dkimpy-milter
|
||||
ExecStart=/usr/local/bin/dkimpy-milter /usr/local/etc/dkimpy-milter.conf
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Reference in New Issue
Block a user