Compare commits

..

1 Commits

Author SHA1 Message Date
Alex d028796ca1
Merge 541d2bd77d into b144d9926f 2025-10-08 20:59:02 +00:00
5 changed files with 47 additions and 185 deletions

View File

@ -512,12 +512,7 @@ func handleSessionRequest(
_ = wsjson.Write(context.Background(), c, gin.H{"error": "session manager not initialized"})
return fmt.Errorf("session manager not initialized")
}
scopedLogger.Debug().Msg("About to call AddSession")
err = sessionManager.AddSession(session, req.SessionSettings)
scopedLogger.Debug().
Bool("addSessionSucceeded", err == nil).
Str("error", fmt.Sprintf("%v", err)).
Msg("AddSession returned")
if err != nil {
scopedLogger.Warn().Err(err).Msg("failed to add session to session manager")
if err == ErrMaxSessionsReached {
@ -527,7 +522,6 @@ func handleSessionRequest(
}
return err
}
scopedLogger.Debug().Msg("AddSession completed successfully, continuing")
if session.HasPermission(PermissionPaste) {
cancelKeyboardMacro()

View File

@ -126,13 +126,8 @@ func NewSessionManager(logger *zerolog.Logger) *SessionManager {
}
func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSettings) error {
sm.logger.Debug().
Str("sessionID", session.ID).
Msg("AddSession ENTRY")
// Basic input validation
if session == nil {
sm.logger.Error().Msg("AddSession: session is nil")
return errors.New("session cannot be nil")
}
// Validate nickname if provided (matching frontend validation)
@ -168,10 +163,6 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
// Check if a session with this ID already exists (reconnection)
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
if existing.Identity != session.Identity || existing.Source != session.Source {
return fmt.Errorf("session ID already in use by different user (identity mismatch)")
@ -229,18 +220,11 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
// NOTE: Skip validation during reconnection to preserve grace period
// validateSinglePrimary() would clear primary slot during reconnection window
sm.logger.Debug().
Str("sessionID", session.ID).
Msg("AddSession: RETURNING from reconnection path")
go sm.broadcastSessionListUpdate()
return nil
}
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
}
@ -428,101 +412,60 @@ func (sm *SessionManager) RemoveSession(sessionID string) {
// Remove from queue if present
sm.removeFromQueue(sessionID)
// Check if this session was marked for immediate removal (intentional logout)
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)
// Add a grace period for reconnection for all sessions
// Use configured grace period or default to 10 seconds
gracePeriod := 10
if currentSessionSettings != nil && currentSessionSettings.ReconnectGrace > 0 {
gracePeriod = currentSessionSettings.ReconnectGrace
}
// Only add grace period if this is NOT an intentional logout
if !isIntentionalLogout {
// Add a grace period for reconnection for all sessions
// Limit grace period entries to prevent memory exhaustion (DoS protection)
const maxGraceEntries = 10 // Reduced from 20 to limit memory usage
for len(sm.reconnectGrace) >= maxGraceEntries {
// Find and remove the oldest grace period entry
var oldestID string
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
// Limit grace period entries to prevent memory exhaustion (DoS protection)
const maxGraceEntries = 10 // Reduced from 20 to limit memory usage
for len(sm.reconnectGrace) >= maxGraceEntries {
// Find and remove the oldest grace period entry
var oldestID string
var oldestTime time.Time
for id, graceTime := range sm.reconnectGrace {
if oldestTime.IsZero() || graceTime.Before(oldestTime) {
oldestID = id
oldestTime = graceTime
}
}
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 oldestID != "" {
delete(sm.reconnectGrace, oldestID)
delete(sm.reconnectInfo, oldestID)
} else {
break // Safety check to prevent infinite loop
}
}
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 wasPrimary {
if isIntentionalLogout {
// Intentional logout: clear immediately and promote right away
sm.primarySessionID = ""
sm.lastPrimaryID = ""
sm.logger.Info().
Str("sessionID", sessionID).
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
sm.lastPrimaryID = sessionID // Remember this was the primary for grace period
sm.primarySessionID = "" // Clear primary slot so other sessions can be promoted
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")
// Clear all blacklists to allow emergency promotion after grace period expires
// 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
// Immediate promotion check: if there are observers waiting, trigger validation
// This allows immediate promotion while still respecting grace period protection
if len(sm.sessions) > 0 {
sm.logger.Debug().
Str("removedPrimaryID", sessionID).
Bool("intentionalLogout", isIntentionalLogout).
Int("remainingSessions", len(sm.sessions)).
Msg("Triggering immediate validation for potential promotion")
sm.validateSinglePrimary()
@ -566,28 +509,6 @@ func (sm *SessionManager) IsInGracePeriod(sessionID string) bool {
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
func (sm *SessionManager) isSessionBlacklisted(sessionID string) bool {
now := time.Now()
@ -1372,7 +1293,6 @@ func (sm *SessionManager) findMostTrustedSessionForEmergency() string {
bestSessionID := ""
bestScore := -1
// First pass: try to find observers or queued sessions (preferred)
for sessionID, session := range sm.sessions {
// Skip if blacklisted, primary, or not eligible modes
if sm.isSessionBlacklisted(sessionID) ||
@ -1388,23 +1308,6 @@ 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
if bestSessionID != "" {
sm.logger.Info().
@ -1601,11 +1504,9 @@ func (sm *SessionManager) cleanupInactiveSessions(ctx context.Context) {
needsBroadcast := false
// Check for expired grace periods and promote if needed
gracePeriodExpired := false
for sessionID, graceTime := range sm.reconnectGrace {
if now.After(graceTime) {
delete(sm.reconnectGrace, sessionID)
gracePeriodExpired = true
wasHoldingPrimarySlot := (sm.lastPrimaryID == sessionID)
@ -1818,18 +1719,12 @@ func (sm *SessionManager) cleanupInactiveSessions(ctx context.Context) {
}
}
// Run validation immediately if a grace period expired, otherwise run periodically
if gracePeriodExpired {
sm.logger.Debug().Msg("Running immediate validation after grace period expiration")
// 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()
} 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()

View File

@ -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"
>
<div className="relative flex flex-wrap items-center gap-x-2 gap-y-2">
{developerMode && hasPermission(Permission.TERMINAL_ACCESS) && (
{developerMode && (
<Button
size="XS"
theme="light"

31
web.go
View File

@ -357,13 +357,6 @@ func handleWebRTCSignalWsMessages(
typ, msg, err := wsCon.Read(runCtx)
if err != nil {
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
}
if typ != websocket.MessageText {
@ -488,30 +481,10 @@ func handleLogin(c *gin.Context) {
}
func handleLogout(c *gin.Context) {
// 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
// Only clear the cookies for this session, don't invalidate the token
// The token should remain valid for other sessions
c.SetCookie("authToken", "", -1, "/", "", false, true)
c.SetCookie("sessionId", "", -1, "/", "", false, true)
c.SetCookie("sessionId", "", -1, "/", "", false, true) // Clear session ID cookie too
c.JSON(http.StatusOK, gin.H{"message": "Logout successful"})
}

View File

@ -68,7 +68,7 @@ type Session struct {
// CheckRPCRateLimit checks if the session has exceeded RPC rate limits (DoS protection)
func (s *Session) CheckRPCRateLimit() bool {
const (
maxRPCPerSecond = 100 // Increased from 20 to accommodate multi-session polling and reconnections
maxRPCPerSecond = 20
rateLimitWindow = time.Second
)