3x-ui/web/cache/redisstore.go
2026-01-12 00:57:04 +03:00

176 lines
4.6 KiB
Go

// Package cache provides Redis store for gin sessions.
package cache
import (
"bytes"
"context"
"encoding/base32"
"encoding/gob"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-contrib/sessions"
gorillasessions "github.com/gorilla/sessions"
"github.com/gorilla/securecookie"
"github.com/redis/go-redis/v9"
)
const (
defaultMaxAge = 86400 * 7 // 7 days
)
// RedisStore stores sessions in Redis.
type RedisStore struct {
client *redis.Client
Codecs []securecookie.Codec
options *sessions.Options
}
// NewRedisStore creates a new Redis store.
func NewRedisStore(client *redis.Client, keyPairs ...[]byte) *RedisStore {
rs := &RedisStore{
client: client,
Codecs: securecookie.CodecsFromPairs(keyPairs...),
options: &sessions.Options{
Path: "/",
MaxAge: defaultMaxAge,
},
}
return rs
}
// Options sets the options for the store.
func (s *RedisStore) Options(opts sessions.Options) {
s.options = &opts
}
// Get retrieves a session from Redis.
func (s *RedisStore) Get(r *http.Request, name string) (*gorillasessions.Session, error) {
return gorillasessions.GetRegistry(r).Get(s, name)
}
// New creates a new session.
func (s *RedisStore) New(r *http.Request, name string) (*gorillasessions.Session, error) {
session := gorillasessions.NewSession(s, name)
session.Options = &gorillasessions.Options{
Path: s.options.Path,
Domain: s.options.Domain,
MaxAge: s.options.MaxAge,
Secure: s.options.Secure,
HttpOnly: s.options.HttpOnly,
SameSite: s.options.SameSite,
}
session.IsNew = true
// Try to load existing session from cookie
if c, errCookie := r.Cookie(name); errCookie == nil {
err := securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...)
if err == nil {
// Successfully decoded session ID, try to load from Redis
err = s.load(session)
if err == nil {
session.IsNew = false
}
// If load fails, continue with new session (session.IsNew = true)
}
// If decode fails (e.g., old cookie format), ignore and create new session
}
return session, nil
}
// Save saves a session to Redis.
func (s *RedisStore) Save(r *http.Request, w http.ResponseWriter, session *gorillasessions.Session) error {
// Delete if max age is < 0
if session.Options.MaxAge < 0 {
if err := s.delete(session); err != nil {
return err
}
http.SetCookie(w, s.newCookie(session, ""))
return nil
}
if session.ID == "" {
session.ID = strings.TrimRight(
base32.StdEncoding.EncodeToString(
securecookie.GenerateRandomKey(32),
), "=")
}
if err := s.save(session); err != nil {
return err
}
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, s.Codecs...)
if err != nil {
return err
}
http.SetCookie(w, s.newCookie(session, encoded))
return nil
}
// newCookie creates a new HTTP cookie for the session.
func (s *RedisStore) newCookie(session *gorillasessions.Session, value string) *http.Cookie {
cookie := &http.Cookie{
Name: session.Name(),
Value: value,
Path: session.Options.Path,
Domain: session.Options.Domain,
MaxAge: session.Options.MaxAge,
Secure: session.Options.Secure,
HttpOnly: session.Options.HttpOnly,
SameSite: session.Options.SameSite,
}
if session.Options.MaxAge > 0 {
cookie.Expires = time.Now().Add(time.Duration(session.Options.MaxAge) * time.Second)
}
return cookie
}
// save stores session data in Redis.
func (s *RedisStore) save(session *gorillasessions.Session) error {
// Use gob encoding to preserve types (especially for model.User)
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(session.Values); err != nil {
return fmt.Errorf("failed to encode session values: %w", err)
}
maxAge := session.Options.MaxAge
if maxAge == 0 {
maxAge = s.options.MaxAge
}
key := fmt.Sprintf("session:%s", session.ID)
return s.client.Set(context.Background(), key, buf.Bytes(), time.Duration(maxAge)*time.Second).Err()
}
// load retrieves session data from Redis.
func (s *RedisStore) load(session *gorillasessions.Session) error {
key := fmt.Sprintf("session:%s", session.ID)
data, err := s.client.Get(context.Background(), key).Bytes()
if err == redis.Nil {
return fmt.Errorf("session not found")
}
if err != nil {
return err
}
// Use gob decoding to preserve types (especially for model.User)
buf := bytes.NewBuffer(data)
dec := gob.NewDecoder(buf)
if err := dec.Decode(&session.Values); err != nil {
return fmt.Errorf("failed to decode session data: %w", err)
}
return nil
}
// delete removes session from Redis.
func (s *RedisStore) delete(session *gorillasessions.Session) error {
key := fmt.Sprintf("session:%s", session.ID)
return s.client.Del(context.Background(), key).Err()
}