AdGuardHome/internal/home/auth.go

408 lines
8.2 KiB
Go
Raw Normal View History

package home
import (
Pull request: 2271 handle nolint Merge in DNS/adguard-home from 2271-handle-nolint to master Closes #2271. Squashed commit of the following: commit fde5c8795ac79e1f7d02ba8c8e369b5a724a000e Merge: fc2acd898 642dcd647 Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Nov 20 17:12:28 2020 +0300 Merge branch 'master' into 2271-handle-nolint commit fc2acd89871de08c39e80ace9e5bb8a7acb7afba Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 17 11:55:29 2020 +0300 dnsforward: fix test output strings commit c4ebae6ea9c293bad239519c44ca5a6c576bb921 Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 16 22:43:20 2020 +0300 dnsfilter: make package pass tests commit f2d98c6acabd8977f3b1b361987eaa31eb6eb9ad Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 16 20:05:00 2020 +0300 querylog: make decoding pass tests commit ab5850d24c50d53b8393f2de448cc340241351d7 Merge: 6ed2066bf 8a9c6e8a0 Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 16 19:48:31 2020 +0300 Merge branch 'master' into 2271-handle-nolint commit 6ed2066bf567e13dd14cfa16fc7b109b59fa39ef Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 16 18:13:45 2020 +0300 home: fix tests naming commit af691081fb02b7500a746b16492f01f7f9befe9a Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 16 12:15:49 2020 +0300 home: impove code quality commit 2914cd3cd23ef2a1964116baab9187d89b377f86 Author: Eugene Burkov <e.burkov@adguard.com> Date: Wed Nov 11 15:46:39 2020 +0300 * querylog: remove useless check commit 9996840650e784ccc76d1f29964560435ba27dc7 Author: Eugene Burkov <e.burkov@adguard.com> Date: Wed Nov 11 13:18:34 2020 +0300 * all: fix noticed defects commit 2b15293e59337f70302fbc0db81ebb26bee0bed2 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 20:15:53 2020 +0300 * stats: remove last nolint directive commit b2e1ddf7b58196a2fdbf879f084edb41ca1aa1eb Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 18:35:41 2020 +0300 * all: remove another nolint directive commit c6fc5cfcc9c95ab9e570a95ab41c3e5c0125e62e Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 18:11:28 2020 +0300 * querylog: remove nolint directive commit 226ddbf2c92f737f085b44a4ddf6daec7b602153 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 16:35:26 2020 +0300 * home: remove nolint directive commit 2ea3086ad41e9003282add7e996ae722d72d878b Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 16:13:57 2020 +0300 * home: reduce cyclomatic complexity of run function commit f479b480c48e0bb832ddef8f57586f56b8a55bab Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 15:35:46 2020 +0300 * home: use crypto/rand instead of math/rand commit a28d4a53e3b930136b036606fc7e78404f1d208b Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 14:11:07 2020 +0300 * dnsforward: remove gocyclo nolint directive commit 64a0a324cc2b20614ceec3ccc6505e960fe526e9 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 11:45:49 2020 +0300 all *: remove some nolint directives Updates #2271.
2020-11-20 17:32:41 +03:00
"crypto/rand"
"encoding/binary"
"encoding/hex"
"fmt"
"net/http"
"sync"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"go.etcd.io/bbolt"
"golang.org/x/crypto/bcrypt"
)
// sessionTokenSize is the length of session token in bytes.
const sessionTokenSize = 16
type session struct {
userName string
// expire is the expiration time, in seconds.
expire uint32
}
func (s *session) serialize() []byte {
const (
expireLen = 4
nameLen = 2
)
data := make([]byte, expireLen+nameLen+len(s.userName))
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 false
}
s.userName = string(data)
return true
}
// Auth - global object
type Auth struct {
db *bbolt.DB
rateLimiter *authRateLimiter
sessions map[string]*session
users []webUser
lock sync.Mutex
sessionTTL uint32
}
// webUser represents a user of the Web UI.
//
// TODO(s.chzhen): Improve naming.
type webUser struct {
Name string `yaml:"name"`
PasswordHash string `yaml:"password"`
}
// InitAuth - create a global object
func InitAuth(dbFilename string, users []webUser, sessionTTL uint32, rateLimiter *authRateLimiter) *Auth {
2020-04-15 15:17:57 +03:00
log.Info("Initializing auth module: %s", dbFilename)
a := &Auth{
sessionTTL: sessionTTL,
rateLimiter: rateLimiter,
sessions: make(map[string]*session),
users: users,
}
var err error
Pull request:* all: remove github.com/joomcode/errorx dependency Merge in DNS/adguard-home from 2240-removing-errorx-dependency to master Squashed commit of the following: commit 5bbe0567356f06e3b9ee5b3dc38d357b472cacb1 Merge: a6040850d 02d16a0b4 Author: Eugene Burkov <e.burkov@adguard.com> Date: Thu Nov 5 14:32:22 2020 +0300 Merge branch 'master' into 2240-removing-errorx-dependency commit a6040850da3cefb131208097477b0956e80063fb Author: Eugene Burkov <e.burkov@adguard.com> Date: Thu Nov 5 14:23:36 2020 +0300 * dhcpd: convert some abbreviations to lowercase. commit d05bd51b994906b0ff52c5a8e779bd1f512f4bb7 Author: Eugene Burkov <e.burkov@adguard.com> Date: Thu Nov 5 12:47:20 2020 +0300 * agherr: last final fixes commit 164bca55035ff44e50b0abb33e129a0d24ffe87c Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Tue Nov 3 19:11:10 2020 +0300 * all: final fixes again commit a0ac26f409c0b28a176cf2861d52c2f471b59484 Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Tue Nov 3 18:51:39 2020 +0300 * all: final fixes commit 6147b02d402b513323b07e85856b348884f3a088 Merge: 9fd3af1a3 62cc334f4 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 3 18:26:03 2020 +0300 Merge branch 'master' into 2240-removing-errorx-dependency commit 9fd3af1a39a3189b5c41315a8ad1568ae5cb4fc9 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 3 18:23:08 2020 +0300 * all: remove useless helper commit 7cd9aeae639762b28b25f354d69c5cf74f670211 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 3 17:19:26 2020 +0300 * agherr: improved code tidiness commit a74a49236e9aaace070646dac710de9201105262 Merge: dc9dedbf2 df34ee5c0 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 3 16:54:29 2020 +0300 Merge branch 'master' into 2240-removing-errorx-dependency commit dc9dedbf205756e3adaa3bc776d349bf3d8c69a5 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 3 16:40:08 2020 +0300 * agherr: improve and cover by tests commit fd6bfe9e282156cc60e006cb7cd46cce4d3a07a8 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 3 14:06:27 2020 +0300 * all: improve code quality commit ea00c2f8c5060e9611f9a80cfd0e4a039526d0c4 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 3 13:03:57 2020 +0300 * all: fix linter style warnings commit 8e75e1a681a7218c2b4c69adfa2b7e1e2966f9ac Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 3 12:29:26 2020 +0300 * all: remove github.com/joomcode/errorx dependency Closes #2240.
2020-11-05 15:20:57 +03:00
a.db, err = bbolt.Open(dbFilename, 0o644, 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
}
// Close - close module
func (a *Auth) Close() {
_ = a.db.Close()
}
func bucketName() []byte {
return []byte("sessions-2")
}
// load sessions from file, remove expired sessions
func (a *Auth) loadSessions() {
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 {
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 {
2019-09-18 13:17:35 +03:00
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)
}
// store session data in 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)
}
}
// store session data in file
func (a *Auth) storeSession(data []byte, s *session) bool {
tx, err := a.db.Begin(true)
if err != nil {
log.Error("auth: bbolt.Begin: %s", err)
return false
}
defer func() {
_ = tx.Rollback()
}()
bkt, err := tx.CreateBucketIfNotExists(bucketName())
if err != nil {
log.Error("auth: bbolt.CreateBucketIfNotExists: %s", err)
return false
}
err = bkt.Put(data, s.serialize())
if err != nil {
log.Error("auth: bbolt.Put: %s", err)
return false
}
err = tx.Commit()
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.
func (a *Auth) removeSessionFromFile(sess []byte) {
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")
}
2020-12-22 21:05:12 +03:00
// 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()
2020-12-22 21:05:12 +03:00
s, ok := a.sessions[sess]
if !ok {
2020-12-22 21:05:12 +03:00
return checkSessionNotFound
}
2020-12-22 21:05:12 +03:00
if s.expire <= now {
delete(a.sessions, sess)
key, _ := hex.DecodeString(sess)
a.removeSessionFromFile(key)
2020-12-22 21:05:12 +03:00
return checkSessionExpired
}
2019-11-12 14:23:00 +03:00
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)
}
}
2020-12-22 21:05:12 +03:00
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 {
return errors.Error("empty password")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("generating hash: %w", err)
}
u.PasswordHash = string(hash)
a.lock.Lock()
defer a.lock.Unlock()
a.users = append(a.users, *u)
log.Debug("auth: added user with login %q", u.Name)
return nil
}
// findUser returns a user if there is one.
func (a *Auth) findUser(login, password string) (u webUser, ok bool) {
a.lock.Lock()
defer a.lock.Unlock()
for _, u = range a.users {
if u.Name == login &&
bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil {
return u, true
}
}
return webUser{}, false
}
2020-12-22 21:09:53 +03:00
// getCurrentUser returns the current user. It returns an empty User if the
// user is not found.
func (a *Auth) getCurrentUser(r *http.Request) (u webUser) {
cookie, err := r.Cookie(sessionCookieName)
if err != nil {
2020-12-22 21:09:53 +03:00
// There's no Cookie, check Basic authentication.
user, pass, ok := r.BasicAuth()
if ok {
u, _ = Context.auth.findUser(user, pass)
return u
}
2020-12-22 21:09:53 +03:00
return webUser{}
}
a.lock.Lock()
defer a.lock.Unlock()
2020-12-22 21:09:53 +03:00
s, ok := a.sessions[cookie.Value]
if !ok {
return webUser{}
}
2020-12-22 21:09:53 +03:00
for _, u = range a.users {
if u.Name == s.userName {
return u
}
}
2020-12-22 21:09:53 +03:00
return webUser{}
}
// usersList returns a copy of a users list.
func (a *Auth) usersList() (users []webUser) {
a.lock.Lock()
defer a.lock.Unlock()
users = make([]webUser, len(a.users))
copy(users, a.users)
return users
}
// authRequired returns true if a authentication is required.
func (a *Auth) authRequired() bool {
if GLMode {
return true
}
a.lock.Lock()
defer a.lock.Unlock()
return len(a.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, err error) {
randData := make([]byte, sessionTokenSize)
_, err = rand.Read(randData)
if err != nil {
return nil, err
}
return randData, nil
}