Merge: + Auth: add "web_session_ttl" setting; improve logging

Close #1006

* commit 'b03b36e47e1f230567fce0936ee1285b6861350a':
  * auth: improve logging
  + config: "web_session_ttl" setting
This commit is contained in:
Simon Zolin 2019-11-20 14:58:20 +03:00
commit 1b7ef0e4f4
4 changed files with 45 additions and 31 deletions

View File

@ -18,7 +18,6 @@ import (
) )
const cookieTTL = 365 * 24 // in hours const cookieTTL = 365 * 24 // in hours
const expireTime = 30 * 24 // in hours
type session struct { type session struct {
userName string userName string
@ -56,10 +55,11 @@ func (s *session) deserialize(data []byte) bool {
// Auth - global object // Auth - global object
type Auth struct { type Auth struct {
db *bbolt.DB db *bbolt.DB
sessions map[string]*session // session name -> session data sessions map[string]*session // session name -> session data
lock sync.Mutex lock sync.Mutex
users []User users []User
sessionTTL uint32 // in seconds
} }
// User object // User object
@ -69,8 +69,9 @@ type User struct {
} }
// InitAuth - create a global object // InitAuth - create a global object
func InitAuth(dbFilename string, users []User) *Auth { func InitAuth(dbFilename string, users []User, sessionTTL uint32) *Auth {
a := Auth{} a := Auth{}
a.sessionTTL = sessionTTL
a.sessions = make(map[string]*session) a.sessions = make(map[string]*session)
rand.Seed(time.Now().UTC().Unix()) rand.Seed(time.Now().UTC().Unix())
var err error var err error
@ -145,18 +146,21 @@ func (a *Auth) loadSessions() {
// store session data in file // store session data in file
func (a *Auth) addSession(data []byte, s *session) { func (a *Auth) addSession(data []byte, s *session) {
name := hex.EncodeToString(data)
a.lock.Lock() a.lock.Lock()
a.sessions[hex.EncodeToString(data)] = s a.sessions[name] = s
a.lock.Unlock() a.lock.Unlock()
a.storeSession(data, s) if a.storeSession(data, s) {
log.Info("Auth: created session %s: expire=%d", name, s.expire)
}
} }
// store session data in file // store session data in file
func (a *Auth) storeSession(data []byte, s *session) { func (a *Auth) storeSession(data []byte, s *session) bool {
tx, err := a.db.Begin(true) tx, err := a.db.Begin(true)
if err != nil { if err != nil {
log.Error("Auth: bbolt.Begin: %s", err) log.Error("Auth: bbolt.Begin: %s", err)
return return false
} }
defer func() { defer func() {
_ = tx.Rollback() _ = tx.Rollback()
@ -165,21 +169,20 @@ func (a *Auth) storeSession(data []byte, s *session) {
bkt, err := tx.CreateBucketIfNotExists(bucketName()) bkt, err := tx.CreateBucketIfNotExists(bucketName())
if err != nil { if err != nil {
log.Error("Auth: bbolt.CreateBucketIfNotExists: %s", err) log.Error("Auth: bbolt.CreateBucketIfNotExists: %s", err)
return return false
} }
err = bkt.Put(data, s.serialize()) err = bkt.Put(data, s.serialize())
if err != nil { if err != nil {
log.Error("Auth: bbolt.Put: %s", err) log.Error("Auth: bbolt.Put: %s", err)
return return false
} }
err = tx.Commit() err = tx.Commit()
if err != nil { if err != nil {
log.Error("Auth: bbolt.Commit: %s", err) log.Error("Auth: bbolt.Commit: %s", err)
return return false
} }
return true
log.Debug("Auth: stored session in DB")
} }
// remove session from file // remove session from file
@ -233,7 +236,7 @@ func (a *Auth) CheckSession(sess string) int {
return 1 return 1
} }
newExpire := now + expireTime*60*60 newExpire := now + a.sessionTTL
if s.expire/(24*60*60) != newExpire/(24*60*60) { if s.expire/(24*60*60) != newExpire/(24*60*60) {
// update expiration time once a day // update expiration time once a day
update = true update = true
@ -244,7 +247,9 @@ func (a *Auth) CheckSession(sess string) int {
if update { if update {
key, _ := hex.DecodeString(sess) key, _ := hex.DecodeString(sess)
a.storeSession(key, s) if a.storeSession(key, s) {
log.Debug("Auth: updated session %s: expire=%d", sess, s.expire)
}
} }
return 0 return 0
@ -270,8 +275,8 @@ func getSession(u *User) []byte {
return hash[:] return hash[:]
} }
func httpCookie(req loginJSON) string { func (a *Auth) httpCookie(req loginJSON) string {
u := config.auth.UserFind(req.Name, req.Password) u := a.UserFind(req.Name, req.Password)
if len(u.Name) == 0 { if len(u.Name) == 0 {
return "" return ""
} }
@ -286,8 +291,8 @@ func httpCookie(req loginJSON) string {
s := session{} s := session{}
s.userName = u.Name s.userName = u.Name
s.expire = uint32(now.Unix()) + expireTime*60*60 s.expire = uint32(now.Unix()) + a.sessionTTL
config.auth.addSession(sess, &s) a.addSession(sess, &s)
return fmt.Sprintf("session=%s; Path=/; HttpOnly; Expires=%s", hex.EncodeToString(sess), expstr) return fmt.Sprintf("session=%s; Path=/; HttpOnly; Expires=%s", hex.EncodeToString(sess), expstr)
} }
@ -300,10 +305,11 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
cookie := httpCookie(req) cookie := config.auth.httpCookie(req)
if len(cookie) == 0 { if len(cookie) == 0 {
log.Info("Auth: invalid user name or password: name='%s'", req.Name)
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
httpError(w, http.StatusBadRequest, "invalid login or password") http.Error(w, "invalid user name or password", http.StatusBadRequest)
return return
} }
@ -365,7 +371,7 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
return return
} else if r < 0 { } else if r < 0 {
log.Debug("Auth: invalid cookie value: %s", cookie) log.Info("Auth: invalid cookie value: %s", cookie)
} }
} }
@ -382,7 +388,7 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
if r == 0 { if r == 0 {
ok = true ok = true
} else if r < 0 { } else if r < 0 {
log.Debug("Auth: invalid cookie value: %s", cookie) log.Info("Auth: invalid cookie value: %s", cookie)
} }
} else { } else {
// there's no Cookie, check Basic authentication // there's no Cookie, check Basic authentication
@ -391,6 +397,8 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
u := config.auth.UserFind(user, pass) u := config.auth.UserFind(user, pass)
if len(u.Name) != 0 { if len(u.Name) != 0 {
ok = true ok = true
} else {
log.Info("Auth: invalid Basic Authorization value")
} }
} }
} }

View File

@ -27,7 +27,7 @@ func TestAuth(t *testing.T) {
users := []User{ users := []User{
User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
} }
a := InitAuth(fn, nil) a := InitAuth(fn, nil, 60)
s := session{} s := session{}
user := User{Name: "name"} user := User{Name: "name"}
@ -54,7 +54,7 @@ func TestAuth(t *testing.T) {
a.Close() a.Close()
// load saved session // load saved session
a = InitAuth(fn, users) a = InitAuth(fn, users, 60)
// the session is still alive // the session is still alive
assert.True(t, a.CheckSession(sessStr) == 0) assert.True(t, a.CheckSession(sessStr) == 0)
@ -69,7 +69,7 @@ func TestAuth(t *testing.T) {
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
// load and remove expired sessions // load and remove expired sessions
a = InitAuth(fn, users) a = InitAuth(fn, users, 60)
assert.True(t, a.CheckSession(sessStr) == -1) assert.True(t, a.CheckSession(sessStr) == -1)
a.Close() a.Close()
@ -100,7 +100,7 @@ func TestAuthHTTP(t *testing.T) {
users := []User{ users := []User{
User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
} }
config.auth = InitAuth(fn, users) config.auth = InitAuth(fn, users, 60)
handlerCalled := false handlerCalled := false
handler := func(w http.ResponseWriter, r *http.Request) { handler := func(w http.ResponseWriter, r *http.Request) {
@ -129,7 +129,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
// perform login // perform login
cookie := httpCookie(loginJSON{Name: "name", Password: "password"}) cookie := config.auth.httpCookie(loginJSON{Name: "name", Password: "password"})
assert.True(t, cookie != "") assert.True(t, cookie != "")
// get / // get /

View File

@ -91,6 +91,10 @@ type configuration struct {
Language string `yaml:"language"` // two-letter ISO 639-1 language code Language string `yaml:"language"` // two-letter ISO 639-1 language code
RlimitNoFile uint `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default) RlimitNoFile uint `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default)
// TTL for a web session (in hours)
// An active session is automatically refreshed once a day.
WebSessionTTLHours uint32 `yaml:"web_session_ttl"`
DNS dnsConfig `yaml:"dns"` DNS dnsConfig `yaml:"dns"`
TLS tlsConfig `yaml:"tls"` TLS tlsConfig `yaml:"tls"`
Filters []filter `yaml:"filters"` Filters []filter `yaml:"filters"`
@ -210,6 +214,8 @@ func initConfig() {
Transport: config.transport, Transport: config.transport,
} }
config.WebSessionTTLHours = 30 * 24
if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" { if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" {
// Use plain DNS on MIPS, encryption is too slow // Use plain DNS on MIPS, encryption is too slow
defaultDNS = []string{"1.1.1.1", "1.0.0.1"} defaultDNS = []string{"1.1.1.1", "1.0.0.1"}

View File

@ -68,7 +68,7 @@ func initDNSServer() {
config.dnsServer = dnsforward.NewServer(config.dnsFilter, config.stats, config.queryLog) config.dnsServer = dnsforward.NewServer(config.dnsFilter, config.stats, config.queryLog)
sessFilename := filepath.Join(baseDir, "sessions.db") sessFilename := filepath.Join(baseDir, "sessions.db")
config.auth = InitAuth(sessFilename, config.Users) config.auth = InitAuth(sessFilename, config.Users, config.WebSessionTTLHours*60*60)
config.Users = nil config.Users = nil
config.dnsctx.rdns = InitRDNS(&config.clients) config.dnsctx.rdns = InitRDNS(&config.clients)