Files
GopherGate/internal/server/sessions.go
T
hobokenchicken 8a8d8d1477
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
fix: Phase 1 - security & stability patches
- AuthMiddleware now requires auth on /v1/* routes (returns 401)
- WebSocket origin check configurable via WSAllowedOrigin
- Removed debug fmt.Printf leaks (config, ollama, server)
- Registry access protected by sync.RWMutex (race condition fix)
- Session cleanup goroutine runs every 15 min
- RevokeSession returns error instead of silent no-op
2026-04-26 14:45:22 -04:00

174 lines
3.9 KiB
Go

package server
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
type Session struct {
Username string `json:"username"`
DisplayName string `json:"display_name"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
SessionID string `json:"session_id"`
}
type SessionManager struct {
sessions map[string]Session
mu sync.RWMutex
secret []byte
ttl time.Duration
}
type sessionPayload struct {
SessionID string `json:"session_id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
Role string `json:"role"`
Exp int64 `json:"exp"`
}
func NewSessionManager(secret []byte, ttl time.Duration) *SessionManager {
return &SessionManager{
sessions: make(map[string]Session),
secret: secret,
ttl: ttl,
}
}
func (m *SessionManager) CreateSession(username, displayName, role string) (string, error) {
sessionID := uuid.New().String()
now := time.Now()
expiresAt := now.Add(m.ttl)
m.mu.Lock()
m.sessions[sessionID] = Session{
Username: username,
DisplayName: displayName,
Role: role,
CreatedAt: now,
ExpiresAt: expiresAt,
SessionID: sessionID,
}
m.mu.Unlock()
return m.createSignedToken(sessionID, username, displayName, role, expiresAt.Unix())
}
func (m *SessionManager) createSignedToken(sessionID, username, displayName, role string, exp int64) (string, error) {
payload := sessionPayload{
SessionID: sessionID,
Username: username,
DisplayName: displayName,
Role: role,
Exp: exp,
}
payloadJSON, err := json.Marshal(payload)
if err != nil {
return "", err
}
payloadB64 := base64.RawURLEncoding.EncodeToString(payloadJSON)
h := hmac.New(sha256.New, m.secret)
h.Write(payloadJSON)
signature := h.Sum(nil)
signatureB64 := base64.RawURLEncoding.EncodeToString(signature)
return fmt.Sprintf("%s.%s", payloadB64, signatureB64), nil
}
func (m *SessionManager) ValidateSession(token string) (*Session, string, error) {
parts := strings.Split(token, ".")
if len(parts) != 2 {
return nil, "", fmt.Errorf("invalid token format")
}
payloadB64 := parts[0]
signatureB64 := parts[1]
payloadJSON, err := base64.RawURLEncoding.DecodeString(payloadB64)
if err != nil {
return nil, "", err
}
signature, err := base64.RawURLEncoding.DecodeString(signatureB64)
if err != nil {
return nil, "", err
}
h := hmac.New(sha256.New, m.secret)
h.Write(payloadJSON)
if !hmac.Equal(signature, h.Sum(nil)) {
return nil, "", fmt.Errorf("invalid signature")
}
var payload sessionPayload
if err := json.Unmarshal(payloadJSON, &payload); err != nil {
return nil, "", err
}
if time.Now().Unix() > payload.Exp {
return nil, "", fmt.Errorf("token expired")
}
m.mu.RLock()
session, ok := m.sessions[payload.SessionID]
m.mu.RUnlock()
if !ok {
return nil, "", fmt.Errorf("session not found")
}
return &session, "", nil
}
func (m *SessionManager) RevokeSession(token string) error {
parts := strings.Split(token, ".")
if len(parts) != 2 {
return fmt.Errorf("invalid token format")
}
payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return fmt.Errorf("failed to decode payload: %w", err)
}
var payload sessionPayload
if err := json.Unmarshal(payloadJSON, &payload); err != nil {
return fmt.Errorf("failed to parse payload: %w", err)
}
m.mu.Lock()
delete(m.sessions, payload.SessionID)
m.mu.Unlock()
return nil
}
// StartCleanup runs a background goroutine that removes expired sessions every 15 minutes.
func (m *SessionManager) StartCleanup() {
go func() {
ticker := time.NewTicker(15 * time.Minute)
for range ticker.C {
m.mu.Lock()
now := time.Now()
for id, s := range m.sessions {
if now.After(s.ExpiresAt) {
delete(m.sessions, id)
}
}
m.mu.Unlock()
}
}()
}