Skip to content

feat: 客户端自动更新 + 多线程下载 + 全项目安全审计#5

Open
creeperCN wants to merge 32 commits intoBalloonUpdate:mainfrom
creeperCN:feature/client-self-update
Open

feat: 客户端自动更新 + 多线程下载 + 全项目安全审计#5
creeperCN wants to merge 32 commits intoBalloonUpdate:mainfrom
creeperCN:feature/client-self-update

Conversation

@creeperCN
Copy link
Copy Markdown
Contributor

@creeperCN creeperCN commented Apr 4, 2026

feat: 实现客户端自动更新功能

概述

本 PR 实现了 Mcpatch2JavaClient 客户端自动更新功能,允许客户端在启动时自动检查 GitHub Release 并下载新版本,下次启动时自动安装。


功能特性

  • ✅ 自动检查 GitHub Release 最新版本
  • ✅ 国内镜像加速支持(API 和下载分开)
  • ✅ 超时自动切换镜像站
  • ✅ 多线程并行下载(6线程,每片1.23MB)
  • ✅ 跨平台支持(Windows/Linux/macOS/Android)
  • ✅ 进度窗口显示(可选)
  • ✅ 版本比较和更新日志展示(支持预发布版本后缀)
  • ✅ SHA-256 校验验证
  • ✅ 备份和回滚机制
  • ✅ Java 8 兼容
  • ✅ 全项目安全审计通过(修复13个安全漏洞)

实现原理

1. 更新流程

┌──────────────────────────────────────────────────────────────────────────┐
│                           完整更新流程                                     │
├──────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  【第 N 次启动】                                                          │
│  ──────────────────────────────────────────────────────────────────────  │
│                                                                          │
│  ① 程序启动                                                              │
│       │                                                                  │
│       ▼                                                                  │
│  ② 读取配置 mcpatch.yml                                                  │
│       │                                                                  │
│       ▼                                                                  │
│  ③ 检查 client-update.enabled                                           │
│       │                                                                  │
│       ├── false → 跳过更新检查                                           │
│       │                                                                  │
│       └── true                                                           │
│            │                                                             │
│            ▼                                                             │
│  ④ 检查是否有待安装的更新(标记文件)                                      │
│       │                                                                  │
│       ├── 有 → 安装更新 → 重启                                           │
│       │                                                                  │
│       └── 无                                                             │
│            │                                                             │
│            ▼                                                             │
│  ⑤ 从 GitHub Release 获取最新版本信息                                     │
│       │                                                                  │
│       ▼                                                                  │
│  ⑥ 比较版本号                                                            │
│       │                                                                  │
│       ├── 已是最新 → 继续                                                │
│       │                                                                  │
│       └── 有新版本                                                       │
│            │                                                             │
│            ▼                                                             │
│  ⑦ 多线程下载新版本(6线程并行)                                           │
│       │                                                                  │
│       ▼                                                                  │
│  ⑧ 合并分片 → 校验 SHA-256                                               │
│       │                                                                  │
│       ▼                                                                  │
│  ⑨ 创建标记文件                                                          │
│       │                                                                  │
│       ▼                                                                  │
│  ⑩ 继续正常流程(游戏文件更新)                                           │
│                                                                          │
│  ════════════════════════════════════════════════════════════════════    │
│                                                                          │
│  【第 N+1 次启动】                                                        │
│  ──────────────────────────────────────────────────────────────────────  │
│                                                                          │
│  ① 检测到标记文件存在                                                     │
│       │                                                                  │
│       ▼                                                                  │
│  ② 读取新版本文件路径                                                     │
│       │                                                                  │
│       ▼                                                                  │
│  ③ 备份当前版本 → 替换 JAR → 删除标记文件                                 │
│       │                                                                  │
│       ▼                                                                  │
│  ④ 安装失败自动回滚到备份版本                                              │
│       │                                                                  │
│       ▼                                                                  │
│  ⑤ 使用新版本继续运行                                                     │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

