20 "golang.org/x/crypto/bcrypt"
22 "github.com/mjl-/sherpa"
24 "github.com/mjl-/mox/config"
25 "github.com/mjl-/mox/dns"
26 "github.com/mjl-/mox/mlog"
27 "github.com/mjl-/mox/mox-"
28 "github.com/mjl-/mox/store"
29 "github.com/mjl-/mox/webauth"
32var ctxbg = context.Background()
36 webauth.BadAuthDelay = 0
39func tneedErrorCode(t *testing.T, code string, fn func()) {
46 t.Fatalf("expected sherpa user error, saw success")
48 if err, ok := x.(*sherpa.Error); !ok {
50 t.Fatalf("expected sherpa error, saw %#v", x)
51 } else if err.Code != code {
53 t.Fatalf("expected sherpa error code %q, saw other sherpa error %#v", code, err)
60func tcheck(t *testing.T, err error, msg string) {
63 t.Fatalf("%s: %s", msg, err)
67func readBody(r io.Reader) string {
68 buf, err := io.ReadAll(r)
70 return fmt.Sprintf("read error: %s", err)
72 return fmt.Sprintf("data: %q", buf)
75func TestAdminAuth(t *testing.T) {
76 os.RemoveAll("../testdata/webadmin/data")
77 mox.ConfigStaticPath = filepath.FromSlash("../testdata/webadmin/mox.conf")
78 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
79 mox.MustLoadConfig(true, false)
81 adminpwhash, err := bcrypt.GenerateFromPassword([]byte("moxtest123"), bcrypt.DefaultCost)
82 tcheck(t, err, "generate bcrypt hash")
84 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
85 err = os.WriteFile(path, adminpwhash, 0660)
86 tcheck(t, err, "write password file")
89 api := Admin{cookiePath: "/admin/"}
90 apiHandler, err := makeSherpaHandler(api.cookiePath, false)
91 tcheck(t, err, "sherpa handler")
93 respRec := httptest.NewRecorder()
94 reqInfo := requestInfo{"", respRec, &http.Request{RemoteAddr: "127.0.0.1:1234"}}
95 ctx := context.WithValue(ctxbg, requestInfoCtxKey, reqInfo)
97 // Missing login token.
98 tneedErrorCode(t, "user:error", func() { api.Login(ctx, "", "moxtest123") })
100 // Login with loginToken.
101 loginCookie := &http.Cookie{Name: "webadminlogin"}
102 loginCookie.Value = api.LoginPrep(ctx)
103 reqInfo.Request.Header = http.Header{"Cookie": []string{loginCookie.String()}}
105 csrfToken := api.Login(ctx, loginCookie.Value, "moxtest123")
106 var sessionCookie *http.Cookie
107 for _, c := range respRec.Result().Cookies() {
108 if c.Name == "webadminsession" {
113 if sessionCookie == nil {
114 t.Fatalf("missing session cookie")
117 // Valid loginToken, but bad credentials.
118 loginCookie.Value = api.LoginPrep(ctx)
119 reqInfo.Request.Header = http.Header{"Cookie": []string{loginCookie.String()}}
120 tneedErrorCode(t, "user:loginFailed", func() { api.Login(ctx, loginCookie.Value, "badauth") })
122 type httpHeaders [][2]string
123 ctJSON := [2]string{"Content-Type", "application/json; charset=utf-8"}
125 cookieOK := &http.Cookie{Name: "webadminsession", Value: sessionCookie.Value}
126 cookieBad := &http.Cookie{Name: "webadminsession", Value: "AAAAAAAAAAAAAAAAAAAAAA"}
127 hdrSessionOK := [2]string{"Cookie", cookieOK.String()}
128 hdrSessionBad := [2]string{"Cookie", cookieBad.String()}
129 hdrCSRFOK := [2]string{"x-mox-csrf", string(csrfToken)}
130 hdrCSRFBad := [2]string{"x-mox-csrf", "AAAAAAAAAAAAAAAAAAAAAA"}
132 testHTTP := func(method, path string, headers httpHeaders, expStatusCode int, expHeaders httpHeaders, check func(resp *http.Response)) {
135 req := httptest.NewRequest(method, path, nil)
136 for _, kv := range headers {
137 req.Header.Add(kv[0], kv[1])
139 rr := httptest.NewRecorder()
140 rr.Body = &bytes.Buffer{}
141 handle(apiHandler, false, rr, req)
142 if rr.Code != expStatusCode {
143 t.Fatalf("got status %d, expected %d (%s)", rr.Code, expStatusCode, readBody(rr.Body))
147 for _, h := range expHeaders {
148 if resp.Header.Get(h[0]) != h[1] {
149 t.Fatalf("for header %q got value %q, expected %q", h[0], resp.Header.Get(h[0]), h[1])
157 testHTTPAuthAPI := func(method, path string, expStatusCode int, expHeaders httpHeaders, check func(resp *http.Response)) {
159 testHTTP(method, path, httpHeaders{hdrCSRFOK, hdrSessionOK}, expStatusCode, expHeaders, check)
162 userAuthError := func(resp *http.Response, expCode string) {
165 var response struct {
166 Error *sherpa.Error `json:"error"`
168 err := json.NewDecoder(resp.Body).Decode(&response)
169 tcheck(t, err, "parsing response as json")
170 if response.Error == nil {
171 t.Fatalf("expected sherpa error with code %s, no error", expCode)
173 if response.Error.Code != expCode {
174 t.Fatalf("got sherpa error code %q, expected %s", response.Error.Code, expCode)
177 badAuth := func(resp *http.Response) {
179 userAuthError(resp, "user:badAuth")
181 noAuth := func(resp *http.Response) {
183 userAuthError(resp, "user:noAuth")
186 testHTTP("POST", "/api/Bogus", httpHeaders{}, http.StatusOK, nil, noAuth)
187 testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFBad}, http.StatusOK, nil, noAuth)
188 testHTTP("POST", "/api/Bogus", httpHeaders{hdrSessionBad}, http.StatusOK, nil, noAuth)
189 testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFBad, hdrSessionBad}, http.StatusOK, nil, badAuth)
190 testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFOK}, http.StatusOK, nil, noAuth)
191 testHTTP("POST", "/api/Bogus", httpHeaders{hdrSessionOK}, http.StatusOK, nil, noAuth)
192 testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFBad, hdrSessionOK}, http.StatusOK, nil, badAuth)
193 testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFOK, hdrSessionBad}, http.StatusOK, nil, badAuth)
194 testHTTPAuthAPI("GET", "/api/Transports", http.StatusMethodNotAllowed, nil, nil)
195 testHTTPAuthAPI("POST", "/api/Transports", http.StatusOK, httpHeaders{ctJSON}, nil)
197 // Logout needs session token.
198 reqInfo.SessionToken = store.SessionToken(strings.SplitN(sessionCookie.Value, " ", 2)[0])
199 ctx = context.WithValue(ctxbg, requestInfoCtxKey, reqInfo)
202 tneedErrorCode(t, "server:error", func() { api.Logout(ctx) })
205func TestCheckDomain(t *testing.T) {
206 // NOTE: we aren't currently looking at the results, having the code paths executed is better than nothing.
208 log := mlog.New("webadmin", nil)
210 resolver := dns.MockResolver{
211 MX: map[string][]*net.MX{
212 "mox.example.": {{Host: "mail.mox.example.", Pref: 10}},
214 A: map[string][]string{
215 "mail.mox.example.": {"127.0.0.2"},
217 AAAA: map[string][]string{
218 "mail.mox.example.": {"127.0.0.2"},
220 TXT: map[string][]string{
221 "mox.example.": {"v=spf1 mx -all"},
222 "test._domainkey.mox.example.": {"v=DKIM1;h=sha256;k=ed25519;p=ln5zd/JEX4Jy60WAhUOv33IYm2YZMyTQAdr9stML504="},
223 "_dmarc.mox.example.": {"v=DMARC1; p=reject; rua=mailto:mjl@mox.example"},
224 "_smtp._tls.mox.example": {"v=TLSRPTv1; rua=mailto:tlsrpt@mox.example;"},
225 "_mta-sts.mox.example": {"v=STSv1; id=20160831085700Z"},
227 CNAME: map[string]string{},
230 listener := config.Listener{
231 IPs: []string{"127.0.0.2"},
232 Hostname: "mox.example",
233 HostnameDomain: dns.Domain{ASCII: "mox.example"},
235 listener.SMTP.Enabled = true
236 listener.AutoconfigHTTPS.Enabled = true
237 listener.MTASTSHTTPS.Enabled = true
239 mox.Conf.Static.Listeners = map[string]config.Listener{
242 domain := config.Domain{
244 Selectors: map[string]config.Selector{
246 HashEffective: "sha256",
247 HeadersEffective: []string{"From", "Date", "Subject"},
248 Key: ed25519.NewKeyFromSeed(make([]byte, 32)), // warning: fake zero key, do not copy this code.
249 Domain: dns.Domain{ASCII: "test"},
252 HashEffective: "sha256",
253 HeadersEffective: []string{"From", "Date", "Subject"},
254 Key: ed25519.NewKeyFromSeed(make([]byte, 32)), // warning: fake zero key, do not copy this code.
255 Domain: dns.Domain{ASCII: "missing"},
258 Sign: []string{"test", "test2"},
261 mox.Conf.Dynamic.Domains = map[string]config.Domain{
262 "mox.example": domain,
265 // Make a dialer that fails immediately before actually connecting.
266 done := make(chan struct{})
268 dialer := &net.Dialer{Deadline: time.Now().Add(-time.Second), Cancel: done}
270 checkDomain(ctxbg, resolver, dialer, "mox.example")
271 // todo: check returned data
273 Admin{}.Domains(ctxbg) // todo: check results
274 dnsblsStatus(ctxbg, log, resolver) // todo: check results