Skip to content

feat: default user agent#389

Open
tnqzh123 wants to merge 2 commits into
xfl03:15-developfrom
tnqzh123:15-develop
Open

feat: default user agent#389
tnqzh123 wants to merge 2 commits into
xfl03:15-developfrom
tnqzh123:15-develop

Conversation

@tnqzh123

Copy link
Copy Markdown
Contributor

This Pull Request resolves #388 .

为 CustomSkinLoader 添加默认 User Agent,详见上述 Issue。

代码由 OpenAI Codex 使用 GPT 5.6 Sol 模型生成,由人类完成测试,不过没测 Quilt,因为 HMCL 的 Quilt 自动安装炸了。

以下是 Codex 生成的改动说明:(点击查看)

变更概述

本次变更为 CustomSkinLoader 的 HTTP 请求添加统一的默认 User-Agent,并允许 loadlist 和 ExtraList 中的自定义 userAgent 使用运行时占位符。

默认 User-Agent 至少包含以下信息:

  • CustomSkinLoader 版本
  • Minecraft 游戏版本
  • 当前 Mod 加载器名称及版本
  • 当前 JRE 版本

默认格式如下:

CustomSkinLoader/<CSL版本> (Minecraft/<游戏版本>; <加载器名称>/<加载器版本>; Java/<JRE版本>)

示例:

CustomSkinLoader/15.0.1 (Minecraft/26.2; NeoForge/26.2.0.35-beta; Java/25.0.1)

同时,自定义 User-Agent 可以使用以下占位符:

{DEFAULT_USER_AGENT}
{CSL_VERSION}
{MINECRAFT_VERSION}
{MOD_LOADER_NAME}
{MOD_LOADER_VERSION}
{JAVA_VERSION}

变更背景

CustomSkinLoader 会向 Mojang API、第三方皮肤 API 和皮肤纹理地址发起 HTTP 请求。此前这些请求没有统一的默认 User-Agent,第三方皮肤服务端无法稳定获得客户端的 Minecraft、Mod 加载器、Java 和 CustomSkinLoader 版本信息。

loadlist 虽然已经提供 userAgent 字段,但该字段只能使用固定字符串。服务端维护者如果希望在 User-Agent 中包含 Minecraft 或加载器版本,只能让用户手动填写,配置无法在不同游戏实例之间复用。

本次变更的目标是:

  1. 让所有经过 HttpRequestUtil 的网络请求拥有可识别的默认 User-Agent。
  2. 保留现有 loadlist 自定义 User-Agent 能力。
  3. 允许自定义值引用当前运行环境,而不需要用户手动更新版本。
  4. 继续支持 Forge Legacy、Forge ModLauncher、NeoForge、Fabric 和 Quilt 环境。
  5. 避免 Common 模块直接依赖任意特定加载器 API。

在 Minecraft 26.1.2、Fabric Loader 0.19.2 环境的后续实测中,还发现 Fabric 加载器版本可能回退为 unknown。检查 Fabric Loader 0.19.2 后确认,其 jar Manifest 没有提供可用的 Implementation-Version,因此原有的包版本回退路径无法覆盖该情况。本次变更同时补充了适用于 Fabric Loader 0.19.x 的版本兜底。

改动前的行为

HttpRequest.userAgent 的默认值为 null。发送请求时,只有该字段不为 null 才会调用:

c.setRequestProperty("User-Agent", request.userAgent);

因此改动前的行为如下:

场景 改动前行为
请求未配置 userAgent CustomSkinLoader 不主动设置 User-Agent,最终值取决于 JRE 的 HttpURLConnection 实现
loadlist 配置普通字符串 将字符串原样发送
loadlist 配置 {MINECRAFT_VERSION} 等内容 将大括号和其中的文字原样发送,不进行替换
Mojang API 等未传入自定义值的请求 没有 CustomSkinLoader 默认 User-Agent
显式配置空字符串 设置空的 User-Agent

项目中也没有统一收集加载器名称、加载器版本和其他运行时版本信息的工具。

具体改动

1. 新增 UserAgentUtil

新增 Common/src/main/java/customskinloader/utils/UserAgentUtil.java,集中负责:

  • 收集运行时版本信息
  • 检测当前 Mod 加载器
  • 构造默认 User-Agent
  • 展开自定义 User-Agent 占位符
  • 规范化自动插入的 Header 字段
  • 缓存已经确认的运行时信息

将这些逻辑放在独立工具类中,可以保证默认 User-Agent 和自定义占位符始终使用相同的数据来源和格式规则。

2. 生成默认 User-Agent

默认 User-Agent 使用以下运行时数据:

信息 数据来源
CustomSkinLoader 版本 CustomSkinLoader.CustomSkinLoader_FULL_VERSION
Minecraft 版本 MinecraftUtil.getMinecraftMainVersion()
Mod 加载器名称及版本 加载器运行时 API 或 Mod 元数据
JRE 版本 System.getProperty("java.version")

使用 CustomSkinLoader_FULL_VERSION 而不是基础版本,是为了让开发构建保留快照和构建编号。例如:

15.0.1-SNAPSHOT-0

正式构建仍会显示正常版本号:

15.0.1

3. 检测 Mod 加载器及版本

当前支持以下加载器:

  • NeoForge
  • Forge ModLauncher
  • Forge Legacy
  • Quilt
  • Fabric

现代 NeoForge 和 Forge 优先通过各自的 ModList 查找加载器 Mod 容器,并从 Mod 元数据读取实际运行版本。

如果 Mod 容器尚未提供版本,还会尝试以下回退接口:

  • NeoForge 的 NeoForgeVersion
  • NeoForge 的 FMLLoader.versionInfo()
  • Forge 的 ForgeVersion
  • Forge 的 FMLLoader.versionInfo()
  • Forge Legacy 的 net.minecraftforge.common.ForgeVersion

Fabric 和 Quilt 分别读取 fabricloaderquilt_loader 的 Mod 元数据版本。Fabric 的首选路径在语义上等同于调用以下公开 API:

FabricLoader.getInstance()
        .getModContainer("fabricloader")
        .map(container -> container.getMetadata()
                .getVersion()
                .getFriendlyString());

由于该逻辑位于同时供多个加载器使用的 Common 模块中,实际实现通过反射调用这组公开 API,避免 Common 在编译期和类加载期直接依赖 Fabric Loader。

如果公开 Mod 元数据路径没有返回版本,Fabric 检测依次尝试:

  1. 读取 net.fabricmc.loader.impl.FabricLoaderImpl.VERSION
  2. 读取 FabricLoader 所属包的 Implementation-Version
  3. 两者均不可用时返回 unknown

FabricLoaderImpl.VERSION 位于 Fabric Loader 的 impl 包,不属于其稳定公开 API,因此只作为兼容性兜底,不作为首选数据来源。Fabric Loader 0.19.2 的 jar Manifest 没有提供可用的 Implementation-Version,只保留包版本回退会导致实测环境仍然得到 unknown

这里查询的是 Mod ID fabricloader,而不是 fabric-api。Fabric API 是可选安装的 Mod,不是实际承载游戏的 Mod 加载器;将其发行版本写入 {MOD_LOADER_VERSION} 会使 User Agent 的含义不正确。

所有加载器访问都通过反射完成。Common 模块因此不需要直接链接任意加载器 API,也不会因为缺少其他加载器的类而产生 ClassNotFoundException

加载器检测顺序为:

  1. NeoForge
  2. Forge ModLauncher
  3. Forge Legacy
  4. Quilt
  5. Fabric

Forge 和 NeoForge 被放在 Fabric 之前,是为了兼容安装了 Fabric API 兼容层的环境。即使类路径中出现 FabricLoader API,也应优先报告实际承载游戏的 Forge 或 NeoForge。

4. 延迟缓存运行时信息

