2019-08-29 02:34:07 -07:00
package home
import (
2020-11-20 07:32:41 -07:00
"crypto/rand"
2019-08-29 02:34:07 -07:00
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
2020-11-20 07:32:41 -07:00
"math"
"math/big"
2019-08-29 02:34:07 -07:00
"net/http"
"strings"
"sync"
"time"
"github.com/AdguardTeam/golibs/log"
2020-04-05 08:21:26 -07:00
"go.etcd.io/bbolt"
2019-08-29 02:34:07 -07:00
"golang.org/x/crypto/bcrypt"
)
2020-11-05 05:20:57 -07:00
const (
cookieTTL = 365 * 24 // in hours
sessionCookieName = "agh_session"
)
2019-08-29 02:34:07 -07:00
2019-10-21 07:44:07 -07:00
type session struct {
userName string
expire uint32 // expiration time (in seconds)
}
func ( s * session ) serialize ( ) [ ] byte {
2020-11-06 02:15:08 -07:00
const (
expireLen = 4
nameLen = 2
)
data := make ( [ ] byte , expireLen + nameLen + len ( s . userName ) )
2019-10-21 07:44:07 -07:00
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
}
2019-08-29 02:34:07 -07:00
// Auth - global object
type Auth struct {
2019-11-12 04:23:00 -07:00
db * bbolt . DB
2020-12-22 11:05:12 -07:00
sessions map [ string ] * session
2019-11-12 04:23:00 -07:00
users [ ] User
2020-12-22 11:05:12 -07:00
lock sync . Mutex
sessionTTL uint32
2019-08-29 02:34:07 -07:00
}
// User object
type User struct {
Name string ` yaml:"name" `
PasswordHash string ` yaml:"password" ` // bcrypt hash
}
// InitAuth - create a global object
2019-11-12 04:23:00 -07:00
func InitAuth ( dbFilename string , users [ ] User , sessionTTL uint32 ) * Auth {
2020-04-15 05:17:57 -07:00
log . Info ( "Initializing auth module: %s" , dbFilename )
2019-08-29 02:34:07 -07:00
a := Auth { }
2019-11-12 04:23:00 -07:00
a . sessionTTL = sessionTTL
2019-10-21 07:44:07 -07:00
a . sessions = make ( map [ string ] * session )
2019-08-29 02:34:07 -07:00
var err error
2020-11-05 05:20:57 -07:00
a . db , err = bbolt . Open ( dbFilename , 0 o644 , nil )
2019-08-29 02:34:07 -07:00
if err != nil {
2020-07-02 06:52:29 -07:00
log . Error ( "Auth: open DB: %s: %s" , dbFilename , err )
if err . Error ( ) == "invalid argument" {
2020-10-30 03:32:02 -07:00
log . Error ( "AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/internal/wiki/Getting-Started#limitations" )
2020-07-02 06:52:29 -07:00
}
2019-08-29 02:34:07 -07:00
return nil
}
a . loadSessions ( )
a . users = users
2020-04-15 05:17:57 -07:00
log . Info ( "Auth: initialized. users:%d sessions:%d" , len ( a . users ) , len ( a . sessions ) )
2019-08-29 02:34:07 -07:00
return & a
}
// Close - close module
func ( a * Auth ) Close ( ) {
_ = a . db . Close ( )
}
2019-10-21 07:44:07 -07:00
func bucketName ( ) [ ] byte {
return [ ] byte ( "sessions-2" )
}
2019-08-29 02:34:07 -07:00
// 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 ( )
} ( )
2019-10-21 07:44:07 -07:00
bkt := tx . Bucket ( bucketName ( ) )
2019-08-29 02:34:07 -07:00
if bkt == nil {
return
}
removed := 0
2019-10-21 07:44:07 -07:00
if tx . Bucket ( [ ] byte ( "sessions" ) ) != nil {
_ = tx . DeleteBucket ( [ ] byte ( "sessions" ) )
removed = 1
}
2019-08-29 02:34:07 -07:00
now := uint32 ( time . Now ( ) . UTC ( ) . Unix ( ) )
forEach := func ( k , v [ ] byte ) error {
2019-10-21 07:44:07 -07:00
s := session { }
if ! s . deserialize ( v ) || s . expire <= now {
2019-08-29 02:34:07 -07:00
err = bkt . Delete ( k )
if err != nil {
log . Error ( "Auth: bbolt.Delete: %s" , err )
} else {
removed ++
}
return nil
}
2019-10-21 07:44:07 -07:00
a . sessions [ hex . EncodeToString ( k ) ] = & s
2019-08-29 02:34:07 -07:00
return nil
}
_ = bkt . ForEach ( forEach )
if removed != 0 {
2019-09-18 03:17:35 -07:00
err = tx . Commit ( )
if err != nil {
log . Error ( "bolt.Commit(): %s" , err )
}
2019-08-29 02:34:07 -07:00
}
log . Debug ( "Auth: loaded %d sessions from DB (removed %d expired)" , len ( a . sessions ) , removed )
}
// store session data in file
2019-10-21 07:44:07 -07:00
func ( a * Auth ) addSession ( data [ ] byte , s * session ) {
2019-11-12 04:24:27 -07:00
name := hex . EncodeToString ( data )
2019-08-29 02:34:07 -07:00
a . lock . Lock ( )
2019-11-12 04:24:27 -07:00
a . sessions [ name ] = s
2019-08-29 02:34:07 -07:00
a . lock . Unlock ( )
2019-11-12 04:24:27 -07:00
if a . storeSession ( data , s ) {
2020-02-13 08:42:07 -07:00
log . Debug ( "Auth: created session %s: expire=%d" , name , s . expire )
2019-11-12 04:24:27 -07:00
}
2019-10-21 07:44:07 -07:00
}
2019-08-29 02:34:07 -07:00
2019-10-21 07:44:07 -07:00
// store session data in file
2019-11-12 04:24:27 -07:00
func ( a * Auth ) storeSession ( data [ ] byte , s * session ) bool {
2019-08-29 02:34:07 -07:00
tx , err := a . db . Begin ( true )
if err != nil {
log . Error ( "Auth: bbolt.Begin: %s" , err )
2019-11-12 04:24:27 -07:00
return false
2019-08-29 02:34:07 -07:00
}
defer func ( ) {
_ = tx . Rollback ( )
} ( )
2019-10-21 07:44:07 -07:00
bkt , err := tx . CreateBucketIfNotExists ( bucketName ( ) )
2019-08-29 02:34:07 -07:00
if err != nil {
log . Error ( "Auth: bbolt.CreateBucketIfNotExists: %s" , err )
2019-11-12 04:24:27 -07:00
return false
2019-08-29 02:34:07 -07:00
}
2019-10-21 07:44:07 -07:00
err = bkt . Put ( data , s . serialize ( ) )
2019-08-29 02:34:07 -07:00
if err != nil {
log . Error ( "Auth: bbolt.Put: %s" , err )
2019-11-12 04:24:27 -07:00
return false
2019-08-29 02:34:07 -07:00
}
err = tx . Commit ( )
if err != nil {
log . Error ( "Auth: bbolt.Commit: %s" , err )
2019-11-12 04:24:27 -07:00
return false
2019-08-29 02:34:07 -07:00
}
2019-11-12 04:24:27 -07:00
return true
2019-08-29 02:34:07 -07:00
}
// remove session from file
func ( a * Auth ) removeSession ( sess [ ] byte ) {
tx , err := a . db . Begin ( true )
if err != nil {
log . Error ( "Auth: bbolt.Begin: %s" , err )
return
}
defer func ( ) {
_ = tx . Rollback ( )
} ( )
2019-10-21 07:44:07 -07:00
bkt := tx . Bucket ( bucketName ( ) )
2019-08-29 02:34:07 -07:00
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 11:05:12 -07: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 ) {
2019-08-29 02:34:07 -07:00
now := uint32 ( time . Now ( ) . UTC ( ) . Unix ( ) )
update := false
a . lock . Lock ( )
2020-12-21 11:39:39 -07:00
defer a . lock . Unlock ( )
2020-12-22 11:05:12 -07:00
2019-10-21 07:44:07 -07:00
s , ok := a . sessions [ sess ]
2019-08-29 02:34:07 -07:00
if ! ok {
2020-12-22 11:05:12 -07:00
return checkSessionNotFound
2019-08-29 02:34:07 -07:00
}
2020-12-22 11:05:12 -07:00
2019-10-21 07:44:07 -07:00
if s . expire <= now {
2019-08-29 02:34:07 -07:00
delete ( a . sessions , sess )
key , _ := hex . DecodeString ( sess )
a . removeSession ( key )
2020-12-22 11:05:12 -07:00
return checkSessionExpired
2019-08-29 02:34:07 -07:00
}
2019-11-12 04:23:00 -07:00
newExpire := now + a . sessionTTL
2019-10-21 07:44:07 -07:00
if s . expire / ( 24 * 60 * 60 ) != newExpire / ( 24 * 60 * 60 ) {
2019-08-29 02:34:07 -07:00
// update expiration time once a day
update = true
2019-10-21 07:44:07 -07:00
s . expire = newExpire
2019-08-29 02:34:07 -07:00
}
if update {
key , _ := hex . DecodeString ( sess )
2019-11-12 04:24:27 -07:00
if a . storeSession ( key , s ) {
log . Debug ( "Auth: updated session %s: expire=%d" , sess , s . expire )
}
2019-08-29 02:34:07 -07:00
}
2020-12-22 11:05:12 -07:00
return checkSessionOK
2019-08-29 02:34:07 -07:00
}
// RemoveSession - remove session
func ( a * Auth ) RemoveSession ( sess string ) {
key , _ := hex . DecodeString ( sess )
a . lock . Lock ( )
delete ( a . sessions , sess )
a . lock . Unlock ( )
a . removeSession ( key )
}
type loginJSON struct {
Name string ` json:"name" `
Password string ` json:"password" `
}
2020-11-20 07:32:41 -07:00
func getSession ( u * User ) ( [ ] byte , error ) {
maxSalt := big . NewInt ( math . MaxUint32 )
salt , err := rand . Int ( rand . Reader , maxSalt )
if err != nil {
return nil , err
}
d := [ ] byte ( fmt . Sprintf ( "%s%s%s" , salt , u . Name , u . PasswordHash ) )
2019-08-29 02:34:07 -07:00
hash := sha256 . Sum256 ( d )
2020-11-20 07:32:41 -07:00
return hash [ : ] , nil
2019-08-29 02:34:07 -07:00
}
2020-11-20 07:32:41 -07:00
func ( a * Auth ) httpCookie ( req loginJSON ) ( string , error ) {
2019-11-12 04:23:00 -07:00
u := a . UserFind ( req . Name , req . Password )
2019-08-29 02:34:07 -07:00
if len ( u . Name ) == 0 {
2020-11-20 07:32:41 -07:00
return "" , nil
2019-08-29 02:34:07 -07:00
}
2020-11-20 07:32:41 -07:00
sess , err := getSession ( & u )
if err != nil {
return "" , err
}
2019-08-29 02:34:07 -07:00
now := time . Now ( ) . UTC ( )
expire := now . Add ( cookieTTL * time . Hour )
expstr := expire . Format ( time . RFC1123 )
expstr = expstr [ : len ( expstr ) - len ( "UTC" ) ] // "UTC" -> "GMT"
expstr += "GMT"
2019-10-21 07:44:07 -07:00
s := session { }
s . userName = u . Name
2019-11-12 04:23:00 -07:00
s . expire = uint32 ( now . Unix ( ) ) + a . sessionTTL
a . addSession ( sess , & s )
2019-08-29 02:34:07 -07:00
2019-11-25 05:45:50 -07:00
return fmt . Sprintf ( "%s=%s; Path=/; HttpOnly; Expires=%s" ,
2020-11-20 07:32:41 -07:00
sessionCookieName , hex . EncodeToString ( sess ) , expstr ) , nil
2019-10-11 02:41:01 -07:00
}
func handleLogin ( w http . ResponseWriter , r * http . Request ) {
req := loginJSON { }
err := json . NewDecoder ( r . Body ) . Decode ( & req )
if err != nil {
httpError ( w , http . StatusBadRequest , "json decode: %s" , err )
return
}
2020-11-20 07:32:41 -07:00
cookie , err := Context . auth . httpCookie ( req )
if err != nil {
httpError ( w , http . StatusBadRequest , "crypto rand reader: %s" , err )
return
}
2019-10-11 02:41:01 -07:00
if len ( cookie ) == 0 {
2020-11-05 05:20:57 -07:00
log . Info ( "Auth: invalid user name or password: name=%q" , req . Name )
2019-10-11 02:41:01 -07:00
time . Sleep ( 1 * time . Second )
2019-11-12 04:24:27 -07:00
http . Error ( w , "invalid user name or password" , http . StatusBadRequest )
2019-10-11 02:41:01 -07:00
return
}
w . Header ( ) . Set ( "Set-Cookie" , cookie )
2019-08-29 02:34:07 -07:00
w . Header ( ) . Set ( "Cache-Control" , "no-store, no-cache, must-revalidate, proxy-revalidate" )
w . Header ( ) . Set ( "Pragma" , "no-cache" )
w . Header ( ) . Set ( "Expires" , "0" )
returnOK ( w )
}
func handleLogout ( w http . ResponseWriter , r * http . Request ) {
cookie := r . Header . Get ( "Cookie" )
sess := parseCookie ( cookie )
2020-02-13 08:42:07 -07:00
Context . auth . RemoveSession ( sess )
2019-08-29 02:34:07 -07:00
w . Header ( ) . Set ( "Location" , "/login.html" )
2019-11-25 05:45:50 -07:00
s := fmt . Sprintf ( "%s=; Path=/; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT" ,
sessionCookieName )
2019-08-29 02:34:07 -07:00
w . Header ( ) . Set ( "Set-Cookie" , s )
w . WriteHeader ( http . StatusFound )
}
// RegisterAuthHandlers - register handlers
func RegisterAuthHandlers ( ) {
2021-01-13 07:26:57 -07:00
Context . mux . Handle ( "/control/login" , postInstallHandler ( ensureHandler ( http . MethodPost , handleLogin ) ) )
httpRegister ( http . MethodGet , "/control/logout" , handleLogout )
2019-08-29 02:34:07 -07:00
}
func parseCookie ( cookie string ) string {
pairs := strings . Split ( cookie , ";" )
for _ , pair := range pairs {
pair = strings . TrimSpace ( pair )
kv := strings . SplitN ( pair , "=" , 2 )
if len ( kv ) != 2 {
continue
}
2019-11-25 05:45:50 -07:00
if kv [ 0 ] == sessionCookieName {
2019-08-29 02:34:07 -07:00
return kv [ 1 ]
}
}
return ""
}
2020-11-20 07:32:41 -07:00
// optionalAuthThird return true if user should authenticate first.
func optionalAuthThird ( w http . ResponseWriter , r * http . Request ) ( authFirst bool ) {
authFirst = false
// redirect to login page if not authenticated
ok := false
cookie , err := r . Cookie ( sessionCookieName )
if glProcessCookie ( r ) {
log . Debug ( "Auth: authentification was handled by GL-Inet submodule" )
ok = true
} else if err == nil {
2020-12-22 11:05:12 -07:00
r := Context . auth . checkSession ( cookie . Value )
if r == checkSessionOK {
2020-11-20 07:32:41 -07:00
ok = true
} else if r < 0 {
log . Debug ( "Auth: invalid cookie value: %s" , cookie )
}
} else {
// there's no Cookie, check Basic authentication
user , pass , ok2 := r . BasicAuth ( )
if ok2 {
u := Context . auth . UserFind ( user , pass )
if len ( u . Name ) != 0 {
ok = true
} else {
log . Info ( "Auth: invalid Basic Authorization value" )
}
}
}
if ! ok {
if r . URL . Path == "/" || r . URL . Path == "/index.html" {
if glProcessRedirect ( w , r ) {
log . Debug ( "Auth: redirected to login page by GL-Inet submodule" )
} else {
w . Header ( ) . Set ( "Location" , "/login.html" )
w . WriteHeader ( http . StatusFound )
}
} else {
w . WriteHeader ( http . StatusForbidden )
_ , _ = w . Write ( [ ] byte ( "Forbidden" ) )
}
authFirst = true
}
return authFirst
}
2019-08-29 02:34:07 -07:00
func optionalAuth ( handler func ( http . ResponseWriter , * http . Request ) ) func ( http . ResponseWriter , * http . Request ) {
return func ( w http . ResponseWriter , r * http . Request ) {
if r . URL . Path == "/login.html" {
// redirect to dashboard if already authenticated
2020-02-13 08:42:07 -07:00
authRequired := Context . auth != nil && Context . auth . AuthRequired ( )
2019-11-25 05:45:50 -07:00
cookie , err := r . Cookie ( sessionCookieName )
2019-08-29 02:34:07 -07:00
if authRequired && err == nil {
2020-12-22 11:05:12 -07:00
r := Context . auth . checkSession ( cookie . Value )
if r == checkSessionOK {
2019-08-29 02:34:07 -07:00
w . Header ( ) . Set ( "Location" , "/" )
w . WriteHeader ( http . StatusFound )
2020-12-22 11:05:12 -07:00
2019-08-29 02:34:07 -07:00
return
2020-12-22 11:05:12 -07:00
} else if r == checkSessionNotFound {
2020-04-05 08:21:26 -07:00
log . Debug ( "Auth: invalid cookie value: %s" , cookie )
2019-08-29 02:34:07 -07:00
}
}
2020-05-14 05:03:00 -07:00
} else if strings . HasPrefix ( r . URL . Path , "/assets/" ) ||
2020-05-14 08:37:25 -07:00
strings . HasPrefix ( r . URL . Path , "/login." ) {
2019-08-29 02:34:07 -07:00
// process as usual
2020-05-14 08:37:25 -07:00
// no additional auth requirements
2020-02-13 08:42:07 -07:00
} else if Context . auth != nil && Context . auth . AuthRequired ( ) {
2020-11-20 07:32:41 -07:00
if optionalAuthThird ( w , r ) {
2019-08-29 02:34:07 -07:00
return
}
}
handler ( w , r )
}
}
type authHandler struct {
handler http . Handler
}
func ( a * authHandler ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
optionalAuth ( a . handler . ServeHTTP ) ( w , r )
}
func optionalAuthHandler ( handler http . Handler ) http . Handler {
return & authHandler { handler }
}
// UserAdd - add new user
func ( a * Auth ) UserAdd ( u * User , password string ) {
if len ( password ) == 0 {
return
}
hash , err := bcrypt . GenerateFromPassword ( [ ] byte ( password ) , bcrypt . DefaultCost )
if err != nil {
log . Error ( "bcrypt.GenerateFromPassword: %s" , err )
return
}
u . PasswordHash = string ( hash )
a . lock . Lock ( )
a . users = append ( a . users , * u )
a . lock . Unlock ( )
log . Debug ( "Auth: added user: %s" , u . Name )
}
// UserFind - find a user
2020-11-06 02:15:08 -07:00
func ( a * Auth ) UserFind ( login , password string ) User {
2019-08-29 02:34:07 -07:00
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
}
}
return User { }
}
2020-12-22 11:09:53 -07:00
// getCurrentUser returns the current user. It returns an empty User if the
// user is not found.
func ( a * Auth ) getCurrentUser ( r * http . Request ) User {
2019-11-25 05:45:50 -07:00
cookie , err := r . Cookie ( sessionCookieName )
2019-10-21 07:44:07 -07:00
if err != nil {
2020-12-22 11:09:53 -07:00
// There's no Cookie, check Basic authentication.
2019-10-21 07:44:07 -07:00
user , pass , ok := r . BasicAuth ( )
if ok {
2020-12-22 11:09:53 -07:00
return Context . auth . UserFind ( user , pass )
2019-10-21 07:44:07 -07:00
}
2020-12-22 11:09:53 -07:00
2019-10-25 01:01:29 -07:00
return User { }
2019-10-21 07:44:07 -07:00
}
a . lock . Lock ( )
2020-12-21 11:39:39 -07:00
defer a . lock . Unlock ( )
2020-12-22 11:09:53 -07:00
2019-10-21 07:44:07 -07:00
s , ok := a . sessions [ cookie . Value ]
if ! ok {
return User { }
}
2020-12-22 11:09:53 -07:00
2019-10-21 07:44:07 -07:00
for _ , u := range a . users {
if u . Name == s . userName {
return u
}
}
2020-12-22 11:09:53 -07:00
2019-10-21 07:44:07 -07:00
return User { }
}
2019-08-29 02:34:07 -07:00
// GetUsers - get users
func ( a * Auth ) GetUsers ( ) [ ] User {
a . lock . Lock ( )
users := a . users
a . lock . Unlock ( )
return users
}
// AuthRequired - if authentication is required
func ( a * Auth ) AuthRequired ( ) bool {
2020-07-03 10:34:08 -07:00
if GLMode {
return true
}
2019-08-29 02:34:07 -07:00
a . lock . Lock ( )
r := ( len ( a . users ) != 0 )
a . lock . Unlock ( )
return r
}