mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
114 lines
3.5 KiB
Go
114 lines
3.5 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
_ "embed"
|
|
|
|
"github.com/mjl-/sherpa"
|
|
"github.com/mjl-/sherpaprom"
|
|
|
|
"github.com/mjl-/mox/metrics"
|
|
"github.com/mjl-/mox/mlog"
|
|
"github.com/mjl-/mox/mox-"
|
|
"github.com/mjl-/mox/moxvar"
|
|
"github.com/mjl-/mox/store"
|
|
)
|
|
|
|
//go:embed accountapi.json
|
|
var accountapiJSON []byte
|
|
|
|
//go:embed account.html
|
|
var accountHTML []byte
|
|
|
|
var accountDoc = mustParseAPI(accountapiJSON)
|
|
|
|
var accountSherpaHandler http.Handler
|
|
|
|
func init() {
|
|
collector, err := sherpaprom.NewCollector("moxaccount", nil)
|
|
if err != nil {
|
|
xlog.Fatalx("creating sherpa prometheus collector", err)
|
|
}
|
|
|
|
accountSherpaHandler, err = sherpa.NewHandler("/account/api/", moxvar.Version, Account{}, &accountDoc, &sherpa.HandlerOpts{Collector: collector, AdjustFunctionNames: "none"})
|
|
if err != nil {
|
|
xlog.Fatalx("sherpa handler", err)
|
|
}
|
|
}
|
|
|
|
// Account exports web API functions for the account web interface. All its
|
|
// methods are exported under /account/api/. Function calls require valid HTTP
|
|
// Authentication credentials of a user.
|
|
type Account struct{}
|
|
|
|
func accountHandle(w http.ResponseWriter, r *http.Request) {
|
|
ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
|
|
log := xlog.WithContext(ctx).Fields(mlog.Field("userauth", ""))
|
|
var accountName string
|
|
authResult := "error"
|
|
defer func() {
|
|
metrics.AuthenticationInc("httpaccount", "httpbasic", authResult)
|
|
}()
|
|
// todo: should probably add a cache here instead of looking up password in database all the time, just like in admin.go
|
|
if auth := r.Header.Get("Authorization"); auth == "" || !strings.HasPrefix(auth, "Basic ") {
|
|
} else if authBuf, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic ")); err != nil {
|
|
log.Infox("parsing base64", err)
|
|
} else if t := strings.SplitN(string(authBuf), ":", 2); len(t) != 2 {
|
|
log.Info("bad user:pass form")
|
|
} else if acc, err := store.OpenEmailAuth(t[0], t[1]); err != nil {
|
|
if errors.Is(err, store.ErrUnknownCredentials) {
|
|
authResult = "badcreds"
|
|
}
|
|
log.Infox("open account", err)
|
|
} else {
|
|
accountName = acc.Name
|
|
authResult = "ok"
|
|
}
|
|
if accountName == "" {
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="mox account - login with email address and password"`)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
fmt.Fprintln(w, "http 401 - unauthorized - mox account - login with email address and password")
|
|
return
|
|
}
|
|
|
|
if r.Method == "GET" && r.URL.Path == "/account/" {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-cache; max-age=0")
|
|
f, err := os.Open("http/account.html")
|
|
if err == nil {
|
|
defer f.Close()
|
|
io.Copy(w, f)
|
|
} else {
|
|
w.Write(accountHTML)
|
|
}
|
|
return
|
|
}
|
|
accountSherpaHandler.ServeHTTP(w, r.WithContext(context.WithValue(ctx, authCtxKey, accountName)))
|
|
}
|
|
|
|
type ctxKey string
|
|
|
|
var authCtxKey ctxKey = "account"
|
|
|
|
// SetPassword saves a new password for the account, invalidating the previous password.
|
|
// Sessions are not interrupted, and will keep working. New login attempts must use the new password.
|
|
// Password must be at least 8 characters.
|
|
func (Account) SetPassword(ctx context.Context, password string) {
|
|
if len(password) < 8 {
|
|
panic(&sherpa.Error{Code: "user:error", Message: "password must be at least 8 characters"})
|
|
}
|
|
accountName := ctx.Value(authCtxKey).(string)
|
|
acc, err := store.OpenAccount(accountName)
|
|
xcheckf(ctx, err, "open account")
|
|
defer acc.Close()
|
|
err = acc.SetPassword(password)
|
|
xcheckf(ctx, err, "setting password")
|
|
}
|