成功读取加载器名称和版本后,结果会缓存在不可变的 RuntimeInfo 中。

缓存字段使用 volatile,使多个皮肤加载线程能够安全读取已经初始化的结果。

如果调用发生得过早,加载器元数据暂时只能返回 unknown,该结果不会被永久缓存。后续真正发送请求时会重新检测,避免启动早期的临时状态影响整个游戏生命周期。

5. 统一接入 HTTP 请求

HttpRequestUtil.makeHttpRequest 在建立连接前统一调用:

String userAgent = UserAgentUtil.expandUserAgent(request.userAgent);

随后始终设置最终 User-Agent:

c.setRequestProperty("User-Agent", userAgent);

将处理放在 HttpRequestUtil 而不是分别修改每个 Profile Loader,可以覆盖现有及未来所有通过该工具发出的请求,并避免不同 Loader 产生不一致行为。

目前 JsonAPILoaderLegacyLoader 会将 loadlist 中的 userAgent 传入 HttpRequest,因此这些请求可以展开自定义模板。没有传入自定义值的请求,例如现有 Mojang API 请求,会自动使用完整默认 User-Agent。

6. 添加自定义占位符

loadlist 和 ExtraList 的 userAgent 字段支持以下占位符:

占位符 替换内容
{DEFAULT_USER_AGENT} 完整的默认 CustomSkinLoader User-Agent
{CSL_VERSION} CustomSkinLoader 完整版本
{MINECRAFT_VERSION} Minecraft 游戏版本
{MOD_LOADER_NAME} 当前 Mod 加载器名称
{MOD_LOADER_VERSION} 当前 Mod 加载器版本
{JAVA_VERSION} 当前 JRE 版本

占位符采用精确的字面量替换,具有以下规则:

  • 名称区分大小写。
  • 占位符名称必须全大写。
  • 占位符两边必须使用 {} 包裹。
  • 未知占位符保持原样。
  • 小写写法保持原样。
  • 不对用户模板中占位符以外的内容进行改写。

例如:

{
  "name": "ExampleSkinServer",
  "type": "CustomSkinAPI",
  "root": "https://example.com/",
  "userAgent": "ExampleSkinClient/1.0 (CustomSkinLoader/{CSL_VERSION}; Minecraft/{MINECRAFT_VERSION}; {MOD_LOADER_NAME}/{MOD_LOADER_VERSION}; Java/{JAVA_VERSION})"
}

在示例 NeoForge 环境中会展开为:

ExampleSkinClient/1.0 (CustomSkinLoader/15.0.1; Minecraft/26.2; NeoForge/26.2.0.35-beta; Java/25.0.1)

也可以在自定义前缀或后缀中嵌入完整默认值:

{
  "userAgent": "ExampleSkinClient/1.0 {DEFAULT_USER_AGENT}"
}

7. 规范化自动插入的 Header 字段

Minecraft、加载器、Java 和 CSL 版本来自运行时环境。为了避免空格、分号、换行或其他特殊字符破坏 HTTP Header,自动插入的字段会被规范化为合法的 HTTP token 字符。

例如:

15.0.1 SNAPSHOT -> 15.0.1_SNAPSHOT
26.2 Pre-Release -> 26.2_Pre-Release

无法读取或为空的字段会回退为:

unknown

该处理仅针对自动插入的版本组件。用户自定义 User-Agent 模板的其他部分不会被擅自修改。

8. 添加测试

Common/build.gradle 新增 JUnit 4.13.2 测试依赖。

新增 Common/src/test/java/customskinloader/utils/UserAgentUtilTest.java,覆盖:

  • 默认 User-Agent 格式
  • Minecraft、加载器、Java 和 CSL 版本字段
  • 非法 Header 字符规范化
  • 各字段占位符展开
  • {DEFAULT_USER_AGENT} 展开
  • 未知占位符保持原样
  • 小写占位符保持原样
  • 自定义值为 null 时回退默认 User-Agent
  • 用于 Fabric 兼容性兜底的公开静态字符串字段读取
  • Forge Legacy、Fabric 和 NeoForge 风格的版本示例

