MFA fix, added IP blacklist, update webmail client
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"mailgoserver/internal/smime"
|
||||
)
|
||||
|
||||
// webmailCertsPage shows both of a mailbox owner's certificate/key types: S/MIME
|
||||
// certificates (generate/import/remove/download, used for signing, stored plain)
|
||||
// and PGP keys (used for encryption — see webmail_pgp.go — each protected by its
|
||||
// own passphrase), plus their collected contact certificates/keys for each
|
||||
// protocol.
|
||||
func (a *App) webmailCertsPage(w http.ResponseWriter, r *http.Request) {
|
||||
mbox := mailboxFromContext(r)
|
||||
token := sessionToken(r)
|
||||
|
||||
identities, err := a.DB.ListSMIMEIdentities(mbox.ID)
|
||||
if err != nil {
|
||||
setFlash(w, "error", "Error loading your S/MIME certificates")
|
||||
}
|
||||
contacts, err := a.DB.ListSMIMEContacts(mbox.ID)
|
||||
if err != nil {
|
||||
setFlash(w, "error", "Error loading S/MIME contact certificates")
|
||||
}
|
||||
|
||||
pgpIdentities, err := a.DB.ListPGPIdentities(mbox.ID)
|
||||
if err != nil {
|
||||
setFlash(w, "error", "Error loading your PGP keys")
|
||||
}
|
||||
pgpContacts, err := a.DB.ListPGPContacts(mbox.ID)
|
||||
if err != nil {
|
||||
setFlash(w, "error", "Error loading PGP contact keys")
|
||||
}
|
||||
pgpUnlocked := make(map[int64]bool, len(pgpIdentities))
|
||||
for _, id := range pgpIdentities {
|
||||
if _, ok := a.pgpKeys.get(token, id.ID); ok {
|
||||
pgpUnlocked[id.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
a.render(w, r, "webmail_certs.html", M{
|
||||
"mailbox": mbox,
|
||||
"identities": identities,
|
||||
"contacts": contacts,
|
||||
"pgp_identities": pgpIdentities,
|
||||
"pgp_unlocked": pgpUnlocked,
|
||||
"pgp_contacts": pgpContacts,
|
||||
"flashes": popFlashes(w, r),
|
||||
})
|
||||
}
|
||||
|
||||
// storeIdentity adds a new identity with certPEM/keyPEM stored as-is — no
|
||||
// passphrase wrapping (see the schema comment on esrv_mailbox_smime_identities).
|
||||
func (a *App) storeIdentity(mailboxID int64, certPEM, keyPEM []byte) error {
|
||||
cert, err := smime.ParseCertPEM(certPEM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = a.DB.CreateSMIMEIdentity(mailboxID, string(certPEM), string(keyPEM), cert.NotAfter)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) webmailSMIMEGenerate(w http.ResponseWriter, r *http.Request) {
|
||||
mbox := mailboxFromContext(r)
|
||||
certPEM, keyPEM, err := smime.GenerateSelfSigned(mbox.Email, smime.DefaultValidity)
|
||||
if err == nil {
|
||||
err = a.storeIdentity(mbox.ID, certPEM, keyPEM)
|
||||
}
|
||||
if err != nil {
|
||||
a.Logger.Error("smime generate for mailbox %d: %v", mbox.ID, err)
|
||||
setFlash(w, "error", "Error generating certificate")
|
||||
} else {
|
||||
setFlash(w, "success", "S/MIME certificate generated")
|
||||
}
|
||||
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *App) webmailSMIMEImport(w http.ResponseWriter, r *http.Request) {
|
||||
mbox := mailboxFromContext(r)
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
setFlash(w, "error", "Error reading upload")
|
||||
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
|
||||
return
|
||||
}
|
||||
file, _, err := r.FormFile("p12_file")
|
||||
if err != nil {
|
||||
setFlash(w, "error", "Please choose a .p12/.pfx file")
|
||||
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := io.ReadAll(file)
|
||||
if err == nil {
|
||||
var certPEM, keyPEM []byte
|
||||
// p12_password unlocks the uploaded .p12 bundle itself, a one-time-use secret
|
||||
// distinct from anything stored afterward — the key is kept plain from here on.
|
||||
certPEM, keyPEM, err = smime.ImportPKCS12(data, r.FormValue("p12_password"))
|
||||
if err == nil {
|
||||
err = a.storeIdentity(mbox.ID, certPEM, keyPEM)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
setFlash(w, "error", "Error importing certificate: "+err.Error())
|
||||
} else {
|
||||
setFlash(w, "success", "S/MIME certificate imported")
|
||||
}
|
||||
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *App) webmailSMIMERemoveIdentity(w http.ResponseWriter, r *http.Request) {
|
||||
mbox := mailboxFromContext(r)
|
||||
identityID := int64(atoi(r.PathValue("identity_id")))
|
||||
if err := a.DB.DeleteSMIMEIdentity(mbox.ID, identityID); err != nil {
|
||||
setFlash(w, "error", "Error removing identity")
|
||||
} else {
|
||||
setFlash(w, "success", "S/MIME identity removed")
|
||||
}
|
||||
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *App) webmailSMIMEDownloadCert(w http.ResponseWriter, r *http.Request) {
|
||||
mbox := mailboxFromContext(r)
|
||||
identityID := int64(atoi(r.PathValue("identity_id")))
|
||||
identity, err := a.DB.GetSMIMEIdentity(mbox.ID, identityID)
|
||||
if err != nil || identity == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-x509-user-cert")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+mbox.Email+`.crt"`)
|
||||
w.Write([]byte(identity.CertPEM))
|
||||
}
|
||||
|
||||
func (a *App) webmailSMIMEAddContact(w http.ResponseWriter, r *http.Request) {
|
||||
mbox := mailboxFromContext(r)
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
setFlash(w, "error", "Error reading upload")
|
||||
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
|
||||
return
|
||||
}
|
||||
email := strings.TrimSpace(r.FormValue("email"))
|
||||
file, _, err := r.FormFile("cert_file")
|
||||
if email == "" || err != nil {
|
||||
setFlash(w, "error", "Please provide an email and a certificate file")
|
||||
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := io.ReadAll(file)
|
||||
if err == nil {
|
||||
_, err = smime.ParseCertPEM(data)
|
||||
}
|
||||
if err != nil {
|
||||
setFlash(w, "error", "That doesn't look like a valid certificate file")
|
||||
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
|
||||
return
|
||||
}
|
||||
if err := a.DB.UpsertSMIMEContact(mbox.ID, email, string(data)); err != nil {
|
||||
setFlash(w, "error", "Error saving contact certificate")
|
||||
} else {
|
||||
setFlash(w, "success", "Contact certificate added")
|
||||
}
|
||||
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *App) webmailSMIMERemoveContact(w http.ResponseWriter, r *http.Request) {
|
||||
mbox := mailboxFromContext(r)
|
||||
contactID := int64(atoi(r.PathValue("contact_id")))
|
||||
if err := a.DB.DeleteSMIMEContact(mbox.ID, contactID); err != nil {
|
||||
setFlash(w, "error", "Error removing contact")
|
||||
} else {
|
||||
setFlash(w, "success", "Contact removed")
|
||||
}
|
||||
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
|
||||
}
|
||||
Reference in New Issue
Block a user