mirror of https://github.com/jetkvm/kvm.git
Compare commits
9 Commits
d028796ca1
...
b8bf5f8c60
| Author | SHA1 | Date |
|---|---|---|
|
|
b8bf5f8c60 | |
|
|
c8b456bf6a | |
|
|
57f4be2846 | |
|
|
b388bc3c62 | |
|
|
7901677551 | |
|
|
ba8caf3448 | |
|
|
b144d9926f | |
|
|
e755a6e1b1 | |
|
|
99a8c2711c |
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "JetKVM",
|
"name": "JetKVM docker devcontainer",
|
||||||
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
|
@ -32,4 +32,5 @@ wget https://github.com/jetkvm/rv1106-system/releases/download/${BUILDKIT_VERSIO
|
||||||
sudo mkdir -p /opt/jetkvm-native-buildkit && \
|
sudo mkdir -p /opt/jetkvm-native-buildkit && \
|
||||||
sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \
|
sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \
|
||||||
rm buildkit.tar.zst
|
rm buildkit.tar.zst
|
||||||
popd
|
popd
|
||||||
|
rm -rf "${BUILDKIT_TMPDIR}"
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "JetKVM podman devcontainer",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
// Should match what is defined in ui/package.json
|
||||||
|
"version": "22.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runArgs": [
|
||||||
|
"--userns=keep-id",
|
||||||
|
"--security-opt=label=disable",
|
||||||
|
"--security-opt=label=nested"
|
||||||
|
],
|
||||||
|
"containerUser": "vscode",
|
||||||
|
"containerEnv": {
|
||||||
|
"HOME": "/home/vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
cloud.go
6
cloud.go
|
|
@ -512,7 +512,12 @@ func handleSessionRequest(
|
||||||
_ = wsjson.Write(context.Background(), c, gin.H{"error": "session manager not initialized"})
|
_ = wsjson.Write(context.Background(), c, gin.H{"error": "session manager not initialized"})
|
||||||
return fmt.Errorf("session manager not initialized")
|
return fmt.Errorf("session manager not initialized")
|
||||||
}
|
}
|
||||||
|
scopedLogger.Debug().Msg("About to call AddSession")
|
||||||
err = sessionManager.AddSession(session, req.SessionSettings)
|
err = sessionManager.AddSession(session, req.SessionSettings)
|
||||||
|
scopedLogger.Debug().
|
||||||
|
Bool("addSessionSucceeded", err == nil).
|
||||||
|
Str("error", fmt.Sprintf("%v", err)).
|
||||||
|
Msg("AddSession returned")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to add session to session manager")
|
scopedLogger.Warn().Err(err).Msg("failed to add session to session manager")
|
||||||
if err == ErrMaxSessionsReached {
|
if err == ErrMaxSessionsReached {
|
||||||
|
|
@ -522,6 +527,7 @@ func handleSessionRequest(
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
scopedLogger.Debug().Msg("AddSession completed successfully, continuing")
|
||||||
|
|
||||||
if session.HasPermission(PermissionPaste) {
|
if session.HasPermission(PermissionPaste) {
|
||||||
cancelKeyboardMacro()
|
cancelKeyboardMacro()
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,13 @@ func NewSessionManager(logger *zerolog.Logger) *SessionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSettings) error {
|
func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSettings) error {
|
||||||
|
sm.logger.Debug().
|
||||||
|
Str("sessionID", session.ID).
|
||||||
|
Msg("AddSession ENTRY")
|
||||||
|
|
||||||
// Basic input validation
|
// Basic input validation
|
||||||
if session == nil {
|
if session == nil {
|
||||||
|
sm.logger.Error().Msg("AddSession: session is nil")
|
||||||
return errors.New("session cannot be nil")
|
return errors.New("session cannot be nil")
|
||||||
}
|
}
|
||||||
// Validate nickname if provided (matching frontend validation)
|
// Validate nickname if provided (matching frontend validation)
|
||||||
|
|
@ -163,6 +168,10 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
|
|
||||||
// Check if a session with this ID already exists (reconnection)
|
// Check if a session with this ID already exists (reconnection)
|
||||||
if existing, exists := sm.sessions[session.ID]; exists {
|
if existing, exists := sm.sessions[session.ID]; exists {
|
||||||
|
sm.logger.Debug().
|
||||||
|
Str("sessionID", session.ID).
|
||||||
|
Msg("AddSession: session ID already exists - RECONNECTION PATH")
|
||||||
|
|
||||||
// SECURITY: Verify identity matches to prevent session hijacking
|
// SECURITY: Verify identity matches to prevent session hijacking
|
||||||
if existing.Identity != session.Identity || existing.Source != session.Source {
|
if existing.Identity != session.Identity || existing.Source != session.Source {
|
||||||
return fmt.Errorf("session ID already in use by different user (identity mismatch)")
|
return fmt.Errorf("session ID already in use by different user (identity mismatch)")
|
||||||
|
|
@ -220,11 +229,18 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
// NOTE: Skip validation during reconnection to preserve grace period
|
// NOTE: Skip validation during reconnection to preserve grace period
|
||||||
// validateSinglePrimary() would clear primary slot during reconnection window
|
// validateSinglePrimary() would clear primary slot during reconnection window
|
||||||
|
|
||||||
|
sm.logger.Debug().
|
||||||
|
Str("sessionID", session.ID).
|
||||||
|
Msg("AddSession: RETURNING from reconnection path")
|
||||||
go sm.broadcastSessionListUpdate()
|
go sm.broadcastSessionListUpdate()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sm.sessions) >= sm.maxSessions {
|
if len(sm.sessions) >= sm.maxSessions {
|
||||||
|
sm.logger.Warn().
|
||||||
|
Int("currentSessions", len(sm.sessions)).
|
||||||
|
Int("maxSessions", sm.maxSessions).
|
||||||
|
Msg("AddSession: MAX SESSIONS REACHED")
|
||||||
return ErrMaxSessionsReached
|
return ErrMaxSessionsReached
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,60 +428,101 @@ func (sm *SessionManager) RemoveSession(sessionID string) {
|
||||||
// Remove from queue if present
|
// Remove from queue if present
|
||||||
sm.removeFromQueue(sessionID)
|
sm.removeFromQueue(sessionID)
|
||||||
|
|
||||||
// Add a grace period for reconnection for all sessions
|
// Check if this session was marked for immediate removal (intentional logout)
|
||||||
// Use configured grace period or default to 10 seconds
|
isIntentionalLogout := false
|
||||||
|
if graceTime, exists := sm.reconnectGrace[sessionID]; exists {
|
||||||
|
// If grace period is already expired, this was intentional logout
|
||||||
|
if time.Now().After(graceTime) {
|
||||||
|
isIntentionalLogout = true
|
||||||
|
sm.logger.Info().
|
||||||
|
Str("sessionID", sessionID).
|
||||||
|
Msg("Detected intentional logout - skipping grace period")
|
||||||
|
delete(sm.reconnectGrace, sessionID)
|
||||||
|
delete(sm.reconnectInfo, sessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine grace period duration (used for logging even if intentional logout)
|
||||||
gracePeriod := 10
|
gracePeriod := 10
|
||||||
if currentSessionSettings != nil && currentSessionSettings.ReconnectGrace > 0 {
|
if currentSessionSettings != nil && currentSessionSettings.ReconnectGrace > 0 {
|
||||||
gracePeriod = currentSessionSettings.ReconnectGrace
|
gracePeriod = currentSessionSettings.ReconnectGrace
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit grace period entries to prevent memory exhaustion (DoS protection)
|
// Only add grace period if this is NOT an intentional logout
|
||||||
const maxGraceEntries = 10 // Reduced from 20 to limit memory usage
|
if !isIntentionalLogout {
|
||||||
for len(sm.reconnectGrace) >= maxGraceEntries {
|
// Add a grace period for reconnection for all sessions
|
||||||
// Find and remove the oldest grace period entry
|
|
||||||
var oldestID string
|
// Limit grace period entries to prevent memory exhaustion (DoS protection)
|
||||||
var oldestTime time.Time
|
const maxGraceEntries = 10 // Reduced from 20 to limit memory usage
|
||||||
for id, graceTime := range sm.reconnectGrace {
|
for len(sm.reconnectGrace) >= maxGraceEntries {
|
||||||
if oldestTime.IsZero() || graceTime.Before(oldestTime) {
|
// Find and remove the oldest grace period entry
|
||||||
oldestID = id
|
var oldestID string
|
||||||
oldestTime = graceTime
|
var oldestTime time.Time
|
||||||
|
for id, graceTime := range sm.reconnectGrace {
|
||||||
|
if oldestTime.IsZero() || graceTime.Before(oldestTime) {
|
||||||
|
oldestID = id
|
||||||
|
oldestTime = graceTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if oldestID != "" {
|
||||||
|
delete(sm.reconnectGrace, oldestID)
|
||||||
|
delete(sm.reconnectInfo, oldestID)
|
||||||
|
} else {
|
||||||
|
break // Safety check to prevent infinite loop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if oldestID != "" {
|
|
||||||
delete(sm.reconnectGrace, oldestID)
|
sm.reconnectGrace[sessionID] = time.Now().Add(time.Duration(gracePeriod) * time.Second)
|
||||||
delete(sm.reconnectInfo, oldestID)
|
|
||||||
} else {
|
// Store session info for potential reconnection
|
||||||
break // Safety check to prevent infinite loop
|
sm.reconnectInfo[sessionID] = &SessionData{
|
||||||
|
ID: session.ID,
|
||||||
|
Mode: session.Mode,
|
||||||
|
Source: session.Source,
|
||||||
|
Identity: session.Identity,
|
||||||
|
Nickname: session.Nickname,
|
||||||
|
CreatedAt: session.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sm.reconnectGrace[sessionID] = time.Now().Add(time.Duration(gracePeriod) * time.Second)
|
|
||||||
|
|
||||||
// Store session info for potential reconnection
|
|
||||||
sm.reconnectInfo[sessionID] = &SessionData{
|
|
||||||
ID: session.ID,
|
|
||||||
Mode: session.Mode,
|
|
||||||
Source: session.Source,
|
|
||||||
Identity: session.Identity,
|
|
||||||
Nickname: session.Nickname,
|
|
||||||
CreatedAt: session.CreatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this was the primary session, clear primary slot and track for grace period
|
// If this was the primary session, clear primary slot and track for grace period
|
||||||
if wasPrimary {
|
if wasPrimary {
|
||||||
sm.lastPrimaryID = sessionID // Remember this was the primary for grace period
|
if isIntentionalLogout {
|
||||||
sm.primarySessionID = "" // Clear primary slot so other sessions can be promoted
|
// Intentional logout: clear immediately and promote right away
|
||||||
sm.logger.Info().
|
sm.primarySessionID = ""
|
||||||
Str("sessionID", sessionID).
|
sm.lastPrimaryID = ""
|
||||||
Dur("gracePeriod", time.Duration(gracePeriod)*time.Second).
|
sm.logger.Info().
|
||||||
Int("remainingSessions", len(sm.sessions)).
|
Str("sessionID", sessionID).
|
||||||
Msg("Primary session removed, grace period active")
|
Int("remainingSessions", len(sm.sessions)).
|
||||||
|
Msg("Primary session removed via intentional logout - immediate promotion")
|
||||||
|
} else {
|
||||||
|
// Accidental disconnect: use grace period
|
||||||
|
sm.lastPrimaryID = sessionID // Remember this was the primary for grace period
|
||||||
|
sm.primarySessionID = "" // Clear primary slot so other sessions can be promoted
|
||||||
|
|
||||||
// Immediate promotion check: if there are observers waiting, trigger validation
|
// Clear all blacklists to allow emergency promotion after grace period expires
|
||||||
// This allows immediate promotion while still respecting grace period protection
|
// The blacklist is meant to prevent immediate re-promotion during manual transfers,
|
||||||
|
// but should not block emergency promotion after accidental disconnects
|
||||||
|
if len(sm.transferBlacklist) > 0 {
|
||||||
|
sm.logger.Info().
|
||||||
|
Int("clearedBlacklistEntries", len(sm.transferBlacklist)).
|
||||||
|
Str("disconnectedPrimaryID", sessionID).
|
||||||
|
Msg("Clearing transfer blacklist to allow grace period promotion")
|
||||||
|
sm.transferBlacklist = make([]TransferBlacklistEntry, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.logger.Info().
|
||||||
|
Str("sessionID", sessionID).
|
||||||
|
Dur("gracePeriod", time.Duration(gracePeriod)*time.Second).
|
||||||
|
Int("remainingSessions", len(sm.sessions)).
|
||||||
|
Msg("Primary session removed, grace period active")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger validation for potential promotion
|
||||||
if len(sm.sessions) > 0 {
|
if len(sm.sessions) > 0 {
|
||||||
sm.logger.Debug().
|
sm.logger.Debug().
|
||||||
Str("removedPrimaryID", sessionID).
|
Str("removedPrimaryID", sessionID).
|
||||||
|
Bool("intentionalLogout", isIntentionalLogout).
|
||||||
Int("remainingSessions", len(sm.sessions)).
|
Int("remainingSessions", len(sm.sessions)).
|
||||||
Msg("Triggering immediate validation for potential promotion")
|
Msg("Triggering immediate validation for potential promotion")
|
||||||
sm.validateSinglePrimary()
|
sm.validateSinglePrimary()
|
||||||
|
|
@ -509,6 +566,28 @@ func (sm *SessionManager) IsInGracePeriod(sessionID string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearGracePeriod removes the grace period for a session (for intentional logout/disconnect)
|
||||||
|
// This marks the session for immediate removal without grace period protection
|
||||||
|
// Actual promotion will happen in RemoveSession when it detects no grace period
|
||||||
|
func (sm *SessionManager) ClearGracePeriod(sessionID string) {
|
||||||
|
sm.mu.Lock()
|
||||||
|
defer sm.mu.Unlock()
|
||||||
|
|
||||||
|
// Clear grace period and reconnect info to prevent grace period from being added
|
||||||
|
delete(sm.reconnectGrace, sessionID)
|
||||||
|
delete(sm.reconnectInfo, sessionID)
|
||||||
|
|
||||||
|
// Mark this session with a special "immediate removal" grace period (already expired)
|
||||||
|
// This signals to RemoveSession that this was intentional and should skip grace period
|
||||||
|
sm.reconnectGrace[sessionID] = time.Now().Add(-1 * time.Second) // Already expired
|
||||||
|
|
||||||
|
sm.logger.Info().
|
||||||
|
Str("sessionID", sessionID).
|
||||||
|
Str("lastPrimaryID", sm.lastPrimaryID).
|
||||||
|
Str("primarySessionID", sm.primarySessionID).
|
||||||
|
Msg("Marked session for immediate removal (intentional logout)")
|
||||||
|
}
|
||||||
|
|
||||||
// isSessionBlacklisted checks if a session was recently demoted via transfer and should not become primary
|
// isSessionBlacklisted checks if a session was recently demoted via transfer and should not become primary
|
||||||
func (sm *SessionManager) isSessionBlacklisted(sessionID string) bool {
|
func (sm *SessionManager) isSessionBlacklisted(sessionID string) bool {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
@ -1293,6 +1372,7 @@ func (sm *SessionManager) findMostTrustedSessionForEmergency() string {
|
||||||
bestSessionID := ""
|
bestSessionID := ""
|
||||||
bestScore := -1
|
bestScore := -1
|
||||||
|
|
||||||
|
// First pass: try to find observers or queued sessions (preferred)
|
||||||
for sessionID, session := range sm.sessions {
|
for sessionID, session := range sm.sessions {
|
||||||
// Skip if blacklisted, primary, or not eligible modes
|
// Skip if blacklisted, primary, or not eligible modes
|
||||||
if sm.isSessionBlacklisted(sessionID) ||
|
if sm.isSessionBlacklisted(sessionID) ||
|
||||||
|
|
@ -1308,6 +1388,23 @@ func (sm *SessionManager) findMostTrustedSessionForEmergency() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no observers/queued found, try pending sessions as last resort
|
||||||
|
if bestSessionID == "" {
|
||||||
|
for sessionID, session := range sm.sessions {
|
||||||
|
if sm.isSessionBlacklisted(sessionID) || session.Mode == SessionModePrimary {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.Mode == SessionModePending {
|
||||||
|
score := sm.getSessionTrustScore(sessionID)
|
||||||
|
if score > bestScore {
|
||||||
|
bestScore = score
|
||||||
|
bestSessionID = sessionID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the selection decision for audit trail
|
// Log the selection decision for audit trail
|
||||||
if bestSessionID != "" {
|
if bestSessionID != "" {
|
||||||
sm.logger.Info().
|
sm.logger.Info().
|
||||||
|
|
@ -1504,9 +1601,11 @@ func (sm *SessionManager) cleanupInactiveSessions(ctx context.Context) {
|
||||||
needsBroadcast := false
|
needsBroadcast := false
|
||||||
|
|
||||||
// Check for expired grace periods and promote if needed
|
// Check for expired grace periods and promote if needed
|
||||||
|
gracePeriodExpired := false
|
||||||
for sessionID, graceTime := range sm.reconnectGrace {
|
for sessionID, graceTime := range sm.reconnectGrace {
|
||||||
if now.After(graceTime) {
|
if now.After(graceTime) {
|
||||||
delete(sm.reconnectGrace, sessionID)
|
delete(sm.reconnectGrace, sessionID)
|
||||||
|
gracePeriodExpired = true
|
||||||
|
|
||||||
wasHoldingPrimarySlot := (sm.lastPrimaryID == sessionID)
|
wasHoldingPrimarySlot := (sm.lastPrimaryID == sessionID)
|
||||||
|
|
||||||
|
|
@ -1719,12 +1818,18 @@ func (sm *SessionManager) cleanupInactiveSessions(ctx context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Periodic validateSinglePrimary to catch deadlock states
|
// Run validation immediately if a grace period expired, otherwise run periodically
|
||||||
validationCounter++
|
if gracePeriodExpired {
|
||||||
if validationCounter >= 10 { // Every 10 seconds
|
sm.logger.Debug().Msg("Running immediate validation after grace period expiration")
|
||||||
validationCounter = 0
|
|
||||||
sm.logger.Debug().Msg("Running periodic session validation to catch deadlock states")
|
|
||||||
sm.validateSinglePrimary()
|
sm.validateSinglePrimary()
|
||||||
|
} else {
|
||||||
|
// Periodic validateSinglePrimary to catch deadlock states
|
||||||
|
validationCounter++
|
||||||
|
if validationCounter >= 10 { // Every 10 seconds
|
||||||
|
validationCounter = 0
|
||||||
|
sm.logger.Debug().Msg("Running periodic session validation to catch deadlock states")
|
||||||
|
sm.validateSinglePrimary()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sm.mu.Unlock()
|
sm.mu.Unlock()
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export default function Actionbar({
|
||||||
className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 py-1.5"
|
className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 py-1.5"
|
||||||
>
|
>
|
||||||
<div className="relative flex flex-wrap items-center gap-x-2 gap-y-2">
|
<div className="relative flex flex-wrap items-center gap-x-2 gap-y-2">
|
||||||
{developerMode && (
|
{developerMode && hasPermission(Permission.TERMINAL_ACCESS) && (
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
|
|
|
||||||
|
|
@ -374,8 +374,8 @@ function UrlView({
|
||||||
icon: FedoraIcon,
|
icon: FedoraIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "openSUSE Leap 15.6",
|
name: "openSUSE Leap 16.0",
|
||||||
url: "https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-NET-x86_64-Media.iso",
|
url: "https://download.opensuse.org/distribution/leap/16.0/offline/Leap-16.0-online-installer-x86_64.install.iso",
|
||||||
icon: OpenSUSEIcon,
|
icon: OpenSUSEIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
31
web.go
31
web.go
|
|
@ -357,6 +357,13 @@ func handleWebRTCSignalWsMessages(
|
||||||
typ, msg, err := wsCon.Read(runCtx)
|
typ, msg, err := wsCon.Read(runCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warn().Str("error", err.Error()).Msg("websocket read error")
|
l.Warn().Str("error", err.Error()).Msg("websocket read error")
|
||||||
|
// Clean up session when websocket closes
|
||||||
|
if session := sessionManager.GetSession(connectionID); session != nil && session.peerConnection != nil {
|
||||||
|
l.Info().
|
||||||
|
Str("sessionID", session.ID).
|
||||||
|
Msg("Closing peer connection due to websocket error")
|
||||||
|
_ = session.peerConnection.Close()
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if typ != websocket.MessageText {
|
if typ != websocket.MessageText {
|
||||||
|
|
@ -481,10 +488,30 @@ func handleLogin(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLogout(c *gin.Context) {
|
func handleLogout(c *gin.Context) {
|
||||||
// Only clear the cookies for this session, don't invalidate the token
|
// Get session ID from cookie before clearing
|
||||||
|
sessionID, _ := c.Cookie("sessionId")
|
||||||
|
|
||||||
|
// Close the WebRTC session immediately for intentional logout
|
||||||
|
if sessionID != "" {
|
||||||
|
if session := sessionManager.GetSession(sessionID); session != nil {
|
||||||
|
websocketLogger.Info().
|
||||||
|
Str("sessionID", sessionID).
|
||||||
|
Msg("Closing session due to intentional logout - no grace period")
|
||||||
|
|
||||||
|
// Close peer connection (will trigger cleanupSession)
|
||||||
|
if session.peerConnection != nil {
|
||||||
|
_ = session.peerConnection.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear grace period for intentional logout - observer should be promoted immediately
|
||||||
|
sessionManager.ClearGracePeriod(sessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the cookies for this session, don't invalidate the token
|
||||||
// The token should remain valid for other sessions
|
// The token should remain valid for other sessions
|
||||||
c.SetCookie("authToken", "", -1, "/", "", false, true)
|
c.SetCookie("authToken", "", -1, "/", "", false, true)
|
||||||
c.SetCookie("sessionId", "", -1, "/", "", false, true) // Clear session ID cookie too
|
c.SetCookie("sessionId", "", -1, "/", "", false, true)
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Logout successful"})
|
c.JSON(http.StatusOK, gin.H{"message": "Logout successful"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ type Session struct {
|
||||||
// CheckRPCRateLimit checks if the session has exceeded RPC rate limits (DoS protection)
|
// CheckRPCRateLimit checks if the session has exceeded RPC rate limits (DoS protection)
|
||||||
func (s *Session) CheckRPCRateLimit() bool {
|
func (s *Session) CheckRPCRateLimit() bool {
|
||||||
const (
|
const (
|
||||||
maxRPCPerSecond = 20
|
maxRPCPerSecond = 100 // Increased from 20 to accommodate multi-session polling and reconnections
|
||||||
rateLimitWindow = time.Second
|
rateLimitWindow = time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue