Merge dkimpy-milter:dkg/test-suite into dkimpy-milter:1_0

Proposed by dkg
Status: Merged
Merge reported by: Scott Kitterman
Merged at revision: ad8f396db0700e5bc351d74d2fa57f71df464026
Proposed branch: dkimpy-milter:dkg/test-suite
Merge into: dkimpy-milter:1_0
Diff against target: 422 lines (+283/-11)
12 files modified
.gitignore (+1/-0)
dkimpy_milter/__init__.py (+9/-2)
dkimpy_milter/__main__.py (+6/-0)
dkimpy_milter/config.py (+7/-2)
dkimpy_milter/util.py (+12/-4)
man/dkimpy-milter.conf.5 (+7/-0)
setup.py (+3/-3)
tests/00_minimal.miltertest (+12/-0)
tests/01_connect.miltertest (+40/-0)
tests/02_sign_message.miltertest (+100/-0)
tests/dkimpy-milter (+2/-0)
tests/runtests (+84/-0)
Reviewer Review Type Date Requested Status
Scott Kitterman Pending
Review via email: mp+363526@code.launchpad.net

Commit message

This series fixes several minor bugs and adds a rudimentary overall test suite, invokable via:

    tests/runtests

the test suite doesn't cover everything, but it works from the outside of dkimpy-milter itself (using opendkim's miltertest lua scripting language) and exercises basic signing and verification mechanisms.

