fix: 收紧 GUI 安全边界
This commit is contained in:
@@ -2,15 +2,15 @@
|
||||
// the top-nav active state in sync, and re-renders chrome strings when
|
||||
// the locale changes.
|
||||
|
||||
import { escapeHtml, onRoute } from "./state.js?v=review-fixes-1";
|
||||
import { t, currentLocale, setLocale, onLocale } from "./i18n.js?v=review-fixes-1";
|
||||
import { renderRemotes } from "./views/remotes.js?v=review-fixes-1";
|
||||
import { renderBrowse } from "./views/browser.js?v=review-fixes-1";
|
||||
import { renderJobs, renderNewJob, stopJobPolling } from "./views/jobs.js?v=review-fixes-1";
|
||||
import { escapeHtml, onRoute } from "./state.js?v=security-fixes-1";
|
||||
import { t, currentLocale, setLocale, onLocale } from "./i18n.js?v=security-fixes-1";
|
||||
import { renderRemotes } from "./views/remotes.js?v=security-fixes-1";
|
||||
import { renderBrowse } from "./views/browser.js?v=security-fixes-1";
|
||||
import { renderJobs, renderNewJob, stopJobPolling } from "./views/jobs.js?v=security-fixes-1";
|
||||
import {
|
||||
renderConfigureNew,
|
||||
renderConfigureEdit,
|
||||
} from "./views/configure.js?v=review-fixes-1";
|
||||
} from "./views/configure.js?v=security-fixes-1";
|
||||
|
||||
const views = {
|
||||
remotes: renderRemotes,
|
||||
|
||||
@@ -15,6 +15,7 @@ const AUTH_PASS = params.get("pass");
|
||||
let authHeader = null;
|
||||
if (AUTH_USER && AUTH_PASS) {
|
||||
authHeader = "Basic " + btoa(`${AUTH_USER}:${AUTH_PASS}`);
|
||||
scrubAuthQuery();
|
||||
}
|
||||
|
||||
export function rcURL() {
|
||||
@@ -30,6 +31,13 @@ export function isNoAuth() {
|
||||
return !authHeader;
|
||||
}
|
||||
|
||||
function scrubAuthQuery() {
|
||||
const clean = new URL(location.href);
|
||||
clean.searchParams.delete("user");
|
||||
clean.searchParams.delete("pass");
|
||||
history.replaceState(null, "", `${clean.pathname}${clean.search}${clean.hash}`);
|
||||
}
|
||||
|
||||
// POST JSON to an RC endpoint. Returns the parsed JSON response, or throws.
|
||||
export async function post(path, body = {}, options = {}) {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
@@ -72,14 +80,13 @@ export async function uploadFile(fs, remote, files) {
|
||||
export function downloadURL(remoteFs, remotePath, fileName) {
|
||||
const base = RC_BASE.replace(/\/$/, "");
|
||||
// rc server serves remote files at /<remote>:<path>
|
||||
const trimmed = (remotePath || "").replace(/^\/+|\/+$/g, "");
|
||||
const path = trimmed ? `${remoteFs}/${trimmed}/${fileName}` : `${remoteFs}/${fileName}`;
|
||||
let url = `${base}/${path}`;
|
||||
if (authHeader) {
|
||||
// Embed basic auth into the URL so the browser can fetch it directly.
|
||||
url = url.replace(/^(https?:\/\/)/, `$1${encodeURIComponent(AUTH_USER)}:${encodeURIComponent(AUTH_PASS)}@`);
|
||||
}
|
||||
return url;
|
||||
const path = [remoteFs, remotePath, fileName]
|
||||
.filter(Boolean)
|
||||
.flatMap((part) => String(part).split("/"))
|
||||
.filter((part) => part !== "")
|
||||
.map(encodeURIComponent)
|
||||
.join("/");
|
||||
return `${base}/${path}`;
|
||||
}
|
||||
|
||||
async function parseResponse(res, path, { allowError = false } = {}) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// views/browser.js — file/folder listing with breadcrumbs, mkdir, upload, delete, rename.
|
||||
|
||||
import { post, uploadFile, downloadURL } from "../rc.js?v=review-fixes-1";
|
||||
import { escapeHtml, toast, formatBytes, formatTime } from "../state.js?v=review-fixes-1";
|
||||
import { t } from "../i18n.js?v=review-fixes-1";
|
||||
import { post, uploadFile, downloadURL } from "../rc.js?v=security-fixes-1";
|
||||
import { escapeHtml, toast, formatBytes, formatTime } from "../state.js?v=security-fixes-1";
|
||||
import { t } from "../i18n.js?v=security-fixes-1";
|
||||
|
||||
export async function renderBrowse({ remote, path }) {
|
||||
const app = document.getElementById("app");
|
||||
@@ -169,7 +169,7 @@ function row(fs, path, item) {
|
||||
return `
|
||||
<tr>
|
||||
<td class="col-name">
|
||||
<a href="${downloadURL(fs, path, item.Name)}" download="${escapeHtml(item.Name)}">${icon("file")} ${escapeHtml(item.Name)}</a>
|
||||
<a href="${escapeAttr(downloadURL(fs, path, item.Name))}" download="${escapeAttr(item.Name)}">${icon("file")} ${escapeHtml(item.Name)}</a>
|
||||
</td>
|
||||
<td class="col-num col-mono">${escapeHtml(formatBytes(item.Size))}</td>
|
||||
<td class="col-mono">${escapeHtml(formatTime(item.ModTime))}</td>
|
||||
@@ -296,3 +296,7 @@ export function openModal(title, fields, onSubmit) {
|
||||
if (first) first.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function escapeAttr(s) {
|
||||
return escapeHtml(s);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
// OAuth backends (option named "token" with IsPassword) get a banner
|
||||
// and disabled submit — user must run `rclone config` in a terminal.
|
||||
|
||||
import { post } from "../rc.js?v=review-fixes-1";
|
||||
import { escapeHtml, getState, setState, toast } from "../state.js?v=review-fixes-1";
|
||||
import { t } from "../i18n.js?v=review-fixes-1";
|
||||
import { post } from "../rc.js?v=security-fixes-1";
|
||||
import { escapeHtml, getState, setState, toast } from "../state.js?v=security-fixes-1";
|
||||
import { t } from "../i18n.js?v=security-fixes-1";
|
||||
|
||||
// --- Route entrypoints ---
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function renderConfigureNew({ provider = "" }) {
|
||||
document.getElementById("app").innerHTML = `
|
||||
<div class="empty">
|
||||
<h3>${t("error.unknown_remote")}</h3>
|
||||
<p>${t("error.no_such_provider", provider)}</p>
|
||||
<p>${t("error.no_such_provider", escapeHtml(provider))}</p>
|
||||
<p><a href="#/configure/new">← ${t("configure.new_title")}</a></p>
|
||||
</div>`;
|
||||
return;
|
||||
@@ -53,7 +53,7 @@ export async function renderConfigureEdit({ remote }) {
|
||||
app.innerHTML = `
|
||||
<div class="empty">
|
||||
<h3>${t("error.unknown_remote")}</h3>
|
||||
<p>${t("error.no_such_remote", remote)}</p>
|
||||
<p>${t("error.no_such_remote", escapeHtml(remote))}</p>
|
||||
<p><a href="#/remotes">← ${t("nav.remotes")}</a></p>
|
||||
</div>`;
|
||||
return;
|
||||
@@ -155,6 +155,7 @@ async function renderForm({ provider, mode, remoteName = "", currentValues = {}
|
||||
const requiresOAuth = provider.Options.some(
|
||||
(o) => o.Name === "token" && (o.IsPassword || o.Sensitive),
|
||||
);
|
||||
const blocksCreate = requiresOAuth && !isEdit;
|
||||
|
||||
// Partition options into required, optional-basic, optional-advanced.
|
||||
const required = provider.Options.filter((o) => o.Required && !o.Hide);
|
||||
@@ -206,7 +207,7 @@ async function renderForm({ provider, mode, remoteName = "", currentValues = {}
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<button type="submit" class="btn btn-primary" ${requiresOAuth ? "disabled" : ""}>
|
||||
<button type="submit" class="btn btn-primary" ${blocksCreate ? "disabled" : ""}>
|
||||
${isEdit ? t("configure.save") : t("configure.create")}
|
||||
</button>
|
||||
<a class="btn btn-secondary" href="#/remotes">${t("configure.cancel")}</a>
|
||||
@@ -255,7 +256,7 @@ async function renderForm({ provider, mode, remoteName = "", currentValues = {}
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (requiresOAuth) return;
|
||||
if (blocksCreate) return;
|
||||
|
||||
const name = form.elements._remote_name.value.trim();
|
||||
if (!name) return;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// views/jobs.js — submit sync/copy/move jobs and manage recurring transfers.
|
||||
|
||||
import { post } from "../rc.js?v=review-fixes-1";
|
||||
import { post } from "../rc.js?v=security-fixes-1";
|
||||
import {
|
||||
toast,
|
||||
formatBytes,
|
||||
formatSpeed,
|
||||
formatDuration,
|
||||
escapeHtml,
|
||||
} from "../state.js?v=review-fixes-1";
|
||||
import { t } from "../i18n.js?v=review-fixes-1";
|
||||
} from "../state.js?v=security-fixes-1";
|
||||
import { t } from "../i18n.js?v=security-fixes-1";
|
||||
import {
|
||||
createOneTimeJob,
|
||||
createRecurringJob,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
runRecurringJobNow,
|
||||
stopOneTimeJob,
|
||||
stopRecurringJob,
|
||||
} from "../jobs_api.js?v=review-fixes-1";
|
||||
} from "../jobs_api.js?v=security-fixes-1";
|
||||
|
||||
let pollTimer = null;
|
||||
const LOCAL_FS_VALUE = "__local__";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// views/remotes.js — connector-tile grid of configured remotes with CRUD.
|
||||
|
||||
import { post } from "../rc.js?v=review-fixes-1";
|
||||
import { escapeHtml, getState, setState, toast } from "../state.js?v=review-fixes-1";
|
||||
import { t } from "../i18n.js?v=review-fixes-1";
|
||||
import { post } from "../rc.js?v=security-fixes-1";
|
||||
import { escapeHtml, getState, setState, toast } from "../state.js?v=security-fixes-1";
|
||||
import { t } from "../i18n.js?v=security-fixes-1";
|
||||
|
||||
export async function renderRemotes() {
|
||||
const app = document.getElementById("app");
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link rel="stylesheet" href="/assets/styles/tokens.css">
|
||||
<link rel="stylesheet" href="/assets/styles/base.css">
|
||||
<link rel="stylesheet" href="/assets/styles/components.css?v=review-fixes-1">
|
||||
<link rel="stylesheet" href="/assets/styles/components.css?v=security-fixes-1">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top-nav">
|
||||
@@ -80,6 +80,6 @@
|
||||
<div id="toast-stack" class="toast-stack"></div>
|
||||
<div id="modal-root"></div>
|
||||
|
||||
<script type="module" src="/assets/js/app.js?v=review-fixes-1"></script>
|
||||
<script type="module" src="/assets/js/app.js?v=security-fixes-1"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user