feat(security): shorten download signed URLs to 30s and remove update polling
This commit is contained in:
@@ -97,6 +97,10 @@ const DOWNLOAD_RESERVATION_TTL_MS = Number(process.env.DOWNLOAD_RESERVATION_TTL_
|
|||||||
const DOWNLOAD_LOG_RECONCILE_INTERVAL_MS = Number(process.env.DOWNLOAD_LOG_RECONCILE_INTERVAL_MS || (5 * 60 * 1000)); // 5分钟
|
const DOWNLOAD_LOG_RECONCILE_INTERVAL_MS = Number(process.env.DOWNLOAD_LOG_RECONCILE_INTERVAL_MS || (5 * 60 * 1000)); // 5分钟
|
||||||
const DOWNLOAD_LOG_MAX_FILES_PER_SWEEP = Number(process.env.DOWNLOAD_LOG_MAX_FILES_PER_SWEEP || 40);
|
const DOWNLOAD_LOG_MAX_FILES_PER_SWEEP = Number(process.env.DOWNLOAD_LOG_MAX_FILES_PER_SWEEP || 40);
|
||||||
const DOWNLOAD_LOG_LIST_MAX_KEYS = Number(process.env.DOWNLOAD_LOG_LIST_MAX_KEYS || 200);
|
const DOWNLOAD_LOG_LIST_MAX_KEYS = Number(process.env.DOWNLOAD_LOG_LIST_MAX_KEYS || 200);
|
||||||
|
const DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS = Math.max(
|
||||||
|
10,
|
||||||
|
Math.min(3600, Number(process.env.DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS || 30))
|
||||||
|
);
|
||||||
const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.1';
|
const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.1';
|
||||||
const DEFAULT_DESKTOP_INSTALLER_URL = process.env.DESKTOP_INSTALLER_URL || '';
|
const DEFAULT_DESKTOP_INSTALLER_URL = process.env.DESKTOP_INSTALLER_URL || '';
|
||||||
const DEFAULT_DESKTOP_RELEASE_NOTES = process.env.DESKTOP_RELEASE_NOTES || '';
|
const DEFAULT_DESKTOP_RELEASE_NOTES = process.env.DESKTOP_RELEASE_NOTES || '';
|
||||||
@@ -6433,8 +6437,10 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
|
|||||||
};
|
};
|
||||||
const command = new GetObjectCommand(commandInput);
|
const command = new GetObjectCommand(commandInput);
|
||||||
|
|
||||||
// 生成签名 URL(1小时有效)
|
// 生成签名 URL(短时有效,默认30秒)
|
||||||
const signedUrl = await getSignedUrl(client, command, { expiresIn: 3600 });
|
const signedUrl = await getSignedUrl(client, command, {
|
||||||
|
expiresIn: DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS
|
||||||
|
});
|
||||||
|
|
||||||
// 直连模式:先预扣保留额度(不写入已用),实际用量由 OSS 日志异步确认入账
|
// 直连模式:先预扣保留额度(不写入已用),实际用量由 OSS 日志异步确认入账
|
||||||
if (!trafficState.isUnlimited && fileSize > 0) {
|
if (!trafficState.isUnlimited && fileSize > 0) {
|
||||||
@@ -6454,7 +6460,7 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
downloadUrl: signedUrl,
|
downloadUrl: signedUrl,
|
||||||
expiresIn: 3600,
|
expiresIn: DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS,
|
||||||
direct: true,
|
direct: true,
|
||||||
quotaLimited: !trafficState.isUnlimited
|
quotaLimited: !trafficState.isUnlimited
|
||||||
});
|
});
|
||||||
@@ -8262,8 +8268,10 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
|
|||||||
ResponseContentDisposition: `attachment; filename="${encodeURIComponent(normalizedFilePath.split('/').pop())}"`
|
ResponseContentDisposition: `attachment; filename="${encodeURIComponent(normalizedFilePath.split('/').pop())}"`
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生成签名 URL(1小时有效)
|
// 生成签名 URL(短时有效,默认30秒)
|
||||||
const signedUrl = await getSignedUrl(client, command, { expiresIn: 3600 });
|
const signedUrl = await getSignedUrl(client, command, {
|
||||||
|
expiresIn: DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS
|
||||||
|
});
|
||||||
|
|
||||||
if (!ownerTrafficState.isUnlimited && fileSize > 0) {
|
if (!ownerTrafficState.isUnlimited && fileSize > 0) {
|
||||||
const reserveResult = reserveDirectDownloadTraffic(shareOwner.id, fileSize, {
|
const reserveResult = reserveDirectDownloadTraffic(shareOwner.id, fileSize, {
|
||||||
@@ -8284,7 +8292,7 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
|
|||||||
downloadUrl: signedUrl,
|
downloadUrl: signedUrl,
|
||||||
direct: true,
|
direct: true,
|
||||||
quotaLimited: !ownerTrafficState.isUnlimited,
|
quotaLimited: !ownerTrafficState.isUnlimited,
|
||||||
expiresIn: 3600
|
expiresIn: DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -10588,7 +10596,9 @@ app.get('/d/:code', async (req, res) => {
|
|||||||
ResponseContentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(directFileName)}`
|
ResponseContentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(directFileName)}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const signedUrl = await getSignedUrl(client, command, { expiresIn: 3600 });
|
const signedUrl = await getSignedUrl(client, command, {
|
||||||
|
expiresIn: DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS
|
||||||
|
});
|
||||||
DirectLinkDB.incrementDownloadCount(code);
|
DirectLinkDB.incrementDownloadCount(code);
|
||||||
|
|
||||||
logShare(
|
logShare(
|
||||||
|
|||||||
@@ -132,11 +132,8 @@ const updateState = reactive({
|
|||||||
lastCheckedAt: "",
|
lastCheckedAt: "",
|
||||||
message: "",
|
message: "",
|
||||||
});
|
});
|
||||||
const AUTO_UPDATE_CHECK_INTERVAL_MS = 30 * 60 * 1000;
|
|
||||||
const updateRuntime = reactive({
|
const updateRuntime = reactive({
|
||||||
downloading: false,
|
downloading: false,
|
||||||
promptedVersion: "",
|
|
||||||
mandatoryInstalledVersion: "",
|
|
||||||
});
|
});
|
||||||
const contextMenu = reactive({
|
const contextMenu = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -153,7 +150,6 @@ const dropState = reactive({
|
|||||||
});
|
});
|
||||||
let unlistenDragDrop: UnlistenFn | null = null;
|
let unlistenDragDrop: UnlistenFn | null = null;
|
||||||
let syncTimer: ReturnType<typeof setInterval> | null = null;
|
let syncTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
let updateCheckTimer: ReturnType<typeof setInterval> | null = null;
|
|
||||||
|
|
||||||
const toast = reactive({
|
const toast = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -564,13 +560,6 @@ function clearSyncScheduler() {
|
|||||||
syncState.nextRunAt = "";
|
syncState.nextRunAt = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearUpdateScheduler() {
|
|
||||||
if (updateCheckTimer) {
|
|
||||||
clearInterval(updateCheckTimer);
|
|
||||||
updateCheckTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncFingerprint(item: LocalSyncFileItem) {
|
function syncFingerprint(item: LocalSyncFileItem) {
|
||||||
return `${Number(item.size || 0)}:${Number(item.modifiedMs || 0)}`;
|
return `${Number(item.size || 0)}:${Number(item.modifiedMs || 0)}`;
|
||||||
}
|
}
|
||||||
@@ -656,18 +645,14 @@ async function checkClientUpdate(showResultToast = true): Promise<boolean> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installLatestUpdate(trigger: "manual" | "auto" = "manual"): Promise<boolean> {
|
async function installLatestUpdate(): Promise<boolean> {
|
||||||
if (updateRuntime.downloading) {
|
if (updateRuntime.downloading) {
|
||||||
if (trigger === "manual") {
|
|
||||||
showToast("更新包正在下载,请稍候", "info");
|
showToast("更新包正在下载,请稍候", "info");
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!updateState.downloadUrl) {
|
if (!updateState.downloadUrl) {
|
||||||
if (trigger === "manual") {
|
|
||||||
showToast("当前没有可用的更新下载地址", "info");
|
showToast("当前没有可用的更新下载地址", "info");
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,46 +710,6 @@ async function installLatestUpdate(trigger: "manual" | "auto" = "manual"): Promi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runAutoUpdateCycle(trigger: "startup" | "login" | "timer" = "startup") {
|
|
||||||
const checked = await checkClientUpdate(false);
|
|
||||||
if (!checked) return;
|
|
||||||
|
|
||||||
if (!updateState.available || !updateState.downloadUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestVersion = String(updateState.latestVersion || "").trim();
|
|
||||||
if (!latestVersion) return;
|
|
||||||
|
|
||||||
if (updateState.mandatory) {
|
|
||||||
if (updateRuntime.mandatoryInstalledVersion === latestVersion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast(`检测到强制更新 v${latestVersion},正在下载升级包`, "info");
|
|
||||||
const installed = await installLatestUpdate("auto");
|
|
||||||
if (installed) {
|
|
||||||
updateRuntime.mandatoryInstalledVersion = latestVersion;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateRuntime.promptedVersion === latestVersion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRuntime.promptedVersion = latestVersion;
|
|
||||||
if (trigger === "timer") {
|
|
||||||
showToast(`发现新版本 v${latestVersion},可在“版本更新”中安装`, "info");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmed = window.confirm(`发现新版本 v${latestVersion},是否立即下载并安装?`);
|
|
||||||
if (confirmed) {
|
|
||||||
await installLatestUpdate("auto");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function chooseSyncDirectory() {
|
async function chooseSyncDirectory() {
|
||||||
try {
|
try {
|
||||||
const result = await openDialog({
|
const result = await openDialog({
|
||||||
@@ -822,16 +767,6 @@ function rebuildSyncScheduler() {
|
|||||||
}, intervalMinutes * 60 * 1000);
|
}, intervalMinutes * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildUpdateScheduler() {
|
|
||||||
clearUpdateScheduler();
|
|
||||||
if (!authenticated.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateCheckTimer = setInterval(() => {
|
|
||||||
void runAutoUpdateCycle("timer");
|
|
||||||
}, AUTO_UPDATE_CHECK_INTERVAL_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearSyncSnapshot() {
|
async function clearSyncSnapshot() {
|
||||||
if (!syncState.localDir.trim()) {
|
if (!syncState.localDir.trim()) {
|
||||||
showToast("请先配置本地同步目录", "info");
|
showToast("请先配置本地同步目录", "info");
|
||||||
@@ -1119,10 +1054,8 @@ async function restoreSession() {
|
|||||||
authenticated.value = true;
|
authenticated.value = true;
|
||||||
loadSyncConfig();
|
loadSyncConfig();
|
||||||
rebuildSyncScheduler();
|
rebuildSyncScheduler();
|
||||||
rebuildUpdateScheduler();
|
|
||||||
await loadFiles("/");
|
await loadFiles("/");
|
||||||
await loadShares(true);
|
await loadShares(true);
|
||||||
await runAutoUpdateCycle("startup");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildItemPath(item: FileItem) {
|
function buildItemPath(item: FileItem) {
|
||||||
@@ -1187,12 +1120,10 @@ async function handleLogin() {
|
|||||||
showToast("登录成功,正在同步文件目录", "success");
|
showToast("登录成功,正在同步文件目录", "success");
|
||||||
loadSyncConfig();
|
loadSyncConfig();
|
||||||
rebuildSyncScheduler();
|
rebuildSyncScheduler();
|
||||||
rebuildUpdateScheduler();
|
|
||||||
await loadFiles("/");
|
await loadFiles("/");
|
||||||
if (!user.value) {
|
if (!user.value) {
|
||||||
await loadProfile();
|
await loadProfile();
|
||||||
}
|
}
|
||||||
await runAutoUpdateCycle("login");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1204,7 +1135,6 @@ async function handleLogin() {
|
|||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await invokeBridge("api_logout", { baseUrl: appConfig.baseUrl });
|
await invokeBridge("api_logout", { baseUrl: appConfig.baseUrl });
|
||||||
clearSyncScheduler();
|
clearSyncScheduler();
|
||||||
clearUpdateScheduler();
|
|
||||||
authenticated.value = false;
|
authenticated.value = false;
|
||||||
user.value = null;
|
user.value = null;
|
||||||
files.value = [];
|
files.value = [];
|
||||||
@@ -1226,8 +1156,6 @@ async function handleLogout() {
|
|||||||
syncState.lastSummary = "";
|
syncState.lastSummary = "";
|
||||||
syncState.nextRunAt = "";
|
syncState.nextRunAt = "";
|
||||||
updateRuntime.downloading = false;
|
updateRuntime.downloading = false;
|
||||||
updateRuntime.promptedVersion = "";
|
|
||||||
updateRuntime.mandatoryInstalledVersion = "";
|
|
||||||
showToast("已退出客户端", "info");
|
showToast("已退出客户端", "info");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1702,7 +1630,6 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener("click", handleGlobalClick);
|
window.removeEventListener("click", handleGlobalClick);
|
||||||
window.removeEventListener("keydown", handleGlobalKey);
|
window.removeEventListener("keydown", handleGlobalKey);
|
||||||
clearSyncScheduler();
|
clearSyncScheduler();
|
||||||
clearUpdateScheduler();
|
|
||||||
if (unlistenDragDrop) {
|
if (unlistenDragDrop) {
|
||||||
unlistenDragDrop();
|
unlistenDragDrop();
|
||||||
unlistenDragDrop = null;
|
unlistenDragDrop = null;
|
||||||
@@ -1848,7 +1775,7 @@ onBeforeUnmount(() => {
|
|||||||
<button class="action-btn" :disabled="updateState.checking || updateRuntime.downloading" @click="checkClientUpdate()">
|
<button class="action-btn" :disabled="updateState.checking || updateRuntime.downloading" @click="checkClientUpdate()">
|
||||||
{{ updateState.checking ? "检查中..." : "检查更新" }}
|
{{ updateState.checking ? "检查中..." : "检查更新" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn" :disabled="updateRuntime.downloading || !updateState.available || !updateState.downloadUrl" @click="installLatestUpdate('manual')">
|
<button class="action-btn" :disabled="updateRuntime.downloading || !updateState.available || !updateState.downloadUrl" @click="installLatestUpdate()">
|
||||||
{{ updateRuntime.downloading ? "下载中..." : "立即更新" }}
|
{{ updateRuntime.downloading ? "下载中..." : "立即更新" }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -2053,7 +1980,6 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="update-meta">
|
<div class="update-meta">
|
||||||
<span>自动检查:已开启(每 30 分钟)</span>
|
|
||||||
<span>上次检查:{{ updateState.lastCheckedAt ? formatDate(updateState.lastCheckedAt) : "-" }}</span>
|
<span>上次检查:{{ updateState.lastCheckedAt ? formatDate(updateState.lastCheckedAt) : "-" }}</span>
|
||||||
<span>提示:{{ updateState.message || "可手动点击“检查更新”获取最新信息" }}</span>
|
<span>提示:{{ updateState.message || "可手动点击“检查更新”获取最新信息" }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user