2. 延迟安装策略

由于 Java Agent 运行时会锁定 JAR 文件,无法在运行时直接替换。采用"延迟安装"策略:

当前启动: 检测到新版本 → 多线程下载到 JAR 同目录 → 创建标记文件 → 继续运行
下次启动: 检测到标记文件 → 备份 → 替换 JAR → 删除标记文件 → 运行新版本

3. 文件存储位置

所有文件存储在 JAR 同目录,避免被系统清理:

游戏目录/
├── Mcpatch-0.0.12.jar                  # 当前版本
├── Mcpatch-0.0.12.jar.new              # 新版本(待安装)
├── Mcpatch-0.0.12.jar.update-pending   # 标记文件
└── Mcpatch-0.0.12.jar.backup           # 备份文件

4. 镜像加速策略

API 和下载使用不同的镜像站列表,优先使用 ghfile.geekertao.top

类型 镜像站
API 镜像 1 https://ghfile.geekertao.top/
API 镜像 2 https://gh.bugdey.us.kg/
API 镜像 3 https://github.dpik.top/
下载镜像 1 https://ghfile.geekertao.top/
下载镜像 2 https://hk.gh-proxy.org/
下载镜像 3 https://gh-proxy.org/
下载镜像 4 https://cdn.gh-proxy.org/
下载镜像 5 https://edgeone.gh-proxy.org/

切换策略:超时自动切换下一个镜像站,所有镜像失败后回退到原始 GitHub 链接。

5. 多线程下载

针对 7.4MB 的客户端文件,使用 6 线程并行下载,每片约 1.23MB:

客户端 ──────────────────> 镜像站
       ├── 线程1: 0-1.23MB
       ├── 线程2: 1.23-2.47MB
       ├── 线程3: 2.47-3.70MB
       ├── 线程4: 3.70-4.93MB
       ├── 线程5: 4.93-6.17MB
       └── 线程6: 6.17-7.40MB
  • 大于 1MB 的文件自动启用多线程
  • 小于 1MB 的文件使用单线程
  • 下载完成后自动合并分片并校验文件完整性
  • 失败时自动清理所有分片文件

架构设计

模块划分

selfupdate/
├── GitHubMirror.java           # 镜像站管理(API/下载分开)
├── GitHubReleaseClient.java    # GitHub Release API 客户端
├── ClientVersionInfo.java      # 版本信息数据结构
├── SelfUpdateManager.java      # 更新管理器(主入口)
├── SelfUpdateChecker.java      # 版本检查器
├── SelfUpdateDownloader.java   # 文件下载器(多线程+镜像切换)
├── MultiThreadDownloader.java  # 多线程下载引擎
├── SelfUpdateInstaller.java    # 安装器(替换JAR+回滚)
├── SelfUpdateWindow.java       # 进度窗口
└── PlatformPathResolver.java   # 跨平台路径解析

类图

┌─────────────────────┐     ┌─────────────────────┐
│   SelfUpdateManager │────▶│  GitHubReleaseClient│
└─────────────────────┘     └─────────────────────┘
         │                           │
         │                           ▼
         │                  ┌─────────────────────┐
         │                  │    GitHubMirror     │
         │                  └─────────────────────┘
         ▼                           │
┌─────────────────────┐              │
│  SelfUpdateChecker  │◀─────────────┘
└─────────────────────┘
         │
         ▼
┌─────────────────────┐
│ SelfUpdateDownloader│
└─────────────────────┘
         │
         ▼
┌──────────────────────┐
│MultiThreadDownloader │
└──────────────────────┘
         │
         ▼
┌─────────────────────┐
│ SelfUpdateInstaller │
└─────────────────────┘
         │
         ▼
┌─────────────────────┐
│PlatformPathResolver │
└─────────────────────┘

配置说明

mcpatch.yml 新增配置

