mirror of
https://github.com/hanzoai/mail.git
synced 2026-09-23 23:53:02 +00:00
Fork of github.com/mjl-/mox at 5656ca6d1b
(upstream v0.0.15 plus 55 commits).
Rename the module path in go.mod and the import paths in 268 Go files. Also
update the five scripts that "go install" the module (genwebsite.sh,
docker-release.sh, apidiff.sh, test-upgrade.sh, Dockerfile.release), which
would otherwise build upstream rather than this fork.
Add NOTICE recording the upstream project, author, license, and fork point,
and stating which license covers which files: MIT for the mox source and our
changes, MPL-2.0 for publicsuffix/public_suffix_list.txt alone, and the
vendored dependencies' own licenses under licenses/. Embed NOTICE so
"mail licenses" prints the lineage from the running binary. LICENSE.MIT,
LICENSE.MPLv2.0 and the licenses/ tree are preserved verbatim; no code is
relicensed.
The mox command name, config keys, data directory layout and website content
are unchanged. LLM.md records the fork point, the rename, what was left alone,
and the steps to stand the server up.
Co-authored-by: Hanzo Dev <[email protected]>
125 lines
3.5 KiB
Go
125 lines
3.5 KiB
Go
package webauth
|
|
|
|
import (
|
|
"context"
|
|
cryptorand "crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
"golang.org/x/text/secure/precis"
|
|
|
|
"github.com/hanzoai/mail/mlog"
|
|
"github.com/hanzoai/mail/mox-"
|
|
"github.com/hanzoai/mail/store"
|
|
)
|
|
|
|
// Admin is for admin logins, with authentication by password, and sessions only
|
|
// stored in memory only, with lifetime 12 hour after last use, with a maximum of
|
|
// 10 active sessions.
|
|
var Admin SessionAuth = &adminSessionAuth{
|
|
sessions: map[store.SessionToken]adminSession{},
|
|
}
|
|
|
|
// Good chance of fitting one working day.
|
|
const adminSessionLifetime = 12 * time.Hour
|
|
|
|
type adminSession struct {
|
|
sessionToken store.SessionToken
|
|
csrfToken store.CSRFToken
|
|
expires time.Time
|
|
}
|
|
|
|
type adminSessionAuth struct {
|
|
sync.Mutex
|
|
sessions map[store.SessionToken]adminSession
|
|
}
|
|
|
|
func (a *adminSessionAuth) login(ctx context.Context, log mlog.Log, username, password string) (valid, disabled bool, name string, rerr error) {
|
|
a.Lock()
|
|
defer a.Unlock()
|
|
|
|
p := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
|
|
buf, err := os.ReadFile(p)
|
|
if err != nil {
|
|
return false, false, "", fmt.Errorf("reading password file: %v", err)
|
|
}
|
|
passwordhash := strings.TrimSpace(string(buf))
|
|
// Transform with precis, if valid. ../rfc/8265:679
|
|
pw, err := precis.OpaqueString.String(password)
|
|
if err == nil {
|
|
password = pw
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword([]byte(passwordhash), []byte(password)); err != nil {
|
|
return false, false, "", nil
|
|
}
|
|
|
|
return true, false, "(admin)", nil
|
|
}
|
|
|
|
func (a *adminSessionAuth) add(ctx context.Context, log mlog.Log, accountName string, loginAddress string) (sessionToken store.SessionToken, csrfToken store.CSRFToken, rerr error) {
|
|
a.Lock()
|
|
defer a.Unlock()
|
|
|
|
// Cleanup expired sessions.
|
|
for st, s := range a.sessions {
|
|
if time.Until(s.expires) < 0 {
|
|
delete(a.sessions, st)
|
|
}
|
|
}
|
|
|
|
// Ensure we have at most 10 sessions.
|
|
if len(a.sessions) > 10 {
|
|
var oldest *store.SessionToken
|
|
for _, s := range a.sessions {
|
|
if oldest == nil || s.expires.Before(a.sessions[*oldest].expires) {
|
|
oldest = &s.sessionToken
|
|
}
|
|
}
|
|
delete(a.sessions, *oldest)
|
|
}
|
|
|
|
// Generate new tokens.
|
|
var sessionData, csrfData [16]byte
|
|
cryptorand.Read(sessionData[:])
|
|
cryptorand.Read(csrfData[:])
|
|
sessionToken = store.SessionToken(base64.RawURLEncoding.EncodeToString(sessionData[:]))
|
|
csrfToken = store.CSRFToken(base64.RawURLEncoding.EncodeToString(csrfData[:]))
|
|
|
|
// Register session.
|
|
a.sessions[sessionToken] = adminSession{sessionToken, csrfToken, time.Now().Add(adminSessionLifetime)}
|
|
return sessionToken, csrfToken, nil
|
|
}
|
|
|
|
func (a *adminSessionAuth) use(ctx context.Context, log mlog.Log, accountName string, sessionToken store.SessionToken, csrfToken store.CSRFToken) (loginAddress string, rerr error) {
|
|
a.Lock()
|
|
defer a.Unlock()
|
|
|
|
s, ok := a.sessions[sessionToken]
|
|
if !ok {
|
|
return "", fmt.Errorf("unknown session (due to server restart or 10 new admin sessions)")
|
|
} else if time.Until(s.expires) < 0 {
|
|
return "", fmt.Errorf("session expired (after 12 hours inactivity)")
|
|
} else if csrfToken != "" && csrfToken != s.csrfToken {
|
|
return "", fmt.Errorf("mismatch between csrf and session tokens")
|
|
}
|
|
s.expires = time.Now().Add(adminSessionLifetime)
|
|
a.sessions[sessionToken] = s
|
|
return "", nil
|
|
}
|
|
|
|
func (a *adminSessionAuth) remove(ctx context.Context, log mlog.Log, accountName string, sessionToken store.SessionToken) error {
|
|
a.Lock()
|
|
defer a.Unlock()
|
|
|
|
if _, ok := a.sessions[sessionToken]; !ok {
|
|
return fmt.Errorf("unknown session")
|
|
}
|
|
delete(a.sessions, sessionToken)
|
|
return nil
|
|
}
|