遇到问题
起因
深夜更新 Jenkins 插件的时候,遇到了一个问题:插件下载速度太慢了,并且有大概率失败。
因此我研究了一下 Jenkins 的插件升级机制,研究是否可以使用镜像站点加速。
初步研究
Jenkins 的 升级信息(包含本体和插件)来自于 http://mirrors.jenkins-ci.org/updates/update-center.json 这个 URL。
这个 JSON 文件描述了所有可用插件的 版本信息 和 下载地址 ,并且 Jenkins 在 插件中心 的 高级 选项卡里提供了修改这个地址的功能。
我找到了 jenkins-zh 社区提供的 升级信息 镜像地址 https://updates.jenkins-zh.cn/update-center.json,他们所提供的 JSON 文件内部,将所有 下载地址 替换为等价的 清华 TUNA 镜像站 的地址。
遭遇困境
但是当我把镜像地址填入 Jenkins,点击 更新 时,界面突然弹出来巨长一串超出屏幕的报错,提示 签名验证失败。
为什么
Jenkins 在这个 JSON 文件内做了签名认证。
也就是说,Jenkins 允许你从另外的 URL 获取到 update-center.json 这个文件,但是 JSON 文件内部描述的 下载地址 等信息是 无法修改 的,否则会破坏签名认证。
这也就意味着,插件下载的过程仍然无法使用镜像站点加速。
怎么办
两个方法:
- 重新签名
update-center.json并替换 Jenkins 内的 CA 文件,让修改后的update-center.json变得合法 - 修改代码重新编译 Jenkins,关掉这个签名认证 (既然允许自定义
升级信息地址,又何必非要做签名认证呢?)
方法 1 的问题
如果有一天我不使用这个镜像站了,我还得想办法恢复 CA 文件。
并且每次 update-center.json 有变动,我都得重新签名。尽管可以制作或者寻找现有的自动化工具,但是终究需要额外部署一套工具。
方法 2 的问题
每次 Jenkins 版本更新都需要重新编译 Jenkins,着实麻烦。
更好的办法
我想到了一个更好的办法,使用 Java Agent 机制,在运行时覆盖掉 Jenkins 签名验证方法,让它跳过实际的签名验证,永远返回正确。
这样即便是 Jenkins 版本更新,只要 升级信息 签名验证代码流程不变,这个方法就可以一直有效。
实施流程
什么是 Java Agent
Java Agent 通过在 Java 命令上添加参数 -javaagent:XXXXXX.jar 来启动,它允许你在主类之外,额外加载一个 Jar 包,并提供机制,允许你在类加载时,修改类的字节码。
通过 Java Agent 机制,可以在不修改源代码的情况下,在运行时修改 Java 程序。
定位 Jenkins 代码
简要查阅了 Jenkins 的源代码,定位到了 Jenkins 中,负责校验升级信息签名的方法来自 hudson.model.UpdateSite 类的 verifySignature 方法
/**
* Verifies the signature in the update center data file.
*/
private FormValidation verifySignature(JSONObject o) throws IOException {
return getJsonSignatureValidator().verifySignature(o);
}
我们的目标是,将这个方法的代码体,修改成如下内容
return FormValidation.ok();
这样,修改后的方法,永远会返回校验正常。
编写 Java Agent
知道了要修改什么,就可以开始编写 Java Agent 了。
我们选用 javassist 这个库,这是一个 IBM 推出的库,可以便捷地修改 Java 类字节码。
创建 Maven 工程
首先我们创建一个 Maven 工程,和常见的工程类似,但是 Pom 文件的设置有些许区别:
<!-- 略去了不重要的内容 -->
<dependencies>
<!-- 引入 javassist 包 -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<!-- Java Agent 的 MANIFEST 里面不是 Main-Class-->
<!-- 我们使用 premain 模式,因此要写 Premain-Class -->
<archive>
<manifestEntries>
<Premain-Class>net.landzero.jenkins.tune.Agent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<!-- 使用 shade 插件,把 javassist 直接包含在成品 Jar 包内部 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<filters>
<!-- 过滤掉 javassist 自己的 META-INF 文件(可能多此一举了) -->
<filter>
<artifact>org.javassist:*</artifact>
<excludes>
<exclude>META-INF/license/**</exclude>
<exclude>META-INF/*</exclude>
<exclude>META-INF/maven/**</exclude>
<exclude>LICENSE</exclude>
<exclude>NOTICE</exclude>
<exclude>/*.txt</exclude>
<exclude>build.properties</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
</plugins>
</build>
<!-- 略去了不重要的内容 -->
编写 Agent 主类
package net.landzero.jenkins.tune;
// 此处略却一大堆 import
public class Agent {
// Java Agent 的启动入口不是 main,而是 premain,并且有特定的方法签名
public static void premain(String agentArgs, Instrumentation inst) {
// 注册一个类转换器
inst.addTransformer(new UpdateSiteTransformer());
}
private static class UpdateSiteTransformer implements ClassFileTransformer {
private static final String CLASS_NAME = "hudson.model.UpdateSite";
private static final String CLASS_NAME_INTERNAL = CLASS_NAME.replace('.', '/');
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className == null) {
return null;
}
// 如果不是 hudson.model.UpdateSite 就忽略掉,不做修改
// 这个方法传入的 className 是使用 “/” 分割的类名,而不是标准的 "." 分割的类名
if (!CLASS_NAME_INTERNAL.equals(className)) {
return null;
}
try {
// 加载 javassist 类池
ClassPool cp = ClassPool.getDefault();
// 因为 Jenkins 是 WAR 包,还需要把 loader 补充进去,不然下面 javassist 解析字节码会报告找不到 import 的类
cp.appendClassPath(new LoaderClassPath(loader));
// 把字节码载入进去,生成 CtClass
CtClass cc = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
// 定位到方法 verifySignature
CtMethod cm = cc.getDeclaredMethod("verifySignature");
// 修改方法代码体
cm.setBody("{ return hudson.util.FormValidation.ok(); }");
// 返回重新编译的字节码
return cc.toBytecode();
} catch (Exception e) {
return null;
}
}
}
}
打包
直接 mvn clean package 打包,得到 jenkins-tune-1.0-SNAPSHOT.jar 文件,这里面包含了我们写的 Agent 类和依赖项 javassist。
部署 Java Agent
把 jenkins-tune-1.0.0-SNAPSHOT.jar 复制到服务器上,假设复制到 /usr/local/lib/jenkins-tune.jar 这个位置。
编辑 /etc/init.d/jenkins ,寻找 JAVA_CMD=... 那一行,添加一句 -javaagent:/usr/local/lib/jenkins-tune.jar。
(如果是 yum 包安装的 Jenkins,也可以修改 /etc/default/jenkins 中的 JAVA_ARGS 字段)
(如果使用的是 WAR 包手动部署,则需要修改 Tomcat 的启动脚本)
systemctl restart jenkins 重新启动 Jenkins 就大功告成了。
结果
我重新填入了 jenkins-zh 提供的镜像地址,这次就再也没有提示签名错误了,也可以正常地从 清华 TUNA 镜像站 拉取插件更新,速度自然是飞快。
代码地址
https://github.com/guoyk93/jenkins-tune
如果你不想自己编译 Jar 包,可以在 Release 页面找到预先编译好的 Jar 包
本文讲述了如何使用 Java Agent 在 Jenkins 运行时修改类字节码,以绕过 Jenkins 插件升级的签名验证,实现从镜像站点加速下载。通过定位 Jenkins 代码,创建 Maven 工程并编写 Agent,最终成功部署并解决 Jenkins 插件更新速度慢的问题。
472

被折叠的 条评论
为什么被折叠?



