121 lines
4.1 KiB
Go
121 lines
4.1 KiB
Go
package imapserver_test
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"math/big"
|
|
"net"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/emersion/go-imap/v2/imapclient"
|
|
|
|
"mailgoserver/internal/abuseguard"
|
|
"mailgoserver/internal/db"
|
|
"mailgoserver/internal/imapserver"
|
|
"mailgoserver/internal/mailstore"
|
|
)
|
|
|
|
// genTLSTestCert generates a throwaway self-signed cert for "127.0.0.1" — enough to
|
|
// exercise a real *tls.Conn handshake (the client skips verification instead of trusting
|
|
// a CA, since this test cares about the server-side connection-wrapping order, not
|
|
// certificate validation).
|
|
func genTLSTestCert(t *testing.T) tls.Certificate {
|
|
t.Helper()
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{CommonName: "127.0.0.1"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
|
|
}
|
|
|
|
// TestIMAPLoginOverTLSThroughAbuseguardListener reproduces main.go's real direct-TLS
|
|
// IMAP listener wiring: a raw TCP listener wrapped by abuseguard.GuardListener and
|
|
// imapserver.IdleTimeoutListener, with TLS applied outermost via tls.NewListener. This
|
|
// order matters: go-imap/v2 decides whether LOGIN is allowed (vs. LOGINDISABLED, "TLS is
|
|
// required to authenticate") via a c.conn.(*tls.Conn) type assertion on whatever
|
|
// Accept() returns — wrapping abuseguard's own conn type around an already-established
|
|
// tls.Conn (main.go's previous order) hides that from the library and makes every LOGIN
|
|
// fail before ever reaching Session.Login, even with correct credentials. Regression
|
|
// test for that bug.
|
|
func TestIMAPLoginOverTLSThroughAbuseguardListener(t *testing.T) {
|
|
dir := t.TempDir()
|
|
database, err := db.Open(filepath.Join(dir, "test.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { database.Close() })
|
|
|
|
domainID, err := database.CreateDomain("example.com")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store := mailstore.New(database, mailstore.GenerateDEK(), t.TempDir())
|
|
dek := mailstore.GenerateDEK()
|
|
wrapped, nonce, err := store.WrapDEK(dek)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
portalHash, err := db.HashPassword("portal-password-unused")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mailboxID, err := database.CreateMailbox("inbox@example.com", portalHash, domainID, 5*1024*1024*1024, wrapped, nonce)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
appPassword := db.GenerateAppPassword(25)
|
|
appHash, err := db.HashPassword(appPassword)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := database.CreateAppPassword(mailboxID, "test client", appHash, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cert := genTLSTestCert(t)
|
|
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}}
|
|
|
|
backend := &imapserver.Backend{DB: database, Mailstore: store}
|
|
tlsServer := imapserver.NewTLSServer(backend, tlsConfig)
|
|
|
|
rawListener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
guarded := imapserver.IdleTimeoutListener(abuseguard.GuardListener(rawListener, database, nil, nil), 30*time.Minute)
|
|
finalListener := tls.NewListener(guarded, tlsConfig)
|
|
go tlsServer.Serve(finalListener)
|
|
t.Cleanup(func() { tlsServer.Close() })
|
|
|
|
client, err := imapclient.DialTLS(finalListener.Addr().String(), &imapclient.Options{
|
|
TLSConfig: &tls.Config{InsecureSkipVerify: true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DialTLS: %v", err)
|
|
}
|
|
t.Cleanup(func() { client.Close() })
|
|
|
|
if err := client.Login("inbox@example.com", appPassword).Wait(); err != nil {
|
|
t.Fatalf("login over TLS through abuseguard.GuardListener: %v (this is exactly the bug: LOGIN rejected with PRIVACYREQUIRED because the wrapped conn no longer type-asserts to *tls.Conn)", err)
|
|
}
|
|
}
|