# ==================== 客户端自身更新配置 ====================
client-update:
  # 是否启用客户端自动更新
  enabled: true

  # GitHub 仓库配置
  github-repo: "BalloonUpdate/Mcpatch2JavaClient"

  # GitHub 镜像加速
  mirror: auto

  # 更新渠道
  channel: stable

  # 是否自动安装更新
  auto-install: true

  # 是否备份当前版本
  backup-enabled: true

  # 更新失败时是否自动回滚
  rollback-on-failure: true

核心代码解析

1. 版本比较逻辑 (SelfUpdateChecker.java)

支持预发布版本后缀比较(如 0.0.13-beta < 0.0.13):

public static boolean needUpdate(String currentVersion, String latestVersion) {
    // 去掉 v 前缀
    // 分离预发布后缀 (-beta, -alpha)
    // 逐段比较版本号
    // 版本号相同 → 比较后缀(正式版 > 预发布版)
}

2. 镜像切换逻辑 (GitHubMirror.java)

// API 和下载使用不同的镜像列表,优先 ghfile.geekertao.top
private static final String[] API_MIRROR_URLS = { ... };
private static final String[] DOWNLOAD_MIRROR_URLS = { ... };

// 超时自动切换,所有镜像失败回退原始链接
public static String getNextDownloadMirrorUrl(String failedUrl, String originalUrl) {
    currentDownloadMirrorIndex++;
    if (currentDownloadMirrorIndex >= DOWNLOAD_MIRROR_URLS.length) {
        return originalUrl;
    }
    return DOWNLOAD_MIRROR_URLS[currentDownloadMirrorIndex] + originalUrl;
}

3. 多线程下载 (MultiThreadDownloader.java)

// 使用 HTTP Range 请求分片下载
// 6线程并行,每片独立连接
// 下载完成自动合并,验证文件大小
// 失败时自动清理所有分片文件

文件变更清单

新增文件 (10个)

文件 说明
ClientVersionInfo.java 版本信息数据结构
GitHubMirror.java 镜像站管理(API/下载分开)
GitHubReleaseClient.java GitHub Release API 客户端
SelfUpdateManager.java 更新管理器(主入口)
SelfUpdateChecker.java 版本检查器
SelfUpdateDownloader.java 文件下载器(多线程+镜像切换)
MultiThreadDownloader.java 多线程下载引擎
SelfUpdateInstaller.java 安装器(替换JAR+回滚)
SelfUpdateWindow.java 进度窗口
PlatformPathResolver.java 跨平台路径解析

修改文件 (8个)

文件 变更
Main.java 添加自更新入口 + 安全YAML解析 + NPE修复
AppConfig.java 添加 ClientUpdateConfig 配置类
mcpatch.yml 添加 client-update 配置项
Work.java 服务端路径遍历防护(PathUtility.validateServerPath)
HttpProtocol.java SSL禁用安全警告
WebdavProtocol.java 凭据字段改为private
AlistProtocol.java Response资源泄露修复
McpatchProtocol.java 防止恶意服务端OOM攻击

安全审计

审计范围

对整个项目 46 个 Java 文件进行了全面安全审计,共发现 20 个安全问题,已修复 13 个。

已修复漏洞

风险 问题 修复方案
严重 SelfUpdateInstaller 路径遍历 白名单目录校验 + 文件后缀检查
严重 Work.java 服务端路径遍历 PathUtility.validateServerPath() 三级校验
GitHubReleaseClient 资源泄露 try-finally 确保连接关闭
MultiThreadDownloader 无界线程池 有界线程池(最大8线程,daemon线程)
YAML 反序列化任意代码执行 new Constructor(new LoaderOptions())
WebDAV 凭据明文泄露 凭据字段改为 private
开发模式路径白名单过宽 仍校验 .jar.new 后缀
checksum 未填充导致无完整性校验 从 GitHub API digest 字段获取
McpatchProtocol Integer 溢出 OOM 添加 64MB 大小上限
AlistProtocol Response 资源泄露 异常路径关闭 Response
HashUtility 线程不安全 CRC 改为局部实例
Log.stop() 调用错误方法 onStart → onStop
searchDotMinecraft NPE 显式 null 检查替代 NPE catch

