2019-08-29 09:34:07 +00:00
package home
import (
2020-11-20 14:32:41 +00:00
"crypto/rand"
2019-08-29 09:34:07 +00:00
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
2021-04-06 11:31:20 +00:00
"net"
2019-08-29 09:34:07 +00:00
"net/http"
2021-04-27 15:56:32 +00:00
"strconv"
2019-08-29 09:34:07 +00:00
"strings"
"sync"
"time"
2021-12-16 17:54:59 +00:00
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
2019-08-29 09:34:07 +00:00
"github.com/AdguardTeam/golibs/log"
2021-08-09 13:03:37 +00:00
"github.com/AdguardTeam/golibs/netutil"
2021-09-30 18:17:54 +00:00
"github.com/AdguardTeam/golibs/timeutil"
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"
)
2021-04-06 11:31:20 +00:00
// cookieTTL is the time-to-live of the session cookie.
2021-09-30 18:17:54 +00:00
const cookieTTL = 365 * timeutil . Day
2021-03-01 17:37:28 +00:00
2021-04-06 11:31:20 +00:00
// sessionCookieName is the name of the session cookie.
const sessionCookieName = "agh_session"
// sessionTokenSize is the length of session token in bytes.
const sessionTokenSize = 16
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)
}
func ( s * session ) serialize ( ) [ ] byte {
2020-11-06 09:15:08 +00:00
const (
expireLen = 4
nameLen = 2
)
data := make ( [ ] byte , expireLen + nameLen + len ( s . userName ) )
2019-10-21 14:44:07 +00: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 09:34:07 +00:00
// Auth - global object
type Auth struct {
2019-11-12 11:23:00 +00:00
db * bbolt . DB
2021-04-27 15:56:32 +00:00
blocker * authRateLimiter
2020-12-22 18:05:12 +00:00
sessions map [ string ] * session
2019-11-12 11:23:00 +00:00
users [ ] User
2020-12-22 18:05:12 +00:00
lock sync . Mutex
sessionTTL uint32
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
2021-04-27 15:56:32 +00:00
func InitAuth ( dbFilename string , users [ ] User , sessionTTL uint32 , blocker * authRateLimiter ) * Auth {
2020-04-15 12:17:57 +00:00
log . Info ( "Initializing auth module: %s" , dbFilename )
2021-04-27 15:56:32 +00:00
a := & Auth {
sessionTTL : sessionTTL ,
blocker : blocker ,
sessions : make ( map [ string ] * session ) ,
users : users ,
}
2019-08-29 09:34:07 +00:00
var err error
2020-11-05 12:20:57 +00:00
a . db , err = bbolt . Open ( dbFilename , 0 o644 , nil )
2019-08-29 09:34:07 +00:00
if err != nil {
2021-04-06 11:31:20 +00:00
log . Error ( "auth: open DB: %s: %s" , dbFilename , err )
2020-07-02 13:52:29 +00:00
if err . Error ( ) == "invalid argument" {
2021-04-08 13:44:01 +00:00
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" )
2020-07-02 13:52:29 +00:00
}
2021-04-06 11:31:20 +00:00
2019-08-29 09:34:07 +00:00
return nil
}
a . loadSessions ( )
2021-04-06 11:31:20 +00:00
log . Info ( "auth: initialized. users:%d sessions:%d" , len ( a . users ) , len ( a . sessions ) )
2021-04-27 15:56:32 +00:00
return a
2019-08-29 09:34:07 +00:00
}
// 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 {
2021-04-06 11:31:20 +00:00
log . Error ( "auth: bbolt.Begin: %s" , err )
2019-08-29 09:34:07 +00:00
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 {
2021-04-06 11:31:20 +00:00
log . Error ( "auth: bbolt.Delete: %s" , err )
2019-08-29 09:34:07 +00:00
} else {
removed ++
}
2021-04-06 11:31:20 +00:00
2019-08-29 09:34:07 +00:00
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
}
2021-04-06 11:31:20 +00:00
log . Debug ( "auth: loaded %d sessions from DB (removed %d expired)" , len ( a . sessions ) , removed )
2019-08-29 09:34:07 +00:00
}
// 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 ) {
2021-04-06 11:31:20 +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 {
2021-04-06 11:31:20 +00:00
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 {
2021-04-06 11:31:20 +00:00
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
}
2021-04-06 11:31:20 +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 {
2021-04-06 11:31:20 +00:00
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 {
2021-04-06 11:31:20 +00:00
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
}
2021-04-06 11:31:20 +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 {
2021-04-06 11:31:20 +00:00
log . Error ( "auth: bbolt.Begin: %s" , err )
2019-08-29 09:34:07 +00:00
return
}
2021-04-06 11:31:20 +00:00
2019-08-29 09:34:07 +00:00
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 {
2021-04-06 11:31:20 +00:00
log . Error ( "auth: bbolt.Bucket" )
2019-08-29 09:34:07 +00:00
return
}
2021-04-06 11:31:20 +00:00
2019-08-29 09:34:07 +00:00
err = bkt . Delete ( sess )
if err != nil {
2021-04-06 11:31:20 +00:00
log . Error ( "auth: bbolt.Put: %s" , err )
2019-08-29 09:34:07 +00:00
return
}
err = tx . Commit ( )
if err != nil {
2021-04-06 11:31:20 +00:00
log . Error ( "auth: bbolt.Commit: %s" , err )
2019-08-29 09:34:07 +00:00
return
}
2021-04-06 11:31:20 +00:00
log . Debug ( "auth: removed session from DB" )
2019-08-29 09:34:07 +00:00
}
2020-12-22 18:05:12 +00: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 09:34:07 +00:00
now := uint32 ( time . Now ( ) . UTC ( ) . Unix ( ) )
update := false
a . lock . Lock ( )
2020-12-21 18:39:39 +00:00
defer a . lock . Unlock ( )
2020-12-22 18:05:12 +00:00
2019-10-21 14:44:07 +00:00
s , ok := a . sessions [ sess ]
2019-08-29 09:34:07 +00:00
if ! ok {
2020-12-22 18:05:12 +00:00
return checkSessionNotFound
2019-08-29 09:34:07 +00:00
}
2020-12-22 18:05:12 +00:00
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 )
2020-12-22 18:05:12 +00:00
return checkSessionExpired
2019-08-29 09:34:07 +00:00
}
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
}
if update {
key , _ := hex . DecodeString ( sess )
2019-11-12 11:24:27 +00:00
if a . storeSession ( key , s ) {
2021-04-06 11:31:20 +00:00
log . Debug ( "auth: updated session %s: expire=%d" , sess , s . expire )
2019-11-12 11:24:27 +00:00
}
2019-08-29 09:34:07 +00:00
}
2020-12-22 18:05:12 +00:00
return checkSessionOK
2019-08-29 09:34:07 +00: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" `
}
2021-03-01 17:37:28 +00:00
// 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 )
2020-11-20 14:32:41 +00:00
if err != nil {
return nil , err
}
2021-03-01 17:37:28 +00:00
return randData , nil
}
// cookieTimeFormat is the format to be used in (time.Time).Format for cookie's
// expiry field.
const cookieTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
// cookieExpiryFormat returns the formatted exp to be used in cookie string.
// It's quite simple for now, but probably will be expanded in the future.
func cookieExpiryFormat ( exp time . Time ) ( formatted string ) {
return exp . Format ( cookieTimeFormat )
2019-08-29 09:34:07 +00:00
}
2021-04-27 15:56:32 +00:00
func ( a * Auth ) httpCookie ( req loginJSON , addr string ) ( cookie string , err error ) {
blocker := a . blocker
2019-11-12 11:23:00 +00:00
u := a . UserFind ( req . Name , req . Password )
2019-08-29 09:34:07 +00:00
if len ( u . Name ) == 0 {
2021-04-27 15:56:32 +00:00
if blocker != nil {
blocker . inc ( addr )
}
return "" , err
2019-08-29 09:34:07 +00:00
}
2021-04-27 15:56:32 +00:00
if blocker != nil {
blocker . remove ( addr )
}
var sess [ ] byte
sess , err = newSessionToken ( )
2020-11-20 14:32:41 +00:00
if err != nil {
return "" , err
}
2019-08-29 09:34:07 +00:00
now := time . Now ( ) . UTC ( )
2021-03-01 17:37:28 +00:00
a . addSession ( sess , & session {
userName : u . Name ,
expire : uint32 ( now . Unix ( ) ) + a . sessionTTL ,
} )
return fmt . Sprintf (
"%s=%s; Path=/; HttpOnly; Expires=%s" ,
sessionCookieName , hex . EncodeToString ( sess ) ,
2021-04-06 11:31:20 +00:00
cookieExpiryFormat ( now . Add ( cookieTTL ) ) ,
2021-03-01 17:37:28 +00:00
) , nil
2019-10-11 09:41:01 +00:00
}
2021-04-06 11:31:20 +00:00
// realIP extracts the real IP address of the client from an HTTP request using
// the known HTTP headers.
//
// TODO(a.garipov): Currently, this is basically a copy of a similar function in
// module dnsproxy. This should really become a part of module golibs and be
// replaced both here and there. Or be replaced in both places by
// a well-maintained third-party module.
//
// TODO(a.garipov): Support header Forwarded from RFC 7329.
func realIP ( r * http . Request ) ( ip net . IP , err error ) {
proxyHeaders := [ ] string {
"CF-Connecting-IP" ,
"True-Client-IP" ,
"X-Real-IP" ,
}
for _ , h := range proxyHeaders {
v := r . Header . Get ( h )
ip = net . ParseIP ( v )
if ip != nil {
return ip , nil
}
}
// If none of the above yielded any results, get the leftmost IP address
// from the X-Forwarded-For header.
s := r . Header . Get ( "X-Forwarded-For" )
ipStrs := strings . SplitN ( s , ", " , 2 )
ip = net . ParseIP ( ipStrs [ 0 ] )
if ip != nil {
return ip , nil
}
2021-12-22 18:27:36 +00:00
// When everything else fails, just return the remote address as understood
// by the stdlib.
2021-08-09 13:03:37 +00:00
ipStr , err := netutil . SplitHost ( r . RemoteAddr )
2021-04-06 11:31:20 +00:00
if err != nil {
return nil , fmt . Errorf ( "getting ip from client addr: %w" , err )
}
return net . ParseIP ( ipStr ) , nil
}
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 {
2021-12-16 17:54:59 +00:00
aghhttp . Error ( r , w , http . StatusBadRequest , "json decode: %s" , err )
2021-04-27 15:56:32 +00:00
2019-10-11 09:41:01 +00:00
return
}
2021-04-27 15:56:32 +00:00
var remoteAddr string
2021-12-27 16:40:39 +00:00
// realIP cannot be used here without taking TrustedProxies into account due
2021-12-22 18:27:36 +00:00
// to security issues.
2021-04-27 15:56:32 +00:00
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
//
// TODO(e.burkov): Use realIP when the issue will be fixed.
2021-08-09 13:03:37 +00:00
if remoteAddr , err = netutil . SplitHost ( r . RemoteAddr ) ; err != nil {
2021-12-16 17:54:59 +00:00
aghhttp . Error ( r , w , http . StatusBadRequest , "auth: getting remote address: %s" , err )
2021-04-27 15:56:32 +00:00
return
}
if blocker := Context . auth . blocker ; blocker != nil {
if left := blocker . check ( remoteAddr ) ; left > 0 {
w . Header ( ) . Set ( "Retry-After" , strconv . Itoa ( int ( left . Seconds ( ) ) ) )
2021-12-22 18:29:31 +00:00
aghhttp . Error ( r , w , http . StatusTooManyRequests , "auth: blocked for %s" , left )
2021-04-27 15:56:32 +00:00
return
}
}
var cookie string
cookie , err = Context . auth . httpCookie ( req , remoteAddr )
2020-11-20 14:32:41 +00:00
if err != nil {
2021-12-16 17:54:59 +00:00
aghhttp . Error ( r , w , http . StatusBadRequest , "crypto rand reader: %s" , err )
2021-04-06 11:31:20 +00:00
2020-11-20 14:32:41 +00:00
return
}
2021-12-22 18:27:36 +00:00
// Use realIP here, since this IP address is only used for logging.
ip , err := realIP ( r )
if err != nil {
log . Error ( "auth: getting real ip from request: %s" , err )
} else if ip == nil {
// Technically shouldn't happen.
log . Error ( "auth: unknown ip" )
}
2021-04-06 11:31:20 +00:00
2019-10-11 09:41:01 +00:00
if len ( cookie ) == 0 {
2021-12-22 18:27:36 +00:00
log . Info ( "auth: failed to login user %q from ip %v" , req . Name , ip )
2019-10-11 09:41:01 +00:00
time . Sleep ( 1 * time . Second )
2021-04-06 11:31:20 +00:00
http . Error ( w , "invalid username or password" , http . StatusBadRequest )
2019-10-11 09:41:01 +00:00
return
}
2021-12-22 18:27:36 +00:00
log . Info ( "auth: user %q successfully logged in from ip %v" , req . Name , ip )
2019-08-29 09:34:07 +00:00
2021-12-22 18:27:36 +00:00
h := w . Header ( )
h . Set ( "Set-Cookie" , cookie )
h . Set ( "Cache-Control" , "no-store, no-cache, must-revalidate, proxy-revalidate" )
h . Set ( "Pragma" , "no-cache" )
h . Set ( "Expires" , "0" )
2019-08-29 09:34:07 +00:00
2021-12-16 17:54:59 +00:00
aghhttp . OK ( w )
2019-08-29 09:34:07 +00:00
}
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 ( ) {
2021-01-13 14:26:57 +00:00
Context . mux . Handle ( "/control/login" , postInstallHandler ( ensureHandler ( http . MethodPost , handleLogin ) ) )
httpRegister ( http . MethodGet , "/control/logout" , handleLogout )
2019-08-29 09:34:07 +00: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 12:45:50 +00:00
if kv [ 0 ] == sessionCookieName {
2019-08-29 09:34:07 +00:00
return kv [ 1 ]
}
}
return ""
}
2020-11-20 14:32:41 +00: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 ) {
2021-12-27 16:40:39 +00:00
log . Debug ( "auth: authentication was handled by GL-Inet submodule" )
2020-11-20 14:32:41 +00:00
ok = true
} else if err == nil {
2020-12-22 18:05:12 +00:00
r := Context . auth . checkSession ( cookie . Value )
if r == checkSessionOK {
2020-11-20 14:32:41 +00:00
ok = true
} else if r < 0 {
2021-04-06 11:31:20 +00:00
log . Debug ( "auth: invalid cookie value: %s" , cookie )
2020-11-20 14:32:41 +00:00
}
} 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 {
2021-04-06 11:31:20 +00:00
log . Info ( "auth: invalid Basic Authorization value" )
2020-11-20 14:32:41 +00:00
}
}
}
if ! ok {
if r . URL . Path == "/" || r . URL . Path == "/index.html" {
if glProcessRedirect ( w , r ) {
2021-04-06 11:31:20 +00:00
log . Debug ( "auth: redirected to login page by GL-Inet submodule" )
2020-11-20 14:32:41 +00:00
} else {
w . Header ( ) . Set ( "Location" , "/login.html" )
w . WriteHeader ( http . StatusFound )
}
} else {
w . WriteHeader ( http . StatusForbidden )
_ , _ = w . Write ( [ ] byte ( "Forbidden" ) )
}
authFirst = true
}
2021-04-06 11:31:20 +00:00
2020-11-20 14:32:41 +00:00
return authFirst
}
2019-08-29 09:34:07 +00: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 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-12-22 18:05:12 +00:00
r := Context . auth . checkSession ( cookie . Value )
if r == checkSessionOK {
2019-08-29 09:34:07 +00:00
w . Header ( ) . Set ( "Location" , "/" )
w . WriteHeader ( http . StatusFound )
2020-12-22 18:05:12 +00:00
2019-08-29 09:34:07 +00:00
return
2020-12-22 18:05:12 +00:00
} else if r == checkSessionNotFound {
2021-04-06 11:31:20 +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 ( ) {
2020-11-20 14:32:41 +00:00
if optionalAuthThird ( w , r ) {
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 ( )
2021-04-06 11:31:20 +00:00
log . Debug ( "auth: added user: %s" , u . Name )
2019-08-29 09:34:07 +00:00
}
// UserFind - find a user
2020-11-06 09:15:08 +00:00
func ( a * Auth ) UserFind ( login , password string ) User {
2019-08-29 09:34:07 +00: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 18:09:53 +00: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 12:45:50 +00:00
cookie , err := r . Cookie ( sessionCookieName )
2019-10-21 14:44:07 +00:00
if err != nil {
2020-12-22 18:09:53 +00:00
// There's no Cookie, check Basic authentication.
2019-10-21 14:44:07 +00:00
user , pass , ok := r . BasicAuth ( )
if ok {
2020-12-22 18:09:53 +00:00
return Context . auth . UserFind ( user , pass )
2019-10-21 14:44:07 +00:00
}
2020-12-22 18:09:53 +00:00
2019-10-25 08:01:29 +00:00
return User { }
2019-10-21 14:44:07 +00:00
}
a . lock . Lock ( )
2020-12-21 18:39:39 +00:00
defer a . lock . Unlock ( )
2020-12-22 18:09:53 +00:00
2019-10-21 14:44:07 +00:00
s , ok := a . sessions [ cookie . Value ]
if ! ok {
return User { }
}
2020-12-22 18:09:53 +00:00
2019-10-21 14:44:07 +00:00
for _ , u := range a . users {
if u . Name == s . userName {
return u
}
}
2020-12-22 18:09:53 +00:00
2019-10-21 14:44:07 +00:00
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 {
2020-07-03 17:34:08 +00:00
if GLMode {
return true
}
2019-08-29 09:34:07 +00:00
a . lock . Lock ( )
r := ( len ( a . users ) != 0 )
a . lock . Unlock ( )
return r
}