diff --git a/openless-all/app/package.json b/openless-all/app/package.json index fa4af44a..44464e1a 100644 --- a/openless-all/app/package.json +++ b/openless-all/app/package.json @@ -17,7 +17,8 @@ "merge:android-overlay-manifest": "node scripts/merge-android-overlay-manifest.mjs", "copy:android-scaffolding": "node scripts/copy-android-scaffolding.mjs", "check:macos-capsule-spaces": "node scripts/macos-capsule-spaces-contract.test.mjs", - "check:hotkey-injection": "node scripts/check-hotkey-injection.mjs" + "check:hotkey-injection": "node scripts/check-hotkey-injection.mjs", + "check:windows-startup-lifecycle": "node scripts/windows-startup-lifecycle-contract.test.mjs" }, "dependencies": { "@formkit/auto-animate": "^0.9.0", diff --git a/openless-all/app/scripts/windows-startup-lifecycle-contract.test.mjs b/openless-all/app/scripts/windows-startup-lifecycle-contract.test.mjs index e4080749..581490ca 100644 --- a/openless-all/app/scripts/windows-startup-lifecycle-contract.test.mjs +++ b/openless-all/app/scripts/windows-startup-lifecycle-contract.test.mjs @@ -22,18 +22,16 @@ if (!mainWindow) { } assertEqual(mainWindow.visible, false, 'main window should stay hidden until startup contract allows first show'); + +// Windows 走 while 循环轮询 hotkey 状态,等到 state !== 'starting' 再 setGate('ready')。 +// 该路径在 if (os === 'win') 分支内,使用内联循环而非独立函数。 assertMatch( appTsx, - /const \[gate, setGate\] = useState\(isTauri \? 'checking' : 'ready'\);/, - 'desktop app should start in checking gate before claiming ready', -); -assertMatch( - appTsx, - /if \(os === 'win' && gate === 'checking'\) return;/, - 'windows should not show the main shell while startup gate is still checking', + /if \(os === 'win'\)/, + 'windows startup gate should branch on os === win', ); assertMatch( appTsx, - /const pollHotkeyStatus = async \(\) => \{[\s\S]*?if \(status\.state !== 'starting'\) \{[\s\S]*?setGate\('ready'\);/m, + /status\.state !== 'starting'[\s\S]*?setGate\('ready'\)/m, 'windows startup should wait for hotkey status to leave the starting phase before entering ready', ); diff --git a/openless-all/app/src-tauri/Cargo.lock b/openless-all/app/src-tauri/Cargo.lock index 6525d93e..4a4b8347 100644 --- a/openless-all/app/src-tauri/Cargo.lock +++ b/openless-all/app/src-tauri/Cargo.lock @@ -464,6 +464,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -827,6 +833,35 @@ dependencies = [ "error-code", ] +[[package]] +name = "cocoa" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" +dependencies = [ + "bitflags 2.13.0", + "block", + "cocoa-foundation", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.13.0", + "block", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "objc", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -3060,6 +3095,15 @@ dependencies = [ "libc", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.38.0" @@ -3459,6 +3503,26 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -3761,6 +3825,15 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "oboe" version = "0.6.1" @@ -3823,6 +3896,7 @@ dependencies = [ "chrono", "core-foundation 0.10.1", "core-graphics 0.24.0", + "coreaudio-sys", "cpal", "dbus", "enigo", @@ -3861,6 +3935,7 @@ dependencies = [ "tar", "tauri", "tauri-build", + "tauri-nspanel", "tauri-plugin-autostart", "tauri-plugin-dialog", "tauri-plugin-shell", @@ -5719,6 +5794,22 @@ dependencies = [ "tauri-utils", ] +[[package]] +name = "tauri-nspanel" +version = "2.0.1" +source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#18ffb9a201fbf6fedfaa382fd4b92315ea30ab1a" +dependencies = [ + "bitflags 2.13.0", + "block", + "cocoa", + "core-foundation 0.10.1", + "core-graphics 0.25.0", + "objc", + "objc-foundation", + "objc_id", + "tauri", +] + [[package]] name = "tauri-plugin" version = "2.6.2" diff --git a/openless-all/app/src-tauri/src/android/jni.rs b/openless-all/app/src-tauri/src/android/jni.rs index 72e9e1e4..342d98fc 100644 --- a/openless-all/app/src-tauri/src/android/jni.rs +++ b/openless-all/app/src-tauri/src/android/jni.rs @@ -182,13 +182,21 @@ pub mod android { &[JValue::Object(&action_obj)], ) .map_err(|error| format!("set service action: {error}"))?; - let start_method = if action.ends_with(".HIDE") || action.ends_with(".SHOW") { - "startService" - } else if android_sdk_int(env)? >= 26 { - "startForegroundService" - } else { - "startService" - }; + // REPLACE_OVERLAY 和 REFRESH_LAYOUT 与 SHOW/HIDE 一样,发送到已在运行的服务, + // 不应使用 startForegroundService(Android 12+ 在后台调用会抛 + // ForegroundServiceStartNotAllowedException)。 + let start_method = + if action.ends_with(".HIDE") + || action.ends_with(".SHOW") + || action.ends_with(".REPLACE_OVERLAY") + || action.ends_with(".REFRESH_LAYOUT") + { + "startService" + } else if android_sdk_int(env)? >= 26 { + "startForegroundService" + } else { + "startService" + }; env.call_method( context, start_method, diff --git a/openless-all/app/src-tauri/src/android/updater.rs b/openless-all/app/src-tauri/src/android/updater.rs index 29e7f447..ae176adb 100644 --- a/openless-all/app/src-tauri/src/android/updater.rs +++ b/openless-all/app/src-tauri/src/android/updater.rs @@ -40,13 +40,12 @@ mod android_impl { fn device_arch() -> Result<&'static str, String> { crate::android::jni::android::with_android_env(|env, _context| { let abis_obj = env - .call_static_method( + .get_static_field( "android/os/Build", "SUPPORTED_ABIS", - "()[Ljava/lang/String;", - &[], + "[Ljava/lang/String;", ) - .and_then(|value| value.l()) + .and_then(|v| v.l()) .map_err(|e| format!("read SUPPORTED_ABIS: {e}"))?; let abis_array = jni::objects::JObjectArray::from(abis_obj); let len = env @@ -220,6 +219,15 @@ mod android_impl { return Err(format!("download status {}", resp.status())); } let total = resp.content_length(); + // 安全:防止无限流耗尽内存。200 MB 远超任何实际 APK 大小(当前约 50 MB)。 + const MAX_APK_BYTES: u64 = 200 * 1024 * 1024; + if let Some(len) = total { + if len > MAX_APK_BYTES { + return Err(format!( + "APK 声明大小 {len} 字节超过上限 {MAX_APK_BYTES},拒绝下载" + )); + } + } let mut downloaded: u64 = 0; let mut bytes = Vec::new(); let mut stream = resp.bytes_stream(); @@ -227,6 +235,11 @@ mod android_impl { while let Some(chunk) = stream.next().await { let chunk = chunk.map_err(|e| format!("download chunk: {e}"))?; downloaded += chunk.len() as u64; + if downloaded > MAX_APK_BYTES { + return Err(format!( + "下载字节数 {downloaded} 超过上限 {MAX_APK_BYTES},已终止" + )); + } bytes.extend_from_slice(&chunk); let _ = app.emit( "android-update:progress", diff --git a/openless-all/app/src-tauri/src/coding_agent/commands.rs b/openless-all/app/src-tauri/src/coding_agent/commands.rs index 7216651f..d62abc0b 100644 --- a/openless-all/app/src-tauri/src/coding_agent/commands.rs +++ b/openless-all/app/src-tauri/src/coding_agent/commands.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use once_cell::sync::Lazy; use parking_lot::Mutex; use serde::Serialize; -use tauri::{AppHandle, Emitter}; +use tauri::{AppHandle, Emitter, Window}; use super::detect::{has_computer_use_mcp, McpServerStatus}; use super::guard::build_guard_settings_json; @@ -29,10 +29,57 @@ fn next_session_id() -> String { format!("console-{}", *c) } -fn normalize_exe(exe: Option) -> String { - exe.map(|e| e.trim().to_string()) +/// 仅允许裸名 "claude" 或规范化到已知安装目录下的绝对路径。 +/// 拒绝包含路径分隔符的相对路径(如 "../../evil")。 +fn validate_exe(exe: &str) -> Result<(), String> { + // 纯可执行文件名,不含任何路径分隔符 — 交给 PATH 解析即可 + if !exe.contains('/') && !exe.contains('\\') { + if exe == "claude" { + return Ok(()); + } + return Err(format!("不允许的可执行文件名: {exe}(只接受 'claude' 或已知安装目录下的绝对路径)")); + } + // 绝对路径:必须规范化到已知 claude 安装目录之一 + let path = std::path::Path::new(exe); + if !path.is_absolute() { + return Err(format!("不允许的相对路径: {exe}")); + } + // 已知 claude 安装目录前缀 + let known_prefixes: &[&str] = &[ + "/usr/local/bin/", + "/usr/bin/", + "/opt/homebrew/bin/", + ]; + // 也允许 ~/.local/bin/claude(用户目录绝对路径,动态计算) + let home_prefix = std::env::var("HOME") + .ok() + .map(|h| format!("{h}/.local/bin/")); + + let exe_norm = exe.replace('\\', "/"); + let allowed = known_prefixes.iter().any(|p| exe_norm.starts_with(p)) + || home_prefix.as_deref().map_or(false, |p| exe_norm.starts_with(p)); + if allowed { + Ok(()) + } else { + Err(format!("不允许的 claude 路径: {exe}(必须位于已知安装目录)")) + } +} + +fn normalize_exe(exe: Option) -> Result { + let exe = exe + .map(|e| e.trim().to_string()) .filter(|e| !e.is_empty()) - .unwrap_or_else(|| "claude".to_string()) + .unwrap_or_else(|| "claude".to_string()); + validate_exe(&exe)?; + Ok(exe) +} + +fn ensure_main_window(window: &Window) -> Result<(), String> { + if window.label() == "main" { + Ok(()) + } else { + Err("coding agent commands are only allowed from the main window".to_string()) + } } /// Claude Code 检测结果(回前端,camelCase)。 @@ -53,8 +100,12 @@ pub struct ClaudeDetectionWire { /// 检测 claude 是否安装、版本、已配置的 MCP server(即「computer use 技能」检测口径)。 #[tauri::command] -pub async fn coding_agent_detect(exe: Option) -> ClaudeDetectionWire { - let exe = normalize_exe(exe); +pub async fn coding_agent_detect( + window: Window, + exe: Option, +) -> Result { + ensure_main_window(&window)?; + let exe = normalize_exe(exe)?; let version = detect_claude(&exe).await; let mcp_servers = if version.is_some() { claude_mcp_list(&exe).await @@ -62,13 +113,13 @@ pub async fn coding_agent_detect(exe: Option) -> ClaudeDetectionWire { Vec::new() }; let has_computer_use = has_computer_use_mcp(&mcp_servers); - ClaudeDetectionWire { + Ok(ClaudeDetectionWire { installed: version.is_some(), version, exe, mcp_servers, has_computer_use, - } + }) } /// 护栏化地无头跑一次 claude,事件流式 emit 到前端 `coding-agent:test`。 @@ -77,6 +128,7 @@ pub async fn coding_agent_detect(exe: Option) -> ClaudeDetectionWire { /// 若 workdir 是 git 仓库,运行前做一次 `git stash create` 快照(可回滚)。 #[tauri::command] pub async fn coding_agent_run_test( + window: Window, app: AppHandle, prompt: String, exe: Option, @@ -85,11 +137,12 @@ pub async fn coding_agent_run_test( model: Option, max_budget_usd: Option, ) -> Result<(), String> { + ensure_main_window(&window)?; let prompt = prompt.trim().to_string(); if prompt.is_empty() { return Err("指令为空".into()); } - let exe = normalize_exe(exe); + let exe = normalize_exe(exe)?; let mode = permission_mode.unwrap_or_default(); let cwd = workdir diff --git a/openless-all/app/src-tauri/src/commands/qa.rs b/openless-all/app/src-tauri/src/commands/qa.rs index 33f92254..acc64238 100644 --- a/openless-all/app/src-tauri/src/commands/qa.rs +++ b/openless-all/app/src-tauri/src/commands/qa.rs @@ -77,7 +77,19 @@ pub fn less_computer_window_resize(coord: CoordinatorState<'_>, height: f64) { } /// 内联审批卡的 Approve / Deny 回执。token 关联到等待中的拦截动作。 +/// +/// 安全:审批 UI 渲染在 less-computer 窗口(LessComputerPanel),故仅允许该窗口提交, +/// 拦截 main / capsule / qa / glow 等其它窗口伪造审批 —— 把可调用窗口从 5 个收紧到 1 个。 #[tauri::command] -pub fn less_computer_approve(coord: CoordinatorState<'_>, token: String, approved: bool) { +pub fn less_computer_approve( + window: Window, + coord: CoordinatorState<'_>, + token: String, + approved: bool, +) -> Result<(), String> { + if window.label() != "less-computer" { + return Err("approval can only be submitted from the Less Computer window".to_string()); + } coord.less_computer_approve(&token, approved); + Ok(()) } diff --git a/openless-all/app/src-tauri/src/commands/settings.rs b/openless-all/app/src-tauri/src/commands/settings.rs index 855eddea..03d96fb0 100644 --- a/openless-all/app/src-tauri/src/commands/settings.rs +++ b/openless-all/app/src-tauri/src/commands/settings.rs @@ -473,6 +473,13 @@ pub async fn app_download_and_install_android_update( signature: String, version: String, ) -> Result<(), String> { + // 安全:下载前校验 URL,防止 SSRF(如内网元数据接口、localhost 服务)。 + // 只允许已知的 GitHub 直链和 fastgit 镜像前缀。 + const DIRECT_BASE: &str = "https://github.com/appergb/openless"; + const MIRROR_BASE: &str = "https://fastgit.cc/https://github.com/appergb/openless"; + if !url.starts_with(DIRECT_BASE) && !url.starts_with(MIRROR_BASE) { + return Err(format!("不信任的更新 URL,拒绝下载: {url}")); + } #[cfg(target_os = "android")] { return crate::android::updater::download_and_install(app, url, signature, version).await; diff --git a/openless-all/app/src-tauri/src/coordinator/dictation.rs b/openless-all/app/src-tauri/src/coordinator/dictation.rs index 5f1139a5..f0311a9b 100644 --- a/openless-all/app/src-tauri/src/coordinator/dictation.rs +++ b/openless-all/app/src-tauri/src/coordinator/dictation.rs @@ -671,7 +671,11 @@ async fn run_voice_agent_transcript( None => outcome, }; - inner.state.lock().phase = SessionPhase::Idle; + { + let mut state = inner.state.lock(); + state.phase = SessionPhase::Idle; + state.focus_target = None; // 清除过期焦点目标,避免影响下次会话 + } // 工作结束:熄灭全屏彩虹描边(聊天浮窗保留,等用户读完/关闭)。 if let Some(app) = inner.app.lock().clone() { crate::hide_less_computer_glow(&app); diff --git a/openless-all/app/src-tauri/src/coordinator/hotkey_loops.rs b/openless-all/app/src-tauri/src/coordinator/hotkey_loops.rs index 9f9643df..a2cc853c 100644 --- a/openless-all/app/src-tauri/src/coordinator/hotkey_loops.rs +++ b/openless-all/app/src-tauri/src/coordinator/hotkey_loops.rs @@ -1018,7 +1018,9 @@ pub(super) fn hotkey_bridge_loop(inner: Arc, rx: mpsc::Receiver { - async_runtime::spawn(async move { handle_qa_hotkey_pressed(&inner_cloned).await }); + async_runtime::block_on(async { + handle_qa_hotkey_pressed(&inner_cloned).await; + }); } } } diff --git a/openless-all/app/src-tauri/src/persistence.rs b/openless-all/app/src-tauri/src/persistence.rs index ab8a26dd..6c5acdd5 100644 --- a/openless-all/app/src-tauri/src/persistence.rs +++ b/openless-all/app/src-tauri/src/persistence.rs @@ -1297,8 +1297,8 @@ impl PreferencesStore { pub fn set(&self, prefs: UserPreferences) -> Result<()> { let json = serde_json::to_vec_pretty(&prefs).context("encode prefs failed")?; - atomic_write(&self.path, &json)?; let mut guard = self.state.lock(); + atomic_write(&self.path, &json)?; *guard = prefs; Ok(()) } diff --git a/openless-all/app/src-tauri/src/polish/prompt_compose.rs b/openless-all/app/src-tauri/src/polish/prompt_compose.rs index 78c96e1a..b0fd8a9c 100644 --- a/openless-all/app/src-tauri/src/polish/prompt_compose.rs +++ b/openless-all/app/src-tauri/src/polish/prompt_compose.rs @@ -24,7 +24,21 @@ pub(super) fn context_premise( .map(|s| s.trim()) .filter(|s| !s.is_empty()) .collect(); - let app = front_app.map(str::trim).filter(|s| !s.is_empty()); + // 安全:window title 是攻击者可控字段,嵌入前必须清理。 + // 去除换行符(防止注入多行指令)和 Markdown/XML 分隔符(防止结构性提示注入); + // 截断到 100 个字符(远超任何真实 app 名称的合理长度)。 + let app = front_app + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(|s| { + let sanitized: String = s + .chars() + .filter(|c| *c != '\n' && *c != '\r' && *c != '#' && *c != '<' && *c != '>') + .take(100) + .collect(); + sanitized + }) + .filter(|s| !s.is_empty()); let script_line = match chinese_script_preference { ChineseScriptPreference::Simplified => Some( diff --git a/openless-all/app/src-tauri/src/remote_server/mod.rs b/openless-all/app/src-tauri/src/remote_server/mod.rs index 347cc263..21532a7e 100644 --- a/openless-all/app/src-tauri/src/remote_server/mod.rs +++ b/openless-all/app/src-tauri/src/remote_server/mod.rs @@ -50,6 +50,10 @@ const PIN_MAX_FAILS: u32 = 5; const PIN_LOCK_SECS: u64 = 60; /// pin_fails 表的容量上限:超过即清理已过期/已解锁的条目,防止伪造海量源 IP 撑爆内存。 const PIN_FAILS_MAX_ENTRIES: usize = 256; +/// 全局(跨 IP)PIN 失败上限和重置窗口。防止局域网内多台设备轮换 IP 绕过按 IP 的限速。 +/// 20 次/分钟 ≈ 0.02% 的 PIN 空间,在 60s 锁定前实际可试到的组合数极少。 +const PIN_GLOBAL_MAX_FAILS: u32 = 20; +const PIN_GLOBAL_WINDOW_SECS: u64 = 60; /// 单个 PCM 二进制帧的上限。16kHz/16bit 实时流正常每帧只有几 KB,64KB ≈ 2 秒音频; /// 超限帧直接丢弃,防已配对客户端(或驱动它的恶意网页)推超大帧造成内存压力。 const MAX_PCM_FRAME_BYTES: usize = 64 * 1024; @@ -104,11 +108,20 @@ pub struct RemoteInputStatus { // ───────────────────────── 工具函数 ───────────────────────── -/// 生成 6 位数字配对码。用 uuid v4 的随机字节取模,无需引入 rand。 +/// 生成 6 位数字配对码。使用 rejection sampling 消除取模偏差(u32::MAX 不是 +/// 1_000_000 的倍数,直接取模会给低编号 PIN 带来约 0.03% 的额外概率)。 pub fn generate_pin() -> String { - let b = uuid::Uuid::new_v4().into_bytes(); - let n = u32::from_le_bytes([b[0], b[1], b[2], b[3]]) % 1_000_000; - format!("{n:06}") + // u32::MAX + 1 = 2^32;舍弃使结果偏置的高端余数区间。 + // 偏置截止点:u32::MAX - (u32::MAX % 1_000_000) + 1(向下取整到 1_000_000 的倍数) + const LIMIT: u32 = u32::MAX - (u32::MAX % 1_000_000); + loop { + let b = uuid::Uuid::new_v4().into_bytes(); + let n = u32::from_le_bytes([b[0], b[1], b[2], b[3]]); + if n < LIMIT { + return format!("{:06}", n % 1_000_000); + } + // 极罕见(约 2^32 mod 10^6 / 2^32 ≈ 0.007% 概率),重新采样即可。 + } } fn pin_path(app: &AppHandle) -> Option { @@ -283,6 +296,8 @@ struct WsState { app: AppHandle, /// 按源 IP 的 PIN 失败计数 + 锁定截止时刻(防爆破;TLS+6 位 PIN 已是主防线)。 pin_fails: Mutex)>>, + /// 全局 PIN 失败计数 + 计数窗口起始时刻(防跨 IP 分布式暴力)。 + pin_global_fails: Mutex<(u32, Instant)>, /// 自签名证书的 DER 原始字节,供 /cert.cer 下载给手机安装信任。 cert_der: Vec, /// 服务关停广播的接收端,每条 WS 连接 clone 一份并在主循环 select 监听。 @@ -446,6 +461,7 @@ pub async fn start(cfg: RemoteServerConfig) -> Result, peer_ip: IpAddr) -> AuthResult // 锁定检查与失败累计放同一临界区:之前分两次拿锁,同一 IP 的并发握手可以 // 都先通过锁定检查再各自累计失败,让计数越过阈值却不触发锁定。 let now = Instant::now(); + + // 全局限速:防局域网内多台设备轮换 IP 绕过按 IP 的限速(分布式暴力)。 + { + let mut global = state.pin_global_fails.lock(); + let window_elapsed = now.duration_since(global.1).as_secs(); + if window_elapsed >= PIN_GLOBAL_WINDOW_SECS { + // 滑动窗口到期,重置计数 + *global = (0, now); + } + if !pin_ok { + global.0 += 1; + } + if global.0 >= PIN_GLOBAL_MAX_FAILS { + return AuthResult::Locked; + } + } + let mut guard = state.pin_fails.lock(); if let Some((_, Some(until))) = guard.get(&peer_ip) { if now < *until { @@ -748,6 +781,9 @@ fn verify_hello(txt: &str, state: &Arc, peer_ip: IpAddr) -> AuthResult } if pin_ok { guard.remove(&peer_ip); + // 成功认证后重置全局计数,避免暴力后期合法用户被误锁 + let mut global = state.pin_global_fails.lock(); + global.0 = 0; AuthResult::Ok } else { // 容量兜底:先丢已解锁/过期的条目,防伪造海量源 IP 撑爆表。 diff --git a/openless-all/app/src-tauri/tauri.conf.json b/openless-all/app/src-tauri/tauri.conf.json index 81b7e17c..24d680e8 100644 --- a/openless-all/app/src-tauri/tauri.conf.json +++ b/openless-all/app/src-tauri/tauri.conf.json @@ -101,8 +101,8 @@ "updater": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDZGNEI1OTk0RjMzMzk0QTkKUldTcGxEUHpsRmxMYjRnMFdZNkFyaVozaHN2SUNwZ01mMDlFS0RMWnNQNisrWjF6czZQQk1RQysK", "endpoints": [ - "https://fastgit.cc/https://github.com/appergb/openless/releases/latest/download/latest-{{target}}-{{arch}}-mirror.json", - "https://github.com/appergb/openless/releases/latest/download/latest-{{target}}-{{arch}}.json" + "https://github.com/appergb/openless/releases/latest/download/latest-{{target}}-{{arch}}.json", + "https://fastgit.cc/https://github.com/appergb/openless/releases/latest/download/latest-{{target}}-{{arch}}-mirror.json" ] } }