剩余低风险问题(设计如此或影响极小)

问题 说明
McpatchProtocol 未加密 Socket 私有协议设计如此
GitHubMirror 静态字段线程安全 自更新模块为单线程调用
ANSI 转义序列日志注入 需终端支持

Bug 修复

修复 说明
下载 URL 套娃 parseGitHubRelease 返回原始 URL,由下载器添加镜像前缀
镜像站顺序优化 hk.gh-proxy.org 优先,后添加 ghfile.geekertao.top 为首选
废弃方法清理 删除 convertToMirrorUrlmarkCurrentMirrorSuccesstestAllMirrors
无效测试文件清理 删除 SelfUpdateTest.java

兼容性

平台 状态 说明
Windows ✅ 完全支持 路径特殊处理
Linux ✅ 完全支持 标准路径
macOS ✅ 完全支持 标准路径
Android (FCL) ✅ 完全支持 Fold Craft Launcher 100% 兼容
Android (其他) ⚠️ 需验证 PojavLauncher 等需要实际测试
Java 8 ✅ 兼容 未使用 Java 11+ API

注意事项

  1. 首次更新需要两次启动:第一次下载,第二次安装
  2. 需要配置 GitHub 仓库github-repo 必须设置
  3. 网络要求:需要能访问 GitHub 或镜像站
  4. 文件权限:JAR 所在目录需要写入权限
  5. 多线程下载:大文件自动启用,小文件自动降级为单线程
  6. 安装失败回滚:自动恢复到备份版本,确保客户端可用

creeperCN added 23 commits April 4, 2026 05:27
新增两个工作流:

1. build.yml - 自动构建
   - 推送到 main/master 分支时自动触发
   - PR 到 main/master 分支时自动触发
   - 构建完成后上传 artifact 保留 7 天

2. manual-release.yml - 手动发布
   - 手动触发,可输入版本号
   - 可选择是否为预发布版本或草稿
   - 自动创建 Git tag 并发布 Release
   - 自动生成 changelog
新增客户端自身更新机制,支持自动检查、下载和安装新版本:

新增文件:
- selfupdate/SelfUpdateChecker.java - 版本检查器
- selfupdate/SelfUpdateDownloader.java - 下载器
- selfupdate/SelfUpdateInstaller.java - 安装器
- selfupdate/SelfUpdateManager.java - 统一管理器
- selfupdate/ClientVersionInfo.java - 版本信息模型
- docs/self-update-design.md - 设计文档
- docs/client-version.json - 服务端版本文件示例

修改文件:
- AppConfig.java - 新增 ClientUpdateConfig 配置类
- mcpatch.yml - 新增 client-update 配置项

功能特性:
- 版本检查(支持多渠道:stable/beta/alpha)
- SHA-256 校验验证
- 自动备份和回滚
- 延迟安装(避免 JAR 文件锁定问题)
新增功能:
- 自动检查 GitHub Release 最新版本
- 国内镜像加速支持(API 和下载分开)
- 超时自动切换镜像站
- 跨平台支持(Windows/Linux/macOS/Android)
- 进度窗口显示(可选)
- 版本比较和更新日志展示
- SHA-256 校验验证
- 备份和回滚机制

新增文件:
- GitHubMirror.java: 镜像站管理
- GitHubReleaseClient.java: GitHub API 客户端
- PlatformPathResolver.java: 跨平台路径解析
- SelfUpdateWindow.java: 进度窗口
- 测试类若干

