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}