updated layout for webmail and added http dns letsencrypt
This commit is contained in:
@@ -109,25 +109,74 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
mstore := mailstore.New(database, masterKey, mailstoreBase)
|
||||
relayer.Mailstore = mstore
|
||||
|
||||
// Shared by both the SMTP and IMAP implicit-TLS listeners, so a single Reload()
|
||||
// call (self-signed regeneration today; a Let's Encrypt renewal later) updates
|
||||
// both without restarting the process.
|
||||
certFile := absPath(root, cfg.Section("TLS").Key("TLS_CERT_FILE").String())
|
||||
keyFile := absPath(root, cfg.Section("TLS").Key("TLS_KEY_FILE").String())
|
||||
if err := tlsutil.GenerateSelfSignedCert(certFile, keyFile); err != nil {
|
||||
logger.Error("generate TLS certificate: %v", err)
|
||||
os.Exit(1)
|
||||
// Three independent certificate "slots" — custom (self-signed by default, or your
|
||||
// own uploaded cert/key), letsencrypt_dns, letsencrypt_http — each with its own
|
||||
// file pair and CertReloader. [TLS]'s smtp_tls_cert/imap_tls_cert/web_https_cert
|
||||
// independently pick which slot each listener uses, e.g. an HTTP-01 cert for mail
|
||||
// while a DNS-01 cert serves the dashboard. Every slot is seeded with a self-signed
|
||||
// cert if missing, so a reloader can always be constructed even before any ACME
|
||||
// manager has obtained anything yet.
|
||||
customCertFile := absPath(root, cfg.Section("TLS").Key("TLS_CERT_FILE").String())
|
||||
customKeyFile := absPath(root, cfg.Section("TLS").Key("TLS_KEY_FILE").String())
|
||||
dnsCertFile := absPath(root, "ssl_certs/letsencrypt_dns.crt")
|
||||
dnsKeyFile := absPath(root, "ssl_certs/letsencrypt_dns.key")
|
||||
httpCertFile := absPath(root, "ssl_certs/letsencrypt_http.crt")
|
||||
httpKeyFile := absPath(root, "ssl_certs/letsencrypt_http.key")
|
||||
for _, pair := range [][2]string{
|
||||
{customCertFile, customKeyFile}, {dnsCertFile, dnsKeyFile}, {httpCertFile, httpKeyFile},
|
||||
} {
|
||||
if err := tlsutil.GenerateSelfSignedCert(pair[0], pair[1]); err != nil {
|
||||
logger.Error("generate TLS certificate: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
certReloader, err := tlsutil.NewCertReloader(certFile, keyFile)
|
||||
customReloader, err := tlsutil.NewCertReloader(customCertFile, customKeyFile)
|
||||
if err != nil {
|
||||
logger.Error("load TLS certificate: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
tlsConfig := tlsutil.NewReloadableTLSConfig(certReloader)
|
||||
dnsReloader, err := tlsutil.NewCertReloader(dnsCertFile, dnsKeyFile)
|
||||
if err != nil {
|
||||
logger.Error("load TLS certificate: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
httpReloader, err := tlsutil.NewCertReloader(httpCertFile, httpKeyFile)
|
||||
if err != nil {
|
||||
logger.Error("load TLS certificate: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
certSlots := map[string]*tlsutil.CertReloader{
|
||||
"custom": customReloader, "letsencrypt_dns": dnsReloader, "letsencrypt_http": httpReloader,
|
||||
}
|
||||
resolveCertSlot := func(key string) *tlsutil.CertReloader {
|
||||
if r, ok := certSlots[cfg.Section("TLS").Key(key).MustString("custom")]; ok {
|
||||
return r
|
||||
}
|
||||
return customReloader
|
||||
}
|
||||
smtpTLSConfig := tlsutil.NewReloadableTLSConfig(resolveCertSlot("smtp_tls_cert"))
|
||||
imapTLSConfig := tlsutil.NewReloadableTLSConfig(resolveCertSlot("imap_tls_cert"))
|
||||
webHTTPSConfig := tlsutil.NewReloadableTLSConfig(resolveCertSlot("web_https_cert"))
|
||||
|
||||
acmeDataDir := absPath(root, "server_data/acme")
|
||||
acmeMgr := acmecert.New(cfg, certFile, keyFile, acmeDataDir, certReloader, toolbox.GetLogger("acme"))
|
||||
acmeMgr := acmecert.New(cfg, "LetsEncrypt", "dns-01", dnsCertFile, dnsKeyFile, acmeDataDir, dnsReloader, toolbox.GetLogger("acme"))
|
||||
acmeHTTPMgr := acmecert.New(cfg, "LetsEncryptHTTP", "http-01", httpCertFile, httpKeyFile, acmeDataDir, httpReloader, toolbox.GetLogger("acme-http"))
|
||||
|
||||
// The HTTP-01 challenge responder is long-lived (unlike lego's own ephemeral
|
||||
// per-obtain listener) so an operator behind NAT/a reverse proxy can verify their
|
||||
// port-forwarding actually reaches this host before/without triggering a real,
|
||||
// rate-limited ACME attempt — curling it should return 200 once this is up.
|
||||
// Started once at boot if enabled; toggling [LetsEncryptHTTP] enabled needs a
|
||||
// restart to start or stop this listener, same as the *_cert routing settings.
|
||||
if cfg.Section("LetsEncryptHTTP").Key("enabled").MustBool(false) {
|
||||
httpChallengeServer := acmecert.NewHTTP01Server()
|
||||
httpPort := cfg.Section("Server").Key("HTTP_LETSENCRYPT_PORT").MustString("80")
|
||||
httpChallengeServer.Start(fmt.Sprintf(":%s", httpPort), toolbox.GetLogger("acme-http"))
|
||||
acmeHTTPMgr.HTTP01Server = httpChallengeServer
|
||||
logger.Info("HTTP-01 challenge responder listening on :%s", httpPort)
|
||||
}
|
||||
|
||||
backend := &smtpserver.Backend{
|
||||
DB: database, DKIM: dkimMgr, Relay: relayer, Cfg: cfg, Mailstore: mstore,
|
||||
@@ -147,7 +196,7 @@ func main() {
|
||||
// setting) — a bare ":port" address binds dual-stack on most systems,
|
||||
// which would accept IPv6 connections the Python version never did.
|
||||
plainServer := smtpserver.NewPlainServer(backend, fmt.Sprintf("0.0.0.0:%d", smtpPort), banner)
|
||||
tlsServer := smtpserver.NewTLSServer(backend, fmt.Sprintf("0.0.0.0:%d", smtpTLSPort), banner, tlsConfig)
|
||||
tlsServer := smtpserver.NewTLSServer(backend, fmt.Sprintf("0.0.0.0:%d", smtpTLSPort), banner, smtpTLSConfig)
|
||||
|
||||
smtpRunning.Store(true)
|
||||
logger.Info("Plain SMTP listening on :%d, direct-TLS SMTP listening on :%d", smtpPort, smtpTLSPort)
|
||||
@@ -164,7 +213,7 @@ func main() {
|
||||
logger.Error("plain SMTP server: %v", err)
|
||||
}
|
||||
}()
|
||||
tlsListener, err := tls.Listen("tcp", tlsServer.Addr, tlsConfig)
|
||||
tlsListener, err := tls.Listen("tcp", tlsServer.Addr, smtpTLSConfig)
|
||||
if err != nil {
|
||||
logger.Error("TLS SMTP listen: %v", err)
|
||||
return
|
||||
@@ -179,7 +228,7 @@ func main() {
|
||||
imapTLSPort := cfg.Section("IMAP").Key("IMAP_TLS_PORT").MustInt(993)
|
||||
|
||||
plainServer := imapserver.NewPlainServer(imapBackend)
|
||||
tlsServer := imapserver.NewTLSServer(imapBackend, tlsConfig)
|
||||
tlsServer := imapserver.NewTLSServer(imapBackend, imapTLSConfig)
|
||||
|
||||
logger.Info("Plain IMAP listening on :%d, direct-TLS IMAP listening on :%d", imapPort, imapTLSPort)
|
||||
|
||||
@@ -195,7 +244,7 @@ func main() {
|
||||
logger.Error("plain IMAP server: %v", err)
|
||||
}
|
||||
}()
|
||||
imapTLSListener, err := tls.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", imapTLSPort), tlsConfig)
|
||||
imapTLSListener, err := tls.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", imapTLSPort), imapTLSConfig)
|
||||
if err != nil {
|
||||
logger.Error("TLS IMAP listen: %v", err)
|
||||
return
|
||||
@@ -207,25 +256,28 @@ func main() {
|
||||
|
||||
// runCertRenewal is the first periodic/background job in this codebase — everything
|
||||
// else is purely request-driven. Checks soon after boot (so enabling Let's Encrypt
|
||||
// and restarting converges quickly) and every 12h thereafter. The first check each
|
||||
// process run always attempts ObtainOrRenew regardless of NeedsRenewal's expiry
|
||||
// check, since a fresh self-signed cert has ~1 year left and would otherwise never
|
||||
// get replaced by the very first real certificate.
|
||||
// and restarting converges quickly) and every 12h thereafter. NeedsRenewal is a pure
|
||||
// disk-state check (it reads the cert file itself, including recognizing the
|
||||
// self-signed placeholder by its issuer) — deliberately not gated on any in-memory
|
||||
// "has this process attempted yet" flag, so restarting an already-working setup
|
||||
// never triggers a redundant re-obtain of an already-valid real certificate.
|
||||
runCertRenewal := func() {
|
||||
time.Sleep(1 * time.Minute)
|
||||
checkAndRenew := func() {
|
||||
if !cfg.Section("LetsEncrypt").Key("enabled").MustBool(false) {
|
||||
checkAndRenewOne := func(mgr *acmecert.Manager) {
|
||||
if !mgr.Enabled() {
|
||||
return
|
||||
}
|
||||
if !acmeMgr.Status().LastAttempt.IsZero() {
|
||||
if needs, err := acmeMgr.NeedsRenewal(); err != nil || !needs {
|
||||
return
|
||||
}
|
||||
if needs, err := mgr.NeedsRenewal(); err != nil || !needs {
|
||||
return
|
||||
}
|
||||
if err := acmeMgr.ObtainOrRenew(context.Background()); err != nil {
|
||||
logger.Error("ACME obtain/renew: %v", err)
|
||||
if err := mgr.ObtainOrRenew(context.Background()); err != nil {
|
||||
logger.Error("ACME obtain/renew (%s): %v", mgr.Section, err)
|
||||
}
|
||||
}
|
||||
checkAndRenew := func() {
|
||||
checkAndRenewOne(acmeMgr)
|
||||
checkAndRenewOne(acmeHTTPMgr)
|
||||
}
|
||||
checkAndRenew()
|
||||
ticker := time.NewTicker(12 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
@@ -248,7 +300,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
app, err := webui.New(database, dkimMgr, mstore, acmeMgr, relayer, cfg, configPath, toolbox.GetLogger("web"), smtpRunning.Load, appSecret)
|
||||
app, err := webui.New(database, dkimMgr, mstore, acmeMgr, acmeHTTPMgr, relayer, cfg, configPath, toolbox.GetLogger("web"), smtpRunning.Load, appSecret)
|
||||
if err != nil {
|
||||
logger.Error("init web UI: %v", err)
|
||||
os.Exit(1)
|
||||
@@ -280,7 +332,7 @@ func main() {
|
||||
|
||||
httpsPort := cfg.Section("Server").Key("WEB_HTTPS_PORT").MustInt(5001)
|
||||
httpsAddr := fmt.Sprintf("%s:%d", *host, httpsPort)
|
||||
httpsServer := &http.Server{Addr: httpsAddr, Handler: handler, TLSConfig: tlsConfig}
|
||||
httpsServer := &http.Server{Addr: httpsAddr, Handler: handler, TLSConfig: webHTTPSConfig}
|
||||
go func() {
|
||||
logger.Info("Web interface (HTTPS) starting at https://%s", httpsAddr)
|
||||
// Empty cert/key paths: TLSConfig.GetCertificate (backed by certReloader) supplies
|
||||
|
||||
Reference in New Issue
Block a user