arnaucube
arnaucube/jellyfin-toolsPublic Self-hosted repository
jellyfin-tools/jellyfin-year-review/internal/auth/sessions.go
jellyfin-year-review/internal/auth/sessions.goRaw
  1// Package auth validates online or exported credentials and keeps opaque sessions in server memory.
  2package auth
  3
  4import (
  5	"crypto/rand"
  6	"crypto/sha256"
  7	"encoding/hex"
  8	"sync"
  9	"time"
 10
 11	"reelreview/internal/source"
 12)
 13
 14type Session struct {
 15	Account source.Account
 16	Token   string
 17	Device  string
 18	Expires time.Time
 19}
 20type Store struct {
 21	mu       sync.Mutex
 22	sessions map[[32]byte]Session
 23	Now      func() time.Time
 24	TTL      time.Duration
 25}
 26
 27func New() *Store {
 28	return &Store{sessions: map[[32]byte]Session{}, Now: time.Now, TTL: 12 * time.Hour}
 29}
 30func Random() string {
 31	var b [32]byte
 32	if _, e := rand.Read(b[:]); e != nil {
 33		panic("system randomness unavailable")
 34	}
 35	return hex.EncodeToString(b[:])
 36}
 37func (s *Store) Create(a source.Account, token, device string) (string, bool) {
 38	s.mu.Lock()
 39	defer s.mu.Unlock()
 40	now := s.Now()
 41	for k, v := range s.sessions {
 42		if !now.Before(v.Expires) {
 43			delete(s.sessions, k)
 44		}
 45	}
 46	if len(s.sessions) >= 1000 {
 47		return "", false
 48	}
 49	id := Random()
 50	s.sessions[sha256.Sum256([]byte(id))] = Session{Account: a, Token: token, Device: device, Expires: now.Add(s.TTL)}
 51	return id, true
 52}
 53func (s *Store) Get(id string) (Session, bool) {
 54	s.mu.Lock()
 55	defer s.mu.Unlock()
 56	key := sha256.Sum256([]byte(id))
 57	v, ok := s.sessions[key]
 58	if len(id) != 64 || !ok || !s.Now().Before(v.Expires) {
 59		delete(s.sessions, key)
 60		return Session{}, false
 61	}
 62	return v, true
 63}
 64func (s *Store) Delete(id string) {
 65	s.mu.Lock()
 66	defer s.mu.Unlock()
 67	delete(s.sessions, sha256.Sum256([]byte(id)))
 68}
 69
 70type bucket struct {
 71	Count int
 72	Reset time.Time
 73}
 74type Limiter struct {
 75	mu      sync.Mutex
 76	entries map[string]bucket
 77	Now     func() time.Time
 78}
 79
 80func NewLimiter() *Limiter { return &Limiter{entries: map[string]bucket{}, Now: time.Now} }
 81
 82// Allow uses socket peer IP, never untrusted forwarded headers. A bounded global
 83// bucket also limits distributed login traffic and upstream authentication load.
 84func (l *Limiter) Allow(ip string) bool {
 85	l.mu.Lock()
 86	defer l.mu.Unlock()
 87	now := l.Now()
 88	for k, v := range l.entries {
 89		if !now.Before(v.Reset) {
 90			delete(l.entries, k)
 91		}
 92	}
 93	if len(l.entries) >= 4096 {
 94		return false
 95	}
 96	for _, key := range []string{"global", ip} {
 97		v := l.entries[key]
 98		if v.Reset.IsZero() {
 99			v.Reset = now.Add(time.Minute)
100		}
101		limit := 10
102		if key == "global" {
103			limit = 60
104		}
105		if v.Count >= limit {
106			return false
107		}
108		v.Count++
109		l.entries[key] = v
110	}
111	return true
112}