1package mtastsdb
2
3import (
4 "context"
5 "crypto/ed25519"
6 cryptorand "crypto/rand"
7 "crypto/tls"
8 "crypto/x509"
9 "fmt"
10 "io"
11 golog "log"
12 "math/big"
13 "net"
14 "net/http"
15 "os"
16 "path/filepath"
17 "testing"
18 "testing/synctest"
19 "time"
20
21 "github.com/mjl-/bstore"
22
23 "github.com/mjl-/mox/dns"
24 "github.com/mjl-/mox/mlog"
25 "github.com/mjl-/mox/mox-"
26 "github.com/mjl-/mox/mtasts"
27)
28
29var ctxbg = context.Background()
30
31func TestRefresh(t *testing.T) {
32 mox.Shutdown = ctxbg
33 mox.ConfigStaticPath = filepath.FromSlash("../testdata/mtasts/fake.conf")
34 mox.Conf.Static.DataDir = "."
35
36 dbpath := mox.DataDirPath("mtasts.db")
37 os.MkdirAll(filepath.Dir(dbpath), 0770)
38 os.Remove(dbpath)
39 defer os.Remove(dbpath)
40
41 log := mlog.New("mtastsdb", nil)
42
43 err := Init(false)
44 tcheckf(t, err, "init database")
45 defer func() {
46 err := Close()
47 tcheckf(t, err, "close database")
48 }()
49
50 cert := fakeCert(t, false)
51
52 insert := func(domain string, validEnd, lastUpdate, lastUse time.Time, backoff bool, recordID string, mode mtasts.Mode, maxAge int, mx string) {
53 t.Helper()
54
55 mxd, err := dns.ParseDomain(mx)
56 if err != nil {
57 t.Fatalf("parsing mx domain %q: %s", mx, err)
58 }
59 policy := mtasts.Policy{
60 Version: "STSv1",
61 Mode: mode,
62 MX: []mtasts.MX{{Wildcard: false, Domain: mxd}},
63 MaxAgeSeconds: maxAge,
64 Extensions: nil,
65 }
66
67 pr := PolicyRecord{domain, time.Time{}, validEnd, lastUpdate, lastUse, backoff, recordID, policy, policy.String()}
68 if err := DB.Insert(ctxbg, &pr); err != nil {
69 t.Fatalf("insert policy: %s", err)
70 }
71 }
72
73 resolver := dns.MockResolver{
74 TXT: map[string][]string{
75 "_mta-sts.refresh.mox.example.": {"v=STSv1; id=1"},
76 "_mta-sts.policyok.mox.example.": {"v=STSv1; id=2"},
77 "_mta-sts.policybad.mox.example.": {"v=STSv1; id=2"},
78 },
79 }
80
81 pool := x509.NewCertPool()
82 pool.AddCert(cert.Leaf)
83
84 l, err := net.Listen("tcp", "127.0.0.1:0")
85 tcheckf(t, err, "listen")
86 defer l.Close()
87 go func() {
88 mux := &http.ServeMux{}
89 mux.HandleFunc("/.well-known/mta-sts.txt", func(w http.ResponseWriter, r *http.Request) {
90 if r.Host == "mta-sts.policybad.mox.example" {
91 w.WriteHeader(500)
92 return
93 }
94 fmt.Fprintf(w, "version: STSv1\nmode: enforce\nmx: mx.mox.example.com\nmax_age: 3600\n")
95 })
96 s := &http.Server{
97 Handler: mux,
98 TLSConfig: &tls.Config{
99 Certificates: []tls.Certificate{cert},
100 },
101 ErrorLog: golog.New(io.Discard, "", 0),
102 }
103 s.ServeTLS(l, "", "")
104 }()
105
106 mtasts.HTTPClient.Transport = &http.Transport{
107 Dial: func(network, addr string) (net.Conn, error) {
108 return net.Dial("tcp", l.Addr().String())
109 },
110 TLSClientConfig: &tls.Config{
111 RootCAs: pool,
112 },
113 }
114 defer func() {
115 mtasts.HTTPClient.CloseIdleConnections()
116 mtasts.HTTPClient.Transport = nil
117 }()
118
119 slept := 0
120 sleep := func(d time.Duration) {
121 slept++
122 interval := 3 * time.Hour / 2
123 if d < time.Duration(slept)*interval-interval/2 || d > time.Duration(slept)*interval+interval/2 {
124 t.Fatalf("bad sleep duration %v", d)
125 }
126 }
127
128 // Run with synctest, to ensure all goroutines that could write results are
129 // finished before we check again if all work is finished.
130 synctest.Test(t, func(t *testing.T) {
131 now := time.Now()
132 // Updated just now.
133 insert("mox.example", now.Add(24*time.Hour), now, now, false, "1", mtasts.ModeEnforce, 3600, "mx.mox.example.com")
134 // To be removed.
135 insert("stale.mox.example", now.Add(-time.Hour), now, now.Add(-181*24*time.Hour), false, "1", mtasts.ModeEnforce, 3600, "mx.mox.example.com")
136 // To be refreshed, same id.
137 insert("refresh.mox.example", now.Add(7*24*time.Hour), now.Add(-24*time.Hour), now.Add(-179*24*time.Hour), false, "1", mtasts.ModeEnforce, 3600, "mx.mox.example.com")
138 // To be refreshed and succeed.
139 insert("policyok.mox.example", now.Add(7*24*time.Hour), now.Add(-24*time.Hour), now.Add(-179*24*time.Hour), false, "1", mtasts.ModeEnforce, 3600, "mx.mox.example.com")
140 // To be refreshed and fail to fetch.
141 insert("policybad.mox.example", now.Add(7*24*time.Hour), now.Add(-24*time.Hour), now.Add(-179*24*time.Hour), false, "1", mtasts.ModeEnforce, 3600, "mx.mox.example.com")
142
143 if n, err := refresh1(ctxbg, log, resolver, sleep); err != nil || n != 3 {
144 t.Fatalf("refresh1: err %s, n %d, expected no error, 3", err, n)
145 }
146
147 if slept != 2 {
148 t.Fatalf("bad sleeps, %d instead of 2", slept)
149 }
150 })
151
152 // Should not do any more refreshes and return immediately.
153 q := bstore.QueryDB[PolicyRecord](ctxbg, DB)
154 q.FilterNonzero(PolicyRecord{Domain: "policybad.mox.example"})
155 if _, err := q.Delete(); err != nil {
156 t.Fatalf("delete record that would be refreshed: %v", err)
157 }
158 mox.Context = ctxbg
159 mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
160 mox.ShutdownCancel()
161 n := refresh()
162 if n != 0 {
163 t.Fatalf("refresh found unexpected work, n %d", n)
164 }
165 mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
166}
167
168func fakeCert(t *testing.T, expired bool) tls.Certificate {
169 notAfter := time.Now()
170 if expired {
171 notAfter = notAfter.Add(-time.Hour)
172 } else {
173 notAfter = notAfter.Add(time.Hour)
174 }
175
176 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
177
178 template := &x509.Certificate{
179 SerialNumber: big.NewInt(1), // Required field...
180 DNSNames: []string{"mta-sts.policybad.mox.example", "mta-sts.policyok.mox.example"},
181 NotBefore: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), // synctest time
182 NotAfter: notAfter,
183 }
184 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
185 if err != nil {
186 t.Fatalf("making certificate: %s", err)
187 }
188 cert, err := x509.ParseCertificate(localCertBuf)
189 if err != nil {
190 t.Fatalf("parsing generated certificate: %s", err)
191 }
192 c := tls.Certificate{
193 Certificate: [][]byte{localCertBuf},
194 PrivateKey: privKey,
195 Leaf: cert,
196 }
197 return c
198}
199