2019-08-29 12:34:07 +03:00
package home
import (
2020-11-20 17:32:41 +03:00
"crypto/rand"
2019-08-29 12:34:07 +03:00
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
2021-04-06 14:31:20 +03:00
"net"
2019-08-29 12:34:07 +03:00
"net/http"
2022-09-29 19:10:03 +03:00
"path"
2021-04-27 18:56:32 +03:00
"strconv"
2019-08-29 12:34:07 +03:00
"strings"
"sync"
"time"
2021-12-16 20:54:59 +03:00
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
2022-09-29 19:10:03 +03:00
"github.com/AdguardTeam/golibs/errors"
2019-08-29 12:34:07 +03:00
"github.com/AdguardTeam/golibs/log"
2021-08-09 16:03:37 +03:00
"github.com/AdguardTeam/golibs/netutil"
2021-09-30 21:17:54 +03:00
"github.com/AdguardTeam/golibs/timeutil"
2020-04-05 18:21:26 +03:00
"go.etcd.io/bbolt"
2019-08-29 12:34:07 +03:00
"golang.org/x/crypto/bcrypt"
)
2021-04-06 14:31:20 +03:00
// cookieTTL is the time-to-live of the session cookie.
2021-09-30 21:17:54 +03:00
const cookieTTL = 365 * timeutil . Day
2021-03-01 20:37:28 +03:00
2021-04-06 14:31:20 +03: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 12:34:07 +03:00
2019-10-21 17:44:07 +03:00
type session struct {
userName string
2022-09-29 19:10:03 +03:00
// expire is the expiration time, in seconds.
expire uint32
2019-10-21 17:44:07 +03:00
}
func ( s * session ) serialize ( ) [ ] byte {
2020-11-06 12:15:08 +03:00
const (
expireLen = 4
nameLen = 2
)
data := make ( [ ] byte , expireLen + nameLen + len ( s . userName ) )
2019-10-21 17:44:07 +03: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 12:34:07 +03:00
// Auth - global object
type Auth struct {
2022-09-29 19:10:03 +03:00
db * bbolt . DB
raleLimiter * authRateLimiter
sessions map [ string ] * session
users [ ] webUser
lock sync . Mutex
sessionTTL uint32
2019-08-29 12:34:07 +03:00
}
2022-09-29 19:10:03 +03:00
// webUser represents a user of the Web UI.
type webUser struct {
2019-08-29 12:34:07 +03:00
Name string ` yaml:"name" `
2022-09-29 19:10:03 +03:00
PasswordHash string ` yaml:"password" `
2019-08-29 12:34:07 +03:00
}
// InitAuth - create a global object
2022-09-29 19:10:03 +03:00
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 )
2021-04-27 18:56:32 +03:00
a := & Auth {
2022-09-29 19:10:03 +03:00
sessionTTL : sessionTTL ,
raleLimiter : rateLimiter ,
sessions : make ( map [ string ] * session ) ,
users : users ,
2021-04-27 18:56:32 +03:00
}
2019-08-29 12:34:07 +03:00
var err error
2020-11-05 15:20:57 +03:00
a . db , err = bbolt . Open ( dbFilename , 0 o644 , nil )
2019-08-29 12:34:07 +03:00
if err != nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: open DB: %s: %s" , dbFilename , err )
2020-07-02 16:52:29 +03:00
if err . Error ( ) == "invalid argument" {
2021-04-08 16:44:01 +03: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 16:52:29 +03:00
}
2021-04-06 14:31:20 +03:00
2019-08-29 12:34:07 +03:00
return nil
}
a . loadSessions ( )
2021-04-06 14:31:20 +03:00
log . Info ( "auth: initialized. users:%d sessions:%d" , len ( a . users ) , len ( a . sessions ) )
2021-04-27 18:56:32 +03:00
return a
2019-08-29 12:34:07 +03:00
}
// Close - close module
func ( a * Auth ) Close ( ) {
_ = a . db . Close ( )
}
2019-10-21 17:44:07 +03:00
func bucketName ( ) [ ] byte {
return [ ] byte ( "sessions-2" )
}
2019-08-29 12:34:07 +03:00
// load sessions from file, remove expired sessions
func ( a * Auth ) loadSessions ( ) {
tx , err := a . db . Begin ( true )
if err != nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: bbolt.Begin: %s" , err )
2019-08-29 12:34:07 +03:00
return
}
defer func ( ) {
_ = tx . Rollback ( )
} ( )
2019-10-21 17:44:07 +03:00
bkt := tx . Bucket ( bucketName ( ) )
2019-08-29 12:34:07 +03:00
if bkt == nil {
return
}
removed := 0
2019-10-21 17:44:07 +03:00
if tx . Bucket ( [ ] byte ( "sessions" ) ) != nil {
_ = tx . DeleteBucket ( [ ] byte ( "sessions" ) )
removed = 1
}
2019-08-29 12:34:07 +03:00
now := uint32 ( time . Now ( ) . UTC ( ) . Unix ( ) )
forEach := func ( k , v [ ] byte ) error {
2019-10-21 17:44:07 +03:00
s := session { }
if ! s . deserialize ( v ) || s . expire <= now {
2019-08-29 12:34:07 +03:00
err = bkt . Delete ( k )
if err != nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: bbolt.Delete: %s" , err )
2019-08-29 12:34:07 +03:00
} else {
removed ++
}
2021-04-06 14:31:20 +03:00
2019-08-29 12:34:07 +03:00
return nil
}
2019-10-21 17:44:07 +03:00
a . sessions [ hex . EncodeToString ( k ) ] = & s
2019-08-29 12:34:07 +03:00
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 )
}
2019-08-29 12:34:07 +03:00
}
2021-04-06 14:31:20 +03:00
log . Debug ( "auth: loaded %d sessions from DB (removed %d expired)" , len ( a . sessions ) , removed )
2019-08-29 12:34:07 +03:00
}
// store session data in file
2019-10-21 17:44:07 +03:00
func ( a * Auth ) addSession ( data [ ] byte , s * session ) {
2019-11-12 14:24:27 +03:00
name := hex . EncodeToString ( data )
2019-08-29 12:34:07 +03:00
a . lock . Lock ( )
2019-11-12 14:24:27 +03:00
a . sessions [ name ] = s
2019-08-29 12:34:07 +03:00
a . lock . Unlock ( )
2019-11-12 14:24:27 +03:00
if a . storeSession ( data , s ) {
2021-04-06 14:31:20 +03:00
log . Debug ( "auth: created session %s: expire=%d" , name , s . expire )
2019-11-12 14:24:27 +03:00
}
2019-10-21 17:44:07 +03:00
}
2019-08-29 12:34:07 +03:00
2019-10-21 17:44:07 +03:00
// store session data in file
2019-11-12 14:24:27 +03:00
func ( a * Auth ) storeSession ( data [ ] byte , s * session ) bool {
2019-08-29 12:34:07 +03:00
tx , err := a . db . Begin ( true )
if err != nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: bbolt.Begin: %s" , err )
2019-11-12 14:24:27 +03:00
return false
2019-08-29 12:34:07 +03:00
}
defer func ( ) {
_ = tx . Rollback ( )
} ( )
2019-10-21 17:44:07 +03:00
bkt , err := tx . CreateBucketIfNotExists ( bucketName ( ) )
2019-08-29 12:34:07 +03:00
if err != nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: bbolt.CreateBucketIfNotExists: %s" , err )
2019-11-12 14:24:27 +03:00
return false
2019-08-29 12:34:07 +03:00
}
2021-04-06 14:31:20 +03:00
2019-10-21 17:44:07 +03:00
err = bkt . Put ( data , s . serialize ( ) )
2019-08-29 12:34:07 +03:00
if err != nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: bbolt.Put: %s" , err )
2019-11-12 14:24:27 +03:00
return false
2019-08-29 12:34:07 +03:00
}
err = tx . Commit ( )
if err != nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: bbolt.Commit: %s" , err )
2019-11-12 14:24:27 +03:00
return false
2019-08-29 12:34:07 +03:00
}
2021-04-06 14:31:20 +03:00
2019-11-12 14:24:27 +03:00
return true
2019-08-29 12:34:07 +03:00
}
// remove session from file
func ( a * Auth ) removeSession ( sess [ ] byte ) {
tx , err := a . db . Begin ( true )
if err != nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: bbolt.Begin: %s" , err )
2019-08-29 12:34:07 +03:00
return
}
2021-04-06 14:31:20 +03:00
2019-08-29 12:34:07 +03:00
defer func ( ) {
_ = tx . Rollback ( )
} ( )
2019-10-21 17:44:07 +03:00
bkt := tx . Bucket ( bucketName ( ) )
2019-08-29 12:34:07 +03:00
if bkt == nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: bbolt.Bucket" )
2019-08-29 12:34:07 +03:00
return
}
2021-04-06 14:31:20 +03:00
2019-08-29 12:34:07 +03:00
err = bkt . Delete ( sess )
if err != nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: bbolt.Put: %s" , err )
2019-08-29 12:34:07 +03:00
return
}
err = tx . Commit ( )
if err != nil {
2021-04-06 14:31:20 +03:00
log . Error ( "auth: bbolt.Commit: %s" , err )
2019-08-29 12:34:07 +03:00
return
}
2021-04-06 14:31:20 +03:00
log . Debug ( "auth: removed session from DB" )
2019-08-29 12:34:07 +03:00
}
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 ) {
2019-08-29 12:34:07 +03:00
now := uint32 ( time . Now ( ) . UTC ( ) . Unix ( ) )
update := false
a . lock . Lock ( )
2020-12-21 21:39:39 +03:00
defer a . lock . Unlock ( )
2020-12-22 21:05:12 +03:00
2019-10-21 17:44:07 +03:00
s , ok := a . sessions [ sess ]
2019-08-29 12:34:07 +03:00
if ! ok {
2020-12-22 21:05:12 +03:00
return checkSessionNotFound
2019-08-29 12:34:07 +03:00
}
2020-12-22 21:05:12 +03:00
2019-10-21 17:44:07 +03:00
if s . expire <= now {
2019-08-29 12:34:07 +03:00
delete ( a . sessions , sess )
key , _ := hex . DecodeString ( sess )
a . removeSession ( key )
2020-12-22 21:05:12 +03:00
return checkSessionExpired
2019-08-29 12:34:07 +03:00
}
2019-11-12 14:23:00 +03:00
newExpire := now + a . sessionTTL
2019-10-21 17:44:07 +03:00
if s . expire / ( 24 * 60 * 60 ) != newExpire / ( 24 * 60 * 60 ) {
2019-08-29 12:34:07 +03:00
// update expiration time once a day
update = true
2019-10-21 17:44:07 +03:00
s . expire = newExpire
2019-08-29 12:34:07 +03:00
}
if update {
key , _ := hex . DecodeString ( sess )
2019-11-12 14:24:27 +03:00
if a . storeSession ( key , s ) {
2021-04-06 14:31:20 +03:00
log . Debug ( "auth: updated session %s: expire=%d" , sess , s . expire )
2019-11-12 14:24:27 +03:00
}
2019-08-29 12:34:07 +03:00
}
2020-12-22 21:05:12 +03:00
return checkSessionOK
2019-08-29 12:34:07 +03: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 20:37:28 +03: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 17:32:41 +03:00
if err != nil {
return nil , err
}
2021-03-01 20:37:28 +03:00
return randData , nil
}
2022-09-29 19:10:03 +03:00
// newCookie creates a new authentication cookie.
func ( a * Auth ) newCookie ( req loginJSON , addr string ) ( c * http . Cookie , err error ) {
rateLimiter := a . raleLimiter
u , ok := a . findUser ( req . Name , req . Password )
if ! ok {
if rateLimiter != nil {
rateLimiter . inc ( addr )
2021-04-27 18:56:32 +03:00
}
2022-09-29 19:10:03 +03:00
return nil , errors . Error ( "invalid username or password" )
2019-08-29 12:34:07 +03:00
}
2022-09-29 19:10:03 +03:00
if rateLimiter != nil {
rateLimiter . remove ( addr )
2021-04-27 18:56:32 +03:00
}
2022-09-29 19:10:03 +03:00
sess , err := newSessionToken ( )
2020-11-20 17:32:41 +03:00
if err != nil {
2022-09-29 19:10:03 +03:00
return nil , fmt . Errorf ( "generating token: %w" , err )
2020-11-20 17:32:41 +03:00
}
2019-08-29 12:34:07 +03:00
now := time . Now ( ) . UTC ( )
2021-03-01 20:37:28 +03:00
a . addSession ( sess , & session {
userName : u . Name ,
expire : uint32 ( now . Unix ( ) ) + a . sessionTTL ,
} )
2022-09-29 19:10:03 +03:00
return & http . Cookie {
Name : sessionCookieName ,
Value : hex . EncodeToString ( sess ) ,
Path : "/" ,
Expires : now . Add ( cookieTTL ) ,
HttpOnly : true ,
SameSite : http . SameSiteLaxMode ,
} , nil
2019-10-11 12:41:01 +03:00
}
2021-04-06 14:31:20 +03: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-23 13:58:28 +03:00
// When everything else fails, just return the remote address as understood
// by the stdlib.
2021-08-09 16:03:37 +03:00
ipStr , err := netutil . SplitHost ( r . RemoteAddr )
2021-04-06 14:31:20 +03:00
if err != nil {
return nil , fmt . Errorf ( "getting ip from client addr: %w" , err )
}
return net . ParseIP ( ipStr ) , nil
}
2019-10-11 12:41:01 +03:00
func handleLogin ( w http . ResponseWriter , r * http . Request ) {
req := loginJSON { }
err := json . NewDecoder ( r . Body ) . Decode ( & req )
if err != nil {
2021-12-16 20:54:59 +03:00
aghhttp . Error ( r , w , http . StatusBadRequest , "json decode: %s" , err )
2021-04-27 18:56:32 +03:00
2019-10-11 12:41:01 +03:00
return
}
2021-04-27 18:56:32 +03:00
var remoteAddr string
2021-12-27 19:40:39 +03:00
// realIP cannot be used here without taking TrustedProxies into account due
2021-12-23 13:58:28 +03:00
// to security issues.
2021-04-27 18:56:32 +03:00
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
//
// TODO(e.burkov): Use realIP when the issue will be fixed.
2021-08-09 16:03:37 +03:00
if remoteAddr , err = netutil . SplitHost ( r . RemoteAddr ) ; err != nil {
2021-12-16 20:54:59 +03:00
aghhttp . Error ( r , w , http . StatusBadRequest , "auth: getting remote address: %s" , err )
2021-04-27 18:56:32 +03:00
return
}
2022-09-29 19:10:03 +03:00
if rateLimiter := Context . auth . raleLimiter ; rateLimiter != nil {
if left := rateLimiter . check ( remoteAddr ) ; left > 0 {
2021-04-27 18:56:32 +03:00
w . Header ( ) . Set ( "Retry-After" , strconv . Itoa ( int ( left . Seconds ( ) ) ) )
2021-12-23 13:58:28 +03:00
aghhttp . Error ( r , w , http . StatusTooManyRequests , "auth: blocked for %s" , left )
2021-04-27 18:56:32 +03:00
return
}
}
2022-09-29 19:10:03 +03:00
cookie , err := Context . auth . newCookie ( req , remoteAddr )
2020-11-20 17:32:41 +03:00
if err != nil {
2022-09-29 19:10:03 +03:00
aghhttp . Error ( r , w , http . StatusForbidden , "%s" , err )
2021-04-06 14:31:20 +03:00
2020-11-20 17:32:41 +03:00
return
}
2021-04-06 14:31:20 +03:00
2021-12-23 13:58:28 +03: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" )
}
log . Info ( "auth: user %q successfully logged in from ip %v" , req . Name , ip )
2019-08-29 12:34:07 +03:00
2022-09-29 19:10:03 +03:00
http . SetCookie ( w , cookie )
2021-12-23 13:58:28 +03:00
h := w . Header ( )
h . Set ( "Cache-Control" , "no-store, no-cache, must-revalidate, proxy-revalidate" )
h . Set ( "Pragma" , "no-cache" )
h . Set ( "Expires" , "0" )
2019-08-29 12:34:07 +03:00
2021-12-16 20:54:59 +03:00
aghhttp . OK ( w )
2019-08-29 12:34:07 +03:00
}
func handleLogout ( w http . ResponseWriter , r * http . Request ) {
2022-09-29 19:10:03 +03:00
respHdr := w . Header ( )
c , err := r . Cookie ( sessionCookieName )
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// The user is already logged out.
respHdr . Set ( "Location" , "/login.html" )
w . WriteHeader ( http . StatusFound )
return
}
2019-08-29 12:34:07 +03:00
2022-09-29 19:10:03 +03:00
Context . auth . RemoveSession ( c . Value )
2019-08-29 12:34:07 +03:00
2022-09-29 19:10:03 +03:00
c = & http . Cookie {
Name : sessionCookieName ,
Value : "" ,
Path : "/" ,
Expires : time . Unix ( 0 , 0 ) ,
2019-08-29 12:34:07 +03:00
2022-09-29 19:10:03 +03:00
HttpOnly : true ,
SameSite : http . SameSiteLaxMode ,
}
2019-08-29 12:34:07 +03:00
2022-09-29 19:10:03 +03:00
respHdr . Set ( "Location" , "/login.html" )
respHdr . Set ( "Set-Cookie" , c . String ( ) )
2019-08-29 12:34:07 +03:00
w . WriteHeader ( http . StatusFound )
}
// RegisterAuthHandlers - register handlers
func RegisterAuthHandlers ( ) {
2021-01-13 17:26:57 +03:00
Context . mux . Handle ( "/control/login" , postInstallHandler ( ensureHandler ( http . MethodPost , handleLogin ) ) )
httpRegister ( http . MethodGet , "/control/logout" , handleLogout )
2019-08-29 12:34:07 +03:00
}
2020-11-20 17:32:41 +03:00
// optionalAuthThird return true if user should authenticate first.
2022-09-29 19:10:03 +03:00
func optionalAuthThird ( w http . ResponseWriter , r * http . Request ) ( mustAuth bool ) {
if glProcessCookie ( r ) {
log . Debug ( "auth: authentication is handled by GL-Inet submodule" )
return false
}
2020-11-20 17:32:41 +03:00
// redirect to login page if not authenticated
2022-09-29 19:10:03 +03:00
isAuthenticated := false
2020-11-20 17:32:41 +03:00
cookie , err := r . Cookie ( sessionCookieName )
2022-09-29 19:10:03 +03:00
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 = Context . auth . findUser ( user , pass )
if ! isAuthenticated {
2021-04-06 14:31:20 +03:00
log . Info ( "auth: invalid Basic Authorization value" )
2020-11-20 17:32:41 +03:00
}
}
2022-09-29 19:10:03 +03:00
} else {
res := Context . auth . checkSession ( cookie . Value )
isAuthenticated = res == checkSessionOK
if ! isAuthenticated {
log . Debug ( "auth: invalid cookie value: %s" , cookie )
}
2020-11-20 17:32:41 +03:00
}
2022-09-29 19:10:03 +03:00
if isAuthenticated {
return false
}
if p := r . URL . Path ; p == "/" || p == "/index.html" {
if glProcessRedirect ( w , r ) {
log . Debug ( "auth: redirected to login page by GL-Inet submodule" )
2020-11-20 17:32:41 +03:00
} else {
2022-09-29 19:10:03 +03:00
log . Debug ( "auth: redirected to login page" )
w . Header ( ) . Set ( "Location" , "/login.html" )
w . WriteHeader ( http . StatusFound )
2020-11-20 17:32:41 +03:00
}
2022-09-29 19:10:03 +03:00
} else {
log . Debug ( "auth: responded with forbidden to %s %s" , r . Method , p )
w . WriteHeader ( http . StatusForbidden )
_ , _ = w . Write ( [ ] byte ( "Forbidden" ) )
2020-11-20 17:32:41 +03:00
}
2021-04-06 14:31:20 +03:00
2022-09-29 19:10:03 +03:00
return true
2020-11-20 17:32:41 +03:00
}
2022-09-29 19:10:03 +03:00
// TODO(a.garipov): Use [http.Handler] consistently everywhere throughout the
// project.
func optionalAuth (
h func ( http . ResponseWriter , * http . Request ) ,
) ( wrapped func ( http . ResponseWriter , * http . Request ) ) {
2019-08-29 12:34:07 +03:00
return func ( w http . ResponseWriter , r * http . Request ) {
2022-09-29 19:10:03 +03:00
p := r . URL . Path
authRequired := Context . auth != nil && Context . auth . AuthRequired ( )
if p == "/login.html" {
2019-11-25 15:45:50 +03:00
cookie , err := r . Cookie ( sessionCookieName )
2019-08-29 12:34:07 +03:00
if authRequired && err == nil {
2022-09-29 19:10:03 +03:00
// Redirect to the dashboard if already authenticated.
res := Context . auth . checkSession ( cookie . Value )
if res == checkSessionOK {
2019-08-29 12:34:07 +03:00
w . Header ( ) . Set ( "Location" , "/" )
w . WriteHeader ( http . StatusFound )
2020-12-22 21:05:12 +03:00
2019-08-29 12:34:07 +03:00
return
}
2022-09-29 19:10:03 +03:00
log . Debug ( "auth: invalid cookie value: %s" , cookie )
}
} else if isPublicResource ( p ) {
// Process as usual, no additional auth requirements.
} else if authRequired {
2020-11-20 17:32:41 +03:00
if optionalAuthThird ( w , r ) {
2019-08-29 12:34:07 +03:00
return
}
}
2022-09-29 19:10:03 +03:00
h ( w , r )
2019-08-29 12:34:07 +03:00
}
}
2022-09-29 19:10:03 +03:00
// isPublicResource returns true if p is a path to a public resource.
func isPublicResource ( p string ) ( ok bool ) {
isAsset , err := path . Match ( "/assets/*" , p )
if err != nil {
// The only error that is returned from path.Match is
// [path.ErrBadPattern]. This is a programmer error.
panic ( fmt . Errorf ( "bad asset pattern: %w" , err ) )
}
isLogin , err := path . Match ( "/login.*" , p )
if err != nil {
// Same as above.
panic ( fmt . Errorf ( "bad login pattern: %w" , err ) )
}
return isAsset || isLogin
}
2019-08-29 12:34:07 +03:00
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
2022-09-29 19:10:03 +03:00
func ( a * Auth ) UserAdd ( u * webUser , password string ) {
2019-08-29 12:34:07 +03:00
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 14:31:20 +03:00
log . Debug ( "auth: added user: %s" , u . Name )
2019-08-29 12:34:07 +03:00
}
2022-09-29 19:10:03 +03:00
// findUser returns a user if there is one.
func ( a * Auth ) findUser ( login , password string ) ( u webUser , ok bool ) {
2019-08-29 12:34:07 +03:00
a . lock . Lock ( )
defer a . lock . Unlock ( )
2022-09-29 19:10:03 +03:00
for _ , u = range a . users {
2019-08-29 12:34:07 +03:00
if u . Name == login &&
bcrypt . CompareHashAndPassword ( [ ] byte ( u . PasswordHash ) , [ ] byte ( password ) ) == nil {
2022-09-29 19:10:03 +03:00
return u , true
2019-08-29 12:34:07 +03:00
}
}
2022-09-29 19:10:03 +03:00
return webUser { } , false
2019-08-29 12:34:07 +03:00
}
2020-12-22 21:09:53 +03:00
// getCurrentUser returns the current user. It returns an empty User if the
// user is not found.
2022-09-29 19:10:03 +03:00
func ( a * Auth ) getCurrentUser ( r * http . Request ) ( u webUser ) {
2019-11-25 15:45:50 +03:00
cookie , err := r . Cookie ( sessionCookieName )
2019-10-21 17:44:07 +03:00
if err != nil {
2020-12-22 21:09:53 +03:00
// There's no Cookie, check Basic authentication.
2019-10-21 17:44:07 +03:00
user , pass , ok := r . BasicAuth ( )
if ok {
2022-09-29 19:10:03 +03:00
u , _ = Context . auth . findUser ( user , pass )
return u
2019-10-21 17:44:07 +03:00
}
2020-12-22 21:09:53 +03:00
2022-09-29 19:10:03 +03:00
return webUser { }
2019-10-21 17:44:07 +03:00
}
a . lock . Lock ( )
2020-12-21 21:39:39 +03:00
defer a . lock . Unlock ( )
2020-12-22 21:09:53 +03:00
2019-10-21 17:44:07 +03:00
s , ok := a . sessions [ cookie . Value ]
if ! ok {
2022-09-29 19:10:03 +03:00
return webUser { }
2019-10-21 17:44:07 +03:00
}
2020-12-22 21:09:53 +03:00
2022-09-29 19:10:03 +03:00
for _ , u = range a . users {
2019-10-21 17:44:07 +03:00
if u . Name == s . userName {
return u
}
}
2020-12-22 21:09:53 +03:00
2022-09-29 19:10:03 +03:00
return webUser { }
2019-10-21 17:44:07 +03:00
}
2019-08-29 12:34:07 +03:00
// GetUsers - get users
2022-09-29 19:10:03 +03:00
func ( a * Auth ) GetUsers ( ) [ ] webUser {
2019-08-29 12:34:07 +03:00
a . lock . Lock ( )
users := a . users
a . lock . Unlock ( )
return users
}
// AuthRequired - if authentication is required
func ( a * Auth ) AuthRequired ( ) bool {
2020-07-03 20:34:08 +03:00
if GLMode {
return true
}
2019-08-29 12:34:07 +03:00
a . lock . Lock ( )
r := ( len ( a . users ) != 0 )
a . lock . Unlock ( )
return r
}