Having the test suite in place makes it possible to do things like conversion to python3 while knowing that the major functionality at least is intact.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index 9e59230..8bd7e59 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -1,3 +1,4 @@
6 dist
7 dkimpy_milter.egg-info
8 *.pyc
9+*~
10diff --git a/dkimpy_milter/__init__.py b/dkimpy_milter/__init__.py
11index 3791748..5345fc7 100644
12--- a/dkimpy_milter/__init__.py
13+++ b/dkimpy_milter/__init__.py
14@@ -61,7 +61,9 @@ class dkimMilter(Milter.Base):
15 self.external_connection = False
16 self.hello_name = None
17 # sometimes people put extra space in sendmail config, so we strip
18- self.receiver = self.getsymval('j').strip()
19+ self.receiver = self.getsymval('j')
20+ if self.receiver is not None:
21+ self.receiver = self.receiver.strip()
22 try:
23 self.AuthservID = milterconfig['AuthservID']
24 except:
25@@ -258,7 +260,12 @@ class dkimMilter(Milter.Base):
26 for y in range(self.has_dkim): # Verify _ALL_ the signatures
27 d = dkim.DKIM(txt)
28 try:
29- res = d.verify(idx=y)
30+ dnsoverride = milterconfig.get('DNSOverride')
31+ if isinstance(dnsoverride, str):
32+ syslog.syslog("DNSOverride: {0}".format(dnsoverride))
33+ res = d.verify(idx=y, dnsfunc=lambda _x: dnsoverride)
34+ else:
35+ res = d.verify(idx=y)
36 if res:
37 if d.signature_fields.get(b'a') == 'ed25519-sha256':
38 self.dkim_comment = ('Good {0} signature'
39diff --git a/dkimpy_milter/__main__.py b/dkimpy_milter/__main__.py
40new file mode 100644
41index 0000000..8c5cf9c
42--- /dev/null
43+++ b/dkimpy_milter/__main__.py
44@@ -0,0 +1,6 @@
45+#!/usr/bin/python2
46+
47+from dkimpy_milter import main
48+
49+if __name__ == "__main__":
50+ main()
51diff --git a/dkimpy_milter/config.py b/dkimpy_milter/config.py
52index 9f42af2..d562e97 100644
53--- a/dkimpy_milter/config.py
54+++ b/dkimpy_milter/config.py
55@@ -48,6 +48,7 @@ defaultConfigData = {
56 'DiagnosticDirectory': '',
57 'MacroList': '',
58 'MacroListVerify': '',
59+ 'DNSOverride': None,
60 'debugLevel': 0 # Undocumented config item for developer use
61 }
62
63@@ -334,6 +335,7 @@ def _readConfigFile(path, configData=None, configGlobal={}):
64 'DiagnosticDirectory': 'str',
65 'MacroList': 'dataset',
66 'MacroListVerify': 'dataset',
67+ 'DNSOverride': 'str',
68 'debugLevel': 'int'
69 }
70
71@@ -388,7 +390,10 @@ def _readConfigFile(path, configData=None, configGlobal={}):
72 if conversion == 'bool':
73 configData[name] = _find_boolean(value)
74 elif conversion == 'str':
75- configData[name] = str(value)
76+ if isinstance(value, list):
77+ configData[name] = line.split(None, 1)[1]
78+ else:
79+ configData[name] = str(value)
80 elif conversion == 'int':
81 configData[name] = int(value)
82 elif conversion == 'dataset':
83@@ -399,7 +404,7 @@ def _readConfigFile(path, configData=None, configGlobal={}):
84 configData[name] = conversion(value)
85 fp.close()
86 try:
87- configData['AuthservID'] = _make_authserv_id(configData['AuthservID'])
88+ configData['AuthservID'] = _make_authserv_id(configData.get('AuthservID', 'HOSTNAME'))
89 configData['IntHosts'] = HostsDataset(configData['InternalHosts'])
90 except:
91 pass
92diff --git a/dkimpy_milter/util.py b/dkimpy_milter/util.py
93index 5d3f69d..17857d6 100644
94--- a/dkimpy_milter/util.py
95+++ b/dkimpy_milter/util.py
96@@ -150,10 +150,18 @@ def own_socketfile(milterconfig):
97 """If socket is Unix socket, chown to UserID before dropping privileges"""
98 import os
99 user, group = user_group(milterconfig.get('UserID'))
100- if milterconfig.get('Socket')[:1] == '/':
101- os.chown(milterconfig.get('Socket')[1:], user, group)
102- if milterconfig.get('Socket')[:6] == "local:":
103- os.chown(milterconfig.get('Socket')[6:], user, group)
104+ offset = None
105+ sockname = milterconfig.get('Socket')
106+ if sockname[:1] == '/':
107+ offset = 0
108+ elif sockname[:6] == "local:":
109+ offset = 6
110+ elif sockname[:5] == "unix:":
111+ offset = 5
112+
113+ if offset is not None:
114+ if os.path.exists(sockname[offset:]):
115+ os.chown(sockname[offset:], user, group)
116
117
118 def read_keyfile(milterconfig, keytype):
119diff --git a/man/dkimpy-milter.conf.5 b/man/dkimpy-milter.conf.5
120index 3dd7612..a7e5d31 100644
121--- a/man/dkimpy-milter.conf.5
122+++ b/man/dkimpy-milter.conf.5
123@@ -311,6 +311,13 @@ be set:
124 (b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector;
125 [fooTable options NOT IMPLEMENTED]
126
127+.TP
128+.I DNSOverride (string)
129+Provide a text string that a verifying milter should use instead of
130+consulting the DNS on each message. This is useful primarily for
131+testing purposes in environments where it is awkward to modify the
132+system DNS resolution. It should not be used in production.
133+
134 .TP
135 .I PeerList (dataset)
136 Identifies a set of "peers" that identifies clients whose connections
137diff --git a/setup.py b/setup.py
138index 41a11a5..18c2ec9 100644
139--- a/setup.py
140+++ b/setup.py
141@@ -23,10 +23,10 @@ description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for P
142
143 kw = {} # Work-around for lack of 'or' requires in setuptools.
144 try:
145- import DNS
146- kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'PyDNS']
147-except ImportError: # If PyDNS is not installed, prefer dnspython
148+ import dns
149 kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dnspython']
150+except ImportError: # If PyDNS is not installed, prefer dnspython
151+ kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'PyDNS']
152
153 setup(
154 name='dkimpy-milter',
155diff --git a/tests/00_minimal.miltertest b/tests/00_minimal.miltertest
156new file mode 100644
157index 0000000..fbe0849
158--- /dev/null
159+++ b/tests/00_minimal.miltertest
160@@ -0,0 +1,12 @@
161+-- -*- lua -*-
162+for _, keytype in ipairs({"ed25519", "rsa"}) do
163+ for _, func in ipairs({"signing", "verify"}) do
164+ mt.echo("testing "..keytype.." "..func)
165+ conn = mt.connect("unix:"..keytype.."."..func..".sock")
166+ if conn == nil then
167+ error("mt.connect() failed "..keytype.." "..func)
168+ end
169+ mt.disconnect(conn)
170+ mt.echo(keytype.." "..func.." complete")
171+ end
172+end
173diff --git a/tests/01_connect.miltertest b/tests/01_connect.miltertest
174new file mode 100755
175index 0000000..2f43eff
176--- /dev/null
177+++ b/tests/01_connect.miltertest
178@@ -0,0 +1,40 @@
179+-- -*- lua -*-
180+for _, keytype in ipairs({"ed25519", "rsa"}) do
181+ for _, func in ipairs({"signing", "verify"}) do
182+ mt.echo("testing "..keytype.." "..func)
183+ conn = mt.connect("unix:"..keytype.."."..func..".sock")
184+ if conn == nil then
185+ error("mt.connect() failed "..keytype.." "..func)
186+ end
187+ if mt.conninfo(conn, "localhost", "127.0.0.1") ~= nil then
188+ error("mt.conninfo() failed "..keytype.." "..func)
189+ end
190+ if mt.getreply(conn) ~= SMFIR_CONTINUE then
191+ error("mt.conninfo() unexpected reply "..keytype.." "..func)
192+ end
193+
194+ if mt.test_action(conn, SMFIF_ADDHDRS) then
195+ print("could add headers "..keytype.." "..func)
196+ else
197+ error("mt.test_action() says could not add headers "..keytype.." "..func)
198+ end
199+
200+ if mt.test_action(conn, SMFIF_CHGHDRS) then
201+ print("could change headers "..keytype.." "..func)
202+ else
203+ error("mt.test_action() says could not change headers "..keytype.." "..func)
204+ end
205+
206+-- -- FIXME: this part of the test fails, as apparently the
207+-- -- dkimpy-milter claims the right to change the body of a message,
208+-- -- even though it shouldn't. How can we fix the negotiation?
209+-- if mt.test_action(conn, SMFIF_CHGBODY) then
210+-- error("mt.test_action() says could change body "..keytype.." "..func)
211+-- else
212+-- print("could not change body "..keytype.." "..func)
213+-- end
214+
215+ mt.disconnect(conn)
216+ mt.echo(keytype.." "..func.." test complete")
217+ end
218+end
219diff --git a/tests/02_sign_message.miltertest b/tests/02_sign_message.miltertest
220new file mode 100644
221index 0000000..cb5e7ff
222--- /dev/null
223+++ b/tests/02_sign_message.miltertest
224@@ -0,0 +1,100 @@
225+-- -*- lua -*-
226+
227+msg = {
228+ ['headers'] = {
229+ ['From'] = 'Alice <alice@example.net>',
230+ ['Message-Id'] = '<dkimpy-milter-test-02@example.net>',
231+ ['To'] = 'Bob <bob@example.biz>',
232+ ['Date'] = 'Mon, 18 Feb 2019 08:32:50 -0500',
233+ ['Subject'] = 'Signing test',
234+ ['Content-Type'] = 'text/plain',
235+ },
236+ ['body'] = "This is a test!\r\n",
237+}
238+
239+-- returns miltertest connection object
240+function connect_and_send (sockname, headers, body)
241+ conn = mt.connect(sockname)
242+ if conn == nil then
243+ error "mt.connect() failed"
244+ end
245+ if mt.conninfo(conn, "localhost", "127.0.0.1") ~= nil then
246+ error "mt.conninfo() failed"
247+ end
248+ if mt.getreply(conn) ~= SMFIR_CONTINUE then
249+ error "mt.conninfo() unexpected reply"
250+ end
251+
252+ -- mt.macro(conn, SMFIC_MAIL, "i", "simple-message")
253+ if mt.mailfrom(conn, "<alice@example.net>") ~= nil then
254+ error "mt.mailfrom() failed"
255+ end
256+ if mt.getreply(conn) ~= SMFIR_CONTINUE then
257+ error "mt.mailfrom() unexpected reply"
258+ end
259+ -- mt.rcptto() is called implicitly
260+
261+ -- send headers
262+ for key,value in pairs(headers) do
263+ if mt.header(conn, key, value) ~= nil then
264+ error("mt.header(" .. key .. ") failed")
265+ end
266+ if mt.getreply(conn) ~= SMFIR_CONTINUE then
267+ error("mt.header(" .. key .. ") unexpected reply")
268+ end
269+ end
270+ -- send EOH
271+ if mt.eoh(conn) ~= nil then
272+ error "mt.eoh() failed"
273+ end
274+ if mt.getreply(conn) ~= SMFIR_CONTINUE then
275+ error "mt.eoh() unexpected reply"
276+ end
277+
278+ -- send body
279+ if mt.bodystring(conn, body) ~= nil then
280+ error "mt.bodystring() failed"
281+ end
282+ if mt.getreply(conn) ~= SMFIR_CONTINUE then
283+ error "mt.bodystring() unexpected reply"
284+ end
285+ -- end of message; let the filter react
286+ if mt.eom(conn) ~= nil then
287+ error "mt.eom() failed"
288+ end
289+ reply = mt.getreply(conn)
290+ if reply ~= SMFIR_CONTINUE then
291+ error ("mt.eom() unexpected reply: " .. reply)
292+ end
293+ return conn
294+end
295+
296+for _, keytype in ipairs({"ed25519", "rsa"}) do
297+ mt.echo("testing "..keytype)
298+ signing = connect_and_send("unix:"..keytype..".signing.sock", msg.headers, msg.body)
299+ -- verify that a test header field got added
300+ if not mt.eom_check(signing, MT_HDRINSERT) then
301+ error "no header added by signer"
302+ end
303+
304+ signature = mt.getheader(signing, "DKIM-Signature", 0)
305+
306+ mt.disconnect(signing)
307+
308+ mt.echo("DKIM-Signature: " .. signature)
309+
310+ msg.headers['DKIM-Signature'] = signature
311+
312+ verify = connect_and_send("unix:"..keytype..".verify.sock", msg.headers, msg.body)
313+
314+ if not mt.eom_check(verify, MT_HDRINSERT) then
315+ error "no header added in verify"
316+ end
317+
318+ authres = mt.getheader(verify, "Authentication-Results", 0)
319+ mt.echo("Authentication-Results: "..authres)
320+
321+ mt.disconnect(verify)
322+
323+ mt.echo(keytype.." complete")
324+end
325diff --git a/tests/dkimpy-milter b/tests/dkimpy-milter
326new file mode 100755
327index 0000000..39b64d5
328--- /dev/null
329+++ b/tests/dkimpy-milter
330@@ -0,0 +1,2 @@
331+#!/bin/sh
332+python2 -m dkimpy_milter "$@"
333diff --git a/tests/runtests b/tests/runtests
334new file mode 100755
335index 0000000..7878f17
336--- /dev/null
337+++ b/tests/runtests
338@@ -0,0 +1,84 @@
339+#!/bin/bash
340+
341+set -e
342+WORKDIR=$(mktemp -d)
343+TESTDIR=$(realpath "$(dirname "$0")")
344+DKIMPY_MILTER=${DKIMPY_MILTER:-"$TESTDIR/dkimpy-milter"}
345+KEY_TYPES=(ed25519 rsa)
346+
347+cd "$WORKDIR"
348+
349+printf "Testing %s from directory %s\n" "$DKIMPY_MILTER" "$WORKDIR"
350+
351+for keytype in "${KEY_TYPES[@]}"; do
352+ dknewkey --ktype "$keytype" "testkey.$keytype"
353+ if [ "$keytype" = ed25519 ]; then
354+ keyfile=KeyFileEd25519
355+ selector=SelectorEd25519
356+ else
357+ keyfile=KeyFile
358+ selector=Selector
359+ fi
360+ cat > "$keytype.signing.conf" <<EOF
361+Domain example.net
362+$keyfile testkey.$keytype.key
363+$selector testkey
364+Socket unix:$keytype.signing.sock
365+PidFile $keytype.signing.pid
366+Mode s
367+UserID $(id --name --user):$(id --name --group)
368+EOF
369+
370+ cat > "$keytype.verify.conf" <<EOF
371+Socket unix:$keytype.verify.sock
372+PidFile $keytype.verify.pid
373+Mode v
374+DNSOverride $(cat testkey.$keytype.dns)
375+UserID $(id --name --user):$(id --name --group)
376+EOF
377+done
378+
379+cleanup() {
380+ echo cleaning up jobs:
381+ jobs
382+ for keytype in "${KEY_TYPES[@]}"; do
383+ for func in signing verify; do
384+ if [ -s "$keytype.$func.pid" ] && kill -0 "$(cat "$keytype.$func.pid")"; then
385+ kill "$(cat $keytype.$func.pid)"
386+ fi
387+ done
388+ done
389+ wait
390+ for keytype in "${KEY_TYPES[@]}"; do
391+ for func in signing verify; do
392+ errdata="$keytype.$func.stderr"
393+ if [ -s "$errdata" ]; then
394+ printf -- "-> %s:\n" "$errdata"
395+ cat "$errdata"
396+ printf -- "-> end %s\n" "$errdata"
397+ fi
398+ done
399+ done
400+ rm -rf "$WORKDIR"
401+}
402+
403+for keytype in "${KEY_TYPES[@]}"; do
404+ for func in signing verify; do
405+ PYTHONPATH="$(dirname "$TESTDIR")" "$DKIMPY_MILTER" "$keytype.$func.conf" 2>"$keytype.$func.stderr" &
406+ done
407+done
408+trap cleanup EXIT
409+
410+# ugly ugly (how are we supposed to know that the milters are all ready?):
411+sleep 2
412+
413+# uses miltertest from opendkim:
414+for x in ${TESTS:-"$TESTDIR"/*.miltertest}; do
415+ if ! [ -e "$x" ]; then
416+ if [ -e "$TESTDIR/$x" ]; then
417+ x="$TESTDIR/$x"
418+ fi
419+ fi
420+ printf -- "-> running %s...\n" "$x"
421+ miltertest -s "$x"
422+done

Subscribers

People subscribed via source and target branches

to all changes: