Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7eed8995a2 | |||
| a16d887ac6 | |||
| 23d91b2b50 | |||
| 481fbdae29 | |||
| 4b0c39b0c7 | |||
| 7092874729 | |||
| ea09bab1a8 | |||
| 25fdd3b81c | |||
| 9d5316ca0e | |||
| 391b5352f3 |
@@ -1,5 +1,13 @@
|
|||||||
|
1.1.0 UNRELEASED
|
||||||
|
- Port to python3
|
||||||
|
- Add test suite using opendkim miltertest
|
||||||
|
- When Socket is absolute path, do not strip leading /
|
||||||
|
- Handle unix: socket prefix the same as local:
|
||||||
|
- Set up correct AuthservID defaults
|
||||||
|
- config: Reassemble strings sensibly
|
||||||
|
|
||||||
1.0.1 2019-02-11
|
1.0.1 2019-02-11
|
||||||
* Reorder milter start and dropping privileges so permissions on Unix socket
|
- Reorder milter start and dropping privileges so permissions on Unix socket
|
||||||
are correct (LP: 1797720)
|
are correct (LP: 1797720)
|
||||||
- Make domain checks case insensitive for determining if signing should be
|
- Make domain checks case insensitive for determining if signing should be
|
||||||
done (LP: #1815311)
|
done (LP: #1815311)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ to be installed when installing using setup.py, the following incantation is
|
|||||||
required because setuptools developers decided not being able to do this by
|
required because setuptools developers decided not being able to do this by
|
||||||
default is a feature:
|
default is a feature:
|
||||||
|
|
||||||
python setup.py install --single-version-externally-managed --record=/dev/null
|
python3 setup.py install --single-version-externally-managed --record=/dev/null
|
||||||
|
|
||||||
For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are
|
For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are
|
||||||
available in either the main or backports repositories:
|
available in either the main or backports repositories:
|
||||||
|
|||||||
+42
-32
@@ -1,4 +1,4 @@
|
|||||||
#! /usr/bin/python2
|
#! /usr/bin/python3
|
||||||
# Original dkim-milter.py code:
|
# Original dkim-milter.py code:
|
||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
# Copyright 2007 Business Management Systems, Inc.
|
# Copyright 2007 Business Management Systems, Inc.
|
||||||
@@ -28,8 +28,9 @@ import dkim
|
|||||||
import authres
|
import authres
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import StringIO
|
import io
|
||||||
import re
|
import re
|
||||||
|
import codecs
|
||||||
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
|
||||||
@@ -110,7 +111,7 @@ class dkimMilter(Milter.Base):
|
|||||||
def envfrom(self, f, *str):
|
def envfrom(self, f, *str):
|
||||||
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 2:
|
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 2:
|
||||||
syslog.syslog("mail from: {0} {1}".format(f, str))
|
syslog.syslog("mail from: {0} {1}".format(f, str))
|
||||||
self.fp = StringIO.StringIO()
|
self.fp = io.BytesIO()
|
||||||
self.mailfrom = f
|
self.mailfrom = f
|
||||||
t = parse_addr(f)
|
t = parse_addr(f)
|
||||||
if len(t) == 2:
|
if len(t) == 2:
|
||||||
@@ -142,13 +143,13 @@ class dkimMilter(Milter.Base):
|
|||||||
elif lname == 'authentication-results':
|
elif lname == 'authentication-results':
|
||||||
self.arheaders.append(val)
|
self.arheaders.append(val)
|
||||||
if self.fp:
|
if self.fp:
|
||||||
self.fp.write("%s: %s\n" % (name, val))
|
self.fp.write(b"%s: %s\n" % (codecs.encode(name, 'ascii'), codecs.encode(val, 'ascii')))
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
@Milter.noreply
|
@Milter.noreply
|
||||||
def eoh(self):
|
def eoh(self):
|
||||||
if self.fp:
|
if self.fp:
|
||||||
self.fp.write("\n") # terminate headers
|
self.fp.write(b"\n") # terminate headers
|
||||||
self.bodysize = 0
|
self.bodysize = 0
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
@@ -195,20 +196,20 @@ class dkimMilter(Milter.Base):
|
|||||||
h = authres.AuthenticationResultsHeader(authserv_id=
|
h = authres.AuthenticationResultsHeader(authserv_id=
|
||||||
self.AuthservID,
|
self.AuthservID,
|
||||||
results=self.arresults)
|
results=self.arresults)
|
||||||
h = fold(str(h))
|
h = fold(codecs.encode(str(h), 'ascii'))
|
||||||
if (milterconfig.get('Syslog') and
|
if (milterconfig.get('Syslog') and
|
||||||
milterconfig.get('debugLevel') >= 2):
|
milterconfig.get('debugLevel') >= 2):
|
||||||
syslog.syslog(str(h))
|
syslog.syslog(codecs.decode(h, 'ascii'))
|
||||||
name, val = str(h).split(': ', 1)
|
name, val = codecs.decode(h, 'ascii').split(': ', 1)
|
||||||
self.addheader(name, val, 0)
|
self.addheader(name, val, 0)
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def sign_dkim(self, txt):
|
def sign_dkim(self, txt):
|
||||||
canon = milterconfig.get('Canonicalization')
|
canon = codecs.encode(milterconfig.get('Canonicalization'), 'ascii')
|
||||||
canonicalize = []
|
canonicalize = []
|
||||||
if len(canon.split('/')) == 2:
|
if len(canon.split(b'/')) == 2:
|
||||||
canonicalize.append(canon.split('/')[0])
|
canonicalize.append(canon.split(b'/')[0])
|
||||||
canonicalize.append(canon.split('/')[1])
|
canonicalize.append(canon.split(b'/')[1])
|
||||||
else:
|
else:
|
||||||
canonicalize.append(canon)
|
canonicalize.append(canon)
|
||||||
canonicalize.append(canon)
|
canonicalize.append(canon)
|
||||||
@@ -218,11 +219,12 @@ class dkimMilter(Milter.Base):
|
|||||||
try:
|
try:
|
||||||
if privateRSA:
|
if privateRSA:
|
||||||
d = dkim.DKIM(txt)
|
d = dkim.DKIM(txt)
|
||||||
h = d.sign(milterconfig.get('Selector'), self.fdomain,
|
h = d.sign(codecs.encode(milterconfig.get('Selector'), 'ascii'), codecs.encode(self.fdomain, 'ascii'),
|
||||||
privateRSA, canonicalize=(canonicalize[0],
|
codecs.encode(privateRSA, 'ascii'),
|
||||||
|
canonicalize=(canonicalize[0],
|
||||||
canonicalize[1]))
|
canonicalize[1]))
|
||||||
name, val = h.split(': ', 1)
|
name, val = h.split(b': ', 1)
|
||||||
self.addheader(name, val.strip().replace('\r\n', '\n'), 0)
|
self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0)
|
||||||
if (milterconfig.get('Syslog') and
|
if (milterconfig.get('Syslog') and
|
||||||
(milterconfig.get('SyslogSuccess')
|
(milterconfig.get('SyslogSuccess')
|
||||||
or milterconfig.get('debugLevel') >= 1)):
|
or milterconfig.get('debugLevel') >= 1)):
|
||||||
@@ -233,12 +235,12 @@ class dkimMilter(Milter.Base):
|
|||||||
d.domain.lower()))
|
d.domain.lower()))
|
||||||
if privateEd25519:
|
if privateEd25519:
|
||||||
d = dkim.DKIM(txt)
|
d = dkim.DKIM(txt)
|
||||||
h = d.sign(milterconfig.get('SelectorEd25519'), self.fdomain,
|
h = d.sign(codecs.encode(milterconfig.get('SelectorEd25519'), 'ascii'), codecs.encode(self.fdomain, 'ascii'),
|
||||||
privateEd25519, canonicalize=(canonicalize[0],
|
privateEd25519, canonicalize=(canonicalize[0],
|
||||||
canonicalize[1]),
|
canonicalize[1]),
|
||||||
signature_algorithm='ed25519-sha256')
|
signature_algorithm=b'ed25519-sha256')
|
||||||
name, val = h.split(': ', 1)
|
name, val = h.split(b': ', 1)
|
||||||
self.addheader(name, val.strip().replace('\r\n', '\n'), 0)
|
self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0)
|
||||||
if (milterconfig.get('Syslog') and
|
if (milterconfig.get('Syslog') and
|
||||||
(milterconfig.get('SyslogSuccess')
|
(milterconfig.get('SyslogSuccess')
|
||||||
or milterconfig.get('debugLevel') >= 1)):
|
or milterconfig.get('debugLevel') >= 1)):
|
||||||
@@ -266,20 +268,17 @@ class dkimMilter(Milter.Base):
|
|||||||
res = d.verify(idx=y, dnsfunc=lambda _x: dnsoverride)
|
res = d.verify(idx=y, dnsfunc=lambda _x: dnsoverride)
|
||||||
else:
|
else:
|
||||||
res = d.verify(idx=y)
|
res = d.verify(idx=y)
|
||||||
|
algo = codecs.decode(d.signature_fields.get(b'a'), 'ascii')
|
||||||
if res:
|
if res:
|
||||||
if d.signature_fields.get(b'a') == 'ed25519-sha256':
|
if algo == 'ed25519-sha256':
|
||||||
self.dkim_comment = ('Good {0} signature'
|
self.dkim_comment = ('Good {0} signature'
|
||||||
.format(d.signature_fields
|
.format(algo))
|
||||||
.get(b'a')))
|
|
||||||
else:
|
else:
|
||||||
self.dkim_comment = ('Good {0} bit {1} signature'
|
self.dkim_comment = ('Good {0} bit {1} signature'
|
||||||
.format(d.keysize,
|
.format(d.keysize, algo))
|
||||||
d.signature_fields
|
|
||||||
.get(b'a')))
|
|
||||||
else:
|
else:
|
||||||
self.dkim_comment = ('Bad {0} bit {1} signature.'
|
self.dkim_comment = ('Bad {0} bit {1} signature.'
|
||||||
.format(d.keysize,
|
.format(d.keysize, algo))
|
||||||
d.signature_fields.get(b'a')))
|
|
||||||
except dkim.DKIMException as x:
|
except dkim.DKIMException as x:
|
||||||
self.dkim_comment = str(x)
|
self.dkim_comment = str(x)
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
@@ -288,9 +287,9 @@ class dkimMilter(Milter.Base):
|
|||||||
self.dkim_comment = str(x)
|
self.dkim_comment = str(x)
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
syslog.syslog("check_dkim: {0}".format(x))
|
syslog.syslog("check_dkim: {0}".format(x))
|
||||||
self.header_i = d.signature_fields.get(b'i')
|
self.header_i = codecs.decode(d.signature_fields.get(b'i'), 'ascii')
|
||||||
self.header_d = d.signature_fields.get(b'd')
|
self.header_d = codecs.decode(d.signature_fields.get(b'd'), 'ascii')
|
||||||
self.header_a = d.signature_fields.get(b'a')
|
self.header_a = codecs.decode(d.signature_fields.get(b'a'), 'ascii')
|
||||||
if res:
|
if res:
|
||||||
if (milterconfig.get('Syslog') and
|
if (milterconfig.get('Syslog') and
|
||||||
(milterconfig.get('SyslogSuccess') or
|
(milterconfig.get('SyslogSuccess') or
|
||||||
@@ -355,7 +354,18 @@ def main():
|
|||||||
Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS)
|
Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS)
|
||||||
miltername = 'dkimpy-filter'
|
miltername = 'dkimpy-filter'
|
||||||
socketname = milterconfig.get('Socket')
|
socketname = milterconfig.get('Socket')
|
||||||
own_socketfile(milterconfig)
|
if socketname is None:
|
||||||
|
if int(os.environ.get('LISTEN_PID', '0')) == os.getpid():
|
||||||
|
lfds = os.environ.get('LISTEN_FDS')
|
||||||
|
if lfds is not None:
|
||||||
|
if lfds != '1':
|
||||||
|
syslog.syslog('LISTEN_FDS is set to "{0}", but we only know how to deal with "1", ignoring it'.
|
||||||
|
format(lfds))
|
||||||
|
else:
|
||||||
|
socketname = 'fd:3'
|
||||||
|
if socketname is None:
|
||||||
|
socketname = 'local:/var/run/dkimpy-milter/dkimpy-milter.sock'
|
||||||
|
own_socketfile(milterconfig, socketname)
|
||||||
drop_privileges(milterconfig)
|
drop_privileges(milterconfig)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
Milter.runmilter(miltername, socketname, 240)
|
Milter.runmilter(miltername, socketname, 240)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python2
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from dkimpy_milter import main
|
from dkimpy_milter import main
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -31,16 +31,16 @@ import stat
|
|||||||
import dkim
|
import dkim
|
||||||
import socket
|
import socket
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from dnsplug import Session
|
from .dnsplug import Session
|
||||||
|
|
||||||
# default values
|
# default values
|
||||||
defaultConfigData = {
|
defaultConfigData = {
|
||||||
'Syslog': 'yes',
|
'Syslog': 'yes',
|
||||||
'SyslogFacility': 'mail',
|
'SyslogFacility': 'mail',
|
||||||
'UMask': 007,
|
'UMask': 0o07,
|
||||||
'Mode': 'sv',
|
'Mode': 'sv',
|
||||||
'Socket': 'local:/var/run/dkimpy-milter/dkimpy-milter.sock',
|
'Socket': None,
|
||||||
'PidFile': '/var/run/dkimpy-milter/dkimpy-milter.pid',
|
'PidFile': None,
|
||||||
'UserID': 'dkimpy-milter',
|
'UserID': 'dkimpy-milter',
|
||||||
'Canonicalization': 'relaxed/simple',
|
'Canonicalization': 'relaxed/simple',
|
||||||
'InternalHosts': '127.0.0.1',
|
'InternalHosts': '127.0.0.1',
|
||||||
@@ -85,14 +85,14 @@ class HostsDataset(object):
|
|||||||
self.item = item[1:]
|
self.item = item[1:]
|
||||||
self.negative = True
|
self.negative = True
|
||||||
try:
|
try:
|
||||||
self.item = ipaddress.ip_address(unicode(self.item, "utf-8"))
|
self.item = ipaddress.ip_address(str(self.item, "utf-8"))
|
||||||
if isinstance(self.item, ipaddress.IPv4Address):
|
if isinstance(self.item, ipaddress.IPv4Address):
|
||||||
self.isipv4 = True
|
self.isipv4 = True
|
||||||
elif isinstance(self.item, ipaddress.IPv6Address):
|
elif isinstance(self.item, ipaddress.IPv6Address):
|
||||||
self.isipv6 = True
|
self.isipv6 = True
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
try:
|
try:
|
||||||
self.item = ipaddress.ip_network(unicode
|
self.item = ipaddress.ip_network(str
|
||||||
(self.item, "utf-8"),
|
(self.item, "utf-8"),
|
||||||
strict=False)
|
strict=False)
|
||||||
if isinstance(self.item, ipaddress.IPv4Network):
|
if isinstance(self.item, ipaddress.IPv4Network):
|
||||||
@@ -110,7 +110,7 @@ class HostsDataset(object):
|
|||||||
|
|
||||||
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(str(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/domains first
|
result = self.matchname(source) # Match host/domains first
|
||||||
@@ -160,13 +160,13 @@ class HostsDataset(object):
|
|||||||
if isinstance(source, ipaddress.IPv4Address):
|
if isinstance(source, ipaddress.IPv4Address):
|
||||||
ips = s.dns(name, 'A')
|
ips = s.dns(name, 'A')
|
||||||
for ip in ips:
|
for ip in ips:
|
||||||
ip = ipaddress.IPv4Address(unicode(ip, 'UTF-8'))
|
ip = ipaddress.IPv4Address(str(ip, 'UTF-8'))
|
||||||
if ip == source:
|
if ip == source:
|
||||||
results.append(name)
|
results.append(name)
|
||||||
if isinstance(source, ipaddress.IPv6Address):
|
if isinstance(source, ipaddress.IPv6Address):
|
||||||
ips = s.dns(name, 'AAAA')
|
ips = s.dns(name, 'AAAA')
|
||||||
for ip in ips:
|
for ip in ips:
|
||||||
ip = ipaddress.IPv6Address(unicode(ip, 'UTF-8'))
|
ip = ipaddress.IPv6Address(str(ip, 'UTF-8'))
|
||||||
if ip == source:
|
if ip == source:
|
||||||
results.append(name)
|
results.append(name)
|
||||||
return results
|
return results
|
||||||
@@ -225,13 +225,13 @@ def _processConfigFile(filename=None, configdata=None, useSyslog=1,
|
|||||||
'''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
|
from . import config
|
||||||
if configdata is None:
|
if configdata is None:
|
||||||
configdata = config.defaultConfigData
|
configdata = config.defaultConfigData
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
try:
|
try:
|
||||||
_readConfigFile(filename, configdata)
|
_readConfigFile(filename, configdata)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
raise
|
raise
|
||||||
if useSyslog:
|
if useSyslog:
|
||||||
syslog.syslog(e.args[0])
|
syslog.syslog(e.args[0])
|
||||||
@@ -342,7 +342,7 @@ def _readConfigFile(path, configData=None, configGlobal={}):
|
|||||||
# 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 as e:
|
||||||
syslog.syslog(syslog.LOG_ERR, 'ERROR stating "%s": %s'
|
syslog.syslog(syslog.LOG_ERR, 'ERROR stating "%s": %s'
|
||||||
% (path, e.strerror))
|
% (path, e.strerror))
|
||||||
return(configData)
|
return(configData)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class Session(object):
|
|||||||
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
|
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
|
||||||
cnames[name] = cname
|
cnames[name] = cname
|
||||||
if cname in cnames:
|
if cname in cnames:
|
||||||
raise DNSError, 'CNAME loop'
|
raise DNSError('CNAME loop')
|
||||||
result = self.dns(cname, qtype, cnames=cnames)
|
result = self.dns(cname, qtype, cnames=cnames)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -103,16 +103,16 @@ def DNSLookup_pydns(name, qtype, tcpfallback=True, timeout=30):
|
|||||||
#
|
#
|
||||||
if resp.header['tc'] == True:
|
if resp.header['tc'] == True:
|
||||||
if not tcpfallback:
|
if not tcpfallback:
|
||||||
raise DNS.DNSError, 'DNS: Truncated UDP Reply, SPF records should fit in a UDP packet'
|
raise DNS.DNSError('DNS: Truncated UDP Reply, SPF records should fit in a UDP packet')
|
||||||
try:
|
try:
|
||||||
req = DNS.DnsRequest(name, qtype=qtype, protocol='tcp',
|
req = DNS.DnsRequest(name, qtype=qtype, protocol='tcp',
|
||||||
timeout=timeout)
|
timeout=timeout)
|
||||||
resp = req.req()
|
resp = req.req()
|
||||||
except DNS.DNSError, x:
|
except DNS.DNSError as x:
|
||||||
raise DNS.DNSError, 'TCP Fallback error: ' + str(x)
|
raise DNS.DNSError('TCP Fallback error: ' + str(x))
|
||||||
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
|
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
|
||||||
except IOError, x:
|
except IOError as x:
|
||||||
raise DNS.DNSError, 'DNS: ' + str(x)
|
raise DNS.DNSError('DNS: ' + str(x))
|
||||||
|
|
||||||
def DNSLookup_dnspython(name,qtype,tcpfallback=True,timeout=30):
|
def DNSLookup_dnspython(name,qtype,tcpfallback=True,timeout=30):
|
||||||
retVal = []
|
retVal = []
|
||||||
@@ -164,5 +164,5 @@ if __name__ == '__main__':
|
|||||||
import sys
|
import sys
|
||||||
s = Session()
|
s = Session()
|
||||||
for n,t in zip(*[iter(sys.argv[1:])]*2):
|
for n,t in zip(*[iter(sys.argv[1:])]*2):
|
||||||
print n,t
|
print(n,t)
|
||||||
print s.dns(n,t)
|
print(s.dns(n,t))
|
||||||
|
|||||||
+15
-9
@@ -115,43 +115,49 @@ 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
|
||||||
import syslog
|
import syslog
|
||||||
if not os.path.isfile(milterconfig.get('PidFile')):
|
pidfile = milterconfig.get('PidFile')
|
||||||
|
if pidfile is None:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(pidfile):
|
||||||
pid = str(os.getpid())
|
pid = str(os.getpid())
|
||||||
try:
|
try:
|
||||||
f = open(milterconfig.get('PidFile'), 'w')
|
f = open(pidfile, 'w')
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
if str(e)[:35] == '[Errno 2] No such file or directory':
|
if str(e)[:35] == '[Errno 2] No such file or directory':
|
||||||
piddir = milterconfig.get('PidFile').rsplit('/', 1)[0]
|
piddir = pidfile.rsplit('/', 1)[0]
|
||||||
os.mkdir(piddir)
|
os.mkdir(piddir)
|
||||||
user, group = user_group(milterconfig.get('UserID'))
|
user, group = user_group(milterconfig.get('UserID'))
|
||||||
os.chown(piddir, user, group)
|
os.chown(piddir, user, group)
|
||||||
f = open(milterconfig.get('PidFile'), 'w')
|
f = open(pidfile, 'w')
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
syslog.syslog('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}'
|
syslog.syslog('Unable to write pidfle {0}. IOError: {1}'
|
||||||
.format(milterconfig.get('PidFile'), e))
|
.format(pidfile, e))
|
||||||
raise
|
raise
|
||||||
f.write(pid)
|
f.write(pid)
|
||||||
f.close()
|
f.close()
|
||||||
user, group = user_group(milterconfig.get('UserID'))
|
user, group = user_group(milterconfig.get('UserID'))
|
||||||
os.chown(milterconfig.get('PidFile'), user, group)
|
os.chown(pidfile, user, group)
|
||||||
else:
|
else:
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
syslog.syslog('Unable to write pidfle {0}. File exists.'
|
syslog.syslog('Unable to write pidfle {0}. File exists.'
|
||||||
.format(milterconfig.get('PidFile')))
|
.format(pidfile))
|
||||||
raise RuntimeError('Unable to write pidfle {0}. File exists.'
|
raise RuntimeError('Unable to write pidfle {0}. File exists.'
|
||||||
.format(milterconfig.get('PidFile')))
|
.format(pidfile))
|
||||||
return pid
|
return pid
|
||||||
|
|
||||||
|
|
||||||
def own_socketfile(milterconfig):
|
def own_socketfile(milterconfig, sockname=None):
|
||||||
"""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
|
||||||
user, group = user_group(milterconfig.get('UserID'))
|
user, group = user_group(milterconfig.get('UserID'))
|
||||||
offset = None
|
offset = None
|
||||||
|
if sockname is None:
|
||||||
sockname = milterconfig.get('Socket')
|
sockname = milterconfig.get('Socket')
|
||||||
|
if sockname is None:
|
||||||
|
return
|
||||||
if sockname[:1] == '/':
|
if sockname[:1] == '/':
|
||||||
offset = 0
|
offset = 0
|
||||||
elif sockname[:6] == "local:":
|
elif sockname[:6] == "local:":
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ will be checked. [PeerList NOT IMPLEMENTED - included for reference only]
|
|||||||
.TP
|
.TP
|
||||||
.I PidFile (string)
|
.I PidFile (string)
|
||||||
Specifies the path to a file that should be created at process start
|
Specifies the path to a file that should be created at process start
|
||||||
containing the process ID.
|
containing the process ID. If not specified, no such file will be created.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.I Selector (string)
|
.I Selector (string)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python3
|
||||||
# dkimpy-milter: A DKIM signing/verification Milter application
|
# dkimpy-milter: A DKIM signing/verification Milter application
|
||||||
# Author: Scott Kitterman <scott@kitterman.com>
|
# Author: Scott Kitterman <scott@kitterman.com>
|
||||||
# Copyright 2018 Scott Kitterman
|
# Copyright 2018,2019 Scott Kitterman
|
||||||
""" This program is free software; you can redistribute it and/or modify
|
""" This program is free software; you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation; either version 2 of the License, or
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
@@ -24,13 +24,13 @@ description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for P
|
|||||||
kw = {} # Work-around for lack of 'or' requires in setuptools.
|
kw = {} # Work-around for lack of 'or' requires in setuptools.
|
||||||
try:
|
try:
|
||||||
import dns
|
import dns
|
||||||
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dnspython']
|
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'dnspython']
|
||||||
except ImportError: # If PyDNS is not installed, prefer dnspython
|
except ImportError: # If PyDNS is not installed, prefer dnspython
|
||||||
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'PyDNS']
|
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'PyDNS']
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='dkimpy-milter',
|
name='dkimpy-milter',
|
||||||
version='1.0.1',
|
version='1.1.0',
|
||||||
author='Scott Kitterman',
|
author='Scott Kitterman',
|
||||||
author_email='scott@kitterman.com',
|
author_email='scott@kitterman.com',
|
||||||
url='https://launchpad.net/dkimpy-milter',
|
url='https://launchpad.net/dkimpy-milter',
|
||||||
@@ -43,7 +43,7 @@ setup(
|
|||||||
'License :: OSI Approved :: GNU General Public License (GPL)',
|
'License :: OSI Approved :: GNU General Public License (GPL)',
|
||||||
'Natural Language :: English',
|
'Natural Language :: English',
|
||||||
'Operating System :: POSIX',
|
'Operating System :: POSIX',
|
||||||
'Programming Language :: Python :: 2 :: Only',
|
'Programming Language :: Python :: 3 :: Only',
|
||||||
'Topic :: Communications :: Email :: Mail Transport Agents',
|
'Topic :: Communications :: Email :: Mail Transport Agents',
|
||||||
'Topic :: Communications :: Email :: Filters',
|
'Topic :: Communications :: Email :: Filters',
|
||||||
'Topic :: Security',
|
'Topic :: Security',
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
This directory contains example systemd unit files for running a
|
||||||
|
supervised, socket-activated instance of dkimpy-milter.
|
||||||
|
|
||||||
|
There are several advantages of using socket activation:
|
||||||
|
|
||||||
|
- dkimpy-milter never runs with elevated privileges, they are dropped
|
||||||
|
before any dkimpy-milter code is executed.
|
||||||
|
|
||||||
|
- The socket is opened before dkimpy-milter runs. This means that
|
||||||
|
clients can connect() to the socket immediately. So even if there
|
||||||
|
is a delay in dkimpy-milter startup, or in libmilter itself, the
|
||||||
|
connection will not fail.
|
||||||
|
|
||||||
|
- You can set the privileges of a listening Unix-domain socket by an
|
||||||
|
override of ListenGroup= in dkimpy-milter.socket (see
|
||||||
|
systemd.unit(5) for how to override). This lets you control who has
|
||||||
|
access to the daemon with finer granularity than is available with
|
||||||
|
dkimpy-milter on its own.
|
||||||
|
|
||||||
|
- dkimpy-milter will not consume system resources if it is not used.
|
||||||
|
|
||||||
|
- A fully-supervised dkimpy-milter needs no PIDFile, UMask, UserID, or
|
||||||
|
Socket configuation. This eliminates common race conditions and
|
||||||
|
startup failures, and simplifies the resulting configuration file.
|
||||||
|
|
||||||
|
There is one downside to using socket activation:
|
||||||
|
|
||||||
|
- it will only work on systems where libmilter can support connection
|
||||||
|
strings like "fd:3". This has been supported on Debian and derived
|
||||||
|
systems since sendmail 8.14.4-6 (before Debian Jessie, in early
|
||||||
|
2014), see for example:
|
||||||
|
https://sources.debian.org/src/sendmail/8.15.2-8/debian/patches/socket_activation.patch/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=DKIMpy Milter
|
||||||
|
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
|
||||||
|
Requires=dkimpy-milter.socket
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/dkimpy-milter /etc/dkimpy-milter.conf
|
||||||
|
User=dkimpy-milter
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
Also=dkimpy-milter.socket
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=DKIMpy Milter socket
|
||||||
|
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=/run/dkimpy-milter/dkimpy-milter.sock
|
||||||
|
SocketMode=0660
|
||||||
|
# override SocketGroup to grant access to members of another system group:
|
||||||
|
SocketGroup=dkimpy-milter
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sockets.target
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
python2 -m dkimpy_milter "$@"
|
python3 -m dkimpy_milter "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user