all: session storage usage

This commit is contained in:
Stanislav Chzhen
2025-04-22 15:42:12 +03:00
parent c7c62ad3b6
commit 53cb84efc0
9 changed files with 236 additions and 445 deletions

View File

@@ -355,8 +355,12 @@ func (ds *DefaultSessionStorage) store(s *Session) (err error) {
return nil return nil
} }
// FindByToken implements the [SessionStorage] interface for *DefaultSessionStorage. // FindByToken implements the [SessionStorage] interface for
func (ds *DefaultSessionStorage) FindByToken(ctx context.Context, t SessionToken) (s *Session, err error) { // *DefaultSessionStorage.
func (ds *DefaultSessionStorage) FindByToken(
ctx context.Context,
t SessionToken,
) (s *Session, err error) {
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()

View File

@@ -1,317 +1,131 @@
package home package home
import ( import (
"crypto/rand" "context"
"encoding/binary"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"sync"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghuser"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"go.etcd.io/bbolt" "github.com/AdguardTeam/golibs/timeutil"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// sessionTokenSize is the length of session token in bytes. // webUser represents a user of the Web UI.
const sessionTokenSize = 16 type webUser struct {
// Name represents the login name of the web user.
Name string `yaml:"name"`
type session struct { // PasswordHash is the hashed representation of the web user password.
userName string PasswordHash string `yaml:"password"`
// expire is the expiration time, in seconds.
expire uint32 // UserID is the unique identifier of the web user.
//
// TODO(s.chzhen): !! Use this.
UserID aghuser.UserID `yaml:"-"`
} }
func (s *session) serialize() []byte { // toUser returns the new properly initialized *aghuser.User using stored
const ( // properties. It panics if there is an error generating the user ID.
expireLen = 4 func (wu *webUser) toUser() (u *aghuser.User) {
nameLen = 2 uid := wu.UserID
) if uid == (aghuser.UserID{}) {
data := make([]byte, expireLen+nameLen+len(s.userName)) uid = aghuser.MustNewUserID()
binary.BigEndian.PutUint32(data[0:4], s.expire)
binary.BigEndian.PutUint16(data[4:6], uint16(len(s.userName)))
copy(data[6:], []byte(s.userName))
return data
}
func (s *session) deserialize(data []byte) bool {
if len(data) < 4+2 {
return false
} }
s.expire = binary.BigEndian.Uint32(data[0:4])
nameLen := binary.BigEndian.Uint16(data[4:6])
data = data[6:]
if len(data) < int(nameLen) { return &aghuser.User{
return false Password: aghuser.NewDefaultPassword(wu.PasswordHash),
Login: aghuser.Login(wu.Name),
ID: uid,
} }
s.userName = string(data)
return true
} }
// Auth is the global authentication object. // Auth is the global authentication object.
type Auth struct { type Auth struct {
trustedProxies netutil.SubnetSet logger *slog.Logger
db *bbolt.DB
rateLimiter *authRateLimiter rateLimiter *authRateLimiter
sessions map[string]*session sessions aghuser.SessionStorage
users []webUser trustedProxies netutil.SubnetSet
lock sync.Mutex users aghuser.DB
sessionTTL uint32
} }
// webUser represents a user of the Web UI. // InitAuth initializes the global authentication object. baseLogger,
// // rateLimiter, trustedProxies must not be nil. dbFilename and sessionTTL
// TODO(s.chzhen): Improve naming. // should not be empty.
type webUser struct {
Name string `yaml:"name"`
PasswordHash string `yaml:"password"`
}
// InitAuth initializes the global authentication object.
func InitAuth( func InitAuth(
ctx context.Context,
baseLogger *slog.Logger,
dbFilename string, dbFilename string,
users []webUser, users []webUser,
sessionTTL uint32, sessionTTL time.Duration,
rateLimiter *authRateLimiter, rateLimiter *authRateLimiter,
trustedProxies netutil.SubnetSet, trustedProxies netutil.SubnetSet,
) (a *Auth) { ) (a *Auth, err error) {
log.Info("Initializing auth module: %s", dbFilename) userDB := aghuser.NewDefaultDB()
for i, u := range users {
a = &Auth{ err = userDB.Create(ctx, u.toUser())
sessionTTL: sessionTTL, if err != nil {
rateLimiter: rateLimiter, return nil, fmt.Errorf("users: at index %d: %w", i, err)
sessions: make(map[string]*session),
users: users,
trustedProxies: trustedProxies,
}
var err error
a.db, err = bbolt.Open(dbFilename, aghos.DefaultPermFile, nil)
if err != nil {
log.Error("auth: open DB: %s: %s", dbFilename, err)
if err.Error() == "invalid argument" {
log.Error("AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#limitations")
} }
return nil
} }
a.loadSessions()
log.Info("auth: initialized. users:%d sessions:%d", len(a.users), len(a.sessions))
return a s, err := aghuser.NewDefaultSessionStorage(ctx, &aghuser.DefaultSessionStorageConfig{
Logger: baseLogger.With(slogutil.KeyPrefix, "session_storage"),
Clock: timeutil.SystemClock{},
UserDB: aghuser.NewDefaultDB(),
DBPath: dbFilename,
SessionTTL: sessionTTL,
})
if err != nil {
return nil, fmt.Errorf("creating session storage: %w", err)
}
return &Auth{
logger: baseLogger.With(slogutil.KeyPrefix, "auth"),
rateLimiter: rateLimiter,
trustedProxies: trustedProxies,
sessions: s,
users: userDB,
}, nil
} }
// Close closes the authentication database. // Close closes the authentication database.
func (a *Auth) Close() { func (a *Auth) Close(ctx context.Context) {
_ = a.db.Close() err := a.sessions.Close()
}
func bucketName() []byte {
return []byte("sessions-2")
}
// loadSessions loads sessions from the database file and removes expired
// sessions.
func (a *Auth) loadSessions() {
tx, err := a.db.Begin(true)
if err != nil { if err != nil {
log.Error("auth: bbolt.Begin: %s", err) a.logger.ErrorContext(ctx, "closing session storage", slogutil.KeyError, err)
return
}
defer func() {
_ = tx.Rollback()
}()
bkt := tx.Bucket(bucketName())
if bkt == nil {
return
}
removed := 0
if tx.Bucket([]byte("sessions")) != nil {
_ = tx.DeleteBucket([]byte("sessions"))
removed = 1
}
now := uint32(time.Now().UTC().Unix())
forEach := func(k, v []byte) error {
s := session{}
if !s.deserialize(v) || s.expire <= now {
err = bkt.Delete(k)
if err != nil {
log.Error("auth: bbolt.Delete: %s", err)
} else {
removed++
}
return nil
}
a.sessions[hex.EncodeToString(k)] = &s
return nil
}
_ = bkt.ForEach(forEach)
if removed != 0 {
err = tx.Commit()
if err != nil {
log.Error("bolt.Commit(): %s", err)
}
}
log.Debug("auth: loaded %d sessions from DB (removed %d expired)", len(a.sessions), removed)
}
// addSession adds a new session to the list of sessions and saves it in the
// database file.
func (a *Auth) addSession(data []byte, s *session) {
name := hex.EncodeToString(data)
a.lock.Lock()
a.sessions[name] = s
a.lock.Unlock()
if a.storeSession(data, s) {
log.Debug("auth: created session %s: expire=%d", name, s.expire)
} }
} }
// storeSession saves a session in the database file. // isValidSession returns true if the session is valid.
func (a *Auth) storeSession(data []byte, s *session) bool { func (a *Auth) isValidSession(ctx context.Context, cookieSess string) (ok bool) {
tx, err := a.db.Begin(true) sess, err := hex.DecodeString(cookieSess)
if err != nil { if err != nil {
log.Error("auth: bbolt.Begin: %s", err) a.logger.ErrorContext(ctx, "checking session: decoding cookie", slogutil.KeyError, err)
return false
}
defer func() {
_ = tx.Rollback()
}()
bkt, err := tx.CreateBucketIfNotExists(bucketName())
if err != nil {
log.Error("auth: bbolt.CreateBucketIfNotExists: %s", err)
return false return false
} }
err = bkt.Put(data, s.serialize()) var t aghuser.SessionToken
copy(t[:], sess)
s, err := a.sessions.FindByToken(ctx, t)
if err != nil { if err != nil {
log.Error("auth: bbolt.Put: %s", err) a.logger.ErrorContext(ctx, "checking session", slogutil.KeyError, err)
return false return false
} }
err = tx.Commit() return s != nil
if err != nil {
log.Error("auth: bbolt.Commit: %s", err)
return false
}
return true
} }
// removeSessionFromFile removes a stored session from the DB file on disk. // addUser adds a new user with the given password. u must not be nil.
func (a *Auth) removeSessionFromFile(sess []byte) { func (a *Auth) addUser(ctx context.Context, u *webUser, password string) (err error) {
tx, err := a.db.Begin(true)
if err != nil {
log.Error("auth: bbolt.Begin: %s", err)
return
}
defer func() {
_ = tx.Rollback()
}()
bkt := tx.Bucket(bucketName())
if bkt == nil {
log.Error("auth: bbolt.Bucket")
return
}
err = bkt.Delete(sess)
if err != nil {
log.Error("auth: bbolt.Put: %s", err)
return
}
err = tx.Commit()
if err != nil {
log.Error("auth: bbolt.Commit: %s", err)
return
}
log.Debug("auth: removed session from DB")
}
// checkSessionResult is the result of checking a session.
type checkSessionResult int
// checkSessionResult constants.
const (
checkSessionOK checkSessionResult = 0
checkSessionNotFound checkSessionResult = -1
checkSessionExpired checkSessionResult = 1
)
// checkSession checks if the session is valid.
func (a *Auth) checkSession(sess string) (res checkSessionResult) {
now := uint32(time.Now().UTC().Unix())
update := false
a.lock.Lock()
defer a.lock.Unlock()
s, ok := a.sessions[sess]
if !ok {
return checkSessionNotFound
}
if s.expire <= now {
delete(a.sessions, sess)
key, _ := hex.DecodeString(sess)
a.removeSessionFromFile(key)
return checkSessionExpired
}
newExpire := now + a.sessionTTL
if s.expire/(24*60*60) != newExpire/(24*60*60) {
// update expiration time once a day
update = true
s.expire = newExpire
}
if update {
key, _ := hex.DecodeString(sess)
if a.storeSession(key, s) {
log.Debug("auth: updated session %s: expire=%d", sess, s.expire)
}
}
return checkSessionOK
}
// removeSession removes the session from the active sessions and the disk.
func (a *Auth) removeSession(sess string) {
key, _ := hex.DecodeString(sess)
a.lock.Lock()
delete(a.sessions, sess)
a.lock.Unlock()
a.removeSessionFromFile(key)
}
// addUser adds a new user with the given password.
func (a *Auth) addUser(u *webUser, password string) (err error) {
if len(password) == 0 { if len(password) == 0 {
return errors.Error("empty password") return errors.Error("empty password")
} }
@@ -323,97 +137,129 @@ func (a *Auth) addUser(u *webUser, password string) (err error) {
u.PasswordHash = string(hash) u.PasswordHash = string(hash)
a.lock.Lock() err = a.users.Create(ctx, u.toUser())
defer a.lock.Unlock() if err != nil {
// Should not happen.
panic(err)
}
a.users = append(a.users, *u) a.logger.DebugContext(ctx, "added user", "login", u.Name)
log.Debug("auth: added user with login %q", u.Name)
return nil return nil
} }
// findUser returns a user if there is one. // findUser returns a user if one exists with the provided login and the
func (a *Auth) findUser(login, password string) (u webUser, ok bool) { // password matches.
a.lock.Lock() func (a *Auth) findUser(ctx context.Context, login, password string) (user *aghuser.User) {
defer a.lock.Unlock() user, err := a.users.ByLogin(ctx, aghuser.Login(login))
if err != nil {
for _, u = range a.users { return nil
if u.Name == login &&
bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil {
return u, true
}
} }
return webUser{}, false ok := user.Password.Authenticate(ctx, password)
if !ok {
return nil
}
return user
} }
// getCurrentUser returns the current user. It returns an empty User if the // getCurrentUser searches for a user using a cookie or credentials from basic
// user is not found. // authentication.
func (a *Auth) getCurrentUser(r *http.Request) (u webUser) { func (a *Auth) getCurrentUser(r *http.Request) (user *aghuser.User) {
ctx := r.Context()
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if err != nil { if err != nil {
// There's no Cookie, check Basic authentication. // There's no Cookie, check Basic authentication.
user, pass, ok := r.BasicAuth() user, pass, ok := r.BasicAuth()
if ok { if ok {
u, _ = globalContext.auth.findUser(user, pass) return a.findUser(ctx, user, pass)
return u
} }
return webUser{} return nil
} }
a.lock.Lock() sess, err := hex.DecodeString(cookie.Value)
defer a.lock.Unlock() if err != nil {
a.logger.ErrorContext(
ctx,
"searching for user: decoding cookie value",
slogutil.KeyError, err,
)
s, ok := a.sessions[cookie.Value] return nil
if !ok {
return webUser{}
} }
for _, u = range a.users { var t aghuser.SessionToken
if u.Name == s.userName { copy(t[:], sess)
return u
} s, err := a.sessions.FindByToken(ctx, t)
if err != nil {
a.logger.ErrorContext(ctx, "searching for user", slogutil.KeyError, err)
return nil
} }
return webUser{} if s == nil {
return nil
}
return &aghuser.User{
Login: s.UserLogin,
ID: s.UserID,
}
}
// removeSession deletes the session from the active sessions and the disk. It
// also logs any occurring errors.
func (a *Auth) removeSession(ctx context.Context, cookieSess string) {
sess, err := hex.DecodeString(cookieSess)
if err != nil {
a.logger.ErrorContext(ctx, "removing session: decoding cookie", slogutil.KeyError, err)
return
}
var t aghuser.SessionToken
copy(t[:], sess)
err = a.sessions.DeleteByToken(ctx, t)
if err != nil {
a.logger.ErrorContext(ctx, "removing session by token", slogutil.KeyError, err)
}
} }
// usersList returns a copy of a users list. // usersList returns a copy of a users list.
func (a *Auth) usersList() (users []webUser) { func (a *Auth) usersList(ctx context.Context) (webUsers []webUser) {
a.lock.Lock() users, err := a.users.All(ctx)
defer a.lock.Unlock() if err != nil {
// Should not happen.
panic(err)
}
users = make([]webUser, len(a.users)) webUsers = make([]webUser, 0, len(users))
copy(users, a.users) for _, u := range users {
webUsers = append(webUsers, webUser{
Name: string(u.Login),
PasswordHash: string(u.Password.Hash()),
UserID: u.ID,
})
}
return users return webUsers
} }
// authRequired returns true if a authentication is required. // authRequired returns true if a authentication is required.
func (a *Auth) authRequired() bool { func (a *Auth) authRequired(ctx context.Context) (ok bool) {
if GLMode { if GLMode {
return true return true
} }
a.lock.Lock() users, err := a.users.All(ctx)
defer a.lock.Unlock() if err != nil {
// Should not happen.
panic(err)
}
return len(a.users) != 0 return len(users) != 0
}
// newSessionToken returns cryptographically secure randomly generated slice of
// bytes of sessionTokenSize length.
//
// TODO(e.burkov): Think about using byte array instead of byte slice.
func newSessionToken() (data []byte) {
randData := make([]byte, sessionTokenSize)
// Since Go 1.24, crypto/rand.Read doesn't return an error and crashes
// unrecoverably instead.
_, _ = rand.Read(randData)
return randData
} }

View File

@@ -1,69 +0,0 @@
package home
import (
"encoding/hex"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuth(t *testing.T) {
dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db")
users := []webUser{{
Name: "name",
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
}}
a := InitAuth(fn, nil, 60, nil, nil)
s := session{}
user := webUser{Name: "name"}
err := a.addUser(&user, "password")
require.NoError(t, err)
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
a.removeSession("notfound")
sess := newSessionToken()
sessStr := hex.EncodeToString(sess)
now := time.Now().UTC().Unix()
// check expiration
s.expire = uint32(now)
a.addSession(sess, &s)
assert.Equal(t, checkSessionExpired, a.checkSession(sessStr))
// add session with TTL = 2 sec
s = session{}
s.expire = uint32(time.Now().UTC().Unix() + 2)
a.addSession(sess, &s)
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
a.Close()
// load saved session
a = InitAuth(fn, users, 60, nil, nil)
// the session is still alive
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
// reset our expiration time because checkSession() has just updated it
s.expire = uint32(time.Now().UTC().Unix() + 2)
a.storeSession(sess, &s)
a.Close()
u, ok := a.findUser("name", "password")
assert.True(t, ok)
assert.NotEmpty(t, u.Name)
time.Sleep(3 * time.Second)
// load and remove expired sessions
a = InitAuth(fn, users, 60, nil, nil)
assert.Equal(t, checkSessionNotFound, a.checkSession(sessStr))
a.Close()
}

View File

@@ -1,6 +1,7 @@
package home package home
import ( import (
"context"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -32,10 +33,14 @@ type loginJSON struct {
} }
// newCookie creates a new authentication cookie. // newCookie creates a new authentication cookie.
func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error) { func (a *Auth) newCookie(
ctx context.Context,
req loginJSON,
addr string,
) (c *http.Cookie, err error) {
rateLimiter := a.rateLimiter rateLimiter := a.rateLimiter
u, ok := a.findUser(req.Name, req.Password) u := a.findUser(ctx, req.Name, req.Password)
if !ok { if u == nil {
if rateLimiter != nil { if rateLimiter != nil {
rateLimiter.inc(addr) rateLimiter.inc(addr)
} }
@@ -47,19 +52,16 @@ func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error)
rateLimiter.remove(addr) rateLimiter.remove(addr)
} }
sess := newSessionToken() s, err := a.sessions.New(ctx, u)
now := time.Now().UTC() if err != nil {
return nil, fmt.Errorf("creating session: %w", err)
a.addSession(sess, &session{ }
userName: u.Name,
expire: uint32(now.Unix()) + a.sessionTTL,
})
return &http.Cookie{ return &http.Cookie{
Name: sessionCookieName, Name: sessionCookieName,
Value: hex.EncodeToString(sess), Value: hex.EncodeToString(s.Token[:]),
Path: "/", Path: "/",
Expires: now.Add(cookieTTL), Expires: time.Now().Add(cookieTTL),
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
}, nil }, nil
@@ -172,7 +174,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err) log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
} }
cookie, err := globalContext.auth.newCookie(req, remoteIP) cookie, err := globalContext.auth.newCookie(r.Context(), req, remoteIP)
if err != nil { if err != nil {
logIP := remoteIP logIP := remoteIP
if globalContext.auth.trustedProxies.Contains(ip.Unmap()) { if globalContext.auth.trustedProxies.Contains(ip.Unmap()) {
@@ -209,7 +211,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
return return
} }
globalContext.auth.removeSession(c.Value) globalContext.auth.removeSession(r.Context(), c.Value)
c = &http.Cookie{ c = &http.Cookie{
Name: sessionCookieName, Name: sessionCookieName,
@@ -242,28 +244,7 @@ func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
return false return false
} }
// redirect to login page if not authenticated if u := globalContext.auth.getCurrentUser(r); u != nil {
isAuthenticated := false
cookie, err := r.Cookie(sessionCookieName)
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// Check Basic authentication.
user, pass, hasBasic := r.BasicAuth()
if hasBasic {
_, isAuthenticated = globalContext.auth.findUser(user, pass)
if !isAuthenticated {
log.Info("%s: invalid basic authorization value", pref)
}
}
} else {
res := globalContext.auth.checkSession(cookie.Value)
isAuthenticated = res == checkSessionOK
if !isAuthenticated {
log.Debug("%s: invalid cookie value: %q", pref, cookie)
}
}
if isAuthenticated {
return false return false
} }
@@ -289,14 +270,14 @@ func optionalAuth(
h func(http.ResponseWriter, *http.Request), h func(http.ResponseWriter, *http.Request),
) (wrapped func(http.ResponseWriter, *http.Request)) { ) (wrapped func(http.ResponseWriter, *http.Request)) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
p := r.URL.Path p := r.URL.Path
authRequired := globalContext.auth != nil && globalContext.auth.authRequired() authRequired := globalContext.auth != nil && globalContext.auth.authRequired(ctx)
if p == "/login.html" { if p == "/login.html" {
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if authRequired && err == nil { if authRequired && err == nil {
// Redirect to the dashboard if already authenticated. // Redirect to the dashboard if already authenticated.
res := globalContext.auth.checkSession(cookie.Value) if globalContext.auth.isValidSession(ctx, cookie.Value) {
if res == checkSessionOK {
http.Redirect(w, r, "", http.StatusFound) http.Redirect(w, r, "", http.StatusFound)
return return

View File

@@ -7,8 +7,10 @@ import (
"net/url" "net/url"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/AdguardTeam/golibs/httphdr" "github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -33,13 +35,20 @@ func (w *testResponseWriter) WriteHeader(statusCode int) {
} }
func TestAuthHTTP(t *testing.T) { func TestAuthHTTP(t *testing.T) {
var (
ctx = testutil.ContextWithTimeout(t, testTimeout)
logger = slogutil.NewDiscardLogger()
err error
)
dir := t.TempDir() dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db") fn := filepath.Join(dir, "sessions.db")
users := []webUser{ users := []webUser{
{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, {Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
} }
globalContext.auth = InitAuth(fn, users, 60, nil, nil) globalContext.auth, err = InitAuth(ctx, logger, fn, users, time.Minute, nil, nil)
require.NoError(t, err)
handlerCalled := false handlerCalled := false
handler := func(_ http.ResponseWriter, _ *http.Request) { handler := func(_ http.ResponseWriter, _ *http.Request) {
@@ -68,7 +77,11 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
// perform login // perform login
cookie, err := globalContext.auth.newCookie(loginJSON{Name: "name", Password: "password"}, "") cookie, err := globalContext.auth.newCookie(
ctx,
loginJSON{Name: "name", Password: "password"},
"",
)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, cookie) require.NotNil(t, cookie)
@@ -114,7 +127,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
r.Header.Del(httphdr.Cookie) r.Header.Del(httphdr.Cookie)
globalContext.auth.Close() globalContext.auth.Close(ctx)
} }
func TestRealIP(t *testing.T) { func TestRealIP(t *testing.T) {

View File

@@ -2,6 +2,7 @@ package home
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"net/netip" "net/netip"
"os" "os"
@@ -748,7 +749,8 @@ func (c *configuration) write(tlsMgr *tlsManager) (err error) {
defer c.Unlock() defer c.Unlock()
if globalContext.auth != nil { if globalContext.auth != nil {
config.Users = globalContext.auth.usersList() // TODO(s.chzhen): Pass context.
config.Users = globalContext.auth.usersList(context.TODO())
} }
if tlsMgr != nil { if tlsMgr != nil {

View File

@@ -392,6 +392,8 @@ const PasswordMinRunes = 8
// Apply new configuration, start DNS server, restart Web server // Apply new configuration, start DNS server, restart Web server
func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, restartHTTP, err := decodeApplyConfigReq(r.Body) req, restartHTTP, err := decodeApplyConfigReq(r.Body)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -439,7 +441,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
u := &webUser{ u := &webUser{
Name: req.Username, Name: req.Username,
} }
err = globalContext.auth.addUser(u, req.Password) err = globalContext.auth.addUser(ctx, u, req.Password)
if err != nil { if err != nil {
globalContext.firstRun = true globalContext.firstRun = true
copyInstallSettings(config, curConfig) copyInstallSettings(config, curConfig)
@@ -452,7 +454,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
// moment we'll allow setting up TLS in the initial configuration or the // moment we'll allow setting up TLS in the initial configuration or the
// configuration itself will use HTTPS protocol, because the underlying // configuration itself will use HTTPS protocol, because the underlying
// functions potentially restart the HTTPS server. // functions potentially restart the HTTPS server.
err = startMods(r.Context(), web.baseLogger, web.tlsManager) err = startMods(ctx, web.baseLogger, web.tlsManager)
if err != nil { if err != nil {
globalContext.firstRun = true globalContext.firstRun = true
copyInstallSettings(config, curConfig) copyInstallSettings(config, curConfig)
@@ -488,11 +490,11 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
// and with its own context, because it waits until all requests are handled // and with its own context, because it waits until all requests are handled
// and will be blocked by it's own caller. // and will be blocked by it's own caller.
go func(timeout time.Duration) { go func(timeout time.Duration) {
ctx, cancel := context.WithTimeout(context.Background(), timeout) shutdownCtx, cancel := context.WithTimeout(context.Background(), timeout)
defer slogutil.RecoverAndLog(ctx, web.logger) defer slogutil.RecoverAndLog(shutdownCtx, web.logger)
defer cancel() defer cancel()
shutdownSrv(ctx, web.logger, web.httpServer) shutdownSrv(shutdownCtx, web.logger, web.httpServer)
}(shutdownTimeout) }(shutdownTimeout)
} }

View File

@@ -668,7 +668,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
GLMode = opts.glinetMode GLMode = opts.glinetMode
// Init auth module. // Init auth module.
globalContext.auth, err = initUsers() globalContext.auth, err = initUsers(ctx, slogLogger)
fatalOnError(err) fatalOnError(err)
web, err := initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, customURL) web, err := initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, customURL)
@@ -786,7 +786,8 @@ func checkPermissions(
} }
// initUsers initializes context auth module. Clears config users field. // initUsers initializes context auth module. Clears config users field.
func initUsers() (auth *Auth, err error) { // baseLogger must not be nil.
func initUsers(ctx context.Context, baseLogger *slog.Logger) (auth *Auth, err error) {
sessFilename := filepath.Join(globalContext.getDataDir(), "sessions.db") sessFilename := filepath.Join(globalContext.getDataDir(), "sessions.db")
var rateLimiter *authRateLimiter var rateLimiter *authRateLimiter
@@ -799,10 +800,17 @@ func initUsers() (auth *Auth, err error) {
trustedProxies := netutil.SliceSubnetSet(netutil.UnembedPrefixes(config.DNS.TrustedProxies)) trustedProxies := netutil.SliceSubnetSet(netutil.UnembedPrefixes(config.DNS.TrustedProxies))
sessionTTL := time.Duration(config.HTTPConfig.SessionTTL).Seconds() auth, err = InitAuth(
auth = InitAuth(sessFilename, config.Users, uint32(sessionTTL), rateLimiter, trustedProxies) ctx,
if auth == nil { baseLogger,
return nil, errors.Error("initializing auth module failed") sessFilename,
config.Users,
time.Duration(config.HTTPConfig.SessionTTL),
rateLimiter,
trustedProxies,
)
if err != nil {
return nil, fmt.Errorf("initializing auth module: %w", err)
} }
config.Users = nil config.Users = nil
@@ -916,7 +924,7 @@ func cleanup(ctx context.Context) {
globalContext.web = nil globalContext.web = nil
} }
if globalContext.auth != nil { if globalContext.auth != nil {
globalContext.auth.Close() globalContext.auth.Close(ctx)
globalContext.auth = nil globalContext.auth = nil
} }

View File

@@ -47,7 +47,11 @@ type profileJSON struct {
// handleGetProfile is the handler for GET /control/profile endpoint. // handleGetProfile is the handler for GET /control/profile endpoint.
func handleGetProfile(w http.ResponseWriter, r *http.Request) { func handleGetProfile(w http.ResponseWriter, r *http.Request) {
name := ""
u := globalContext.auth.getCurrentUser(r) u := globalContext.auth.getCurrentUser(r)
if u != nil {
name = string(u.Login)
}
var resp profileJSON var resp profileJSON
func() { func() {
@@ -55,7 +59,7 @@ func handleGetProfile(w http.ResponseWriter, r *http.Request) {
defer config.RUnlock() defer config.RUnlock()
resp = profileJSON{ resp = profileJSON{
Name: u.Name, Name: name,
Language: config.Language, Language: config.Language,
Theme: config.Theme, Theme: config.Theme,
} }