Merge dev to main - Immich and Terminal fixes #36
@@ -2,6 +2,7 @@ import asyncio
|
||||
import struct
|
||||
import logging
|
||||
import shlex
|
||||
import jwt
|
||||
from collections import Counter
|
||||
from fastapi import WebSocket, Query
|
||||
import asyncssh
|
||||
@@ -90,19 +91,29 @@ async def _release_session(session_id: int | None):
|
||||
_active_sessions.pop(session_id, None)
|
||||
|
||||
|
||||
async def ws_endpoint(websocket: WebSocket, host: str = Query("nas"), token: str | None = Query(None)):
|
||||
async def ws_endpoint(websocket: WebSocket, host: str = Query("nas")):
|
||||
if host not in HOSTS:
|
||||
await websocket.close(code=1008, reason="Unknown host")
|
||||
return
|
||||
|
||||
access_token = websocket.cookies.get(auth.ACCESS_COOKIE_NAME) or token
|
||||
access_token = websocket.cookies.get(auth.ACCESS_COOKIE_NAME)
|
||||
if not access_token:
|
||||
logger.warning("WebSocket auth failed for %s: no access token cookie", host)
|
||||
await websocket.close(code=1008, reason="Unauthorized")
|
||||
return
|
||||
|
||||
try:
|
||||
user = await auth.get_current_user_ws(access_token)
|
||||
except Exception:
|
||||
except jwt.ExpiredSignatureError as e:
|
||||
logger.warning("WebSocket auth failed for %s: token expired - %s", host, e)
|
||||
await websocket.close(code=1008, reason="Token expired")
|
||||
return
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.warning("WebSocket auth failed for %s: invalid token - %s", host, e)
|
||||
await websocket.close(code=1008, reason="Invalid token")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error("WebSocket auth failed for %s: unexpected error - %s", host, e)
|
||||
await websocket.close(code=1008, reason="Unauthorized")
|
||||
return
|
||||
|
||||
|
||||
@@ -196,8 +196,11 @@
|
||||
let reconnecting = false;
|
||||
let authRecoveryInFlight = false;
|
||||
let reconnectAttempts = 0;
|
||||
let consecutiveAuthFailures = 0;
|
||||
const MAX_RECONNECT_DELAY = 30000;
|
||||
const PONG_TIMEOUT = 5000; // 5s to receive pong
|
||||
const MAX_AUTH_FAILURES = 3;
|
||||
const PING_INTERVAL = 30000; // 30s between pings
|
||||
const PONG_TIMEOUT = 10000; // 10s to receive pong
|
||||
|
||||
function clearHeartbeat() {
|
||||
if (heartbeat) {
|
||||
@@ -263,12 +266,18 @@
|
||||
}
|
||||
|
||||
async function connect(forceRefresh = false) {
|
||||
if (forceRefresh) {
|
||||
// Always refresh token before first connection to ensure fresh cookies
|
||||
const d = tabData.get(tabId);
|
||||
const isFirstConnect = !d?.hasConnectedOnce;
|
||||
|
||||
if (forceRefresh || isFirstConnect) {
|
||||
const refreshed = await tryRefreshSession();
|
||||
if (!refreshed) {
|
||||
handleAuthExpired();
|
||||
return null;
|
||||
}
|
||||
// Mark that we've connected once with fresh token
|
||||
if (d) d.hasConnectedOnce = true;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(
|
||||
@@ -278,6 +287,7 @@
|
||||
|
||||
ws.onopen = () => {
|
||||
reconnectAttempts = 0;
|
||||
consecutiveAuthFailures = 0; // Reset auth failure counter on successful connection
|
||||
updateTab(tabId, {
|
||||
connected: true,
|
||||
error: "",
|
||||
@@ -295,7 +305,7 @@
|
||||
}
|
||||
}, PONG_TIMEOUT);
|
||||
}
|
||||
}, 12000);
|
||||
}, PING_INTERVAL);
|
||||
const d = tabData.get(tabId);
|
||||
if (d) {
|
||||
d.ws = ws;
|
||||
@@ -335,7 +345,18 @@
|
||||
const reason = e.reason || `Connection closed (code ${e.code})`;
|
||||
const d = tabData.get(tabId);
|
||||
if (d) d.ws = null;
|
||||
if (e.code === 1008 && reason === "Unauthorized") {
|
||||
|
||||
// Handle auth failures (code 1008)
|
||||
if (e.code === 1008) {
|
||||
consecutiveAuthFailures++;
|
||||
|
||||
// Stop after MAX_AUTH_FAILURES consecutive auth failures
|
||||
if (consecutiveAuthFailures >= MAX_AUTH_FAILURES) {
|
||||
handleAuthExpired(`Session expired after ${MAX_AUTH_FAILURES} attempts — please refresh page`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to refresh session
|
||||
if (!closedManually && !authRecoveryInFlight) {
|
||||
authRecoveryInFlight = true;
|
||||
const refreshed = await tryRefreshSession();
|
||||
@@ -344,7 +365,7 @@
|
||||
reconnecting = true;
|
||||
updateTab(tabId, {
|
||||
connected: false,
|
||||
error: "Refreshing session...",
|
||||
error: `Refreshing session (attempt ${consecutiveAuthFailures}/${MAX_AUTH_FAILURES})...`,
|
||||
statusBanner: tab.persistent ? "Reconnecting to persistent session..." : "",
|
||||
});
|
||||
term.write(`\r\n\x1b[90m[Refreshing session...]\x1b[0m`);
|
||||
@@ -355,6 +376,8 @@
|
||||
handleAuthExpired("Session expired — please log in again");
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-auth failures: continue with normal reconnection
|
||||
if (!closedManually) scheduleReconnect(reason);
|
||||
else {
|
||||
clearHeartbeat();
|
||||
|
||||
Reference in New Issue
Block a user