改动后的行为

输入的 userAgent 改动后行为
未配置或 null 生成并发送完整默认 User-Agent
普通固定字符串 原样发送,保持现有配置兼容
包含已知占位符 发送前替换为实际运行时信息
包含 {DEFAULT_USER_AGENT} 嵌入完整默认 User-Agent
包含未知或小写占位符 保持原样
显式配置空字符串 继续发送空值,保持原有显式空字符串语义

为什么采用这种实现

在统一 HTTP 入口处理

所有请求最终都经过 HttpRequestUtil。在这里处理默认值和占位符,可以一次覆盖全部请求路径,也能确保未来新增的 Profile Loader 自动获得默认 User-Agent。

使用反射检测加载器

项目需要同时支持跨度很大的 Minecraft 和加载器版本。Common 模块如果直接依赖 Forge、NeoForge、Fabric 或 Quilt 中的任意一个 API,会在其他环境中产生链接错误。

反射允许代码在运行时仅访问当前实际存在的加载器类,并为不同版本保留多个回退路径。

保留自定义字符串兼容性

没有占位符的现有 userAgent 会原样发送,不需要用户迁移配置。显式空字符串的行为也没有改变。

未知占位符不报错

保留未知占位符比删除或替换为空字符串更安全。这样既不会损坏用户配置,也允许第三方工具在发送前后继续处理自己的模板字段。

只缓存完整加载器信息

启动早期可能暂时拿不到加载器版本。如果立即缓存 unknown,后续所有请求都会携带错误信息。因此只有名称和版本均可用时才缓存结果。

涉及文件

文件 改动
Common/src/main/java/customskinloader/utils/UserAgentUtil.java 新增默认 User-Agent、加载器检测、Fabric 0.19.x 版本兜底、占位符展开、字段规范化和运行时缓存
Common/src/main/java/customskinloader/utils/HttpRequestUtil.java 在统一请求入口解析自定义值并始终设置 User-Agent
Common/src/test/java/customskinloader/utils/UserAgentUtilTest.java 新增 User-Agent 格式、占位符和 Fabric 静态版本字段读取测试
Common/build.gradle 添加 JUnit 4.13.2 测试依赖

测试与验证

执行完整构建:

./gradlew build

验证结果:

  • UserAgentUtilTest 共 6 项测试,全部通过。
  • Common 模块构建通过。
  • Fabric Bootstrap 构建通过。
  • Forge Legacy Bootstrap 构建通过。
  • Forge ModLauncher Bootstrap 构建通过。
  • NeoForge Bootstrap 构建通过。
  • 最终 Universal jar 的嵌套 Common jar 包含新的 User-Agent 实现。
  • 最终制品包含全部六个公开占位符。
  • 最终制品不包含废弃的占位符名称。
  • 使用官方 Fabric Loader 0.19.2 jar 验证 Fabric 兜底路径,检测结果为 0.19.2
  • git diff --check 通过。

兼容性说明

  • 未配置 userAgent 的请求由“不显式设置”变为“发送完整默认 User-Agent”,这是本次变更的主要行为变化。
  • 已配置普通固定 User-Agent 的 loadlist 不需要修改。
  • 已配置空字符串的 loadlist 仍然保持空字符串行为。
  • 未知占位符不会导致请求失败。
  • 加载器检测失败时使用 Unknown/unknown,不会阻断皮肤加载。
  • Fabric Loader 优先使用公开 Mod 元数据;FabricLoaderImpl.VERSION 仅用于公开路径未返回版本时的兼容性兜底。
  • fabric-api 不参与加载器版本检测,未安装 Fabric API 不会影响 User Agent 中 Fabric Loader 版本的读取。
  • 实现保持 Java 8 源码和字节码兼容要求。

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.

[Suggestion] Default User Agent

1 participant