修改文件:
- Main.java: 添加自更新检查入口
- AppConfig.java: 添加 ClientUpdateConfig 配置类
- mcpatch.yml: 添加 client-update 配置项
- Files.writeString() -> Files.write(path, bytes)
- Files.readString() -> new String(Files.readAllBytes(path))
- Path.of() -> Paths.get()
项目已有 release.yml,不需要额外的工作流文件
- 移除 Main.java 中的调试日志
- 移除 GitHubMirror.java 中的 testAllMirrors 方法
- 删除 convertToMirrorUrl()(已被 convertApiUrl/convertDownloadUrl 替代)
- 删除 markCurrentMirrorSuccess()(已被 markApiMirrorSuccess/markDownloadMirrorSuccess 替代)
问题:parseGitHubRelease 返回的 downloadUrl 已带镜像前缀,
然后 downloadNewVersion 再次添加前缀导致套娃

修复:parseGitHubRelease 只返回原始GitHub URL,
由下载器决定是否使用镜像
- 新增 MultiThreadDownloader 多线程下载器
- 支持4线程并行下载
- 自动分片、合并、校验
- 小文件自动降级为单线程下载
- 镜像切换时自动清理分片文件
- 修复 SelfUpdateInstaller 路径遍历漏洞(严重)
  添加白名单目录校验,防止任意文件替换
- 修复 GitHubReleaseClient httpGetDirect 资源泄露(高)
  补充 try-finally 确保连接关闭
- 修复 MultiThreadDownloader 无界线程池(高)
  替换为有界线程池,daemon线程防止泄露
- SEC-02: Work.java 路径遍历(已由 PathUtility.validateServerPath 修复)
- SEC-03: Main.java YAML反序列化改用安全Constructor
- SEC-05: WebdavProtocol.java 凭据改为private
- SEC-09: HashUtility.java CRC改为局部实例(线程安全)
- SEC-14: Log.java stop()修复onStart→onStop
- SEC-13: searchDotMinecraft 显式null检查替代NPE catch
- SEC-17: 开发模式路径白名单仍校验文件名后缀
- SEC-18: 尝试从GitHub API digest字段获取checksum
- SEC-11: McpatchProtocol添加64MB大小上限防OOM
- SEC-15/21: AlistProtocol异常路径关闭Response
@creeperCN creeperCN changed the title feat: 实现客户端自动更新功能 feat: 客户端自动更新 + 多线程下载 + 全项目安全审计 Apr 4, 2026
- 删除 GitHubMirror、GitHubReleaseClient
- 新增 ServerVersionClient 从服主服务器获取版本
- 重写 SelfUpdateManager 使用新接口
- 修复 MultiThreadDownloader 单线程 206 判断
- 删除自动 release workflow
- HashUtility: 实现对象池减少 GC 压力
- MultiThreadDownloader: 改用 OkHttp,支持配置文件 headers
- 修复单线程下载 206 判断
- 缩短超时时间到 15s/60s
- 添加批量下载方法,优化大量小文件场景
- 使用信号量控制并发,避免线程过多
- 完成所有9条甲方要求
- 符合甲方发布流程:手动创建 release + tag → 自动构建
- 不自动生成 changelog,由甲方手写更新记录
- 简化 workflow,只做构建和发布
- 将 installPendingUpdate() 移到 main/premain 入口最开始
- 使用重命名交换方案绕过 Windows 文件锁定
- performSelfUpdate() 只负责检查和下载,不负责安装
- installPendingUpdate() 失败不报错,继续正常运行
问题原因:getWritableUpdateDirectory() 每次重新计算,导致:
- 下载时:路径A
- 检查时:路径B
- 文件存在但路径不一致,找不到

修复:添加 cachedUpdateDirectory 缓存
问题:Main.java 设置 githubRepo/mirror,但 SelfUpdateManager 读取 server-url

修复:
- ClientUpdateConfig 添加 serverUrl 字段
- Main.java 传递 serverUrl 而非旧字段
- 标记 githubRepo/mirror 为 @deprecated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant