mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
ad51ffc365
ideally both account & admin web pages should be on non-public ips (e.g. a wireguard tunnel). but during setup, users may not have that set up, and they may want to configure the admin/account pages on their public ip's. the auth rate limiting should make it less of issue. users can now also only put the account web page publicly available. useful for if you're the admin and you have a vpn connection, but your other/external users do not have a vpn into your mail server. to make the account page more easily findable, the http root serves the account page. the admin page is still at /admin/, to prevent clash with potential account pages, but if no account page is present, you are helpfully redirected from / to /admin/. this also adds a prometheus metric counting how often auth attempts have been rate limited.
129 lines
4 KiB
Go
129 lines
4 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"net"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/mjl-/mox/config"
|
|
"github.com/mjl-/mox/dns"
|
|
"github.com/mjl-/mox/mox-"
|
|
)
|
|
|
|
func TestAdminAuth(t *testing.T) {
|
|
test := func(passwordfile, authHdr string, expect bool) {
|
|
t.Helper()
|
|
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest("GET", "/ignored", nil)
|
|
if authHdr != "" {
|
|
r.Header.Add("Authorization", authHdr)
|
|
}
|
|
ok := checkAdminAuth(context.Background(), passwordfile, w, r)
|
|
if ok != expect {
|
|
t.Fatalf("got %v, expected %v", ok, expect)
|
|
}
|
|
}
|
|
|
|
const authOK = "Basic YWRtaW46bW94dGVzdDEyMw==" // admin:moxtest123
|
|
const authBad = "Basic YWRtaW46YmFkcGFzc3dvcmQ=" // admin:badpassword
|
|
|
|
const path = "../testdata/http-passwordfile"
|
|
os.Remove(path)
|
|
defer os.Remove(path)
|
|
|
|
test(path, authOK, false) // Password file does not exist.
|
|
|
|
adminpwhash, err := bcrypt.GenerateFromPassword([]byte("moxtest123"), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
t.Fatalf("generate bcrypt hash: %v", err)
|
|
}
|
|
if err := os.WriteFile(path, adminpwhash, 0660); err != nil {
|
|
t.Fatalf("write password file: %v", err)
|
|
}
|
|
// We loop to also exercise the auth cache.
|
|
for i := 0; i < 2; i++ {
|
|
test(path, "", false) // Empty/missing header.
|
|
test(path, "Malformed ", false) // Not "Basic"
|
|
test(path, "Basic malformed ", false) // Bad base64.
|
|
test(path, "Basic dGVzdA== ", false) // base64 is ok, but wrong tokens inside.
|
|
test(path, authBad, false) // Wrong password.
|
|
test(path, authOK, true)
|
|
}
|
|
}
|
|
|
|
func TestCheckDomain(t *testing.T) {
|
|
// NOTE: we aren't currently looking at the results, having the code paths executed is better than nothing.
|
|
|
|
resolver := dns.MockResolver{
|
|
MX: map[string][]*net.MX{
|
|
"mox.example.": {{Host: "mail.mox.example.", Pref: 10}},
|
|
},
|
|
A: map[string][]string{
|
|
"mail.mox.example.": {"127.0.0.2"},
|
|
},
|
|
AAAA: map[string][]string{
|
|
"mail.mox.example.": {"127.0.0.2"},
|
|
},
|
|
TXT: map[string][]string{
|
|
"mox.example.": {"v=spf1 mx -all"},
|
|
"test._domainkey.mox.example.": {"v=DKIM1;h=sha256;k=ed25519;p=ln5zd/JEX4Jy60WAhUOv33IYm2YZMyTQAdr9stML504="},
|
|
"_dmarc.mox.example.": {"v=DMARC1; p=reject; rua=mailto:mjl@mox.example"},
|
|
"_smtp._tls.mox.example": {"v=TLSRPTv1; rua=mailto:tlsrpt@mox.example;"},
|
|
"_mta-sts.mox.example": {"v=STSv1; id=20160831085700Z"},
|
|
},
|
|
CNAME: map[string]string{},
|
|
}
|
|
|
|
listener := config.Listener{
|
|
IPs: []string{"127.0.0.2"},
|
|
Hostname: "mox.example",
|
|
HostnameDomain: dns.Domain{ASCII: "mox.example"},
|
|
}
|
|
listener.SMTP.Enabled = true
|
|
listener.AutoconfigHTTPS.Enabled = true
|
|
listener.MTASTSHTTPS.Enabled = true
|
|
|
|
mox.Conf.Static.Listeners = map[string]config.Listener{
|
|
"public": listener,
|
|
}
|
|
domain := config.Domain{
|
|
DKIM: config.DKIM{
|
|
Selectors: map[string]config.Selector{
|
|
"test": {
|
|
HashEffective: "sha256",
|
|
HeadersEffective: []string{"From", "Date", "Subject"},
|
|
Key: ed25519.NewKeyFromSeed(make([]byte, 32)), // warning: fake zero key, do not copy this code.
|
|
Domain: dns.Domain{ASCII: "test"},
|
|
},
|
|
"missing": {
|
|
HashEffective: "sha256",
|
|
HeadersEffective: []string{"From", "Date", "Subject"},
|
|
Key: ed25519.NewKeyFromSeed(make([]byte, 32)), // warning: fake zero key, do not copy this code.
|
|
Domain: dns.Domain{ASCII: "missing"},
|
|
},
|
|
},
|
|
Sign: []string{"test", "test2"},
|
|
},
|
|
}
|
|
mox.Conf.Dynamic.Domains = map[string]config.Domain{
|
|
"mox.example": domain,
|
|
}
|
|
|
|
// Make a dialer that fails immediately before actually connecting.
|
|
done := make(chan struct{})
|
|
close(done)
|
|
dialer := &net.Dialer{Deadline: time.Now().Add(-time.Second), Cancel: done}
|
|
|
|
checkDomain(context.Background(), resolver, dialer, "mox.example")
|
|
// todo: check returned data
|
|
|
|
Admin{}.Domains(context.Background()) // todo: check results
|
|
dnsblsStatus(context.Background(), resolver) // todo: check results
|
|
}
|