mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-01-13 01:02:46 +00:00
176 lines
4.6 KiB
Go
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()
|
|
}
|