v0.31.3
This commit is contained in:
1 parent
0facfe6f5c
commit
95c55da2ee
39 files changed
+715
-151
No files matched your search
@@ -25,7 +25,7 @@ function panelFromLocation() { if (typeof location === "undefined") return "over
|
||||
let envelope = null, busy = false, connected = false, currentPanel = panelFromLocation(), previous = null;
|
||||
let samples = [], protocolSamples = { h1: [], h2: [], h3: [] }, websocketProtocolSamples = { h1: [], h2: [], h3: [] }, transformSamples = [], toastTimer, editingBackend = null, editingRoute = null;
|
||||
let liveEvents = null, liveConnected = false, liveFallback = false, liveInterval = null, authEpoch = 0, pendingAuthDestination = null;
|
||||
let lastApiLatencyMs = null;
|
||||
let lastApiLatencyMs = null, sessionCheckPromise = null;
|
||||
let adminAuthChannel = null; try { adminAuthChannel = new BroadcastChannel("rustproxy-admin-session"); adminAuthChannel.addEventListener("message", event => { if (event.data === "locked" && authenticated) lock(false); }); } catch { }
|
||||
// Language content is discovered by build.rs from langs/*.json, never compiled
|
||||
// into a two-language switch here. Locale metadata also drives Intl and RTL.
|
||||
@@ -388,7 +388,7 @@ async function activate(info) {
|
||||
document.documentElement.classList.remove("app-startup-failed");
|
||||
rustProxyRefresh.start();
|
||||
const preferences = await loadAdminPreferences();
|
||||
if (!authenticated || epoch !== authEpoch) return;
|
||||
if (!authenticated || epoch !== authEpoch) return false;
|
||||
window.rustProxyLogs?.applyPreferences(preferences);
|
||||
window.rustProxyFileLogs?.applyPreferences(preferences);
|
||||
window.rustProxyVhosts?.applyPreferences(preferences);
|
||||
@@ -399,17 +399,18 @@ async function activate(info) {
|
||||
for (let attempt = 0; attempt < 3 && authenticated && epoch === authEpoch; attempt++) {
|
||||
try { await loadConfig(); break; }
|
||||
catch (error) {
|
||||
if (!authenticated || epoch !== authEpoch || error?.status === 401) return;
|
||||
if (attempt === 2) { showStartupFailure(error); return; }
|
||||
if (!authenticated || epoch !== authEpoch || error?.status === 401) return false;
|
||||
if (attempt === 2) { showStartupFailure(error); return false; }
|
||||
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 400));
|
||||
}
|
||||
}
|
||||
if (!authenticated || epoch !== authEpoch) return;
|
||||
if (!authenticated || epoch !== authEpoch) return false;
|
||||
// A single full snapshot seeds non-live diagnostics; subsequent updates
|
||||
// continue over SSE rather than polling full status on a timer.
|
||||
void refresh(true).catch(error => {
|
||||
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
async function loadAudit() { if (!authenticated) return; const epoch = authEpoch; try { const data = await api("/api/audit"); if (!authenticated || epoch !== authEpoch) return; const rows = data.entries.map(item => { const tr = el("tr"); for (const value of [formatDateTime(item.time * 1000), item.actor, item.action, item.detail]) { const td = el("td", value); td.title = value; tr.append(td); } return tr; }); $("audit-rows").replaceChildren(...rows); $("empty-audit").hidden = rows.length > 0; } catch (e) { toast(e.message, true); } }
|
||||
function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); }
|
||||
@@ -475,26 +476,3 @@ $("tunnels-dialog").addEventListener("close", stopTunnelRefresh);
|
||||
$("config-editor").addEventListener("input", () => setSettingsDirty("config-editor"));
|
||||
$("save-json").addEventListener("click", async event => { const editor = $("config-editor"), page = editor.closest("[data-settings-page]"); if (!envelope || busy) return; try { await withFormSubmitState(page, event.currentTarget, async () => { let config; try { config = JSON.parse(editor.value); } catch { throw fieldError(editor, t("vhJsonInvalid")); } if (await save(config)) setSettingsDirty("config-editor", false); }); } catch (e) { toast(e.message, true); } });
|
||||
$("export-config").addEventListener("click", () => { const url = URL.createObjectURL(new Blob([JSON.stringify(envelope.config, null, 2) + "\n"], { type: "application/json" })); const a = el("a"); a.href = url; a.download = `RustProxy-config-r${envelope.revision}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); });
|
||||
async function revalidateSession() {
|
||||
const epoch = authEpoch;
|
||||
try {
|
||||
const info = await api("/api/auth/session");
|
||||
clearInvalidOriginRecovery();
|
||||
if (epoch !== authEpoch) return;
|
||||
if (!authenticated) { await activate(info); return; }
|
||||
sessionInfo = info; csrf = info.csrf_token; renderSession(); window.rustProxyRefresh?.sync();
|
||||
} catch (error) { if (recoverInvalidOrigin(error)) return; if (error.status !== 401) toast(error.message, true); }
|
||||
}
|
||||
window.addEventListener("pageshow", event => { if (event.persisted) void revalidateSession(); });
|
||||
function showStartupFailure(error) {
|
||||
// Never expose a blank page or an incomplete workspace after startup fails.
|
||||
// The authenticated shell keeps its own retry link; 401 still redirects to login.
|
||||
$("login").hidden = true;
|
||||
$("workspace").hidden = true;
|
||||
$("logout").hidden = !authenticated;
|
||||
document.documentElement.classList.add("app-loading", "app-startup-failed");
|
||||
$("app-startup-title").textContent = languagePacks.size ? t("startupFailed") : "Could not load RustProxy";
|
||||
const detail = $("app-startup-detail");
|
||||
detail.textContent = error?.message || String(error);
|
||||
detail.hidden = false;
|
||||
}
|
||||
@@ -21,9 +21,10 @@ const rustProxyRefresh = (() => {
|
||||
const tasks = new Map();
|
||||
let ready = false, options = { ...REFRESH_DEFAULTS }, panel = currentPanel;
|
||||
let settingsTab = "listeners", watchingVisibility = false;
|
||||
let sessionReady = true, visibleResumeHandled = false;
|
||||
const visible = () => !document.hidden;
|
||||
function allowed(task) {
|
||||
return ready && authenticated && visible() && options.live_enabled &&
|
||||
return ready && authenticated && sessionReady && visible() && options.live_enabled &&
|
||||
task.interval() > 0 && (!task.active || task.active());
|
||||
}
|
||||
function clear(task, abort = false) {
|
||||
@@ -39,7 +40,7 @@ const rustProxyRefresh = (() => {
|
||||
task.timer = setTimeout(() => { task.timer = null; void execute(task, false); }, delay);
|
||||
}
|
||||
function execute(task, manual) {
|
||||
if (!authenticated || (!manual && !allowed(task))) return Promise.resolve();
|
||||
if (!authenticated || !sessionReady || (!manual && !allowed(task))) return Promise.resolve();
|
||||
if (task.inFlight) {
|
||||
if (manual) task.queued = true;
|
||||
return task.inFlight;
|
||||
@@ -92,22 +93,56 @@ const rustProxyRefresh = (() => {
|
||||
}
|
||||
task.seconds = seconds;
|
||||
}
|
||||
if (ready && authenticated && visible() && options.live_enabled) startLive();
|
||||
if (ready && authenticated && sessionReady && visible() && options.live_enabled) startLive();
|
||||
else stopLive();
|
||||
}
|
||||
function onVisibility() { sync(); if (visible() && ready && authenticated) void revalidateSession(); }
|
||||
function start() {
|
||||
ready = true; panel = currentPanel;
|
||||
settingsTab = window.rustProxySettings?.current?.() || "listeners";
|
||||
if (!watchingVisibility) { document.addEventListener("visibilitychange", onVisibility); watchingVisibility = true; }
|
||||
sync();
|
||||
}
|
||||
function stop() {
|
||||
ready = false;
|
||||
function suspendForSessionValidation() {
|
||||
sessionReady = false;
|
||||
for (const task of tasks.values()) {
|
||||
clear(task, true); task.queued = false; task.enabled = false;
|
||||
}
|
||||
if (watchingVisibility) { document.removeEventListener("visibilitychange", onVisibility); watchingVisibility = false; }
|
||||
sync();
|
||||
}
|
||||
function completeSessionValidation(success) {
|
||||
sessionReady = !!success;
|
||||
sync();
|
||||
}
|
||||
function requestSessionResume() {
|
||||
if (!ready || !authenticated || !visible() || visibleResumeHandled) return;
|
||||
visibleResumeHandled = true;
|
||||
void revalidateSession();
|
||||
}
|
||||
function onVisibility() {
|
||||
if (visible()) requestSessionResume();
|
||||
else { visibleResumeHandled = false; sync(); }
|
||||
}
|
||||
function onPageHide() {
|
||||
visibleResumeHandled = false;
|
||||
suspendForSessionValidation();
|
||||
}
|
||||
function onPageShow(event) { if (event.persisted) requestSessionResume(); }
|
||||
function start() {
|
||||
ready = true; sessionReady = true; visibleResumeHandled = false; panel = currentPanel;
|
||||
settingsTab = window.rustProxySettings?.current?.() || "listeners";
|
||||
if (!watchingVisibility) {
|
||||
document.addEventListener("visibilitychange", onVisibility);
|
||||
window.addEventListener("pagehide", onPageHide);
|
||||
window.addEventListener("pageshow", onPageShow);
|
||||
watchingVisibility = true;
|
||||
}
|
||||
sync();
|
||||
}
|
||||
function stop() {
|
||||
ready = false; sessionReady = false; visibleResumeHandled = false;
|
||||
for (const task of tasks.values()) {
|
||||
clear(task, true); task.queued = false; task.enabled = false;
|
||||
}
|
||||
if (watchingVisibility) {
|
||||
document.removeEventListener("visibilitychange", onVisibility);
|
||||
window.removeEventListener("pagehide", onPageHide);
|
||||
window.removeEventListener("pageshow", onPageShow);
|
||||
watchingVisibility = false;
|
||||
}
|
||||
stopLive();
|
||||
}
|
||||
function update(value) { options = normalizeRefreshPreferences(value); sync(); }
|
||||
@@ -151,6 +186,7 @@ const rustProxyRefresh = (() => {
|
||||
register("tunnels", { interval: () => 2, active: () => !!$("tunnels-dialog")?.open,
|
||||
run: async context => { await loadTunnels(context); } });
|
||||
return { register, manual, start, stop, sync, update, setPanel, setSettingsTab,
|
||||
suspendForSessionValidation, completeSessionValidation,
|
||||
current: () => ({ ...options }), diagnostics: () => [...tasks.values()].map(t => ({ id: t.id, enabled: t.enabled, inFlight: !!t.inFlight, scheduled: t.timer !== null })) };
|
||||
})();
|
||||
window.rustProxyRefresh = rustProxyRefresh;
|
||||
|
||||
@@ -21,12 +21,11 @@ async function startAdminPanel() {
|
||||
// app.js belongs to the authenticated shell and must not fall back to its
|
||||
// legacy embedded sign-in form when another startup step fails.
|
||||
const info = await api("/api/auth/session");
|
||||
clearInvalidOriginRecovery();
|
||||
await loadLanguages();
|
||||
setupRequired = false;
|
||||
translate();
|
||||
syncAuthForm();
|
||||
await activate(info);
|
||||
if (await activate(info)) clearInvalidOriginRecovery();
|
||||
} catch (e) {
|
||||
if (recoverInvalidOrigin(e)) return;
|
||||
if (e.status === 401) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// One in-flight validation per authenticated document. Neither BFCache restore nor
|
||||
// visibility resume may trigger a reload: a 403 is a recoverable, visible error.
|
||||
function revalidateSession() {
|
||||
if (!authenticated) return Promise.resolve(false);
|
||||
if (sessionCheckPromise) return sessionCheckPromise;
|
||||
const epoch = authEpoch;
|
||||
window.rustProxyRefresh?.suspendForSessionValidation();
|
||||
const check = (async () => {
|
||||
try {
|
||||
const info = await api("/api/auth/session");
|
||||
if (!authenticated || epoch !== authEpoch) return false;
|
||||
sessionInfo = info;
|
||||
csrf = info.csrf_token;
|
||||
renderSession();
|
||||
hideSessionValidationFailure();
|
||||
window.rustProxyRefresh?.completeSessionValidation(true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// api() locks and navigates to the login document on a 401.
|
||||
if (!authenticated || epoch !== authEpoch || error?.status === 401) return false;
|
||||
window.rustProxyRefresh?.completeSessionValidation(false);
|
||||
showSessionValidationFailure(error);
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
sessionCheckPromise = check;
|
||||
void check.then(
|
||||
() => { if (sessionCheckPromise === check) sessionCheckPromise = null; },
|
||||
() => { if (sessionCheckPromise === check) sessionCheckPromise = null; }
|
||||
);
|
||||
return check;
|
||||
}
|
||||
function displayStartupError(error, sessionResume = false) {
|
||||
$("login").hidden = true;
|
||||
$("workspace").hidden = true;
|
||||
$("logout").hidden = !authenticated;
|
||||
document.documentElement.classList.add("app-loading", "app-startup-failed");
|
||||
document.documentElement.classList.toggle("app-session-failed", sessionResume);
|
||||
$("app-startup-title").textContent = sessionResume
|
||||
? t("sessionValidationFailed")
|
||||
: (languagePacks.size ? t("startupFailed") : "Could not load RustProxy");
|
||||
const detail = $("app-startup-detail");
|
||||
detail.textContent = error?.message || String(error);
|
||||
detail.hidden = false;
|
||||
$("app-startup-reload").hidden = sessionResume;
|
||||
$("app-session-retry").hidden = !sessionResume;
|
||||
}
|
||||
function showSessionValidationFailure(error) {
|
||||
// Keep the workspace inaccessible while its session cannot be verified.
|
||||
displayStartupError(error, true);
|
||||
}
|
||||
function hideSessionValidationFailure() {
|
||||
if (!document.documentElement.classList.contains("app-session-failed")) return;
|
||||
document.documentElement.classList.remove("app-session-failed", "app-loading", "app-startup-failed");
|
||||
$("app-startup-detail").hidden = true;
|
||||
$("app-startup-detail").textContent = "";
|
||||
$("app-startup-reload").hidden = false;
|
||||
$("app-session-retry").hidden = true;
|
||||
$("workspace").hidden = false;
|
||||
$("logout").hidden = false;
|
||||
}
|
||||
$("app-session-retry").addEventListener("click", () => {
|
||||
const button = $("app-session-retry");
|
||||
button.disabled = true;
|
||||
void revalidateSession().then(
|
||||
() => { button.disabled = false; },
|
||||
() => { button.disabled = false; }
|
||||
);
|
||||
});
|
||||
function showStartupFailure(error) {
|
||||
// All startup failure paths stop background tasks and show visible recovery.
|
||||
window.rustProxyRefresh?.stop();
|
||||
displayStartupError(error);
|
||||
}
|
||||
Reference in new issue
Block a user