+ GET /control/profile

* openapi: get /profile

* auth: store user names along with sessions
This commit is contained in:
Simon Zolin 2019-10-21 17:44:07 +03:00
parent e8bb0fdcb7
commit c185f6826a
5 changed files with 164 additions and 24 deletions

View File

@ -54,6 +54,7 @@ Contents:
* Log-in page * Log-in page
* API: Log in * API: Log in
* API: Log out * API: Log out
* API: Get current user info
## Relations between subsystems ## Relations between subsystems
@ -1207,7 +1208,7 @@ YAML configuration:
Session DB file: Session DB file:
session="..." expire=123456 session="..." user=name expire=123456
... ...
Session data is SHA(random()+name+password). Session data is SHA(random()+name+password).
@ -1270,3 +1271,20 @@ Response:
302 Found 302 Found
Location: /login.html Location: /login.html
Set-Cookie: session=...; Expires=Thu, 01 Jan 1970 00:00:00 GMT Set-Cookie: session=...; Expires=Thu, 01 Jan 1970 00:00:00 GMT
### API: Get current user info
Request:
GET /control/profile
Response:
200 OK
{
"name":"..."
}
If no client is configured then authentication is disabled and server sends an empty response.

View File

@ -20,10 +20,44 @@ import (
const cookieTTL = 365 * 24 // in hours const cookieTTL = 365 * 24 // in hours
const expireTime = 30 * 24 // in hours const expireTime = 30 * 24 // in hours
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
}
// Auth - global object // Auth - global object
type Auth struct { type Auth struct {
db *bbolt.DB db *bbolt.DB
sessions map[string]uint32 // session -> expiration time (in seconds) sessions map[string]*session // session name -> session data
lock sync.Mutex lock sync.Mutex
users []User users []User
} }
@ -37,7 +71,7 @@ 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) *Auth {
a := Auth{} a := Auth{}
a.sessions = make(map[string]uint32) a.sessions = make(map[string]*session)
rand.Seed(time.Now().UTC().Unix()) rand.Seed(time.Now().UTC().Unix())
var err error var err error
a.db, err = bbolt.Open(dbFilename, 0644, nil) a.db, err = bbolt.Open(dbFilename, 0644, nil)
@ -56,6 +90,10 @@ func (a *Auth) Close() {
_ = a.db.Close() _ = a.db.Close()
} }
func bucketName() []byte {
return []byte("sessions-2")
}
// load sessions from file, remove expired sessions // load sessions from file, remove expired sessions
func (a *Auth) loadSessions() { func (a *Auth) loadSessions() {
tx, err := a.db.Begin(true) tx, err := a.db.Begin(true)
@ -67,16 +105,22 @@ func (a *Auth) loadSessions() {
_ = tx.Rollback() _ = tx.Rollback()
}() }()
bkt := tx.Bucket([]byte("sessions")) bkt := tx.Bucket(bucketName())
if bkt == nil { if bkt == nil {
return return
} }
removed := 0 removed := 0
if tx.Bucket([]byte("sessions")) != nil {
_ = tx.DeleteBucket([]byte("sessions"))
removed = 1
}
now := uint32(time.Now().UTC().Unix()) now := uint32(time.Now().UTC().Unix())
forEach := func(k, v []byte) error { forEach := func(k, v []byte) error {
i := binary.BigEndian.Uint32(v) s := session{}
if i <= now { if !s.deserialize(v) || s.expire <= now {
err = bkt.Delete(k) err = bkt.Delete(k)
if err != nil { if err != nil {
log.Error("Auth: bbolt.Delete: %s", err) log.Error("Auth: bbolt.Delete: %s", err)
@ -85,7 +129,8 @@ func (a *Auth) loadSessions() {
} }
return nil return nil
} }
a.sessions[hex.EncodeToString(k)] = i
a.sessions[hex.EncodeToString(k)] = &s
return nil return nil
} }
_ = bkt.ForEach(forEach) _ = bkt.ForEach(forEach)
@ -99,11 +144,15 @@ func (a *Auth) loadSessions() {
} }
// store session data in file // store session data in file
func (a *Auth) storeSession(data []byte, expire uint32) { func (a *Auth) addSession(data []byte, s *session) {
a.lock.Lock() a.lock.Lock()
a.sessions[hex.EncodeToString(data)] = expire a.sessions[hex.EncodeToString(data)] = s
a.lock.Unlock() a.lock.Unlock()
a.storeSession(data, s)
}
// store session data in file
func (a *Auth) storeSession(data []byte, s *session) {
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)
@ -113,15 +162,12 @@ func (a *Auth) storeSession(data []byte, expire uint32) {
_ = tx.Rollback() _ = tx.Rollback()
}() }()
bkt, err := tx.CreateBucketIfNotExists([]byte("sessions")) 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
} }
var val []byte err = bkt.Put(data, s.serialize())
val = make([]byte, 4)
binary.BigEndian.PutUint32(val, expire)
err = bkt.Put(data, val)
if err != nil { if err != nil {
log.Error("Auth: bbolt.Put: %s", err) log.Error("Auth: bbolt.Put: %s", err)
return return
@ -147,7 +193,7 @@ func (a *Auth) removeSession(sess []byte) {
_ = tx.Rollback() _ = tx.Rollback()
}() }()
bkt := tx.Bucket([]byte("sessions")) bkt := tx.Bucket(bucketName())
if bkt == nil { if bkt == nil {
log.Error("Auth: bbolt.Bucket") log.Error("Auth: bbolt.Bucket")
return return
@ -174,12 +220,12 @@ func (a *Auth) CheckSession(sess string) int {
update := false update := false
a.lock.Lock() a.lock.Lock()
expire, ok := a.sessions[sess] s, ok := a.sessions[sess]
if !ok { if !ok {
a.lock.Unlock() a.lock.Unlock()
return -1 return -1
} }
if expire <= now { if s.expire <= now {
delete(a.sessions, sess) delete(a.sessions, sess)
key, _ := hex.DecodeString(sess) key, _ := hex.DecodeString(sess)
a.removeSession(key) a.removeSession(key)
@ -188,17 +234,17 @@ func (a *Auth) CheckSession(sess string) int {
} }
newExpire := now + expireTime*60*60 newExpire := now + expireTime*60*60
if 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
a.sessions[sess] = newExpire s.expire = newExpire
} }
a.lock.Unlock() a.lock.Unlock()
if update { if update {
key, _ := hex.DecodeString(sess) key, _ := hex.DecodeString(sess)
a.storeSession(key, expire) a.storeSession(key, s)
} }
return 0 return 0
@ -238,8 +284,10 @@ func httpCookie(req loginJSON) string {
expstr = expstr[:len(expstr)-len("UTC")] // "UTC" -> "GMT" expstr = expstr[:len(expstr)-len("UTC")] // "UTC" -> "GMT"
expstr += "GMT" expstr += "GMT"
expireSess := uint32(now.Unix()) + expireTime*60*60 s := session{}
config.auth.storeSession(sess, expireSess) s.userName = u.Name
s.expire = uint32(now.Unix()) + expireTime*60*60
config.auth.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)
} }
@ -402,6 +450,34 @@ func (a *Auth) UserFind(login string, password string) User {
return User{} return User{}
} }
// GetCurrentUser - get the current user
func (a *Auth) GetCurrentUser(r *http.Request) User {
cookie, err := r.Cookie("session")
if err != nil {
// there's no Cookie, check Basic authentication
user, pass, ok := r.BasicAuth()
if ok {
u := config.auth.UserFind(user, pass)
return u
}
}
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{}
}
// GetUsers - get users // GetUsers - get users
func (a *Auth) GetUsers() []User { func (a *Auth) GetUsers() []User {
a.lock.Lock() a.lock.Lock()

View File

@ -28,6 +28,7 @@ func TestAuth(t *testing.T) {
User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
} }
a := InitAuth(fn, nil) a := InitAuth(fn, nil)
s := session{}
user := User{Name: "name"} user := User{Name: "name"}
a.UserAdd(&user, "password") a.UserAdd(&user, "password")
@ -38,12 +39,16 @@ func TestAuth(t *testing.T) {
sess := getSession(&users[0]) sess := getSession(&users[0])
sessStr := hex.EncodeToString(sess) sessStr := hex.EncodeToString(sess)
now := time.Now().UTC().Unix()
// check expiration // check expiration
a.storeSession(sess, uint32(time.Now().UTC().Unix())) s.expire = uint32(now)
a.addSession(sess, &s)
assert.True(t, a.CheckSession(sessStr) == 1) assert.True(t, a.CheckSession(sessStr) == 1)
// add session with TTL = 2 sec // add session with TTL = 2 sec
a.storeSession(sess, uint32(time.Now().UTC().Unix()+2)) s = session{}
s.expire = uint32(now + 2)
a.addSession(sess, &s)
assert.True(t, a.CheckSession(sessStr) == 0) assert.True(t, a.CheckSession(sessStr) == 0)
a.Close() a.Close()
@ -53,6 +58,9 @@ func TestAuth(t *testing.T) {
// the session is still alive // the session is still alive
assert.True(t, a.CheckSession(sessStr) == 0) assert.True(t, a.CheckSession(sessStr) == 0)
// reset our expiration time because CheckSession() has just updated it
s.expire = uint32(now + 2)
a.storeSession(sess, &s)
a.Close() a.Close()
u := a.UserFind("name", "password") u := a.UserFind("name", "password")

View File

@ -377,6 +377,23 @@ func checkDNS(input string, bootstrap []string) error {
return nil return nil
} }
type profileJSON struct {
Name string `json:"name"`
}
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
pj := profileJSON{}
u := config.auth.GetCurrentUser(r)
pj.Name = u.Name
data, err := json.Marshal(pj)
if err != nil {
httpError(w, http.StatusInternalServerError, "json.Marshal: %s", err)
return
}
_, _ = w.Write(data)
}
// -------------- // --------------
// DNS-over-HTTPS // DNS-over-HTTPS
// -------------- // --------------
@ -416,6 +433,7 @@ func registerControlHandlers() {
httpRegister(http.MethodGet, "/control/access/list", handleAccessList) httpRegister(http.MethodGet, "/control/access/list", handleAccessList)
httpRegister(http.MethodPost, "/control/access/set", handleAccessSet) httpRegister(http.MethodPost, "/control/access/set", handleAccessSet)
httpRegister("GET", "/control/profile", handleGetProfile)
RegisterFilteringHandlers() RegisterFilteringHandlers()
RegisterTLSHandlers() RegisterTLSHandlers()

View File

@ -970,6 +970,18 @@ paths:
302: 302:
description: OK description: OK
/profile:
get:
tags:
- global
operationId: getProfile
summary: ""
responses:
200:
description: OK
schema:
$ref: "#/definitions/ProfileInfo"
definitions: definitions:
ServerStatus: ServerStatus:
type: "object" type: "object"
@ -1559,6 +1571,14 @@ definitions:
description: "Network interfaces dictionary (key is the interface name)" description: "Network interfaces dictionary (key is the interface name)"
additionalProperties: additionalProperties:
$ref: "#/definitions/NetInterface" $ref: "#/definitions/NetInterface"
ProfileInfo:
type: "object"
description: "Information about the current user"
properties:
name:
type: "string"
Client: Client:
type: "object" type: "object"
description: "Client information" description: "Client information"