2019-08-29 09:34:07 +00:00
package home
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"strings"
"sync"
"time"
"github.com/AdguardTeam/golibs/log"
2020-04-05 15:21:26 +00:00
"go.etcd.io/bbolt"
2019-08-29 09:34:07 +00:00
"golang.org/x/crypto/bcrypt"
)
const cookieTTL = 365 * 24 // in hours
2019-11-25 12:45:50 +00:00
const sessionCookieName = "agh_session"
2019-08-29 09:34:07 +00:00
2019-10-21 14:44:07 +00:00
type session struct {
userName string
expire uint32 // expiration time (in seconds)
}
/ *
expire byte [ 4 ]
name_len byte [ 2 ]
name byte [ ]
* /
func ( s * session ) serialize ( ) [ ] byte {
var data [ ] byte
data = make ( [ ] byte , 4 + 2 + 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
}
2019-08-29 09:34:07 +00:00
// Auth - global object
type Auth struct {
2019-11-12 11:23:00 +00:00
db * bbolt . DB
sessions map [ string ] * session // session name -> session data
lock sync . Mutex
users [ ] User
sessionTTL uint32 // in seconds
2019-08-29 09:34:07 +00:00
}
// User object
type User struct {
Name string ` yaml:"name" `
PasswordHash string ` yaml:"password" ` // bcrypt hash
}
// InitAuth - create a global object
2019-11-12 11:23:00 +00:00
func InitAuth ( dbFilename string , users [ ] User , sessionTTL uint32 ) * Auth {
2020-04-15 12:17:57 +00:00
log . Info ( "Initializing auth module: %s" , dbFilename )
2019-08-29 09:34:07 +00:00
a := Auth { }
2019-11-12 11:23:00 +00:00
a . sessionTTL = sessionTTL
2019-10-21 14:44:07 +00:00
a . sessions = make ( map [ string ] * session )
2019-08-29 09:34:07 +00:00
rand . Seed ( time . Now ( ) . UTC ( ) . Unix ( ) )
var err error
a . db , err = bbolt . Open ( dbFilename , 0644 , nil )
if err != nil {
2020-07-02 13:52:29 +00:00
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" )
}
2019-08-29 09:34:07 +00:00
return nil
}
a . loadSessions ( )
a . users = users
2020-04-15 12:17:57 +00:00
log . Info ( "Auth: initialized. users:%d sessions:%d" , len ( a . users ) , len ( a . sessions ) )
2019-08-29 09:34:07 +00:00
return & a
}
// Close - close module
func ( a * Auth ) Close ( ) {
_ = a . db . Close ( )
}
2019-10-21 14:44:07 +00:00
func bucketName ( ) [ ] byte {
return [ ] byte ( "sessions-2" )
}
2019-08-29 09:34:07 +00: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 14:44:07 +00:00
bkt := tx . Bucket ( bucketName ( ) )
2019-08-29 09:34:07 +00:00
if bkt == nil {
return
}
removed := 0
2019-10-21 14:44:07 +00:00
if tx . Bucket ( [ ] byte ( "sessions" ) ) != nil {
_ = tx . DeleteBucket ( [ ] byte ( "sessions" ) )
removed = 1
}
2019-08-29 09:34:07 +00:00
now := uint32 ( time . Now ( ) . UTC ( ) . Unix ( ) )
forEach := func ( k , v [ ] byte ) error {
2019-10-21 14:44:07 +00:00
s := session { }
if ! s . deserialize ( v ) || s . expire <= now {
2019-08-29 09:34:07 +00:00
err = bkt . Delete ( k )
if err != nil {
log . Error ( "Auth: bbolt.Delete: %s" , err )
} else {
removed ++
}
return nil
}
2019-10-21 14:44:07 +00:00
a . sessions [ hex . EncodeToString ( k ) ] = & s
2019-08-29 09:34:07 +00:00
return nil
}
_ = bkt . ForEach ( forEach )
if removed != 0 {
2019-09-18 10:17:35 +00:00
err = tx . Commit ( )
if err != nil {
log . Error ( "bolt.Commit(): %s" , err )
}
2019-08-29 09:34:07 +00:00
}
log . Debug ( "Auth: loaded %d sessions from DB (removed %d expired)" , len ( a . sessions ) , removed )
}
// store session data in file
2019-10-21 14:44:07 +00:00
func ( a * Auth ) addSession ( data [ ] byte , s * session ) {
2019-11-12 11:24:27 +00:00
name := hex . EncodeToString ( data )
2019-08-29 09:34:07 +00:00
a . lock . Lock ( )
2019-11-12 11:24:27 +00:00
a . sessions [ name ] = s
2019-08-29 09:34:07 +00:00
a . lock . Unlock ( )
2019-11-12 11:24:27 +00:00
if a . storeSession ( data , s ) {
2020-02-13 15:42:07 +00:00
log . Debug ( "Auth: created session %s: expire=%d" , name , s . expire )
2019-11-12 11:24:27 +00:00
}
2019-10-21 14:44:07 +00:00
}
2019-08-29 09:34:07 +00:00
2019-10-21 14:44:07 +00:00
// store session data in file
2019-11-12 11:24:27 +00:00
func ( a * Auth ) storeSession ( data [ ] byte , s * session ) bool {
2019-08-29 09:34:07 +00:00
tx , err := a . db . Begin ( true )
if err != nil {
log . Error ( "Auth: bbolt.Begin: %s" , err )
2019-11-12 11:24:27 +00:00
return false
2019-08-29 09:34:07 +00:00
}
defer func ( ) {
_ = tx . Rollback ( )
} ( )
2019-10-21 14:44:07 +00:00
bkt , err := tx . CreateBucketIfNotExists ( bucketName ( ) )
2019-08-29 09:34:07 +00:00
if err != nil {
log . Error ( "Auth: bbolt.CreateBucketIfNotExists: %s" , err )
2019-11-12 11:24:27 +00:00
return false
2019-08-29 09:34:07 +00:00
}
2019-10-21 14:44:07 +00:00
err = bkt . Put ( data , s . serialize ( ) )
2019-08-29 09:34:07 +00:00
if err != nil {
log . Error ( "Auth: bbolt.Put: %s" , err )
2019-11-12 11:24:27 +00:00
return false
2019-08-29 09:34:07 +00:00
}
err = tx . Commit ( )
if err != nil {
log . Error ( "Auth: bbolt.Commit: %s" , err )
2019-11-12 11:24:27 +00:00
return false
2019-08-29 09:34:07 +00:00
}
2019-11-12 11:24:27 +00:00
return true
2019-08-29 09:34:07 +00: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 14:44:07 +00:00
bkt := tx . Bucket ( bucketName ( ) )
2019-08-29 09:34:07 +00: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" )
}
// CheckSession - check if session is valid
// Return 0 if OK; -1 if session doesn't exist; 1 if session has expired
func ( a * Auth ) CheckSession ( sess string ) int {
now := uint32 ( time . Now ( ) . UTC ( ) . Unix ( ) )
update := false
a . lock . Lock ( )
2019-10-21 14:44:07 +00:00
s , ok := a . sessions [ sess ]
2019-08-29 09:34:07 +00:00
if ! ok {
a . lock . Unlock ( )
return - 1
}
2019-10-21 14:44:07 +00:00
if s . expire <= now {
2019-08-29 09:34:07 +00:00
delete ( a . sessions , sess )
key , _ := hex . DecodeString ( sess )
a . removeSession ( key )
a . lock . Unlock ( )
return 1
}
2019-11-12 11:23:00 +00:00
newExpire := now + a . sessionTTL
2019-10-21 14:44:07 +00:00
if s . expire / ( 24 * 60 * 60 ) != newExpire / ( 24 * 60 * 60 ) {
2019-08-29 09:34:07 +00:00
// update expiration time once a day
update = true
2019-10-21 14:44:07 +00:00
s . expire = newExpire
2019-08-29 09:34:07 +00:00
}
a . lock . Unlock ( )
if update {
key , _ := hex . DecodeString ( sess )
2019-11-12 11:24:27 +00:00
if a . storeSession ( key , s ) {
log . Debug ( "Auth: updated session %s: expire=%d" , sess , s . expire )
}
2019-08-29 09:34:07 +00:00
}
return 0
}
// 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" `
}
func getSession ( u * User ) [ ] byte {
d := [ ] byte ( fmt . Sprintf ( "%d%s%s" , rand . Uint32 ( ) , u . Name , u . PasswordHash ) )
hash := sha256 . Sum256 ( d )
return hash [ : ]
}
2019-11-12 11:23:00 +00:00
func ( a * Auth ) httpCookie ( req loginJSON ) string {
u := a . UserFind ( req . Name , req . Password )
2019-08-29 09:34:07 +00:00
if len ( u . Name ) == 0 {
2019-10-11 09:41:01 +00:00
return ""
2019-08-29 09:34:07 +00:00
}
sess := getSession ( & u )
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 14:44:07 +00:00
s := session { }
s . userName = u . Name
2019-11-12 11:23:00 +00:00
s . expire = uint32 ( now . Unix ( ) ) + a . sessionTTL
a . addSession ( sess , & s )
2019-08-29 09:34:07 +00:00
2019-11-25 12:45:50 +00:00
return fmt . Sprintf ( "%s=%s; Path=/; HttpOnly; Expires=%s" ,
sessionCookieName , hex . EncodeToString ( sess ) , expstr )
2019-10-11 09:41:01 +00: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-02-13 15:42:07 +00:00
cookie := Context . auth . httpCookie ( req )
2019-10-11 09:41:01 +00:00
if len ( cookie ) == 0 {
2019-11-12 11:24:27 +00:00
log . Info ( "Auth: invalid user name or password: name='%s'" , req . Name )
2019-10-11 09:41:01 +00:00
time . Sleep ( 1 * time . Second )
2019-11-12 11:24:27 +00:00
http . Error ( w , "invalid user name or password" , http . StatusBadRequest )
2019-10-11 09:41:01 +00:00
return
}
w . Header ( ) . Set ( "Set-Cookie" , cookie )
2019-08-29 09:34:07 +00: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 15:42:07 +00:00
Context . auth . RemoveSession ( sess )
2019-08-29 09:34:07 +00:00
w . Header ( ) . Set ( "Location" , "/login.html" )
2019-11-25 12:45:50 +00:00
s := fmt . Sprintf ( "%s=; Path=/; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT" ,
sessionCookieName )
2019-08-29 09:34:07 +00:00
w . Header ( ) . Set ( "Set-Cookie" , s )
w . WriteHeader ( http . StatusFound )
}
// RegisterAuthHandlers - register handlers
func RegisterAuthHandlers ( ) {
http . Handle ( "/control/login" , postInstallHandler ( ensureHandler ( "POST" , handleLogin ) ) )
httpRegister ( "GET" , "/control/logout" , handleLogout )
}
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 12:45:50 +00:00
if kv [ 0 ] == sessionCookieName {
2019-08-29 09:34:07 +00:00
return kv [ 1 ]
}
}
return ""
}
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 15:42:07 +00:00
authRequired := Context . auth != nil && Context . auth . AuthRequired ( )
2019-11-25 12:45:50 +00:00
cookie , err := r . Cookie ( sessionCookieName )
2019-08-29 09:34:07 +00:00
if authRequired && err == nil {
2020-02-13 15:42:07 +00:00
r := Context . auth . CheckSession ( cookie . Value )
2019-08-29 09:34:07 +00:00
if r == 0 {
w . Header ( ) . Set ( "Location" , "/" )
w . WriteHeader ( http . StatusFound )
return
} else if r < 0 {
2020-04-05 15:21:26 +00:00
log . Debug ( "Auth: invalid cookie value: %s" , cookie )
2019-08-29 09:34:07 +00:00
}
}
2020-05-14 12:03:00 +00:00
} else if strings . HasPrefix ( r . URL . Path , "/assets/" ) ||
2020-05-14 15:37:25 +00:00
strings . HasPrefix ( r . URL . Path , "/login." ) {
2019-08-29 09:34:07 +00:00
// process as usual
2020-05-14 15:37:25 +00:00
// no additional auth requirements
2020-02-13 15:42:07 +00:00
} else if Context . auth != nil && Context . auth . AuthRequired ( ) {
2019-08-29 09:34:07 +00:00
// redirect to login page if not authenticated
ok := false
2019-11-25 12:45:50 +00:00
cookie , err := r . Cookie ( sessionCookieName )
2019-08-29 09:34:07 +00:00
if err == nil {
2020-02-13 15:42:07 +00:00
r := Context . auth . CheckSession ( cookie . Value )
2019-08-29 09:34:07 +00:00
if r == 0 {
ok = true
} else if r < 0 {
2020-04-05 15:21:26 +00:00
log . Debug ( "Auth: invalid cookie value: %s" , cookie )
2019-08-29 09:34:07 +00:00
}
} else {
// there's no Cookie, check Basic authentication
user , pass , ok2 := r . BasicAuth ( )
if ok2 {
2020-02-13 15:42:07 +00:00
u := Context . auth . UserFind ( user , pass )
2019-08-29 09:34:07 +00:00
if len ( u . Name ) != 0 {
ok = true
2019-11-12 11:24:27 +00:00
} else {
log . Info ( "Auth: invalid Basic Authorization value" )
2019-08-29 09:34:07 +00:00
}
}
}
if ! ok {
2020-01-21 09:58:55 +00:00
if r . URL . Path == "/" || r . URL . Path == "/index.html" {
w . Header ( ) . Set ( "Location" , "/login.html" )
w . WriteHeader ( http . StatusFound )
} else {
w . WriteHeader ( http . StatusForbidden )
_ , _ = w . Write ( [ ] byte ( "Forbidden" ) )
}
2019-08-29 09:34:07 +00: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
func ( a * Auth ) UserFind ( login string , password string ) User {
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 { }
}
2019-10-21 14:44:07 +00:00
// GetCurrentUser - get the current user
func ( a * Auth ) GetCurrentUser ( r * http . Request ) User {
2019-11-25 12:45:50 +00:00
cookie , err := r . Cookie ( sessionCookieName )
2019-10-21 14:44:07 +00:00
if err != nil {
// there's no Cookie, check Basic authentication
user , pass , ok := r . BasicAuth ( )
if ok {
2020-02-13 15:42:07 +00:00
u := Context . auth . UserFind ( user , pass )
2019-10-21 14:44:07 +00:00
return u
}
2019-10-25 08:01:29 +00:00
return User { }
2019-10-21 14:44:07 +00:00
}
a . lock . Lock ( )
s , ok := a . sessions [ cookie . Value ]
if ! ok {
a . lock . Unlock ( )
return User { }
}
for _ , u := range a . users {
if u . Name == s . userName {
a . lock . Unlock ( )
return u
}
}
a . lock . Unlock ( )
return User { }
}
2019-08-29 09:34:07 +00: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 {
a . lock . Lock ( )
r := ( len ( a . users ) != 0 )
a . lock . Unlock ( )
return r
}