diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5da101927d..028bbca96f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,11 +147,6 @@ jobs: steps: - uses: actions/checkout@v5 - - name: Setup OpenSSL (Windows, prebuilt) - if: runner.os == 'Windows' - shell: pwsh - run: ./scripts/ci/setup-openssl-windows.ps1 - # Tauri code generation only requires its configured resource roots to # exist during check/test; distributable assets remain frontend-build's owner. - name: Create Tauri resource directories diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index 37e3d77a30..b59bc901ce 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -133,11 +133,6 @@ jobs: with: ref: ${{ needs.prepare.outputs.checkout_ref }} - - name: Setup OpenSSL (Windows, prebuilt) - if: runner.os == 'Windows' - shell: pwsh - run: ./scripts/ci/setup-openssl-windows.ps1 - - name: Install NSIS (Windows) if: runner.os == 'Windows' shell: pwsh diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index fbcaed3487..a05e31a668 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -100,11 +100,6 @@ jobs: steps: - uses: actions/checkout@v5 - - name: Setup OpenSSL (Windows, prebuilt) - if: runner.os == 'Windows' - shell: pwsh - run: ./scripts/ci/setup-openssl-windows.ps1 - - name: Install Linux system dependencies (Tauri bundler) if: runner.os == 'Linux' shell: bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1532ef9bf2..993d6a26c0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,17 +23,6 @@ but project scripts should run on Node.js 22.12+ unless a narrower local guide says otherwise. After switching from an older Node.js version, rerun `pnpm install`. -#### Windows: OpenSSL Setup - -Most Windows contributors do not need to configure OpenSSL manually. Use -`pnpm run desktop:dev` or the normal `desktop:build*` scripts; they bootstrap a -pre-built OpenSSL package when needed. - -Only handle OpenSSL yourself when the bootstrap fails, you are preparing CI, or -you intentionally use `pnpm run desktop:dev:raw`. In that case, run -`scripts/ci/setup-openssl-windows.ps1`, or set `OPENSSL_DIR` to a pre-built x64 -OpenSSL directory and set `OPENSSL_STATIC=1`. - #### Build Prerequisites Check When `cargo check --workspace`, `cargo check -p bitfun-desktop`, or pnpm build diff --git a/CONTRIBUTING_CN.md b/CONTRIBUTING_CN.md index 11953c8910..6b6b268c18 100644 --- a/CONTRIBUTING_CN.md +++ b/CONTRIBUTING_CN.md @@ -22,15 +22,6 @@ GitHub Actions 升级使用的是兼容 Node.js 24 的 action runtime,但项 默认仍以 Node.js 22.12+ 为基线,除非局部指南另有说明。从旧 Node.js 版本切换 后,请重新运行 `pnpm install`。 -#### Windows:OpenSSL 配置 - -大多数 Windows 贡献者不需要手动配置 OpenSSL。使用 `pnpm run desktop:dev` -或常规 `desktop:build*` 脚本即可;脚本会在需要时自动引导预编译的 OpenSSL 包。 - -只有在自动引导失败、准备 CI 环境,或你明确使用 `pnpm run desktop:dev:raw` -时才需要手动处理。此时运行 `scripts/ci/setup-openssl-windows.ps1`,或将 -`OPENSSL_DIR` 指向预编译的 x64 OpenSSL 目录,并设置 `OPENSSL_STATIC=1`。 - #### 构建前置检查 当 `cargo check --workspace`、`cargo check -p bitfun-desktop` 或 pnpm 构建 diff --git a/Cargo.lock b/Cargo.lock index c6df8b380e..6281dfb53f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4355,22 +4355,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.20" @@ -5583,23 +5567,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe 0.2.1", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "ndk" version = "0.9.0" @@ -6356,31 +6323,6 @@ dependencies = [ "libc", ] -[[package]] -name = "openssl" -version = "0.10.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "foreign-types 0.3.2", - "libc", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "openssl-probe" version = "0.1.6" @@ -7916,12 +7858,10 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", "mime_guess", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -7933,7 +7873,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -10451,16 +10390,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" diff --git a/Cargo.toml b/Cargo.toml index 15dc95ee16..9c4e133cd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,7 +121,7 @@ encoding_rs = "0.8.35" url = "2" # HTTP client -reqwest = { version = "0.13.4", default-features = false, features = ["native-tls", "rustls", "http2", "json", "stream", "multipart", "query", "form"] } +reqwest = { version = "0.13.4", default-features = false, features = ["http2", "json", "stream", "multipart", "query", "form"] } semver = "1.0" # Debug Log HTTP Server diff --git a/docs/architecture/platform-portability-design.md b/docs/architecture/platform-portability-design.md index 9bbb7105d9..dd1a06b283 100644 --- a/docs/architecture/platform-portability-design.md +++ b/docs/architecture/platform-portability-design.md @@ -100,7 +100,7 @@ Cargo package `bitfun-cli` 的 `aarch64-unknown-linux-ohos` 目标依赖解析 | 进程与交互终端 | `portable-pty -> termios` 依赖 openpty、shell、信号、进程组和 `/dev` 语义 | 交互 shell、取消和子进程回收不成立 | OHOS 公开进程/PTY 能力与产品可接受的能力范围 | | 文件监听 | `notify` 在目标解析中选择 `inotify` | `target_os=linux` 导致错误后端选择;替代实现可能增加延迟和资源消耗 | 真机 watch 语义、性能预算和不可用时的产品状态 | | Git 与原生库 | `git2 -> libgit2-sys -> openssl-sys` 带入 CMake、zlib、OpenSSL 等原生构建 | 交叉编译、证书、凭据和行为一致性风险 | Git 能力的支持范围和可维护实现路径 | -| 网络与 TLS | 当前闭包同时存在 native-tls、rustls、OpenSSL、aws-lc/ring 等路径 | 产物膨胀、证书来源错误、代理或流式响应异常 | OHOS 网络、根证书、代理、流式响应和取消能力 | +| 网络与 TLS | Reqwest 已收敛为 Rustls + 平台证书验证;Git/libgit2 与其他协议仍有独立原生 TLS/加密路径,aws-lc/ring 选择也尚未针对 OHOS 验证 | 不能把通用客户端单栈误当成 OHOS 网络、证书或代理已适配 | OHOS 网络、根证书、代理、流式响应、取消能力,以及非 Reqwest 协议的 TLS 来源 | | 存储与路径 | `rusqlite/libsqlite3-sys`、`dirs` 及多处路径探测依赖桌面/类 Unix 假设 | 会话损坏、凭据泄露、升级后路径漂移 | 配置、数据、缓存、日志、凭据与工作区边界 | | Tokio 与平台条件 | `tokio(full)` 打开 process、signal、net、fs;源码存在大量 `cfg(unix)`、`cfg(not(windows))` | 未使用能力扩大闭包;OHOS 错走 Linux 分支 | 实际可达能力、系统调用兼容性和取消/事件语义 | | 用户发行与支持 | 普通用户 native CLI 安装、系统终端入口和升级渠道尚未证明 | 开发探针被包装成产品;支持范围无法维护 | 支持的设备、系统、终端、渠道与生命周期 | diff --git a/docs/architecture/rust-build-dependency-boundaries.md b/docs/architecture/rust-build-dependency-boundaries.md index 5506d5f1c0..44f304ac2e 100644 --- a/docs/architecture/rust-build-dependency-boundaries.md +++ b/docs/architecture/rust-build-dependency-boundaries.md @@ -62,6 +62,14 @@ Cargo 会统一同一 package 在依赖图中的 feature;workspace dependency - target-specific dependency 放在最接近平台实现的 owner,不因单一平台需求污染跨平台 crate; - 修改共享 dependency feature 视为构建影响变更,必须检查真实产品组合的 feature graph。 +### 3.4 Reqwest TLS 后端由客户端 owner 选择 + +- workspace 级 `reqwest` 只统一版本以及跨产品共享的 HTTP、序列化和流能力,不启用 TLS 后端; +- 真正创建 HTTPS client 的 app、service 或 adapter 必须在自身依赖声明中显式选择 `reqwest/rustls`,只使用 `reqwest::Url` 的 contract/assembly 路径不加载 TLS; +- capability crate 的每个 Reqwest owner feature 必须独立带齐 `reqwest/rustls`,不能依赖 `product-full` 或其他 feature 的 Cargo feature-union 偶然补齐; +- 边界检查以 Cargo metadata 的解码结果看护全部直接 consumer,并检查 resolved Reqwest feature union,防止传递依赖重新激活 Native TLS; +- 不并列启用 native-tls 兼容栈。只有真实产品场景无法由 Rustls 平台证书验证承载时,才以明确行为证据评审替换方案,而不是重新叠加第二后端。 + ## 4. 依赖 owner 与准入检查 第三方库应位于调用外部系统或实现具体能力的最低合理 owner: diff --git a/docs/performance/01-compile-performance.md b/docs/performance/01-compile-performance.md index d501046f7a..e7bea4ece5 100644 --- a/docs/performance/01-compile-performance.md +++ b/docs/performance/01-compile-performance.md @@ -20,7 +20,7 @@ | F7 | CI:`rust-build-check` 串行等待 `frontend-build`(含大量 lint/audit/test)完成才开始 | CI | 中 | `.github/workflows/ci.yml:72` | | F8 | 每次 `desktop:dev` 无条件重建 mobile-web(先删 target 内产物 + pnpm install + vite build) | 脚本 | 中 | `scripts/dev.cjs:704-714`、`scripts/mobile-web-build.cjs:75-133` | | F9 | 依赖树重复严重:1177 个包中 112 个名字存在多版本(image、thiserror、rand×3、getrandom×4、windows-sys×6、phf×6 等) | Rust | 中 | Cargo.lock 解析;`src/apps/desktop/Cargo.toml:85`(image 0.24 vs workspace 0.25) | -| F10 | reqwest 同时启用 native-tls + rustls,双 TLS 栈都参与编译 | Rust | 中 | `Cargo.toml:121` | +| F10 | reqwest TLS 单栈治理(已完成) | Rust | 已兑现 | workspace transport-only + 客户端 owner 显式 Rustls;见 F10 治理结果 | | F11 | Vite dev watcher 强制 usePolling + 100ms 轮询,Windows 上 CPU 高、拖慢 HMR | 前端 | 中 | `src/web-ui/vite.config.ts:68-74` | | F12 | prompts/公告 md 内嵌进 bitfun-core 的 build.rs 生成代码,改一个提示词 = 重编 202k 行 core + 下游 | Rust | 中 | `src/crates/assembly/core/build.rs:110-183,343-401` | | F13 | beforeBuildCommand 内 web 构建与 mobile-web 构建纯串行;dev.cjs 准备步骤也全串行 | 脚本 | 中 | `src/apps/desktop/tauri.conf.json`(build 块)、`scripts/dev.cjs:668-737` | @@ -149,15 +149,15 @@ Cargo.lock 共 1177 个包,其中 112 个名字存在 2 个以上版本(解析 2. 运行 `cargo tree -d -e normal --workspace`(建议加 `--target x86_64-pc-windows-msvc` 过滤无关平台)输出清单,针对 top 传递源头(如 pull 出 rand 0.7 的 crate)评估升级;`russh 0.45`、`screenshots 0.8`、`syntect-tui` 等旧版是主要嫌疑。 3. 将该清单纳入 nightly CI 做趋势看护(非阻断)。 -### F10(中)reqwest 同时启用 native-tls 与 rustls +### F10(已治理)reqwest TLS 单栈 -**问题描述** -`Cargo.toml:121`:`reqwest = { ..., features = ["native-tls", "rustls", ...] }`。两套 TLS 栈(schannel/native-tls + rustls/ring)全部参与编译;workspace 另有独立 `rustls`(267 行)与 `tokio-tungstenite rustls-tls-native-roots`(245 行),说明 rustls 反正逃不掉,native-tls 则额外拖上 openssl-sys 相关生态(Windows 上还催生了 `ensure-openssl-windows.mjs` 整套预编译 OpenSSL bootstrap)。 - -**预期收益**:中(冷构建时间;若能移除 native-tls,Windows 构建可望摆脱 OpenSSL bootstrap 依赖链)。 +**治理结果(2026-08-03)** +- workspace 级 `reqwest` 只保留 HTTP/序列化能力,URL-only 消费者不再被动加载 TLS;CLI、Desktop、AI Adapter、MiniApp Market 与 Services 的各 Reqwest owner feature 显式选择 Rustls。 +- Reqwest 0.13 的 `rustls` feature 使用平台证书验证器,保留系统信任根行为;三处显式 Client Builder 统一使用 `tls_backend_rustls()`,不再混用默认后端或弃用 API。 +- `Cargo.lock` 已移除 `native-tls`、`hyper-tls`、`tokio-native-tls`、`openssl` 与 `openssl-macros`;`openssl-sys` 只剩非 Windows 的 Git/libgit2 目标路径。Windows 产品图、本地开发、常规构建和三个既有 workflow 不再下载或配置预编译 OpenSSL。 +- 边界检查约束 TLS 必须由真实客户端 owner 选择,避免未来把后端重新提升为 workspace 全局 feature;没有新增 CI job 或测试步骤。 -**优化方案** -- 审计运行时实际选用的 TLS(代码中 `Client::builder().use_native_tls/use_rustls` 调用点),统一到 rustls(`rustls-tls-native-roots` 以保留系统根证书行为),移除 `native-tls` feature;git2 侧确认 `https` feature 的 TLS 来源后同步评估。风险:企业代理/自签证书环境行为差异,需回归远端连接类功能。 +**收益与风险**:Windows CI 每个受影响 job 省去约 6.6 秒 OpenSSL bootstrap(以 PR #1991 的 Windows job 步骤时间戳为基线),并减少一套 TLS 依赖闭包。显式代理配置仍由 Reqwest 处理;未受系统信任的自签证书仍会按安全默认值拒绝。仓库没有 Reqwest client identity 或自定义 native connector 用法,因此未保留第二后端兼容层。 ### F11(中)Vite dev 强制 100ms 轮询 watch @@ -247,7 +247,7 @@ Cargo.lock 共 1177 个包,其中 112 个名字存在 2 个以上版本(解析 | T8 | 提交 Cargo.lock(F6):从 `.gitignore:26` 移除并提交根与 installer 两份 lockfile;CI 删除 `cargo generate-lockfile` 步骤;建立定期依赖更新流程后逐步解除 `Cargo.toml:98-107` 的 `=` 钉版。验收:rust-cache 命中率上升,CI 不再因上游发版突然变慢/失败。 | `.gitignore`、`.github/workflows/*.yml`、`Cargo.toml` | 中(流程变更,需团队确认依赖更新策略) | | T9 | Vite watch 去轮询(F11):删除 `src/web-ui/vite.config.ts:68-74` 的 `usePolling/interval`,保留 ignored 列表;以 `VITE_USE_POLLING=1` 环境变量作为网络盘用户逃生口。验收:dev server 空闲 CPU 占用明显下降,HMR 正常。 | `src/web-ui/vite.config.ts` | 低(个别特殊文件系统需逃生口) | | T10 | build.rs 确定性输出(F18):`assembly/core/build.rs` 与 `src/apps/cli/build.rs` 生成代码前对 key 排序(HashMap→BTreeMap)。验收:连续两次 clean build 生成的 OUT_DIR 文件字节一致。 | `src/crates/assembly/core/build.rs`、`src/apps/cli/build.rs` | 低 | -| T11 | reqwest TLS 单栈(F10):审计 `use_native_tls/use_rustls` 调用点后,从 `Cargo.toml:121` 移除 `native-tls` feature,统一 rustls-tls-native-roots;回归远端连接/订阅鉴权/代理场景;评估 Windows OpenSSL bootstrap 链能否随之精简。验收:Cargo.lock 无 native-tls/openssl-sys(git2 除外),网络功能回归通过。 | `Cargo.toml:121`、相关调用点 | 高(TLS 行为变化,企业代理/自签证书场景需重点回归;可放最后) | +| T11(已完成) | reqwest TLS 单栈(F10):workspace 保持 transport-only,由真实客户端 owner 显式选择 Rustls;删除 native-tls 与无消费者的 Windows OpenSSL bootstrap,保留平台证书验证。验收:Cargo.lock 无 native-tls;Windows 产品图无 openssl-sys,非 Windows 的 Git/libgit2 路径不变;相关最小 feature 与产品入口编译通过。 | 根与客户端 owner `Cargo.toml`、相关调用点、既有 workflow | 已完成;平台信任根行为保留,无第二后端兼容层 | | T12 | beforeBuildCommand 并行(F13):新增 `scripts/frontend-build-all.mjs` 并行跑 build:web 与 prepare:mobile-web,tauri.conf.json / tauri.dev.conf.json 的 beforeBuildCommand 指向它;dev.cjs 准备步骤改 Promise.all。验收:desktop:build 前端阶段时长≈max(两者) 而非 sum。 | `src/apps/desktop/tauri.conf.json`、`tauri.dev.conf.json`、`scripts/dev.cjs`、新脚本 | 低 | | T13 | bitfun-core 拆分启动(F3,长期):先跑 `cargo build --timings` 与 `cargo tree -d` 存档基线;选 1-2 个低耦合子域(如 announcement、debug-log server)试点拆出独立 crate 并保留 re-export;结合 F12 的"dev 运行时读取提示词"改造。验收:改动试点子域后 `cargo build -p bitfun-desktop` 的重编 crate 数与耗时下降。 | `src/crates/assembly/core/**`、根 `Cargo.toml` members | 中-高(架构改动,分多个 PR 渐进) | | T14 | 可选工具链增强(F5):提交 `.cargo/config.toml` 模板(注释形式提供 rust-lld 与 sccache 配置,默认不启用),团队自选开启;CI 冷构建可评估 sccache-action。验收:提供文档,默认行为不变。 | 新增 `.cargo/config.toml`、文档 | 低(默认关闭) | @@ -263,4 +263,4 @@ T2 + T4 + T5 + T6 + T9 + T10 + T12:全部低风险,合计可显著改善日常 d - `BitFun-Installer/src-tauri/Cargo.toml:61-65` 的 release profile 同样是 `lto=true + codegen-units=1`(opt-level="z"),T3 的 thin LTO 评估可一并覆盖。 - Cargo.lock:1177 个包,112 个存在多版本(windows-sys ×6、phf ×6、getrandom ×4、nix ×4、rand ×3、quick-xml ×4 等)。 - 前端:web-ui 1676 个 TS/TSX、366,580 行;dist 共 872 个 asset,JS 总量 14.3MB,入口 chunk 5.4MB(含 Monaco 内核);public/monaco-editor 14MB/103 文件。 -- 现有良好实践(保持):release-fast profile(Cargo.toml:288-293)、cargo-target-gc 缓存清理、Windows 预编译 OpenSSL、sherpa-onnx prebuilt、CI CARGO_INCREMENTAL=0 + debug=0(ci.yml:74-77)、swatinem/rust-cache、pnpm store 缓存、workspace.dependencies 统一版本声明。 +- 现有良好实践(保持):release-fast profile(Cargo.toml:288-293)、cargo-target-gc 缓存清理、Reqwest Rustls 单栈与平台证书验证、sherpa-onnx prebuilt、CI CARGO_INCREMENTAL=0 + debug=0(ci.yml:74-77)、swatinem/rust-cache、pnpm store 缓存、workspace.dependencies 统一版本声明。 diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index 25dcd1631d..4290c8546f 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -11,6 +11,9 @@ import { findCargoLayerViolations, findFeatureGatedTestTargetViolations, findProductEntrypointCoreFeatureViolations, + findReqwestDependencyFeatureViolations, + findResolvedReqwestNativeTlsViolations, + findServicesIntegrationsReqwestFeatureViolations, findServicesIntegrationsTokioFeatureViolations, findTokioDependencyFeatureViolations, } from './core-boundaries/cargo-dependency-boundaries.mjs'; @@ -352,6 +355,125 @@ test('services integrations Tokio owner contracts reject feature-union masking', } }); +test('services integrations Reqwest policy uses Cargo-decoded feature references', () => { + const pkg = servicesIntegrationsPackage(` +[features] +reqwest = ["dep:reqwest"] +announcement = ["reqwest", "reqwest/rustls"] +file-watch = ["reqwest?/__native-tls"] +mcp = ["reqwest"] +speech = ["reqwest", "reqwest/rustls", "reqwest/http3"] +`); + + const messages = findServicesIntegrationsReqwestFeatureViolations(pkg) + .map((violation) => violation.message) + .join('\n'); + assert.match(messages, /file-watch.*outside its reviewed owner features/); + assert.match(messages, /mcp.*missing reqwest\/rustls/); + assert.match(messages, /speech.*unreviewed Reqwest feature reference reqwest\/http3/); +}); + +test('direct Reqwest clients reject extra decoded dependency and package features', () => { + const pkg = { + ...packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [{ + name: 'reqwest', + kind: null, + optional: false, + uses_default_features: false, + features: [ + 'http2', + 'json', + 'stream', + 'multipart', + 'query', + 'form', + 'rustls', + '__native-tls', + ], + }]), + features: { default: ['reqwest?/http3'] }, + }; + + const messages = findReqwestDependencyFeatureViolations([pkg]) + .map((violation) => violation.message) + .join('\n'); + assert.match(messages, /bitfun-cli.*unexpected dependency features: __native-tls/); + assert.match(messages, /bitfun-cli:default.*unreviewed Reqwest feature reference reqwest\?\/http3/); +}); + +test('Reqwest metadata policy covers URL-only and future dependency owners', () => { + const baseFeatures = ['http2', 'json', 'stream', 'multipart', 'query', 'form']; + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml', [{ + name: 'reqwest', + kind: null, + optional: true, + uses_default_features: false, + features: baseFeatures, + }]), + features: { product: ['dep:reqwest', 'reqwest/__native-tls'] }, + }; + const future = packageAt('future-client', 'src/crates/services/future-client/Cargo.toml', [{ + name: 'reqwest', + kind: null, + optional: false, + uses_default_features: false, + features: [...baseFeatures, 'rustls'], + }]); + const duplicate = packageAt( + 'bitfun-services-integrations', + 'src/crates/services/services-integrations/Cargo.toml', + [ + { + name: 'reqwest', + kind: null, + optional: true, + uses_default_features: false, + features: baseFeatures, + }, + { + name: 'reqwest', + rename: 'windows_reqwest', + kind: null, + optional: true, + target: 'cfg(windows)', + uses_default_features: false, + features: [...baseFeatures, '__native-tls'], + }, + ], + ); + + const messages = findReqwestDependencyFeatureViolations([core, future, duplicate]) + .map((violation) => violation.message) + .join('\n'); + assert.match(messages, /bitfun-core:product.*reqwest\/__native-tls/); + assert.match(messages, /future-client.*missing a reviewed owner profile/); + assert.match(messages, /bitfun-services-integrations.*exactly one normal Reqwest dependency/); +}); + +test('resolved Reqwest feature union rejects every native TLS backend alias', () => { + const violations = findResolvedReqwestNativeTlsViolations( + [ + { + name: 'reqwest', + version: '0.13.4', + features: ['rustls', 'rustls-no-provider', '__native-tls', 'native-tls-vendored-no-alpn'], + }, + { + name: 'reqwest', + version: '0.12.28', + features: ['rustls-tls', 'default-tls'], + }, + ], + { root: TEST_ROOT }, + ); + + assert.equal(violations.length, 2); + const messages = violations.map((violation) => violation.message).join('\n'); + assert.match(messages, /__native-tls, native-tls-vendored-no-alpn/); + assert.match(messages, /reqwest 0\.12\.28.*default-tls/); +}); + test('Cargo metadata Tokio policy catches table-style and renamed full dependencies', () => { const pkg = packageAt('table-style', 'src/crates/services/table-style/Cargo.toml', [{ name: 'tokio', diff --git a/scripts/ci/setup-openssl-windows.ps1 b/scripts/ci/setup-openssl-windows.ps1 deleted file mode 100644 index 8c10cbc160..0000000000 --- a/scripts/ci/setup-openssl-windows.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -# Downloads FireDaemon OpenSSL (prebuilt) and sets OPENSSL_* for cargo/openssl-sys on Windows. -# Used by GitHub Actions; developers may run locally if paths are not already set. -# Keep $Version in sync with OPENSSL_VERSION in scripts/ensure-openssl-windows.mjs. -$ErrorActionPreference = "Stop" -$Version = "3.5.5" -$Url = "https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-$Version.zip" -$TempRoot = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP } -$Root = Join-Path $TempRoot "firedaemon-openssl" -$Zip = Join-Path $TempRoot "openssl-$Version.zip" - -New-Item -ItemType Directory -Force -Path $Root | Out-Null -if (-not (Test-Path $Zip)) { - Invoke-WebRequest -Uri $Url -OutFile $Zip -} -Expand-Archive -Path $Zip -DestinationPath $Root -Force - -$X64 = Join-Path $Root "x64" -$LibCrypto = Join-Path $X64 "lib\libcrypto.lib" -if (-not (Test-Path $LibCrypto)) { - throw "Expected prebuilt OpenSSL at $LibCrypto (extract layout changed?)" -} - -$Dir = (Resolve-Path $X64).Path -if ($env:GITHUB_ENV) { - Add-Content -Path $env:GITHUB_ENV -Value "OPENSSL_DIR=$Dir" - Add-Content -Path $env:GITHUB_ENV -Value "OPENSSL_LIB_DIR=$Dir\lib" - Add-Content -Path $env:GITHUB_ENV -Value "OPENSSL_STATIC=1" -} else { - $env:OPENSSL_DIR = $Dir - $env:OPENSSL_LIB_DIR = "$Dir\lib" - $env:OPENSSL_STATIC = "1" - Write-Host "Set OPENSSL_DIR=$Dir (current session only)" -} diff --git a/scripts/core-boundaries/cargo-dependency-boundaries.mjs b/scripts/core-boundaries/cargo-dependency-boundaries.mjs index 4a7a24f038..66af03cabb 100644 --- a/scripts/core-boundaries/cargo-dependency-boundaries.mjs +++ b/scripts/core-boundaries/cargo-dependency-boundaries.mjs @@ -2,6 +2,8 @@ import { readFileSync, readdirSync } from 'node:fs'; import { isAbsolute, join, relative, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; +import { servicesReqwestOwnerFeatures } from './rules/feature-rules.mjs'; + const SKIPPED_DIRECTORIES = new Set([ '.git', '.targets', @@ -169,6 +171,261 @@ export function findServicesIntegrationsTokioFeatureViolations(pkg) { return findOwnedTokioFeatureViolations(pkg, SERVICES_INTEGRATIONS_TOKIO_FEATURES); } +function reqwestDependencyFeatureReferences(references) { + return references.filter( + (reference) => + reference === 'reqwest' + || reference === 'dep:reqwest' + || reference.startsWith('reqwest/') + || reference.startsWith('reqwest?/'), + ); +} + +const REQWEST_TRANSPORT_FEATURES = [ + 'form', + 'http2', + 'json', + 'multipart', + 'query', + 'stream', +]; +const REQWEST_PACKAGE_PROFILES = new Map([ + ['bitfun-installer', { + dependencyFeatures: ['json', 'rustls-tls', 'stream'], + optional: false, + allowedPackageFeatureRefs: new Set(['reqwest/rustls-tls']), + }], + ['bitfun-core', { dependencyFeatures: REQWEST_TRANSPORT_FEATURES, optional: true }], + ['bitfun-services-integrations', { + dependencyFeatures: REQWEST_TRANSPORT_FEATURES, + optional: true, + servicesOwners: true, + }], + ...[ + 'bitfun-ai-adapters', + 'bitfun-cli', + 'bitfun-desktop', + 'bitfun-miniapp-market-service', + ].map((packageName) => [packageName, { + dependencyFeatures: [...REQWEST_TRANSPORT_FEATURES, 'rustls'], + optional: false, + allowedPackageFeatureRefs: new Set(['reqwest/rustls']), + }]), +]); + +function findReqwestPackageProfileViolations(pkg, profile) { + const violations = []; + const dependencies = (pkg.dependencies ?? []).filter( + (dependency) => dependency.name === 'reqwest', + ); + if (dependencies.length !== 1) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: `${pkg.name} must declare exactly one normal Reqwest dependency`, + }); + return violations; + } + + const dependency = dependencies[0]; + if ( + (dependency.kind ?? null) !== null + || (dependency.rename ?? null) !== null + || (dependency.target ?? null) !== null + ) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: + `${pkg.name} Reqwest dependency must be an unrenamed, non-target-specific normal dependency`, + }); + } + if (dependency.uses_default_features !== false) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: `${pkg.name} Reqwest dependency must disable default features`, + }); + } + if (dependency.optional !== profile.optional) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: + `${pkg.name} Reqwest dependency optional=${dependency.optional} does not match its owner profile`, + }); + } + const actualFeatures = new Set(dependency.features ?? []); + const expectedFeatures = new Set(profile.dependencyFeatures); + const missing = [...expectedFeatures] + .filter((feature) => !actualFeatures.has(feature)); + const unexpected = [...actualFeatures] + .filter((feature) => !expectedFeatures.has(feature)); + if (missing.length > 0) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: `${pkg.name} Reqwest dependency missing features: ${missing.join(', ')}`, + }); + } + if (unexpected.length > 0) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: `${pkg.name} Reqwest dependency has unexpected dependency features: ${unexpected.join(', ')}`, + }); + } + + if (profile.servicesOwners) { + violations.push(...findServicesIntegrationsReqwestFeatureViolations(pkg)); + } else { + for (const [featureName, references] of Object.entries(pkg.features ?? {})) { + for (const reference of reqwestDependencyFeatureReferences(references)) { + if ( + (reference.startsWith('reqwest/') || reference.startsWith('reqwest?/')) + && !profile.allowedPackageFeatureRefs?.has(reference) + ) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: + `${pkg.name}:${featureName} has unreviewed Reqwest feature reference ${reference}`, + }); + } + } + } + } + + return violations; +} + +function ungovernedReqwestDependencyViolations(pkg) { + const hasReqwestDependency = (pkg.dependencies ?? []).some( + (dependency) => dependency.name === 'reqwest', + ); + if (!hasReqwestDependency) { + return []; + } + return [ + { + path: pkg.manifest_path, + line: 1, + message: `${pkg.name} Reqwest dependency is missing a reviewed owner profile`, + }, + ]; +} + +export function findReqwestDependencyFeatureViolations(packages) { + return packages.flatMap((pkg) => { + const profile = REQWEST_PACKAGE_PROFILES.get(pkg.name); + if (!profile) { + return ungovernedReqwestDependencyViolations(pkg); + } + return findReqwestPackageProfileViolations(pkg, profile); + }); +} + +export function findResolvedReqwestNativeTlsViolations(records, { root }) { + const reqwestRecords = records.filter((record) => record.name === 'reqwest'); + if (reqwestRecords.length === 0) { + return [{ + path: join(root, 'Cargo.toml'), + line: 1, + message: 'resolved Cargo graph is missing Reqwest feature-union evidence', + }]; + } + + return reqwestRecords.flatMap((record) => { + const nativeTlsFeatures = (record.features ?? []).filter( + (feature) => + feature === 'default-tls' + || feature === '__native-tls' + || feature.startsWith('__native-tls-') + || feature === 'native-tls' + || feature.startsWith('native-tls-'), + ); + if (nativeTlsFeatures.length === 0) { + return []; + } + return [{ + path: join(root, 'Cargo.toml'), + line: 1, + message: + `resolved reqwest ${record.version} feature union enables an unreviewed TLS backend: ` + + nativeTlsFeatures.join(', '), + }]; + }); +} + +export function findServicesIntegrationsReqwestFeatureViolations(pkg) { + const violations = []; + const featureGraph = pkg.features ?? {}; + const ownerFeatures = new Set(servicesReqwestOwnerFeatures); + + for (const featureName of servicesReqwestOwnerFeatures) { + const references = featureGraph[featureName]; + if (!references) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: `${pkg.name}:${featureName} governed Reqwest owner feature is missing`, + }); + continue; + } + if (!references.some((reference) => reference === 'reqwest' || reference === 'dep:reqwest')) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: `${pkg.name}:${featureName} must explicitly enable reqwest`, + }); + } + if (!references.includes('reqwest/rustls')) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: `${pkg.name}:${featureName} is missing reqwest/rustls`, + }); + } + } + + for (const [featureName, references] of Object.entries(featureGraph)) { + const reqwestReferences = reqwestDependencyFeatureReferences(references); + const implicitDependencyFeature = + featureName === 'reqwest' + && reqwestReferences.length === 1 + && reqwestReferences[0] === 'dep:reqwest'; + if (implicitDependencyFeature || reqwestReferences.length === 0) { + continue; + } + if (!ownerFeatures.has(featureName)) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: + `${pkg.name}:${featureName} enables Reqwest outside its reviewed owner features`, + }); + continue; + } + for (const reference of reqwestReferences) { + if ( + reference !== 'reqwest' + && reference !== 'dep:reqwest' + && reference !== 'reqwest/rustls' + ) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: + `${pkg.name}:${featureName} has unreviewed Reqwest feature reference ${reference}`, + }); + } + } + } + + return violations; +} + + export function findServicesCoreTokioFeatureViolations(pkg) { return findOwnedTokioFeatureViolations(pkg, SERVICES_CORE_TOKIO_FEATURES); } @@ -697,6 +954,22 @@ function resolvedDependencyRecords(metadata, root) { return records; } +function resolvedPackageFeatureRecords(metadata) { + const packageById = new Map((metadata.packages ?? []).map((pkg) => [pkg.id, pkg])); + return (metadata.resolve?.nodes ?? []).flatMap((node) => { + const pkg = packageById.get(node.id); + if (!pkg) { + return []; + } + return [{ + name: pkg.name, + version: pkg.version, + source: pkg.source ?? null, + features: node.features ?? [], + }]; + }); +} + export function collectCargoMetadataGraph({ root, manifestPaths = discoverCargoManifestPaths(root), @@ -704,6 +977,7 @@ export function collectCargoMetadataGraph({ }) { const packagesByManifest = new Map(); const dependenciesByKey = new Map(); + const resolvedPackageFeaturesByKey = new Map(); const coveredManifests = new Set(); const workspaceManifest = normalizedPath(join(root, 'Cargo.toml')); const orderedManifests = [...manifestPaths].sort((left, right) => { @@ -747,11 +1021,16 @@ export function collectCargoMetadataGraph({ ].join('|'); dependenciesByKey.set(key, dependency); } + for (const record of resolvedPackageFeatureRecords(metadata)) { + const key = `${record.name}@${record.version}|${record.source ?? ''}`; + resolvedPackageFeaturesByKey.set(key, record); + } } return { packages: [...packagesByManifest.values()], resolvedDependencies: [...dependenciesByKey.values()], + resolvedPackageFeatures: [...resolvedPackageFeaturesByKey.values()], }; } @@ -781,7 +1060,11 @@ export function checkCargoDependencyLayersSafely({ root, crateLayoutRules }) { } export function checkCargoDependencyBoundaries({ root, crateLayoutRules }) { - const { packages, resolvedDependencies } = collectCargoMetadataGraph({ root }); + const { + packages, + resolvedDependencies, + resolvedPackageFeatures, + } = collectCargoMetadataGraph({ root }); return [ ...findCargoLayerViolations( packages, @@ -794,6 +1077,8 @@ export function checkCargoDependencyBoundaries({ root, crateLayoutRules }) { ), ...findFeatureGatedTestTargetViolations(packages), ...findTokioDependencyFeatureViolations(packages), + ...findReqwestDependencyFeatureViolations(packages), + ...findResolvedReqwestNativeTlsViolations(resolvedPackageFeatures, { root }), ...findServicesCorePlatformDependencyFeatureViolations(packages), ]; } diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index cb3132f397..9fe35cfe36 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -1,5 +1,19 @@ // Boundary rules for feature assembly and optional dependency ownership. +export const servicesReqwestOwnerFeatures = [ + 'announcement', + 'browser-control', + 'debug-log', + 'mcp', + 'miniapp-market', + 'miniapp-runtime', + 'remote-connect', + 'remote-ssh-concrete', + 'review-platform', + 'speech', + 'web-tools', +]; + export const optionalDependencyFeatureOwnerRules = [ { crateName: 'services-core', @@ -142,7 +156,7 @@ export const optionalDependencyFeatureOwnerRules = [ // remote-ssh-concrete: one-click relay deploy fetches the signed release // checksum over HTTPS and verifies it on this device, because the target // server has no minisign and no trust root of its own. - { depName: 'reqwest', ownerFeatures: ['announcement', 'browser-control', 'debug-log', 'mcp', 'miniapp-market', 'miniapp-runtime', 'remote-connect', 'remote-ssh-concrete', 'review-platform', 'speech', 'web-tools'] }, + { depName: 'reqwest', ownerFeatures: servicesReqwestOwnerFeatures }, { depName: 'rmcp', ownerFeatures: ['mcp'] }, { depName: 'russh', ownerFeatures: ['remote-ssh-concrete'] }, { depName: 'russh-keys', ownerFeatures: ['remote-ssh-concrete'] }, diff --git a/scripts/core-boundaries/rules/source/required-rules.mjs b/scripts/core-boundaries/rules/source/required-rules.mjs index 274166aac7..9020689cdb 100644 --- a/scripts/core-boundaries/rules/source/required-rules.mjs +++ b/scripts/core-boundaries/rules/source/required-rules.mjs @@ -1,6 +1,33 @@ // Boundary rules for source ownership, facades, and required owner content. export const requiredContentRules = [ + { + path: 'Cargo.toml', + reason: + 'workspace Reqwest defaults must stay transport-only so client owners select one TLS backend explicitly', + patterns: [ + { + regex: /^reqwest[ \t]*=[ \t]*\{[ \t]*version[ \t]*=[ \t]*"[^"]+",[ \t]*default-features[ \t]*=[ \t]*false,[ \t]*features[ \t]*=[ \t]*\[[ \t]*"http2",[ \t]*"json",[ \t]*"stream",[ \t]*"multipart",[ \t]*"query",[ \t]*"form"[ \t]*\][ \t]*\}[ \t]*$/m, + message: + 'workspace Reqwest dependency must use the reviewed transport/data feature allowlist', + }, + ], + }, + ...[ + 'src/apps/cli/Cargo.toml', + 'src/apps/desktop/Cargo.toml', + 'src/crates/adapters/ai-adapters/Cargo.toml', + 'src/crates/services/miniapp-market-service/Cargo.toml', + ].map((path) => ({ + path, + reason: 'first-party Reqwest client owners must select the repository TLS backend explicitly', + patterns: [ + { + regex: /^reqwest\s*=\s*\{\s*workspace\s*=\s*true,\s*features\s*=\s*\[\s*"rustls"\s*\]\s*\}/m, + message: 'Reqwest client dependency must explicitly enable rustls', + }, + ], + })), { path: 'src/crates/services/services-core/src/lib.rs', reason: diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index d22f57ec00..3967bf7d49 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -788,6 +788,44 @@ export function runManifestParserSelfTest({ const servicesOptionalOwnerRule = optionalDependencyFeatureOwnerRules.find( (rule) => rule.crateName === 'services-integrations', ); + const workspaceReqwestRule = requiredContentRules.find((rule) => rule.path === 'Cargo.toml'); + const workspaceReqwestRuleText = workspaceReqwestRule?.patterns + .map((pattern) => pattern.regex.source) + .join('\n') ?? ''; + for (const featureName of ['http2', 'json', 'stream', 'multipart', 'query', 'form']) { + if (!workspaceReqwestRuleText.includes(featureName)) { + throw new Error(`workspace Reqwest boundary must allow only reviewed feature ${featureName}`); + } + } + const workspaceReqwestPattern = workspaceReqwestRule?.patterns[0]?.regex; + const reviewedReqwestDeclaration = + 'reqwest = { version = "0.13.4", default-features = false, features = ["http2", "json", "stream", "multipart", "query", "form"] }'; + if (!workspaceReqwestPattern?.test(reviewedReqwestDeclaration)) { + throw new Error('workspace Reqwest boundary must accept the reviewed transport/data profile'); + } + for (const featureName of ['default-tls', 'http3', '__native-tls']) { + const expandedDeclaration = reviewedReqwestDeclaration.replace( + '"form"]', + `"form", "${featureName}"]`, + ); + if (workspaceReqwestPattern.test(expandedDeclaration)) { + throw new Error(`workspace Reqwest boundary must reject TLS-enabling feature ${featureName}`); + } + } + for (const path of [ + 'src/apps/cli/Cargo.toml', + 'src/apps/desktop/Cargo.toml', + 'src/crates/adapters/ai-adapters/Cargo.toml', + 'src/crates/services/miniapp-market-service/Cargo.toml', + ]) { + const reqwestRule = requiredContentRules.find((rule) => rule.path === path); + const reqwestRuleText = reqwestRule?.patterns + .map((pattern) => pattern.regex.source) + .join('\n') ?? ''; + if (!reqwestRuleText.includes('rustls')) { + throw new Error(`${path} must guard the explicit Reqwest Rustls client dependency`); + } + } const servicesCoreOptionalOwnerRule = optionalDependencyFeatureOwnerRules.find( (rule) => rule.crateName === 'services-core', ); diff --git a/scripts/desktop-tauri-build.mjs b/scripts/desktop-tauri-build.mjs index 0657fe6129..3e7518258a 100644 --- a/scripts/desktop-tauri-build.mjs +++ b/scripts/desktop-tauri-build.mjs @@ -1,8 +1,5 @@ #!/usr/bin/env node -/** - * Runs `tauri build` from src/apps/desktop with CI=true. - * On Windows: shared OpenSSL bootstrap (see ensure-openssl-windows.mjs). - */ +/** Runs `tauri build` from src/apps/desktop with CI=true. */ import { spawnSync } from 'child_process'; import { fileURLToPath } from 'url'; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'path'; @@ -14,7 +11,6 @@ import { statSync, writeFileSync, } from 'fs'; -import { ensureOpenSslWindows } from './ensure-openssl-windows.mjs'; import { ensureFlashgrepBinary } from './prepare-flashgrep-resource.mjs'; import { extractProductConfigArg } from './product-customization/cli.mjs'; import { productBuildEnvironment } from './product-customization/projections.mjs'; @@ -45,7 +41,6 @@ async function main() { Object.assign(process.env, productBuildEnvironment(resolution)); console.log(`[product] ${resolution.assembly.member} ${resolution.assembly.assemblyDigest}`); - await ensureOpenSslWindows(); const flashgrepBinary = ensureFlashgrepBinary(); process.env.FLASHGREP_DAEMON_BIN = flashgrepBinary; diff --git a/scripts/dev.cjs b/scripts/dev.cjs index 4c5e34c8a0..39918b36aa 100644 --- a/scripts/dev.cjs +++ b/scripts/dev.cjs @@ -324,27 +324,7 @@ async function runDesktopTargetGc(profile = 'debug') { } } -async function ensureDesktopOpenSslIfNeeded() { - if (process.platform !== 'win32') { - return; - } - - printInfo('Windows: ensuring prebuilt OpenSSL (cached under .bitfun/cache/)'); - try { - const { ensureOpenSslWindows } = await import( - pathToFileURL(path.join(__dirname, 'ensure-openssl-windows.mjs')).href - ); - await ensureOpenSslWindows(); - } catch (error) { - printError('OpenSSL bootstrap failed'); - printError(error.message || String(error)); - process.exit(1); - } -} - async function rebuildDesktopDebugBinary() { - await ensureDesktopOpenSslIfNeeded(); - const buildEnv = { ...process.env, CARGO_PROFILE_DEV_DEBUG: process.env.CARGO_PROFILE_DEV_DEBUG || '0', @@ -755,7 +735,6 @@ async function main() { try { if (mode === 'desktop') { - await ensureDesktopOpenSslIfNeeded(); const desktopDir = path.join(ROOT_DIR, 'src/apps/desktop'); const tauriConfig = path.join(desktopDir, 'tauri.dev.conf.json'); // Pin the same codegen-unit count the desktop-preview path uses diff --git a/scripts/ensure-openssl-windows.mjs b/scripts/ensure-openssl-windows.mjs deleted file mode 100644 index 0dc8919f44..0000000000 --- a/scripts/ensure-openssl-windows.mjs +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Windows: ensure FireDaemon prebuilt OpenSSL for Cargo (russh / libgit2). - * - Cached under .bitfun/cache/firedaemon-openssl-/x64 (gitignored). - * - Skips download if OPENSSL_DIR already points at a valid tree, or cache hit, or BITFUN_SKIP_OPENSSL_BOOTSTRAP=1. - * Mutates `process.env` by default so child processes (tauri, cargo) inherit OPENSSL_*. - */ -import { spawnSync } from 'child_process'; -import { createWriteStream, existsSync, mkdirSync, realpathSync } from 'fs'; -import { pipeline } from 'stream/promises'; -import { Readable } from 'stream'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const ROOT = join(__dirname, '..'); -const LOG = '[bitfun-openssl]'; - -// Keep in sync with $Version in scripts/ci/setup-openssl-windows.ps1. -export const OPENSSL_VERSION = '3.5.5'; -const OPENSSL_URL = `https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-${OPENSSL_VERSION}.zip`; -export const CACHE_ROOT = join(ROOT, '.bitfun', 'cache', `firedaemon-openssl-${OPENSSL_VERSION}`); - -function libcryptoPath(opensslDir) { - return join(opensslDir, 'lib', 'libcrypto.lib'); -} - -function opensslDirLooksValid(dir) { - return Boolean(dir && existsSync(libcryptoPath(dir))); -} - -async function downloadToFile(url, filePath) { - const res = await fetch(url); - if (!res.ok) { - throw new Error(`OpenSSL download failed: HTTP ${res.status} ${res.statusText}`); - } - if (!res.body) { - throw new Error('OpenSSL download failed: empty body'); - } - await pipeline(Readable.fromWeb(res.body), createWriteStream(filePath)); -} - -function extractZipWindows(zipPath, destDir) { - const esc = (p) => p.replace(/'/g, "''"); - const ps = `Expand-Archive -LiteralPath '${esc(zipPath)}' -DestinationPath '${esc(destDir)}' -Force`; - const r = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { - stdio: 'inherit', - cwd: ROOT, - }); - if (r.error) { - throw r.error; - } - if (r.status !== 0) { - throw new Error('Expand-Archive failed (PowerShell)'); - } -} - -/** - * No-op on non-Windows. On Windows, sets process.env OPENSSL_DIR / OPENSSL_LIB_DIR / OPENSSL_STATIC when needed. - */ -export async function ensureOpenSslWindows() { - if (process.platform !== 'win32') { - return; - } - - if (process.env.BITFUN_SKIP_OPENSSL_BOOTSTRAP === '1') { - console.log(`${LOG} BITFUN_SKIP_OPENSSL_BOOTSTRAP=1, skipping bootstrap`); - return; - } - - if (opensslDirLooksValid(process.env.OPENSSL_DIR)) { - const dir = process.env.OPENSSL_DIR; - if (!process.env.OPENSSL_LIB_DIR) { - process.env.OPENSSL_LIB_DIR = join(dir, 'lib'); - } - if (!process.env.OPENSSL_STATIC) { - process.env.OPENSSL_STATIC = '1'; - } - console.log(`${LOG} Using existing OPENSSL_DIR:`, dir); - return; - } - - mkdirSync(CACHE_ROOT, { recursive: true }); - const x64 = join(CACHE_ROOT, 'x64'); - - if (existsSync(libcryptoPath(x64))) { - process.env.OPENSSL_DIR = x64; - process.env.OPENSSL_LIB_DIR = join(x64, 'lib'); - process.env.OPENSSL_STATIC = '1'; - console.log(`${LOG} Using cached OpenSSL:`, x64); - return; - } - - const zipFile = join(CACHE_ROOT, 'dist.zip'); - if (!existsSync(zipFile)) { - console.log(`${LOG} Downloading prebuilt OpenSSL (cached for future builds)...`); - await downloadToFile(OPENSSL_URL, zipFile); - } else { - console.log(`${LOG} Re-using cached dist.zip, extracting...`); - } - extractZipWindows(zipFile, CACHE_ROOT); - - if (!existsSync(libcryptoPath(x64))) { - throw new Error( - `${LOG} Unexpected layout after extract (missing ${libcryptoPath(x64)}). Delete ${CACHE_ROOT} and retry.`, - ); - } - - process.env.OPENSSL_DIR = x64; - process.env.OPENSSL_LIB_DIR = join(x64, 'lib'); - process.env.OPENSSL_STATIC = '1'; - console.log(`${LOG} OpenSSL ready:`, x64); -} - -function isExecutedAsCli() { - const entry = process.argv[1]; - if (!entry) return false; - try { - const selfPath = realpathSync(fileURLToPath(import.meta.url)); - const entryPath = realpathSync(entry); - return selfPath === entryPath; - } catch { - return false; - } -} - -function printShellEnvHint() { - const dir = process.env.OPENSSL_DIR; - const lib = process.env.OPENSSL_LIB_DIR; - if (!dir || !lib) { - console.log( - `${LOG} No OPENSSL_DIR set in this process (skipped or use your own install). Raw cargo needs these in the shell.`, - ); - return; - } - console.log(`${LOG} For shells that do not inherit Node env (e.g. raw cargo), run in PowerShell before build:`); - console.log(` $env:OPENSSL_DIR="${dir}"`); - console.log(` $env:OPENSSL_LIB_DIR="${lib}"`); - console.log(` $env:OPENSSL_STATIC="1"`); -} - -if (isExecutedAsCli()) { - ensureOpenSslWindows() - .then(() => { - if (process.platform !== 'win32') { - console.log(`${LOG} Not Windows; nothing to do.`); - return; - } - printShellEnvHint(); - }) - .catch((e) => { - console.error(e); - process.exit(1); - }); -} diff --git a/src/apps/cli/Cargo.toml b/src/apps/cli/Cargo.toml index 3178f9a003..3717fc5b54 100644 --- a/src/apps/cli/Cargo.toml +++ b/src/apps/cli/Cargo.toml @@ -85,7 +85,7 @@ fs2 = { workspace = true } base64 = { workspace = true } image = { workspace = true } minisign-verify = "0.2" -reqwest = { workspace = true } +reqwest = { workspace = true, features = ["rustls"] } sha2 = { workspace = true } tar = { workspace = true } tempfile = "3" diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index bdc9ad00eb..934dacbde3 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -66,7 +66,7 @@ dark-light = { workspace = true } similar = { workspace = true } ignore = { workspace = true } urlencoding = { workspace = true } -reqwest = { workspace = true } +reqwest = { workspace = true, features = ["rustls"] } semver = { workspace = true } zip = { workspace = true } tar = { workspace = true } diff --git a/src/crates/adapters/ai-adapters/Cargo.toml b/src/crates/adapters/ai-adapters/Cargo.toml index 1f0d1375bd..22fd440f80 100644 --- a/src/crates/adapters/ai-adapters/Cargo.toml +++ b/src/crates/adapters/ai-adapters/Cargo.toml @@ -24,7 +24,7 @@ futures = { workspace = true } fs2 = { workspace = true, optional = true } libc = { workspace = true, optional = true } log = { workspace = true } -reqwest = { workspace = true } +reqwest = { workspace = true, features = ["rustls"] } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true, optional = true } diff --git a/src/crates/adapters/ai-adapters/src/client/http.rs b/src/crates/adapters/ai-adapters/src/client/http.rs index 826ac82b57..607e411ec4 100644 --- a/src/crates/adapters/ai-adapters/src/client/http.rs +++ b/src/crates/adapters/ai-adapters/src/client/http.rs @@ -9,7 +9,7 @@ pub(crate) fn create_http_client( skip_ssl_verify: bool, ) -> Client { let mut builder = Client::builder() - .use_rustls_tls() + .tls_backend_rustls() .connect_timeout(std::time::Duration::from_secs( AIClient::STREAM_CONNECT_TIMEOUT_SECS, )) diff --git a/src/crates/services/miniapp-market-service/Cargo.toml b/src/crates/services/miniapp-market-service/Cargo.toml index a37003b8ae..e432d6b90c 100644 --- a/src/crates/services/miniapp-market-service/Cargo.toml +++ b/src/crates/services/miniapp-market-service/Cargo.toml @@ -17,7 +17,7 @@ chrono = { workspace = true } hex = { workspace = true } image = { workspace = true } rand = { workspace = true } -reqwest = { workspace = true } +reqwest = { workspace = true, features = ["rustls"] } serde = { workspace = true } serde_json = { workspace = true } semver = { workspace = true } diff --git a/src/crates/services/services-integrations/Cargo.toml b/src/crates/services/services-integrations/Cargo.toml index 599c62c3f9..6fd6df86a7 100644 --- a/src/crates/services/services-integrations/Cargo.toml +++ b/src/crates/services/services-integrations/Cargo.toml @@ -94,8 +94,8 @@ zbus-secret-service-keyring-store = { workspace = true, optional = true } [features] default = [] -announcement = ["reqwest", "tokio/fs", "tokio/sync"] -browser-control = ["anyhow", "bitfun-services-core/process-runtime", "dirs", "reqwest", "thiserror", "tokio/time"] +announcement = ["reqwest", "reqwest/rustls", "tokio/fs", "tokio/sync"] +browser-control = ["anyhow", "bitfun-services-core/process-runtime", "dirs", "reqwest", "reqwest/rustls", "thiserror", "tokio/time"] canvas-runtime = [ "dep:bitfun-product-domains", "oxc", @@ -104,7 +104,7 @@ canvas-runtime = [ "urlencoding", "uuid", ] -debug-log = ["anyhow", "chrono", "reqwest", "tokio/rt", "uuid"] +debug-log = ["anyhow", "chrono", "reqwest", "reqwest/rustls", "tokio/rt", "uuid"] deep-research = ["bitfun-agent-runtime", "tokio/fs"] git = [ "async-trait", @@ -136,6 +136,7 @@ mcp = [ "hex", "rand", "reqwest", + "reqwest/rustls", "rmcp", "rmcp/transport-streamable-http-client-reqwest", "sha2", @@ -157,6 +158,7 @@ miniapp-runtime = [ "dep:bitfun-product-domains", "dirs", "reqwest", + "reqwest/rustls", "tokio/fs", "tokio/io-util", "tokio/net", @@ -176,6 +178,7 @@ miniapp-market = [ "hex", "miniapp-runtime", "reqwest", + "reqwest/rustls", "semver", "sha2", "thiserror", @@ -233,6 +236,7 @@ remote-connect = [ "qrcode", "rand", "reqwest", + "reqwest/rustls", "rustls", "rustls-native-certs", "schannel", @@ -282,6 +286,7 @@ remote-ssh-concrete = [ "minisign-verify", "rand", "reqwest", + "reqwest/rustls", "russh", "russh-sftp", "russh-keys", @@ -297,6 +302,7 @@ review-platform = [ "chrono", "futures", "reqwest", + "reqwest/rustls", "sha2", "thiserror", "tokio/fs", @@ -313,6 +319,7 @@ speech = [ "dep:bitfun-core-types", "futures-util", "reqwest", + "reqwest/rustls", "sha2", "sherpa-onnx", "tar", @@ -349,7 +356,7 @@ script-tool-runtime = [ "tokio/time", "which", ] -web-tools = ["reqwest", "thiserror"] +web-tools = ["reqwest", "reqwest/rustls", "thiserror"] product-full = [ "announcement", "browser-control", diff --git a/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs b/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs index faa0a13bec..bdc0629517 100644 --- a/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs +++ b/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs @@ -423,7 +423,7 @@ impl RemoteMCPTransport { let http_client = reqwest::Client::builder() .connect_timeout(Duration::from_secs(10)) .danger_accept_invalid_certs(false) - .use_rustls_tls() + .tls_backend_rustls() .default_headers(default_headers.clone()) .build() .unwrap_or_else(|e| { diff --git a/src/crates/services/services-integrations/src/review_platform_http.rs b/src/crates/services/services-integrations/src/review_platform_http.rs index 5629aa8818..c6a78da4dc 100644 --- a/src/crates/services/services-integrations/src/review_platform_http.rs +++ b/src/crates/services/services-integrations/src/review_platform_http.rs @@ -33,7 +33,7 @@ pub(crate) struct ReviewHttpClient { impl ReviewHttpClient { pub(crate) fn new_review_platform() -> Result { let inner = reqwest::Client::builder() - .use_native_tls() + .tls_backend_rustls() .redirect(review_redirect_policy()) .timeout(Duration::from_secs(REVIEW_PLATFORM_TIMEOUT_SECS)) .build()