init repo
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const defaultAPISessionExpirationDuration = 30 * time.Second
|
||||
|
||||
var APISessionExpirationDuration = sync.OnceValue[time.Duration](func() time.Duration {
|
||||
expirationDuration, err := strconv.Atoi(os.Getenv("API_SESSION_EXPIRATION_DURATION"))
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to load API_SESSION_EXPIRATION_DURATION")
|
||||
return defaultAPISessionExpirationDuration
|
||||
}
|
||||
return time.Duration(expirationDuration * int(time.Second))
|
||||
})
|
||||
|
||||
var APISecure = sync.OnceValue[bool](func() bool {
|
||||
return os.Getenv("API_SECURE") == "true"
|
||||
})
|
||||
|
||||
var (
|
||||
ErrSessionIdCollision = errors.New("sessionId collision")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
)
|
||||
|
||||
func generateSessionID() (string, error) {
|
||||
sessionID := make([]byte, 32)
|
||||
if _, err := rand.Read(sessionID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(sessionID), nil
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
l sync.RWMutex
|
||||
sessionId string
|
||||
expirationTime time.Time
|
||||
}
|
||||
|
||||
func (s *Session) GenerateCookie() *http.Cookie {
|
||||
s.l.RLock()
|
||||
defer s.l.RUnlock()
|
||||
|
||||
return &http.Cookie{
|
||||
Name: "session_id",
|
||||
Value: s.sessionId,
|
||||
HttpOnly: true,
|
||||
Secure: APISecure(),
|
||||
Expires: s.expirationTime,
|
||||
}
|
||||
}
|
||||
|
||||
type SessionStore struct {
|
||||
l sync.RWMutex
|
||||
|
||||
ctx context.Context
|
||||
fnCancel context.CancelFunc
|
||||
|
||||
sessions map[string]*Session
|
||||
}
|
||||
|
||||
func NewSessionStore(ctx context.Context) *SessionStore {
|
||||
ctxChild, fnCancel := context.WithCancel(ctx)
|
||||
|
||||
s := &SessionStore{
|
||||
ctx: ctxChild,
|
||||
fnCancel: fnCancel,
|
||||
sessions: map[string]*Session{},
|
||||
}
|
||||
s.purgeWorker()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *SessionStore) purge() {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
toDelete := []*Session{}
|
||||
for _, session := range s.sessions {
|
||||
if now.After(session.expirationTime) {
|
||||
toDelete = append(toDelete, session)
|
||||
}
|
||||
}
|
||||
|
||||
for _, session := range toDelete {
|
||||
log.Debug().Str("sessionId", session.sessionId).Msg("purge expired session")
|
||||
delete(s.sessions, session.sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SessionStore) purgeWorker() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.purge()
|
||||
case <-s.ctx.Done():
|
||||
log.Info().Msg("purge worker stopped")
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *SessionStore) Stop() {
|
||||
s.fnCancel()
|
||||
}
|
||||
|
||||
func (s *SessionStore) Done() <-chan struct{} {
|
||||
return s.ctx.Done()
|
||||
}
|
||||
|
||||
func (s *SessionStore) NewSession() (*Session, error) {
|
||||
sessionId, err := generateSessionID()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("unable to generate sessionId")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
|
||||
if _, ok := s.sessions[sessionId]; ok {
|
||||
log.Error().Str("sessionId", sessionId).Msg("sessionId collision")
|
||||
return nil, ErrSessionIdCollision
|
||||
}
|
||||
|
||||
now := time.Now().Add(APISessionExpirationDuration())
|
||||
session := Session{expirationTime: now, sessionId: sessionId}
|
||||
s.sessions[sessionId] = &session
|
||||
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func (s *SessionStore) IsLogged(r *http.Request) bool {
|
||||
cookie, err := r.Cookie("session_id")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
s.l.RLock()
|
||||
defer s.l.RUnlock()
|
||||
|
||||
_, ok := s.sessions[cookie.Value]
|
||||
return ok
|
||||
}
|
||||
Reference in New Issue
Block a user