如何使用 Java Agent 在运行时修改 Java 类(以魔改 Jenkins 为例)

本文讲述了如何使用 Java Agent 在 Jenkins 运行时修改类字节码,以绕过 Jenkins 插件升级的签名验证,实现从镜像站点加速下载。通过定位 Jenkins 代码,创建 Maven 工程并编写 Agent,最终成功部署并解决 Jenkins 插件更新速度慢的问题。

遇到问题

起因

深夜更新 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 文件内部描述的 下载地址 等信息是 无法修改 的,否则会破坏签名认证。

这也就意味着,插件下载的过程仍然无法使用镜像站点加速。

怎么办

两个方法:

  1. 重新签名 update-center.json 并替换 Jenkins 内的 CA 文件,让修改后的 update-center.json 变得合法
  2. 修改代码重新编译 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);
    }

参见 https://github.com/jenkinsci/jenkins/blob/6d6c2793e41539c214241cc49df6515ec0395ff4/core/src/main/java/hudson/model/UpdateSite.java#L267

我们的目标是,将这个方法的代码体,修改成如下内容

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 包

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农炎可

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值