MFA fix, added IP blacklist, update webmail client
This commit is contained in:
+77
-4
@@ -6,6 +6,7 @@ import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"mailgoserver/internal/db"
|
||||
"mailgoserver/internal/dkim"
|
||||
"mailgoserver/internal/mailstore"
|
||||
"mailgoserver/internal/relay"
|
||||
"mailgoserver/internal/toolbox"
|
||||
)
|
||||
|
||||
@@ -25,18 +27,44 @@ type App struct {
|
||||
DKIM *dkim.Manager
|
||||
Mailstore *mailstore.Store
|
||||
ACME *acmecert.Manager
|
||||
Relay *relay.Relay // used by the webmail client's compose/send (see webmail_compose.go)
|
||||
Cfg *ini.File
|
||||
ConfigPath string
|
||||
Logger *toolbox.Logger
|
||||
SMTPUp func() bool // reports whether the SMTP listeners are currently running
|
||||
|
||||
// pgpKeys caches unlocked PGP identities for the rest of a login session — purely
|
||||
// in-memory server state, not an external dependency, so it's built internally
|
||||
// rather than threaded in as a New(...) parameter. S/MIME has no equivalent cache
|
||||
// since its private keys are stored plain, not passphrase-protected.
|
||||
pgpKeys *pgpKeyCache
|
||||
|
||||
// trustedProxies gates requestIP's use of forwarded headers — see trusted_proxy.go.
|
||||
trustedProxies []netip.Prefix
|
||||
|
||||
// loginLimiter throttles login POSTs per source IP — see ratelimit.go. Separate
|
||||
// from the per-account lockout (login.go/webmail_login.go), which uses
|
||||
// esrv_auth_logs via CountRecentFailedAttempts instead of in-memory state.
|
||||
loginLimiter *ipRateLimiter
|
||||
|
||||
// appSecret signs CSRF tokens — see csrf.go and LoadOrCreateAppSecret (secret.go).
|
||||
appSecret []byte
|
||||
|
||||
templates map[string]*template.Template
|
||||
}
|
||||
|
||||
// New builds the web UI. Templates and static assets come from the embedded
|
||||
// filesystem (embed.go), not disk, so no directory paths are needed for them.
|
||||
func New(database *db.DB, dkimMgr *dkim.Manager, mstore *mailstore.Store, acmeMgr *acmecert.Manager, cfg *ini.File, configPath string, logger *toolbox.Logger, smtpUp func() bool) (*App, error) {
|
||||
a := &App{DB: database, DKIM: dkimMgr, Mailstore: mstore, ACME: acmeMgr, Cfg: cfg, ConfigPath: configPath, Logger: logger, SMTPUp: smtpUp}
|
||||
// appSecret is loaded by the caller via LoadOrCreateAppSecret, mirroring how
|
||||
// mailstore's master key is loaded in main.go and threaded in rather than resolved
|
||||
// internally (both are file paths relative to the app's root working directory,
|
||||
// which this package doesn't otherwise know).
|
||||
func New(database *db.DB, dkimMgr *dkim.Manager, mstore *mailstore.Store, acmeMgr *acmecert.Manager, relayer *relay.Relay, cfg *ini.File, configPath string, logger *toolbox.Logger, smtpUp func() bool, appSecret []byte) (*App, error) {
|
||||
trustedProxies := parseTrustedProxies(cfg.Section("Server").Key("trusted_proxies").MustString(""), logger)
|
||||
a := &App{
|
||||
DB: database, DKIM: dkimMgr, Mailstore: mstore, ACME: acmeMgr, Relay: relayer, Cfg: cfg, ConfigPath: configPath, Logger: logger, SMTPUp: smtpUp,
|
||||
pgpKeys: newPGPKeyCache(), trustedProxies: trustedProxies, loginLimiter: newIPRateLimiter(20, time.Minute), appSecret: appSecret,
|
||||
}
|
||||
if err := a.loadTemplates(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -82,6 +110,11 @@ func (a *App) Mux() *http.ServeMux {
|
||||
panic(err) // embed.go's directive is malformed if this ever fails
|
||||
}
|
||||
outer.Handle("GET "+Prefix+"/static/", http.StripPrefix(Prefix+"/static/", http.FileServerFS(staticFS)))
|
||||
// Webmail templates reference the same vendored assets (Bootstrap/Quill/etc. —
|
||||
// see static/vendor/) but live under a different URL prefix, so they need their
|
||||
// own route to the identical embedded files rather than reaching across into the
|
||||
// admin prefix.
|
||||
outer.Handle("GET "+MailboxPrefix+"/static/", http.StripPrefix(MailboxPrefix+"/static/", http.FileServerFS(staticFS)))
|
||||
|
||||
outer.HandleFunc("GET "+Prefix+"/login", a.loginForm)
|
||||
outer.HandleFunc("POST "+Prefix+"/login", a.loginSubmit)
|
||||
@@ -102,7 +135,8 @@ func (a *App) Mux() *http.ServeMux {
|
||||
outer.HandleFunc("POST "+MailboxPrefix+"/logout", a.webmailLogout)
|
||||
|
||||
webmailMux := http.NewServeMux()
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/", a.webmailDashboard)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/", a.webmailMailRoot)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/account", a.webmailDashboard)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mfa-setup", a.webmailMFASetupRequiredPage)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/password", a.webmailChangePassword)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/totp/setup", a.webmailTOTPSetupBegin)
|
||||
@@ -113,6 +147,39 @@ func (a *App) Mux() *http.ServeMux {
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/passkey/{id}/remove", a.webmailPasskeyRemove)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/apppasswords/add", a.webmailAddAppPassword)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/apppasswords/{pw_id}/revoke", a.webmailRevokeAppPassword)
|
||||
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail", a.webmailMailRoot)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/compose", a.webmailComposeForm)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/compose", a.webmailComposeSend)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/save-draft", a.webmailComposeSaveDraft)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/search", a.webmailSearch)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/recipients", a.webmailRecipientSuggest)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/{folder}", a.webmailFolderView)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/{folder}/{uid}", a.webmailMessageView)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/{uid}/delete", a.webmailMessageDelete)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/{uid}/move", a.webmailMessageMove)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/{folder}/{uid}/attachment/{idx}", a.webmailAttachmentDownload)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/folders/add", a.webmailAddFolder)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/folders/{name}/remove", a.webmailDeleteFolder)
|
||||
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/rules", a.webmailRulesList)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/rules/add", a.webmailAddRule)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/rules/{rule_id}/remove", a.webmailRemoveRule)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/certs", a.webmailCertsPage)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/smime/identity/generate", a.webmailSMIMEGenerate)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/smime/identity/import", a.webmailSMIMEImport)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/smime/identity/{identity_id}/remove", a.webmailSMIMERemoveIdentity)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/smime/identity/{identity_id}/download", a.webmailSMIMEDownloadCert)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/smime/contacts/add", a.webmailSMIMEAddContact)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/smime/contacts/{contact_id}/remove", a.webmailSMIMERemoveContact)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/identity/generate", a.webmailPGPGenerate)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/identity/import", a.webmailPGPImport)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/identity/{identity_id}/remove", a.webmailPGPRemoveIdentity)
|
||||
webmailMux.HandleFunc("GET "+MailboxPrefix+"/pgp/identity/{identity_id}/download", a.webmailPGPDownloadKey)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/unlock", a.webmailPGPUnlock)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/contacts/add", a.webmailPGPAddContact)
|
||||
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/contacts/{contact_id}/remove", a.webmailPGPRemoveContact)
|
||||
|
||||
outer.Handle(MailboxPrefix+"/", a.requireMailboxAuth(webmailMux))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
@@ -188,6 +255,13 @@ func (a *App) Mux() *http.ServeMux {
|
||||
mux.HandleFunc("GET "+Prefix+"/ips/{id}/edit", a.editIPForm)
|
||||
mux.HandleFunc("POST "+Prefix+"/ips/{id}/edit", a.editIP)
|
||||
|
||||
mux.HandleFunc("GET "+Prefix+"/blacklist", a.requireGlobalAdmin(a.blacklistPage))
|
||||
mux.HandleFunc("POST "+Prefix+"/blacklist/add", a.requireGlobalAdmin(a.addBlacklistEntry))
|
||||
mux.HandleFunc("POST "+Prefix+"/blacklist/{id}/remove", a.requireGlobalAdmin(a.removeBlacklistEntry))
|
||||
mux.HandleFunc("POST "+Prefix+"/blacklist/{id}/whitelist", a.requireGlobalAdmin(a.whitelistBlacklistedIP))
|
||||
mux.HandleFunc("POST "+Prefix+"/abuse-whitelist/add", a.requireGlobalAdmin(a.addAbuseWhitelistEntry))
|
||||
mux.HandleFunc("POST "+Prefix+"/abuse-whitelist/{id}/remove", a.requireGlobalAdmin(a.removeAbuseWhitelistEntry))
|
||||
|
||||
mux.HandleFunc("GET "+Prefix+"/dkim", a.dkimList)
|
||||
mux.HandleFunc("POST "+Prefix+"/dkim/create", a.createDKIM)
|
||||
mux.HandleFunc("POST "+Prefix+"/dkim/{id}/regenerate", a.regenerateDKIM)
|
||||
@@ -217,7 +291,6 @@ func (a *App) Mux() *http.ServeMux {
|
||||
|
||||
mux.HandleFunc("GET "+Prefix+"/msg/content/{id}", a.viewMessageContent)
|
||||
mux.HandleFunc("GET "+Prefix+"/msg/attachment/{id}/download", a.downloadAttachment)
|
||||
mux.HandleFunc("GET "+Prefix+"/msg/attachment/{id}/delete", a.deleteAttachment)
|
||||
mux.HandleFunc("POST "+Prefix+"/msg/attachment/{id}/delete", a.deleteAttachment)
|
||||
|
||||
mux.HandleFunc("GET "+Prefix, func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user