mirror of
https://github.com/mjl-/mox.git
synced 2025-07-10 07:14:40 +03:00
mox!
This commit is contained in:
226
tlsrpt/parse.go
Normal file
226
tlsrpt/parse.go
Normal file
@ -0,0 +1,226 @@
|
||||
package tlsrpt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Extension is an additional key/value pair for a TLSRPT record.
|
||||
type Extension struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Record is a parsed TLSRPT record, to be served under "_smtp._tls.<domain>".
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// v=TLSRPTv1; rua=mailto:tlsrpt@mox.example;
|
||||
type Record struct {
|
||||
Version string // "TLSRPTv1", for "v=".
|
||||
RUAs [][]string // Aggregate reporting URI, for "rua=". "rua=" can occur multiple times, each can be a list. Must be URL-encoded strings, with ",", "!" and ";" encoded.
|
||||
Extensions []Extension
|
||||
}
|
||||
|
||||
// String returns a string or use as a TLSRPT DNS TXT record.
|
||||
func (r Record) String() string {
|
||||
b := &strings.Builder{}
|
||||
fmt.Fprint(b, "v="+r.Version)
|
||||
for _, rua := range r.RUAs {
|
||||
fmt.Fprint(b, "; rua="+strings.Join(rua, ","))
|
||||
}
|
||||
for _, p := range r.Extensions {
|
||||
fmt.Fprint(b, "; "+p.Key+"="+p.Value)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type parseErr string
|
||||
|
||||
func (e parseErr) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
var _ error = parseErr("")
|
||||
|
||||
// ParseRecord parses a TLSRPT record.
|
||||
func ParseRecord(txt string) (record *Record, istlsrpt bool, err error) {
|
||||
defer func() {
|
||||
x := recover()
|
||||
if x == nil {
|
||||
return
|
||||
}
|
||||
if xerr, ok := x.(parseErr); ok {
|
||||
record = nil
|
||||
err = fmt.Errorf("%w: %s", ErrRecordSyntax, xerr)
|
||||
return
|
||||
}
|
||||
panic(x)
|
||||
}()
|
||||
|
||||
p := newParser(txt)
|
||||
|
||||
record = &Record{
|
||||
Version: "TLSRPTv1",
|
||||
}
|
||||
|
||||
p.xtake("v=TLSRPTv1")
|
||||
p.xdelim()
|
||||
istlsrpt = true
|
||||
for {
|
||||
k := p.xkey()
|
||||
p.xtake("=")
|
||||
// note: duplicates are allowed.
|
||||
switch k {
|
||||
case "rua":
|
||||
record.RUAs = append(record.RUAs, p.xruas())
|
||||
default:
|
||||
v := p.xvalue()
|
||||
record.Extensions = append(record.Extensions, Extension{k, v})
|
||||
}
|
||||
if !p.delim() || p.empty() {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !p.empty() {
|
||||
p.xerrorf("leftover chars")
|
||||
}
|
||||
if record.RUAs == nil {
|
||||
p.xerrorf("missing rua")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
s string
|
||||
o int
|
||||
}
|
||||
|
||||
func newParser(s string) *parser {
|
||||
return &parser{s: s}
|
||||
}
|
||||
|
||||
func (p *parser) xerrorf(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
if p.o < len(p.s) {
|
||||
msg += fmt.Sprintf(" (remain %q)", p.s[p.o:])
|
||||
}
|
||||
panic(parseErr(msg))
|
||||
}
|
||||
|
||||
func (p *parser) xtake(s string) string {
|
||||
if !p.prefix(s) {
|
||||
p.xerrorf("expected %q", s)
|
||||
}
|
||||
p.o += len(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *parser) xdelim() {
|
||||
if !p.delim() {
|
||||
p.xerrorf("expected semicolon")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) xtaken(n int) string {
|
||||
r := p.s[p.o : p.o+n]
|
||||
p.o += n
|
||||
return r
|
||||
}
|
||||
|
||||
func (p *parser) prefix(s string) bool {
|
||||
return strings.HasPrefix(p.s[p.o:], s)
|
||||
}
|
||||
|
||||
func (p *parser) take(s string) bool {
|
||||
if p.prefix(s) {
|
||||
p.o += len(s)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *parser) xtakefn1(fn func(rune, int) bool) string {
|
||||
for i, b := range p.s[p.o:] {
|
||||
if !fn(b, i) {
|
||||
if i == 0 {
|
||||
p.xerrorf("expected at least one char")
|
||||
}
|
||||
return p.xtaken(i)
|
||||
}
|
||||
}
|
||||
if p.empty() {
|
||||
p.xerrorf("expected at least 1 char")
|
||||
}
|
||||
return p.xtaken(len(p.s) - p.o)
|
||||
}
|
||||
|
||||
// ../rfc/8460:368
|
||||
func (p *parser) xkey() string {
|
||||
return p.xtakefn1(func(b rune, i int) bool {
|
||||
return i < 32 && (b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || (i > 0 && b == '_' || b == '-' || b == '.'))
|
||||
})
|
||||
}
|
||||
|
||||
// ../rfc/8460:371
|
||||
func (p *parser) xvalue() string {
|
||||
return p.xtakefn1(func(b rune, i int) bool {
|
||||
return b > ' ' && b < 0x7f && b != '=' && b != ';'
|
||||
})
|
||||
}
|
||||
|
||||
// ../rfc/8460:399
|
||||
func (p *parser) delim() bool {
|
||||
o := p.o
|
||||
e := len(p.s)
|
||||
for o < e && (p.s[o] == ' ' || p.s[o] == '\t') {
|
||||
o++
|
||||
}
|
||||
if o >= e || p.s[o] != ';' {
|
||||
return false
|
||||
}
|
||||
o++
|
||||
for o < e && (p.s[o] == ' ' || p.s[o] == '\t') {
|
||||
o++
|
||||
}
|
||||
p.o = o
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *parser) empty() bool {
|
||||
return p.o >= len(p.s)
|
||||
}
|
||||
|
||||
func (p *parser) wsp() {
|
||||
for p.o < len(p.s) && (p.s[p.o] == ' ' || p.s[p.o] == '\t') {
|
||||
p.o++
|
||||
}
|
||||
}
|
||||
|
||||
// ../rfc/8460:358
|
||||
func (p *parser) xruas() []string {
|
||||
l := []string{p.xuri()}
|
||||
p.wsp()
|
||||
for p.take(",") {
|
||||
p.wsp()
|
||||
l = append(l, p.xuri())
|
||||
p.wsp()
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// ../rfc/8460:360
|
||||
func (p *parser) xuri() string {
|
||||
v := p.xtakefn1(func(b rune, i int) bool {
|
||||
return b != ',' && b != '!' && b != ' ' && b != '\t' && b != ';'
|
||||
})
|
||||
u, err := url.Parse(v)
|
||||
if err != nil {
|
||||
p.xerrorf("parsing uri %q: %s", v, err)
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
p.xerrorf("missing scheme in uri")
|
||||
}
|
||||
return v
|
||||
}
|
Reference in New Issue
Block a user