3x-ui/web/middleware/session_security.go

96 lines
2.8 KiB
Go
Raw Normal View History

package middleware
import (
"crypto/sha256"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v2/logger"
redisutil "github.com/mhsanaei/3x-ui/v2/util/redis"
"github.com/mhsanaei/3x-ui/v2/web/session"
)
// DeviceFingerprint generates device fingerprint
func DeviceFingerprint(c *gin.Context) string {
userAgent := c.GetHeader("User-Agent")
ip := c.ClientIP()
acceptLanguage := c.GetHeader("Accept-Language")
acceptEncoding := c.GetHeader("Accept-Encoding")
data := fmt.Sprintf("%s|%s|%s|%s", userAgent, ip, acceptLanguage, acceptEncoding)
hash := sha256.Sum256([]byte(data))
return fmt.Sprintf("%x", hash)
}
// SessionSecurityMiddleware enforces session security
func SessionSecurityMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
user := session.GetLoginUser(c)
if user == nil {
c.Next()
return
}
// Get device fingerprint
fingerprint := DeviceFingerprint(c)
sessionKey := fmt.Sprintf("session:%d", user.Id)
deviceKey := fmt.Sprintf("device:%d:%s", user.Id, fingerprint)
// Check if device is registered
deviceExists, err := redisutil.Exists(deviceKey)
if err == nil && !deviceExists {
// New device - check max devices limit
// TODO: Get from settings
maxDevices := 5 // Default, should be configurable
devices, _ := redisutil.SMembers(fmt.Sprintf("devices:%d", user.Id))
if len(devices) >= maxDevices {
logger.Warningf("User %d attempted to login from too many devices", user.Id)
session.ClearSession(c)
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"msg": "Maximum number of devices reached",
})
c.Abort()
return
}
// Register new device
redisutil.SAdd(fmt.Sprintf("devices:%d", user.Id), fingerprint)
redisutil.Set(deviceKey, time.Now().Format(time.RFC3339), 30*24*time.Hour)
}
// Check session validity
sessionData, err := redisutil.HGetAll(sessionKey)
if err == nil {
// Check IP change
if storedIP, ok := sessionData["ip"]; ok && storedIP != c.ClientIP() {
logger.Warningf("IP change detected for user %d: %s -> %s", user.Id, storedIP, c.ClientIP())
// Optionally force re-login on IP change
// session.ClearSession(c)
// c.Abort()
// return
}
// Update last activity
redisutil.HSet(sessionKey, "last_activity", time.Now().Unix())
redisutil.HSet(sessionKey, "ip", c.ClientIP())
redisutil.Expire(sessionKey, 24*time.Hour)
}
c.Next()
}
}
// ForceLogoutDevice forces logout from specific device
func ForceLogoutDevice(userId int, fingerprint string) error {
deviceKey := fmt.Sprintf("device:%d:%s", userId, fingerprint)
return redisutil.Del(deviceKey)
}
// GetUserDevices returns all devices for user
func GetUserDevices(userId int) ([]string, error) {
return redisutil.SMembers(fmt.Sprintf("devices:%d", userId))
}