1package mtasts
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "github.com/mjl-/mox/dns"
9)
10
11type parseErr string
12
13func (e parseErr) Error() string {
14 return string(e)
15}
16
17var _ error = parseErr("")
18
19// ParseRecord parses an MTA-STS record.
20func ParseRecord(txt string) (record *Record, ismtasts bool, err error) {
21 defer func() {
22 x := recover()
23 if x == nil {
24 return
25 }
26 if xerr, ok := x.(parseErr); ok {
27 record = nil
28 err = fmt.Errorf("%w: %s", ErrRecordSyntax, xerr)
29 return
30 }
31 panic(x)
32 }()
33
34 // Parsing is mostly case-sensitive.
35 // ../rfc/8461:306
36 p := newParser(txt)
37 record = &Record{
38 Version: "STSv1",
39 }
40 seen := map[string]struct{}{}
41 p.xtake("v=STSv1")
42 p.xdelim()
43 ismtasts = true
44 for {
45 k := p.xkey()
46 p.xtake("=")
47
48 // Section 3.1 about the TXT record does not say anything about duplicate fields.
49 // But section 3.2 about (parsing) policies has a paragraph that starts
50 // requirements on both TXT and policy records. That paragraph ends with a note
51 // about handling duplicate fields. Let's assume that note also applies to TXT
52 // records. ../rfc/8461:517
53 _, dup := seen[k]
54 seen[k] = struct{}{}
55
56 switch k {
57 case "id":
58 if !dup {
59 record.ID = p.xid()
60 }
61 default:
62 v := p.xvalue()
63 record.Extensions = append(record.Extensions, Pair{k, v})
64 }
65 if !p.delim() || p.empty() {
66 break
67 }
68 }
69 if !p.empty() {
70 p.xerrorf("leftover characters")
71 }
72 if record.ID == "" {
73 p.xerrorf("missing id")
74 }
75 return
76}
77
78// ParsePolicy parses an MTA-STS policy.
79func ParsePolicy(s string) (policy *Policy, err error) {
80 defer func() {
81 x := recover()
82 if x == nil {
83 return
84 }
85 if xerr, ok := x.(parseErr); ok {
86 policy = nil
87 err = fmt.Errorf("%w: %s", ErrPolicySyntax, xerr)
88 return
89 }
90 panic(x)
91 }()
92
93 // ../rfc/8461:426
94 p := newParser(s)
95 policy = &Policy{
96 Version: "STSv1",
97 }
98 seen := map[string]struct{}{}
99 for {
100 k := p.xkey()
101 // For fields except "mx", only the first must be used. ../rfc/8461:517
102 _, dup := seen[k]
103 seen[k] = struct{}{}
104 p.xtake(":")
105 p.wsp()
106 switch k {
107 case "version":
108 policy.Version = p.xtake("STSv1")
109 case "mode":
110 mode := Mode(p.xtakelist("testing", "enforce", "none"))
111 if !dup {
112 policy.Mode = mode
113 }
114 case "max_age":
115 maxage := p.xmaxage()
116 if !dup {
117 policy.MaxAgeSeconds = maxage
118 }
119 case "mx":
120 policy.MX = append(policy.MX, p.xmx())
121 default:
122 v := p.xpolicyvalue()
123 policy.Extensions = append(policy.Extensions, Pair{k, v})
124 }
125 p.wsp()
126 if !p.eol() || p.empty() {
127 break
128 }
129 }
130 if !p.empty() {
131 p.xerrorf("leftover characters")
132 }
133 required := []string{"version", "mode", "max_age"}
134 for _, req := range required {
135 if _, ok := seen[req]; !ok {
136 p.xerrorf("missing field %q", req)
137 }
138 }
139 if _, ok := seen["mx"]; !ok && policy.Mode != ModeNone {
140 // ../rfc/8461:437
141 p.xerrorf("missing mx given mode")
142 }
143 return
144}
145
146type parser struct {
147 s string
148 o int
149}
150
151func newParser(s string) *parser {
152 return &parser{s: s}
153}
154
155func (p *parser) xerrorf(format string, args ...any) {
156 msg := fmt.Sprintf(format, args...)
157 if p.o < len(p.s) {
158 msg += fmt.Sprintf(" (remain %q)", p.s[p.o:])
159 }
160 panic(parseErr(msg))
161}
162
163func (p *parser) xtake(s string) string {
164 if !p.prefix(s) {
165 p.xerrorf("expected %q", s)
166 }
167 p.o += len(s)
168 return s
169}
170
171func (p *parser) xdelim() {
172 if !p.delim() {
173 p.xerrorf("expected semicolon")
174 }
175}
176
177func (p *parser) xtaken(n int) string {
178 r := p.s[p.o : p.o+n]
179 p.o += n
180 return r
181}
182
183func (p *parser) xtakefn1(fn func(rune, int) bool) string {
184 for i, b := range p.s[p.o:] {
185 if !fn(b, i) {
186 if i == 0 {
187 p.xerrorf("expected at least one char")
188 }
189 return p.xtaken(i)
190 }
191 }
192 if p.empty() {
193 p.xerrorf("expected at least 1 char")
194 }
195 return p.xtaken(len(p.s) - p.o)
196}
197
198func (p *parser) prefix(s string) bool {
199 return strings.HasPrefix(p.s[p.o:], s)
200}
201
202// File name, the known values match this syntax.
203// ../rfc/8461:482
204func (p *parser) xkey() string {
205 return p.xtakefn1(func(b rune, i int) bool {
206 return i < 32 && (b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || (i > 0 && b == '_' || b == '-' || b == '.'))
207 })
208}
209
210// ../rfc/8461:319
211func (p *parser) xid() string {
212 return p.xtakefn1(func(b rune, i int) bool {
213 return i < 32 && (b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9')
214 })
215}
216
217// ../rfc/8461:326
218func (p *parser) xvalue() string {
219 return p.xtakefn1(func(b rune, i int) bool {
220 return b > ' ' && b < 0x7f && b != '=' && b != ';'
221 })
222}
223
224// ../rfc/8461:315
225func (p *parser) delim() bool {
226 o := p.o
227 e := len(p.s)
228 for o < e && (p.s[o] == ' ' || p.s[o] == '\t') {
229 o++
230 }
231 if o >= e || p.s[o] != ';' {
232 return false
233 }
234 o++
235 for o < e && (p.s[o] == ' ' || p.s[o] == '\t') {
236 o++
237 }
238 p.o = o
239 return true
240}
241
242func (p *parser) empty() bool {
243 return p.o >= len(p.s)
244}
245
246// ../rfc/8461:485
247func (p *parser) eol() bool {
248 return p.take("\n") || p.take("\r\n")
249}
250
251func (p *parser) xtakelist(l ...string) string {
252 for _, s := range l {
253 if p.prefix(s) {
254 return p.xtaken(len(s))
255 }
256 }
257 p.xerrorf("expected one of %s", strings.Join(l, ", "))
258 return "" // not reached
259}
260
261// ../rfc/8461:476
262func (p *parser) xmaxage() int {
263 digits := p.xtakefn1(func(b rune, i int) bool {
264 return b >= '0' && b <= '9' && i < 10
265 })
266 v, err := strconv.ParseInt(digits, 10, 32)
267 if err != nil {
268 p.xerrorf("parsing int: %s", err)
269 }
270 return int(v)
271}
272
273func (p *parser) take(s string) bool {
274 if p.prefix(s) {
275 p.o += len(s)
276 return true
277 }
278 return false
279}
280
281// ../rfc/8461:469
282func (p *parser) xmx() (mx STSMX) {
283 if p.prefix("*.") {
284 mx.Wildcard = true
285 p.o += 2
286 }
287 mx.Domain = p.xdomain()
288 return mx
289}
290
291// ../rfc/5321:2291
292func (p *parser) xdomain() dns.Domain {
293 s := p.xsubdomain()
294 for p.take(".") {
295 s += "." + p.xsubdomain()
296 }
297 d, err := dns.ParseDomain(s)
298 if err != nil {
299 p.xerrorf("parsing domain %q: %s", s, err)
300 }
301 return d
302}
303
304// ../rfc/8461:487
305func (p *parser) xsubdomain() string {
306 // note: utf-8 is valid, but U-labels are explicitly not allowed. ../rfc/8461:411 ../rfc/5321:2303
307 unicode := false
308 s := p.xtakefn1(func(c rune, i int) bool {
309 if c > 0x7f {
310 unicode = true
311 }
312 return c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || (i > 0 && c == '-') || c > 0x7f
313 })
314 if unicode {
315 p.xerrorf("domain must be specified in A labels, not U labels (unicode)")
316 }
317 return s
318}
319
320// ../rfc/8461:487
321func (p *parser) xpolicyvalue() string {
322 e := len(p.s)
323 for i, c := range p.s[p.o:] {
324 if c > ' ' && c < 0x7f || c >= 0x80 || (c == ' ' && i > 0) {
325 continue
326 }
327 e = p.o + i
328 break
329 }
330 // Walk back on trailing spaces.
331 for e > p.o && p.s[e-1] == ' ' {
332 e--
333 }
334 n := e - p.o
335 if n <= 0 {
336 p.xerrorf("empty extension value")
337 }
338 return p.xtaken(n)
339}
340
341// "*WSP"
342func (p *parser) wsp() {
343 n := len(p.s)
344 for p.o < n && (p.s[p.o] == ' ' || p.s[p.o] == '\t') {
345 p.o++
346 }
347}
348