mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-12-23 06:42:41 +00:00
96 lines
2.8 KiB
Go
96 lines
2.8 KiB
Go
|
|
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))
|
||
|
|
}
|