fix: dual-key rate limiting for auth + websocket reconnect guard

- Pass client IP (X-Real-IP header) through Flask to daemon for both
  password login and WebAuthn authenticate-finish endpoints
- Rate limiter now checks both IP and username buckets: IP layer catches
  enumeration/brute-force attacks across multiple usernames; username
  layer protects against single-account targeting from multiple IPs
- Add _wsRefreshing flag to prevent double-scheduling reconnect when
  onclose fires during token refresh; simplify async IIFE to .then()/.catch()
- Reset _wsRefreshing on websocket onopen for safety
This commit is contained in:
2026-07-28 00:54:48 +00:00
parent 76cd219050
commit 358573567d
4 changed files with 30 additions and 20 deletions
+13 -16
View File
@@ -12,6 +12,7 @@ import { tryRefreshToken } from './api.js?v=12';
let _wsConn = null;
let _wsReconnectMs = 0;
let _wsFailCount = 0;
let _wsRefreshing = false;
/** Direct onMessage handlers — { topics, handler, unsubscribed }[] */
const _directHandlers = [];
@@ -44,28 +45,24 @@ function _wsConnect() {
_wsConn.onopen = () => {
_wsReconnectMs = 0;
_wsFailCount = 0;
_wsRefreshing = false;
};
_wsConn.onclose = () => {
if (!window.__auth_token__) return;
_wsFailCount++;
if (_wsFailCount >= 3) {
// Attempt token refresh after repeated failures. The reconnect
// is handled inside the IIFE to avoid double-scheduling when
// refresh succeeds.
(async () => {
const ok = await tryRefreshToken();
if (ok) {
_wsFailCount = 0;
_wsReconnectMs = 0;
_wsConn = null;
setTimeout(_wsConnect, 100);
} else {
_wsReconnectMs = Math.min(_wsReconnectMs * 2 + 1000, 15000);
setTimeout(_wsConnect, _wsReconnectMs);
}
})();
if (_wsFailCount >= 3 && !_wsRefreshing) {
_wsRefreshing = true;
tryRefreshToken().then(ok => {
_wsRefreshing = false;
_wsFailCount = 0;
_wsReconnectMs = 0;
_wsConn = null;
setTimeout(_wsConnect, 100);
}).catch(() => {
_wsRefreshing = false;
});
return;
}