Skip to content

Commit 85ee974

Browse files
committed
authres: parser rework
Rewrite of the Authentication-Results header parser for a complete RFC 8601 implementation. Ignore any header comments in parenthesis. Allow escape sequences and semi-colons in comments and quoted strings as values. Fixes: #32
1 parent 5a36d97 commit 85ee974

File tree

2 files changed

+259
-38
lines changed

2 files changed

+259
-38
lines changed

authres/parse.go

+180-38
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package authres
22

33
import (
4+
"bufio"
45
"errors"
6+
"io"
57
"strings"
68
"unicode"
79
)
@@ -222,58 +224,94 @@ var results = map[string]newResultFunc{
222224
// Parse parses the provided Authentication-Results header field. It returns the
223225
// authentication service identifier and authentication results.
224226
func Parse(v string) (identifier string, results []Result, err error) {
225-
parts := strings.Split(v, ";")
227+
p := newParser(v)
226228

227-
identifier = strings.TrimSpace(parts[0])
228-
i := strings.IndexFunc(identifier, unicode.IsSpace)
229-
if i > 0 {
230-
version := strings.TrimSpace(identifier[i:])
231-
if version != "1" {
232-
return "", nil, errors.New("msgauth: unsupported version")
233-
}
229+
identifier, err = p.getIndentifier()
230+
if err != nil {
231+
return identifier, nil, err
232+
}
234233

235-
identifier = identifier[:i]
234+
for {
235+
result, err := p.getResult()
236+
if result == nil {
237+
break
238+
}
239+
results = append(results, result)
240+
if err == io.EOF {
241+
break
242+
} else if err != nil {
243+
return identifier, results, err
244+
}
236245
}
237246

238-
for i := 1; i < len(parts); i++ {
239-
s := strings.TrimSpace(parts[i])
240-
if s == "" {
247+
return identifier, results, nil
248+
}
249+
250+
type parser struct {
251+
r *bufio.Reader
252+
}
253+
254+
func newParser(v string) *parser {
255+
return &parser{r: bufio.NewReader(strings.NewReader(v))}
256+
}
257+
258+
// getIdentifier parses the authserv-id of the authres header and checks the
259+
// version id when present. Ignore header comments in parenthesis.
260+
func (p *parser) getIndentifier() (identifier string, err error) {
261+
for {
262+
c, err := p.r.ReadByte()
263+
if err == io.EOF {
264+
return identifier, nil
265+
} else if err != nil {
266+
return identifier, err
267+
}
268+
if c == '(' {
269+
p.r.UnreadByte()
270+
p.readComment()
241271
continue
242272
}
243-
244-
result, err := parseResult(s)
245-
if err != nil {
246-
return identifier, results, err
273+
if c == ';' {
274+
break
247275
}
248-
if result != nil {
249-
results = append(results, result)
276+
identifier += string(c)
277+
}
278+
279+
fields := strings.Fields(identifier)
280+
if len(fields) > 1 {
281+
version := strings.TrimSpace(fields[1])
282+
if version != "1" {
283+
return "", errors.New("msgauth: unknown version")
250284
}
285+
} else if len(fields) == 0 {
286+
return "", errors.New("msgauth: no identifier found")
251287
}
252-
return
288+
return strings.TrimSpace(fields[0]), nil
253289
}
254290

255-
func parseResult(s string) (Result, error) {
256-
// TODO: ignore header comments in parenthesis
257-
258-
parts := strings.Fields(s)
259-
if len(parts) == 0 || parts[0] == "none" {
291+
// getResults parses the authentication part of the authres header and returns
292+
// a Result struct. Ignore header comments in parenthesis.
293+
func (p *parser) getResult() (result Result, err error) {
294+
method, resultvalue, err := p.keyValue()
295+
if method == "none" {
260296
return nil, nil
261297
}
262-
263-
k, v, err := parseParam(parts[0])
264298
if err != nil {
265299
return nil, err
266300
}
267-
method, value := k, ResultValue(strings.ToLower(v))
301+
value := ResultValue(strings.ToLower(resultvalue))
268302

269303
params := make(map[string]string)
270-
for i := 1; i < len(parts); i++ {
271-
k, v, err := parseParam(parts[i])
272-
if err != nil {
273-
continue
304+
var k, v string
305+
for {
306+
k, v, err = p.keyValue()
307+
if k != "" {
308+
params[k] = v
309+
}
310+
if err == io.EOF {
311+
break
312+
} else if err != nil {
313+
return nil, err
274314
}
275-
276-
params[k] = v
277315
}
278316

279317
newResult, ok := results[method]
@@ -293,10 +331,114 @@ func parseResult(s string) (Result, error) {
293331
return r, nil
294332
}
295333

296-
func parseParam(s string) (k string, v string, err error) {
297-
kv := strings.SplitN(s, "=", 2)
298-
if len(kv) != 2 {
299-
return "", "", errors.New("msgauth: malformed authentication method and value")
334+
// keyValue parses a sequence of key=value parameters
335+
func (p *parser) keyValue() (k, v string, err error) {
336+
k, err = p.readKey()
337+
if err != nil {
338+
return
339+
}
340+
v, err = p.readValue()
341+
if err != nil {
342+
return
300343
}
301-
return strings.ToLower(strings.TrimSpace(kv[0])), strings.TrimSpace(kv[1]), nil
344+
return
345+
}
346+
347+
// readKey reads a method, reason or ptype.property as defined in RFC 8601
348+
// Section 2.2. Ignore the method-version of the methodspec. Stop at EOF or the
349+
// equal sign.
350+
func (p *parser) readKey() (k string, err error) {
351+
var c byte
352+
for err != io.EOF {
353+
c, err = p.r.ReadByte()
354+
if err != nil {
355+
break
356+
}
357+
switch c {
358+
case ';':
359+
err = io.EOF
360+
break
361+
case '=':
362+
break
363+
case '(':
364+
p.r.UnreadByte()
365+
_, err = p.readComment()
366+
continue
367+
case '/':
368+
p.r.ReadBytes('=')
369+
p.r.UnreadByte()
370+
default:
371+
if !unicode.IsSpace(rune(c)) {
372+
k += string(c)
373+
}
374+
}
375+
if c == '=' {
376+
break
377+
}
378+
}
379+
k = strings.TrimSpace(strings.ToLower(k))
380+
return
381+
}
382+
383+
// readValue reads a result or value as defined in RFC 8601 Section 2.2. Value
384+
// is defined as either a token or quoted string according to RFC 2045 Section
385+
// 5.1. Stop at EOF, white space or semi-colons.
386+
func (p *parser) readValue() (v string, err error) {
387+
var c byte
388+
for err != io.EOF {
389+
c, err = p.r.ReadByte()
390+
if err != nil {
391+
break
392+
}
393+
switch c {
394+
case ';':
395+
err = io.EOF
396+
break
397+
case '(':
398+
p.r.UnreadByte()
399+
_, err = p.readComment()
400+
continue
401+
case '"':
402+
v, err = p.r.ReadString(c)
403+
v = strings.TrimSuffix(v, string(c))
404+
default:
405+
if !unicode.IsSpace(rune(c)) {
406+
v += string(c)
407+
}
408+
}
409+
if unicode.IsSpace(rune(c)) {
410+
if v != "" {
411+
break
412+
}
413+
}
414+
}
415+
v = strings.TrimSpace(v)
416+
return
417+
}
418+
419+
func (p *parser) readComment() (comment string, err error) {
420+
count := 0
421+
var c byte
422+
for {
423+
c, err = p.r.ReadByte()
424+
if err != nil {
425+
break
426+
}
427+
switch c {
428+
case '\\':
429+
c, _ = p.r.ReadByte()
430+
comment += "\\" + string(c)
431+
case '(':
432+
count++
433+
case ')':
434+
count--
435+
default:
436+
comment += string(c)
437+
}
438+
if count == 0 {
439+
break
440+
}
441+
}
442+
comment = strings.TrimSpace(comment)
443+
return
302444
}

authres/parse_test.go

+79
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ var parseTests = []msgauthTest{
2424
&SPFResult{Value: ResultPass, From: "example.net"},
2525
},
2626
},
27+
{
28+
value: "example.com;" +
29+
"dkim=pass reason=\"good signature\" [email protected];",
30+
identifier: "example.com",
31+
results: []Result{
32+
&DKIMResult{Value: ResultPass, Reason: "good signature", Identifier: "@mail-router.example.net"},
33+
},
34+
},
35+
{
36+
value: "example.com;" +
37+
"dkim=pass reason=\"good; signature\" [email protected];",
38+
identifier: "example.com",
39+
results: []Result{
40+
&DKIMResult{Value: ResultPass, Reason: "good; signature", Identifier: "@mail-router.example.net"},
41+
},
42+
},
2743
{
2844
value: "example.com;" +
2945
" auth=pass (cram-md5) [email protected];",
@@ -32,6 +48,63 @@ var parseTests = []msgauthTest{
3248
&AuthResult{Value: ResultPass, Auth: "[email protected]"},
3349
},
3450
},
51+
{
52+
value: "example.com;" +
53+
" auth=pass (cram-md5) [email protected];" +
54+
" spf=pass smtp.mailfrom=example.net",
55+
identifier: "example.com",
56+
results: []Result{
57+
&AuthResult{Value: ResultPass, Auth: "[email protected]"},
58+
&SPFResult{Value: ResultPass, From: "example.net"},
59+
},
60+
},
61+
{
62+
value: "example.com;" +
63+
" auth=pass (cram-md5 (comment inside comment)) [email protected];",
64+
identifier: "example.com",
65+
results: []Result{
66+
&AuthResult{Value: ResultPass, Auth: "[email protected]"},
67+
},
68+
},
69+
{
70+
value: "example.com;" +
71+
" auth=pass (cram-md5; comment with semicolon) [email protected];",
72+
identifier: "example.com",
73+
results: []Result{
74+
&AuthResult{Value: ResultPass, Auth: "[email protected]"},
75+
},
76+
},
77+
{
78+
value: "example.com;" +
79+
" auth=pass (cram-md5 \\( comment with escaped char) [email protected];",
80+
identifier: "example.com",
81+
results: []Result{
82+
&AuthResult{Value: ResultPass, Auth: "[email protected]"},
83+
},
84+
},
85+
{
86+
value: "foo.example.net (foobar) 1 (baz);" +
87+
" dkim (Because I like it) / 1 (One yay) = (wait for it) fail" +
88+
" policy (A dot can go here) . (like that) expired" +
89+
" (this surprised me) = (as I wasn't expecting it) 1362471462",
90+
identifier: "foo.example.net",
91+
results: []Result{
92+
&DKIMResult{Value: ResultFail, Reason: "", Domain: "", Identifier: ""},
93+
},
94+
},
95+
}
96+
97+
var mustFailParseTests = []msgauthTest{
98+
{
99+
value: " ; ",
100+
identifier: "",
101+
results: nil,
102+
},
103+
{
104+
value: "example.com 2; none",
105+
identifier: "example.com",
106+
results: nil,
107+
},
35108
}
36109

37110
func TestParse(t *testing.T) {
@@ -51,4 +124,10 @@ func TestParse(t *testing.T) {
51124
}
52125
}
53126
}
127+
for _, test := range mustFailParseTests {
128+
_, _, err := Parse(test.value)
129+
if err == nil {
130+
t.Errorf("Expected an error when parsing header, but got none.")
131+
}
132+
}
54133
}

0 commit comments

Comments
 (0)