From 7b93e89189c19e8b2b38ba7004defb2051be4c7a Mon Sep 17 00:00:00 2001 From: digua Date: Tue, 14 Apr 2026 19:45:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E7=B4=A7=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=8B=89=E5=8F=96=E5=B9=B6=E5=BC=BA=E5=8C=96?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=AE=89=E8=A3=85=E7=A1=AE=E8=AE=A4(resolve?= =?UTF-8?q?=20#137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/ipc/window.ts | 56 ++++++++++++++++++++++++++++++++++--- electron/main/update.ts | 13 ++++----- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/electron/main/ipc/window.ts b/electron/main/ipc/window.ts index 5c42797f..bc439fd8 100644 --- a/electron/main/ipc/window.ts +++ b/electron/main/ipc/window.ts @@ -12,6 +12,26 @@ type AppWithQuitFlag = typeof app & { isQuiting?: boolean } // 通过类型扩展记录应用退出意图,避免使用 @ts-ignore。 const appWithQuitFlag = app as AppWithQuitFlag +const REMOTE_CONFIG_ALLOWED_DOMAINS = ['chatlab.fun', '1app.top'] +const REMOTE_CONFIG_TIMEOUT_MS = 8000 +const REMOTE_CONFIG_MAX_BYTES = 1024 * 1024 // 1MB + +function isAllowedRemoteConfigUrl(rawUrl: string): boolean { + let parsed: URL + try { + parsed = new URL(rawUrl) + } catch { + return false + } + + if (parsed.protocol !== 'https:') return false + if (parsed.username || parsed.password) return false + if (parsed.port && parsed.port !== '443') return false + + const hostname = parsed.hostname.toLowerCase() + return REMOTE_CONFIG_ALLOWED_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`)) +} + /** * 注册窗口和文件系统操作 IPC 处理器 */ @@ -89,27 +109,55 @@ export function registerWindowHandlers(ctx: IpcContext): void { // 获取远程配置(支持 JSON 和纯文本/Markdown) ipcMain.handle('app:fetchRemoteConfig', async (_, url: string) => { + const normalizedUrl = typeof url === 'string' ? url.trim() : '' + if (!isAllowedRemoteConfigUrl(normalizedUrl)) { + return { success: false, error: 'URL is not allowed' } + } + + const abortController = new AbortController() + const timeout = setTimeout(() => abortController.abort(), REMOTE_CONFIG_TIMEOUT_MS) + try { - const response = await fetch(url) + const response = await fetch(normalizedUrl, { signal: abortController.signal }) + const finalUrl = response.url || normalizedUrl + if (!isAllowedRemoteConfigUrl(finalUrl)) { + return { success: false, error: 'Redirect URL is not allowed' } + } + const contentType = response.headers.get('content-type') || '' + const contentLength = Number(response.headers.get('content-length') || 0) + + if (Number.isFinite(contentLength) && contentLength > REMOTE_CONFIG_MAX_BYTES) { + return { success: false, error: 'Response is too large' } + } if (!response.ok) { return { success: false, error: `HTTP ${response.status}: ${response.statusText}` } } + const buffer = Buffer.from(await response.arrayBuffer()) + if (buffer.length > REMOTE_CONFIG_MAX_BYTES) { + return { success: false, error: 'Response is too large' } + } + // 根据 Content-Type 或 URL 后缀决定解析方式 - const isJson = contentType.includes('application/json') || url.endsWith('.json') + const isJson = contentType.includes('application/json') || finalUrl.endsWith('.json') if (isJson) { - const data = await response.json() + const data = JSON.parse(buffer.toString('utf-8')) return { success: true, data } } else { // 纯文本/Markdown 等其他格式 - const data = await response.text() + const data = buffer.toString('utf-8') return { success: true, data } } } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + return { success: false, error: 'Request timeout' } + } return { success: false, error: String(error) } + } finally { + clearTimeout(timeout) } }) diff --git a/electron/main/update.ts b/electron/main/update.ts index a8aee47b..de0f566a 100644 --- a/electron/main/update.ts +++ b/electron/main/update.ts @@ -16,7 +16,7 @@ const R2_MIRROR_URL = 'https://chatlab.1app.top/releases/download' // 更新源类型 type UpdateSource = 'github' | 'r2' -// 当前使用的更新源(默认 R2 优先) +// 当前使用的更新源(默认 R2 优先,GitHub 作为网络失败兜底) let currentSource: UpdateSource = 'r2' // 是否已尝试过备用源 @@ -57,7 +57,6 @@ function switchToR2Mirror(): void { */ function switchToGitHub(): void { currentSource = 'github' - // 使用 GitHub 作为 generic provider autoUpdater.setFeedURL({ provider: 'github', owner: 'hellodigua', @@ -117,7 +116,7 @@ const checkUpdate = (win) => { configureUpdateProxy() autoUpdater.autoDownload = false // 自动下载 - autoUpdater.autoInstallOnAppQuit = true // 应用退出后自动安装 + autoUpdater.autoInstallOnAppQuit = false // 关闭退出自动安装,必须显式确认安装 // 开发模式下模拟更新检测(需要创建 dev-app-update.yml 文件) // 取消下面的注释来启用开发模式更新测试 @@ -187,9 +186,9 @@ const checkUpdate = (win) => { .showMessageBox({ title: t('update.downloadComplete'), message: t('update.readyToInstall'), - buttons: [t('update.install'), platform.isMacOS ? t('update.remindLater') : t('update.installOnQuit')], + buttons: [t('update.install'), t('update.remindLater')], defaultId: 1, - cancelId: 2, + cancelId: 1, type: 'question', }) .then(async (result) => { @@ -230,11 +229,11 @@ const checkUpdate = (win) => { } }) - // 错误处理(智能切换备用源) + // 错误处理(网络失败时切换备用源) autoUpdater.on('error', (err) => { logger.error(`[Update] Update error (${currentSource}): ${err.message || err}`) - // 如果是 R2 源且为网络错误,尝试切换到 GitHub 备用源 + // 默认 R2 源网络失败时,尝试切换到 GitHub if (currentSource === 'r2' && !hasTriedFallback && isNetworkError(err)) { hasTriedFallback = true logger.info('[Update] R2 mirror failed, trying GitHub fallback...')