feat: support packer config

This commit is contained in:
ReaJason
2026-02-26 22:55:45 +08:00
parent 0d3b56de63
commit 9243070126
85 changed files with 2410 additions and 658 deletions
@@ -29,6 +29,12 @@ public class GlobalExceptionHandler {
return new ErrorResponse(exception.getMessage()); 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) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Throwable.class) @ExceptionHandler(Throwable.class)
public ErrorResponse handleThrowable(Throwable throwable) { public ErrorResponse handleThrowable(Throwable throwable) {
@@ -34,10 +34,24 @@ public class ConfigController {
} }
@RequestMapping("/packers") @RequestMapping("/packers")
public List<String> getPackers() { public List<PackerCategoryDTO> getPackers() {
return Arrays.stream(Packers.values()) List<PackerCategoryDTO> result = new ArrayList<>();
.filter(packers -> packers.getParentPacker() == null) for (Map.Entry<String, List<Packers>> entry : Packers.groupedPackers().entrySet()) {
.map(Packers::name).toList(); PackerCategoryDTO category = new PackerCategoryDTO();
category.setName(entry.getKey());
List<PackerOptionDTO> 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 @RequestMapping
@@ -66,4 +80,64 @@ public class ConfigController {
commandConfigVO.setImplementationClasses(Arrays.stream(CommandConfig.ImplementationClass.values()).toList()); commandConfigVO.setImplementationClasses(Arrays.stream(CommandConfig.ImplementationClass.values()).toList());
return commandConfigVO; return commandConfigVO;
} }
}
public static class PackerCategoryDTO {
private String name;
private List<PackerOptionDTO> packers;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<PackerOptionDTO> getPackers() {
return packers;
}
public void setPackers(List<PackerOptionDTO> 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;
}
}
}
@@ -7,9 +7,11 @@ import com.reajason.javaweb.memshell.MemShellResult;
import com.reajason.javaweb.memshell.config.InjectorConfig; import com.reajason.javaweb.memshell.config.InjectorConfig;
import com.reajason.javaweb.memshell.config.ShellConfig; import com.reajason.javaweb.memshell.config.ShellConfig;
import com.reajason.javaweb.memshell.config.ShellToolConfig; 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.JarPacker;
import com.reajason.javaweb.packer.JarPackerConfig;
import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Base64; import java.util.Base64;
@@ -22,19 +24,24 @@ import java.util.Base64;
@RequestMapping("/api/memshell/generate") @RequestMapping("/api/memshell/generate")
@CrossOrigin("*") @CrossOrigin("*")
public class MemShellGeneratorController { public class MemShellGeneratorController {
@PostMapping @PostMapping
public MemShellGenerateResponse generate(@RequestBody MemShellGenerateRequest request) { public MemShellGenerateResponse generate(@RequestBody MemShellGenerateRequest request) {
ShellConfig shellConfig = request.getShellConfig(); ShellConfig shellConfig = request.getShellConfig();
ShellToolConfig shellToolConfig = request.parseShellToolConfig(); ShellToolConfig shellToolConfig = request.parseShellToolConfig();
InjectorConfig injectorConfig = request.getInjectorConfig(); InjectorConfig injectorConfig = request.getInjectorConfig();
MemShellResult generateResult = MemShellGenerator.generate(shellConfig, injectorConfig, shellToolConfig); MemShellResult generateResult = MemShellGenerator.generate(shellConfig, injectorConfig, shellToolConfig);
Packer packer = request.getPacker().getInstance(); if (request.getPackerSpec() == null) {
if (packer instanceof AggregatePacker) { throw new IllegalArgumentException("packerSpec is required");
return new MemShellGenerateResponse(generateResult, ((AggregatePacker) packer).packAll(generateResult.toClassPackerConfig()));
} }
Packers packers = Packers.fromName(request.getPackerSpec().getName());
Packer<?> packer = packers.getInstance();
if (packer instanceof JarPacker) { 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));
} }
} }
@@ -2,8 +2,9 @@ package com.reajason.javaweb.boot.controller;
import com.reajason.javaweb.boot.dto.ProbeShellGenerateRequest; import com.reajason.javaweb.boot.dto.ProbeShellGenerateRequest;
import com.reajason.javaweb.boot.dto.ProbeShellGenerateResponse; 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.Packer;
import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.probe.ProbeShellGenerator; import com.reajason.javaweb.probe.ProbeShellGenerator;
import com.reajason.javaweb.probe.ProbeShellResult; import com.reajason.javaweb.probe.ProbeShellResult;
import com.reajason.javaweb.probe.config.ProbeConfig; import com.reajason.javaweb.probe.config.ProbeConfig;
@@ -23,11 +24,13 @@ public class ProbeShellGeneratorController {
ProbeConfig probeConfig = request.getProbeConfig(); ProbeConfig probeConfig = request.getProbeConfig();
ProbeContentConfig probeContentConfig = request.parseProbeContentConfig(); ProbeContentConfig probeContentConfig = request.parseProbeContentConfig();
ProbeShellResult generateResult = ProbeShellGenerator.generate(probeConfig, probeContentConfig); ProbeShellResult generateResult = ProbeShellGenerator.generate(probeConfig, probeContentConfig);
Packer packer = request.getPacker().getInstance(); if (request.getPackerSpec() == null) {
if (packer instanceof AggregatePacker) { throw new IllegalArgumentException("packerSpec is required");
return new ProbeShellGenerateResponse(generateResult, ((AggregatePacker) packer).packAll(generateResult.toClassPackerConfig()));
} else {
return new ProbeShellGenerateResponse(generateResult, packer.pack(generateResult.toClassPackerConfig()));
} }
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));
} }
} }
@@ -1,7 +1,6 @@
package com.reajason.javaweb.boot.dto; package com.reajason.javaweb.boot.dto;
import com.reajason.javaweb.memshell.config.*; import com.reajason.javaweb.memshell.config.*;
import com.reajason.javaweb.packer.Packers;
import lombok.Data; import lombok.Data;
import static com.reajason.javaweb.memshell.ShellTool.*; import static com.reajason.javaweb.memshell.ShellTool.*;
@@ -15,7 +14,7 @@ public class MemShellGenerateRequest {
private ShellConfig shellConfig; private ShellConfig shellConfig;
private ShellToolConfigDTO shellToolConfig; private ShellToolConfigDTO shellToolConfig;
private InjectorConfig injectorConfig; private InjectorConfig injectorConfig;
private Packers packer; private PackerRequestSpecDTO packerSpec;
@Data @Data
public static class ShellToolConfigDTO { public static class ShellToolConfigDTO {
@@ -84,4 +83,4 @@ public class MemShellGenerateRequest {
default -> throw new UnsupportedOperationException("unknown shell tool " + shellConfig.getShellTool()); default -> throw new UnsupportedOperationException("unknown shell tool " + shellConfig.getShellTool());
}; };
} }
} }
@@ -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<String, Object> config = new LinkedHashMap<>();
public PackerRequestSpec toPackerRequestSpec() {
PackerRequestSpec spec = new PackerRequestSpec();
spec.setName(name);
if (config != null) {
spec.setConfig(config);
}
return spec;
}
}
@@ -1,6 +1,5 @@
package com.reajason.javaweb.boot.dto; package com.reajason.javaweb.boot.dto;
import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.probe.config.*; import com.reajason.javaweb.probe.config.*;
import lombok.Data; import lombok.Data;
@@ -12,7 +11,7 @@ import lombok.Data;
public class ProbeShellGenerateRequest { public class ProbeShellGenerateRequest {
private ProbeConfig probeConfig; private ProbeConfig probeConfig;
private ProbeContentConfigDTO probeContentConfig; private ProbeContentConfigDTO probeContentConfig;
private Packers packer; private PackerRequestSpecDTO packerSpec;
@Data @Data
static class ProbeContentConfigDTO { static class ProbeContentConfigDTO {
@@ -9,9 +9,12 @@ import org.springframework.http.ResponseEntity;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals; 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** /**
* @author ReaJason * @author ReaJason
@@ -37,11 +40,4 @@ public class ConfigControllerIntegrationTest {
assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody()); assertNotNull(response.getBody());
} }
}
@Test
public void testConfigPackersEndpoint() {
ResponseEntity<List> response = restTemplate.getForEntity("/api/config/packers", List.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
}
@@ -3,11 +3,11 @@ package com.reajason.javaweb.boot.controller;
import com.reajason.javaweb.Server; import com.reajason.javaweb.Server;
import com.reajason.javaweb.boot.dto.MemShellGenerateRequest; import com.reajason.javaweb.boot.dto.MemShellGenerateRequest;
import com.reajason.javaweb.boot.dto.MemShellGenerateResponse; 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.ShellTool;
import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.memshell.config.InjectorConfig; import com.reajason.javaweb.memshell.config.InjectorConfig;
import com.reajason.javaweb.memshell.config.ShellConfig; import com.reajason.javaweb.memshell.config.ShellConfig;
import com.reajason.javaweb.packer.Packers;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; 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.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -30,6 +32,23 @@ class MemShellGeneratorControllerTest {
@Test @Test
void generateShell() { void generateShell() {
MemShellGenerateRequest request = buildRequest("ScriptEngine", null);
ResponseEntity<MemShellGenerateResponse> 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<MemShellGenerateResponse> response = restTemplate.postForEntity(
"/api/memshell/generate", request, MemShellGenerateResponse.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
private static MemShellGenerateRequest buildRequest(String packerName, Map<String, Object> packerConfig) {
MemShellGenerateRequest request = new MemShellGenerateRequest(); MemShellGenerateRequest request = new MemShellGenerateRequest();
request.setShellConfig(ShellConfig.builder() request.setShellConfig(ShellConfig.builder()
.server(Server.Tomcat) .server(Server.Tomcat)
@@ -43,16 +62,18 @@ class MemShellGeneratorControllerTest {
request.setInjectorConfig(InjectorConfig.builder() request.setInjectorConfig(InjectorConfig.builder()
.urlPattern("/*") .urlPattern("/*")
.build()); .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(); MemShellGenerateRequest.ShellToolConfigDTO shellToolConfigDTO = new MemShellGenerateRequest.ShellToolConfigDTO();
shellToolConfigDTO.setGodzillaKey("key"); shellToolConfigDTO.setGodzillaKey("key");
shellToolConfigDTO.setGodzillaPass("pass"); shellToolConfigDTO.setGodzillaPass("pass");
shellToolConfigDTO.setHeaderName("User-Agent"); shellToolConfigDTO.setHeaderName("User-Agent");
shellToolConfigDTO.setHeaderValue("hello"); shellToolConfigDTO.setHeaderValue("hello");
request.setShellToolConfig(shellToolConfigDTO); request.setShellToolConfig(shellToolConfigDTO);
ResponseEntity<MemShellGenerateResponse> response = restTemplate.postForEntity( return request;
"/api/memshell/generate", request, MemShellGenerateResponse.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ idea {
} }
} }
version = "2.6.0" version = "2.7.0-SNAPSHOT"
tasks.register("publishAllToMavenCentral") { tasks.register("publishAllToMavenCentral") {
dependsOn(":memshell-party-common:publishToMavenCentral") dependsOn(":memshell-party-common:publishToMavenCentral")
File diff suppressed because one or more lines are too long
+15 -15
View File
@@ -1,5 +1,5 @@
[versions] [versions]
asm = "9.9.1" asm = "9.9.1" # https://mvnrepository.com/artifact/org.ow2.asm/asm
jna = "5.13.0" # 为适配 JDK6+ 这个不可修改 jna = "5.13.0" # 为适配 JDK6+ 这个不可修改
bcel = "5.2" bcel = "5.2"
javax-servlet-api = "3.0.1" javax-servlet-api = "3.0.1"
@@ -12,21 +12,21 @@ reactor-netty = "1.1.25"
jackson = "2.19.0" jackson = "2.19.0"
jetbrains-annotations = "26.0.2" jetbrains-annotations = "26.0.2"
byte-buddy = "1.18.4" byte-buddy = "1.18.5" # https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy
commons-io = "2.21.0" commons-io = "2.21.0" # https://mvnrepository.com/artifact/commons-io/commons-io
commons-lang3 = "3.20.0" commons-lang3 = "3.20.0" # https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
commons-codec = "1.20.0" commons-codec = "1.21.0" # https://mvnrepository.com/artifact/commons-codec/commons-codec
logback = "1.5.24" logback = "1.5.32" # https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
okhttp3 = "5.3.2" okhttp3 = "5.3.2" # https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
fastjson2 = "2.0.60" fastjson2 = "2.0.61" # https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2
java-websocket = "1.6.0" 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" 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-pioneer = "2.3.0"
junit-platform = "1.14.2" junit-platform = "1.14.3" # https://mvnrepository.com/artifact/org.junit.platform/junit-platform-launcher
testcontainers = "2.0.3" testcontainers = "2.0.3" # https://mvnrepository.com/artifact/org.testcontainers/testcontainers
[libraries] [libraries]
byte-buddy = { module = "net.bytebuddy:byte-buddy", version.ref = "byte-buddy" } 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"] testcontainers = ["testcontainers", "testcontainers-junit-jupiter"]
[plugins] [plugins]
lombok = { id = "io.freefair.lombok", version = "9.2.0" } 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"} shadow = { id = "com.gradleup.shadow", version = "9.3.1"} # https://plugins.gradle.org/plugin/com.gradleup.shadow
@@ -129,7 +129,7 @@ public abstract class AbstractContainerTest {
} }
@ParameterizedTest @ParameterizedTest
@EnumSource(names = {"ClassLoaderJSP", "ClassLoaderJSPUnicode", "DefineClassJSP", "DefineClassJSPUnicode", "JSPX", "JSPXUnicode"}) @EnumSource(names = {"ClassLoaderJSP", "DefineClassJSP", "JSPX"})
void testJspPackers(Packers packer) { void testJspPackers(Packers packer) {
ContainerTestConfig config = getConfig(); ContainerTestConfig config = getConfig();
if (config.isEnableJspPackerTest()) { if (config.isEnableJspPackerTest()) {
@@ -400,14 +400,14 @@ public class ShellAssertion {
public static void injectIsOk(String url, String shellType, String shellTool, String content, Packers packer, GenericContainer<?> container) { public static void injectIsOk(String url, String shellType, String shellTool, String content, Packers packer, GenericContainer<?> container) {
switch (packer) { switch (packer) {
case JSP, ClassLoaderJSP, ClassLoaderJSPUnicode, DefineClassJSP, DefineClassJSPUnicode -> { case JSP, ClassLoaderJSP, DefineClassJSP -> {
String uploadEntry = url + "/upload"; String uploadEntry = url + "/upload";
String filename = shellType + shellTool + packer + ".jsp"; String filename = shellType + shellTool + packer + ".jsp";
String shellUrl = url + "/" + filename; String shellUrl = url + "/" + filename;
VulTool.uploadJspFileToServer(uploadEntry, filename, content); VulTool.uploadJspFileToServer(uploadEntry, filename, content);
VulTool.urlIsOk(shellUrl); VulTool.urlIsOk(shellUrl);
} }
case JSPX, JSPXUnicode -> { case JSPX -> {
String uploadEntry = url + "/upload"; String uploadEntry = url + "/upload";
String filename = shellType + shellTool + packer + ".jspx"; String filename = shellType + shellTool + packer + ".jspx";
String shellUrl = url + "/" + filename; String shellUrl = url + "/" + filename;
+1 -4
View File
@@ -5,7 +5,7 @@ plugins {
} }
group = "io.github.reajason" group = "io.github.reajason"
description = "Java deserialize payload for MemShellParty" description = "Java payload packer for MemShellParty"
version = rootProject.version version = rootProject.version
dependencies { dependencies {
@@ -22,9 +22,6 @@ dependencies {
} }
java { java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
}
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
@@ -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<String, String> 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<Packers> packersWithParent = Packers.getPackersWithParent(this.getClass());
if (packersWithParent.isEmpty()) {
return null;
}
return packersWithParent.get(0).getInstance().pack(config);
}
}
@@ -7,9 +7,10 @@ import lombok.Data;
* @since 2025/6/27 * @since 2025/6/27
*/ */
@Data @Data
public class ClassPackerConfig { public class ClassPackerConfig<T> {
private String className; private String className;
private byte[] classBytes; private byte[] classBytes;
private String classBytesBase64Str; private String classBytesBase64Str;
private boolean byPassJavaModule; private boolean byPassJavaModule;
private T customConfig;
} }
@@ -9,7 +9,8 @@ import java.util.Map;
* @since 2025/6/27 * @since 2025/6/27
*/ */
@Data @Data
public class JarPackerConfig { public class JarPackerConfig<T> {
private String mainClassName; private String mainClassName;
private transient Map<String, byte[]> classBytes; private transient Map<String, byte[]> classBytes;
private T customConfig;
} }
@@ -1,10 +1,19 @@
package com.reajason.javaweb.packer; 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 * @author ReaJason
* @since 2024/11/26 * @since 2024/11/26
*/ */
public interface Packer { public interface Packer<T> {
TypeCache TYPE_CACHE = new TypeCache();
/** /**
* 将自定义类打包成特定 payload * 将自定义类打包成特定 payload
@@ -12,7 +21,71 @@ public interface Packer {
* @param classPackerConfig 自定义类信息 * @param classPackerConfig 自定义类信息
* @return 字符串 payload * @return 字符串 payload
*/ */
default String pack(ClassPackerConfig classPackerConfig) { default String pack(ClassPackerConfig<T> classPackerConfig) {
throw new UnsupportedOperationException("当前 " + this.getClass().getSimpleName() + " 不支持 string 生成"); throw new UnsupportedOperationException("当前 " + this.getClass().getSimpleName() + " 不支持 string 生成");
} }
}
@SuppressWarnings("unchecked")
default Class<T> customConfigType() {
Optional<Class<?>> resolved = TYPE_CACHE.cache.computeIfAbsent(this.getClass(), TypeResolver::resolveCustomConfigType);
return (Class<T>) resolved.orElse(null);
}
default T resolveCustomConfig(Object rawCustomConfig) {
Class<T> 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<Class<?>, Optional<Class<?>>> cache = new ConcurrentHashMap<>();
}
final class TypeResolver {
private TypeResolver() {
}
private static Optional<Class<?>> resolveCustomConfigType(Class<?> clazz) {
Class<?> current = clazz;
while (current != null && current != Object.class) {
Optional<Class<?>> fromInterfaces = resolveFromTypes(current.getGenericInterfaces());
if (fromInterfaces.isPresent()) {
return fromInterfaces;
}
current = current.getSuperclass();
}
return Optional.empty();
}
private static Optional<Class<?>> 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();
}
}
}
@@ -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> R convert(Object customConfig, Class<R> 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);
}
}
}
@@ -1,10 +1,8 @@
package com.reajason.javaweb.packer; package com.reajason.javaweb.packer;
import com.reajason.javaweb.packer.spec.PackerSchema;
import com.reajason.javaweb.packer.aviator.AviatorPacker; import com.reajason.javaweb.packer.aviator.AviatorPacker;
import com.reajason.javaweb.packer.base64.Base64Packer; 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.bsh.BeanShellPacker;
import com.reajason.javaweb.packer.deserialize.hessian.Hessian2Packer; import com.reajason.javaweb.packer.deserialize.hessian.Hessian2Packer;
import com.reajason.javaweb.packer.deserialize.hessian.Hessian2XSLTScriptEnginePacker; 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 com.reajason.javaweb.packer.xmldecoder.XMLDecoderScriptEnginePacker;
import lombok.Getter; import lombok.Getter;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -67,20 +67,14 @@ public enum Packers {
* Base64 * Base64
*/ */
Base64(new Base64Packer()), Base64(new Base64Packer()),
DefaultBase64(new DefaultBase64Packer(), Base64Packer.class),
Base64URLEncoded(new Base64URLEncoded(), Base64Packer.class),
GzipBase64(new GzipBase64Packer(), Base64Packer.class),
/** /**
* JSP 打包器 * JSP 打包器
*/ */
JSP(new JspPacker()), JSP(new JspPacker()),
ClassLoaderJSP(new ClassLoaderJspPacker(), JspPacker.class), ClassLoaderJSP(new ClassLoaderJspPacker(), JspPacker.class),
ClassLoaderJSPUnicode(new ClassLoaderJspUnicodePacker(), JspPacker.class),
DefineClassJSP(new DefineClassJspPacker(), JspPacker.class), DefineClassJSP(new DefineClassJspPacker(), JspPacker.class),
DefineClassJSPUnicode(new DefineClassJspUnicodePacker(), JspPacker.class),
JSPX(new JspxPacker(), JspPacker.class), JSPX(new JspxPacker(), JspPacker.class),
JSPXUnicode(new JspxUnicodePacker(), JspPacker.class),
/** /**
* BigInteger * BigInteger
@@ -193,7 +187,48 @@ public enum Packers {
this.parentPacker = parentPacker; this.parentPacker = parentPacker;
} }
public static List<Packers> getPackersWithParent(Class<?> parentPacker) { public static Packers fromName(String name) {
return Stream.of(Packers.values()).filter(p -> Objects.equals(p.getParentPacker(), parentPacker)).collect(Collectors.toList()); 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<String, List<Packers>> groupedPackers() {
Map<String, List<Packers>> groupedPackers = new LinkedHashMap<>();
for (Packers packer : Packers.values()) {
String groupName = packer.getCategoryName();
groupedPackers.computeIfAbsent(groupName, k -> new java.util.ArrayList<>()).add(packer);
}
return groupedPackers;
} }
} }
@@ -3,9 +3,12 @@ package com.reajason.javaweb.packer;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Objects; import java.util.Objects;
import java.util.zip.GZIPOutputStream;
/** /**
* @author ReaJason * @author ReaJason
@@ -19,4 +22,13 @@ public class Util {
return IOUtils.toString(Objects.requireNonNull(stream), Charset.defaultCharset()); 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();
}
} }
@@ -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;
}
}
@@ -1,10 +1,42 @@
package com.reajason.javaweb.packer.base64; 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 * @author ReaJason
* @since 2024/12/17 * @since 2024/12/17
*/ */
public class Base64Packer implements AggregatePacker { public class Base64Packer implements Packer<Base64CustomPackerConfig> {
}
@Override
@SneakyThrows
public String pack(ClassPackerConfig<Base64CustomPackerConfig> 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();
}
}
@@ -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());
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.deserialize.hessian; 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 * @author ReaJason
* @since 2025/2/20 * @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);
}
} }
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.deserialize.hessian; 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 * @author ReaJason
* @since 2025/2/20 * @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);
}
} }
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.deserialize.java; 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 * @author ReaJason
* @since 2024/12/10 * @since 2024/12/10
*/ */
public class JavaDeserializePacker implements AggregatePacker { public class JavaDeserializePacker implements Packer {
@Override
} public String pack(ClassPackerConfig classPackerConfig) {
return Packers.JavaCommonsBeanutils19.getInstance().pack(classPackerConfig);
}
}
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.groovy; 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 * @author ReaJason
* @since 2024/12/13 * @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);
}
} }
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.h2; 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 * @author ReaJason
* @since 2025/6/28 * @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);
}
} }
@@ -24,7 +24,8 @@ public class DefaultJarPacker implements JarPacker {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (JarOutputStream targetJar = new JarOutputStream(byteArrayOutputStream, manifest)) { try (JarOutputStream targetJar = new JarOutputStream(byteArrayOutputStream, manifest)) {
for (Map.Entry<String, byte[]> entry : jarPackerConfig.getClassBytes().entrySet()) { Map<String, byte[]> classBytes = ((Map<String, byte[]>) jarPackerConfig.getClassBytes());
for (Map.Entry<String, byte[]> entry : classBytes.entrySet()) {
targetJar.putNextEntry(new JarEntry(entry.getKey().replace('.', '/') + ".class")); targetJar.putNextEntry(new JarEntry(entry.getKey().replace('.', '/') + ".class"));
targetJar.write(entry.getValue()); targetJar.write(entry.getValue());
targetJar.closeEntry(); targetJar.closeEntry();
@@ -8,6 +8,7 @@ import lombok.SneakyThrows;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream; import java.util.jar.JarOutputStream;
import java.util.jar.Manifest; import java.util.jar.Manifest;
@@ -21,7 +22,7 @@ public class GroovyTransformJarPacker implements JarPacker {
@SneakyThrows @SneakyThrows
public byte[] packBytes(JarPackerConfig config) { public byte[] packBytes(JarPackerConfig config) {
String mainClassName = config.getMainClassName(); String mainClassName = config.getMainClassName();
byte[] mainClassBytes = config.getClassBytes().get(mainClassName); byte[] mainClassBytes = ((Map<String, byte[]>) config.getClassBytes()).get(mainClassName);
mainClassBytes = ClassInterfaceUtils.addInterface(mainClassBytes, "org.codehaus.groovy.transform.ASTTransformation"); mainClassBytes = ClassInterfaceUtils.addInterface(mainClassBytes, "org.codehaus.groovy.transform.ASTTransformation");
mainClassBytes = ClassAnnotationUtils.setAnnotation(mainClassBytes, "org.codehaus.groovy.transform.GroovyASTTransformation"); mainClassBytes = ClassAnnotationUtils.setAnnotation(mainClassBytes, "org.codehaus.groovy.transform.GroovyASTTransformation");
@@ -7,6 +7,7 @@ import lombok.SneakyThrows;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream; import java.util.jar.JarOutputStream;
import java.util.jar.Manifest; import java.util.jar.Manifest;
@@ -21,7 +22,7 @@ public class ScriptEngineJarPacker implements JarPacker {
@SneakyThrows @SneakyThrows
public byte[] packBytes(JarPackerConfig config) { public byte[] packBytes(JarPackerConfig config) {
String mainClassName = config.getMainClassName(); String mainClassName = config.getMainClassName();
byte[] mainClassBytes = config.getClassBytes().get(mainClassName); byte[] mainClassBytes = ((Map<String, byte[]>) config.getClassBytes()).get(mainClassName);
byte[] bytes = ClassInterfaceUtils.addInterface(mainClassBytes, "javax.script.ScriptEngineFactory"); byte[] bytes = ClassInterfaceUtils.addInterface(mainClassBytes, "javax.script.ScriptEngineFactory");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (JarOutputStream targetJar = new JarOutputStream(outputStream, new Manifest())) { try (JarOutputStream targetJar = new JarOutputStream(outputStream, new Manifest())) {
@@ -3,20 +3,31 @@ package com.reajason.javaweb.packer.jsp;
import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Util; import com.reajason.javaweb.packer.Util;
import com.reajason.javaweb.packer.spec.PackerSchema;
import lombok.SneakyThrows; import lombok.SneakyThrows;
/** /**
* @author ReaJason * @author ReaJason
* @since 2024/11/26 * @since 2024/11/26
*/ */
public class ClassLoaderJspPacker implements Packer { public class ClassLoaderJspPacker implements Packer<JspCustomPackerConfig> {
private final String jspTemplate = Util.loadTemplateFromResource("/memshell-party/shell.jsp"); private final String jspTemplate = Util.loadTemplateFromResource("/memshell-party/shell.jsp");
@Override @Override
@SneakyThrows @SneakyThrows
public String pack(ClassPackerConfig config) { public String pack(ClassPackerConfig<JspCustomPackerConfig> config) {
return jspTemplate.replace("{{className}}", config.getClassName()) String content = jspTemplate.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", config.getClassBytesBase64Str()); .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();
} }
} }
@@ -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);
}
}
@@ -3,27 +3,38 @@ package com.reajason.javaweb.packer.jsp;
import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Util; import com.reajason.javaweb.packer.Util;
import com.reajason.javaweb.packer.spec.PackerSchema;
import lombok.SneakyThrows; import lombok.SneakyThrows;
/** /**
* @author ReaJason * @author ReaJason
* @since 2024/11/26 * @since 2024/11/26
*/ */
public class DefineClassJspPacker implements Packer { public class DefineClassJspPacker implements Packer<JspCustomPackerConfig> {
private final String template = Util.loadTemplateFromResource("/memshell-party/shell1.jsp"); private final String template = Util.loadTemplateFromResource("/memshell-party/shell1.jsp");
private final String bypassTemplate = Util.loadTemplateFromResource("/memshell-party/shell2.jsp"); private final String bypassTemplate = Util.loadTemplateFromResource("/memshell-party/shell2.jsp");
@Override @Override
@SneakyThrows @SneakyThrows
public String pack(ClassPackerConfig config) { public String pack(ClassPackerConfig<JspCustomPackerConfig> config) {
String injectorBytesBase64Str = config.getClassBytesBase64Str(); String injectorBytesBase64Str = config.getClassBytesBase64Str();
String injectorClassName = config.getClassName(); String injectorClassName = config.getClassName();
String template = this.template; String template = this.template;
if (config.isByPassJavaModule()) { if (config.isByPassJavaModule()) {
template = bypassTemplate; template = bypassTemplate;
} }
return template.replace("{{className}}", injectorClassName) String content = template.replace("{{className}}", injectorClassName)
.replace("{{base64Str}}", injectorBytesBase64Str); .replace("{{base64Str}}", injectorBytesBase64Str);
JspCustomPackerConfig customConfig = config.getCustomConfig();
if (customConfig != null && customConfig.isUnicode()) {
return JspUnicoder.encode(content, true);
}
return content;
} }
}
@Override
public PackerSchema schema() {
return JspCustomPackerConfig.schema();
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -1,10 +1,16 @@
package com.reajason.javaweb.packer.jsp; 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 * @author ReaJason
* @since 2024/11/26 * @since 2024/11/26
*/ */
public class JspPacker implements AggregatePacker { public class JspPacker implements Packer<JspCustomPackerConfig> {
} @Override
public String pack(ClassPackerConfig<JspCustomPackerConfig> classPackerConfig) {
return Packers.ClassLoaderJSP.getInstance().pack(classPackerConfig);
}
}
@@ -3,20 +3,31 @@ package com.reajason.javaweb.packer.jsp;
import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Util; import com.reajason.javaweb.packer.Util;
import com.reajason.javaweb.packer.spec.PackerSchema;
import lombok.SneakyThrows; import lombok.SneakyThrows;
/** /**
* @author ReaJason * @author ReaJason
* @since 2024/11/26 * @since 2024/11/26
*/ */
public class JspxPacker implements Packer { public class JspxPacker implements Packer<JspCustomPackerConfig> {
private final String jspxTemplate = Util.loadTemplateFromResource("/memshell-party/shell.jspx"); private final String jspxTemplate = Util.loadTemplateFromResource("/memshell-party/shell.jspx");
@Override @Override
@SneakyThrows @SneakyThrows
public String pack(ClassPackerConfig config) { public String pack(ClassPackerConfig<JspCustomPackerConfig> config) {
return jspxTemplate.replace("{{className}}", config.getClassName()) String content = jspxTemplate.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", config.getClassBytesBase64Str()); .replace("{{base64Str}}", config.getClassBytesBase64Str());
JspCustomPackerConfig customConfig = config.getCustomConfig();
if (customConfig != null && customConfig.isUnicode()) {
return JspUnicoder.encode(content, false);
}
return content;
} }
}
@Override
public PackerSchema schema() {
return JspCustomPackerConfig.schema();
}
}
@@ -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);
}
}
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.jxpath; 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 * @author ReaJason
* @since 2024/12/13 * @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);
}
} }
@@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.jxpath;
import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers; 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; import static com.reajason.javaweb.packer.spel.SpELSpringGzipJDK17Packer.assertClassNameValid;
@@ -18,6 +20,6 @@ public class JXPathSpringGzipJDK17Packer implements Packer {
String className = config.getClassName(); String className = config.getClassName();
assertClassNameValid(className); assertClassNameValid(className);
return template.replace("{{className}}", className) return template.replace("{{className}}", className)
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
} }
} }
@@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.jxpath;
import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers; import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.Util;
import org.apache.commons.codec.binary.Base64;
/** /**
* @author ReaJason * @author ReaJason
@@ -14,6 +16,6 @@ public class JXPathSpringGzipPacker implements Packer {
@Override @Override
public String pack(ClassPackerConfig config) { public String pack(ClassPackerConfig config) {
return template.replace("{{className}}", config.getClassName()) return template.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
} }
} }
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.ognl; 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 * @author ReaJason
* @since 2024/12/14 * @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);
}
} }
@@ -3,7 +3,9 @@ package com.reajason.javaweb.packer.ognl;
import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers; import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.Util;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Base64;
import static com.reajason.javaweb.packer.spel.SpELSpringGzipJDK17Packer.assertClassNameValid; import static com.reajason.javaweb.packer.spel.SpELSpringGzipJDK17Packer.assertClassNameValid;
@@ -20,6 +22,6 @@ public class OGNLSpringGzipJDK17Packer implements Packer {
String className = config.getClassName(); String className = config.getClassName();
assertClassNameValid(className); assertClassNameValid(className);
return template.replace("{{className}}", className) return template.replace("{{className}}", className)
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
} }
} }
@@ -3,7 +3,9 @@ package com.reajason.javaweb.packer.ognl;
import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers; import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.Util;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Base64;
/** /**
* @author ReaJason * @author ReaJason
@@ -16,6 +18,6 @@ public class OGNLSpringGzipPacker implements Packer {
@SneakyThrows @SneakyThrows
public String pack(ClassPackerConfig config) { public String pack(ClassPackerConfig config) {
return template.replace("{{className}}", config.getClassName()) return template.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
} }
} }
@@ -1,10 +1,16 @@
package com.reajason.javaweb.packer.scriptengine; 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 * @author ReaJason
* @since 2024/12/3 * @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);
}
} }
@@ -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<PackerOptionValue> options = new ArrayList<>();
}
@@ -0,0 +1,11 @@
package com.reajason.javaweb.packer.spec;
/**
* Supported packer config field types.
*/
public enum PackerFieldType {
BOOLEAN,
STRING,
ENUM,
INTEGER
}
@@ -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;
}
@@ -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<String, Object> config = new LinkedHashMap<>();
}
@@ -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<PackerFieldSchema> fields = new ArrayList<>();
private Map<String, Object> defaultConfig = new LinkedHashMap<>();
public static PackerSchema empty() {
return new PackerSchema();
}
}
@@ -1,10 +1,16 @@
package com.reajason.javaweb.packer.spel; 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 * @author ReaJason
* @since 2025/1/26 * @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);
}
} }
@@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.spel;
import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers; import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.Util;
import org.apache.commons.codec.binary.Base64;
/** /**
* @author ReaJason * @author ReaJason
@@ -16,7 +18,7 @@ public class SpELSpringGzipJDK17Packer implements Packer {
String className = config.getClassName(); String className = config.getClassName();
assertClassNameValid(className); assertClassNameValid(className);
return template.replace("{{className}}", 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) { public static void assertClassNameValid(String className) {
@@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.spel;
import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer; import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers; 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 @Override
public String pack(ClassPackerConfig config) { public String pack(ClassPackerConfig config) {
return template.replace("{{className}}", config.getClassName()) return template.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config)); .replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
} }
} }
@@ -1,10 +1,16 @@
package com.reajason.javaweb.packer.translet; 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 * @author ReaJason
* @since 2025/11/19 * @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);
}
} }
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.xmldecoder; 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 * @author ReaJason
* @since 2025/7/22 * @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);
}
} }
@@ -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<DemoCustomConfig> {
}
static class TypedDemoPackerWithDefault implements Packer<DemoCustomConfig> {
@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;
}
}
}
@@ -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<JspCustomPackerConfig> 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<JspCustomPackerConfig> 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<JspCustomPackerConfig> 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<JspCustomPackerConfig> 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<JspCustomPackerConfig> buildConfig() {
ClassPackerConfig<JspCustomPackerConfig> config = new ClassPackerConfig<>();
config.setClassName("hello.world.Injector");
config.setClassBytesBase64Str("QUJDRA==");
config.setClassBytes(new byte[]{1, 2, 3, 4});
return config;
}
}
@@ -1,11 +1,18 @@
import { PackageIcon } from "lucide-react"; import { PackageIcon } from "lucide-react";
import { useMemo } from "react"; import { useEffect, useMemo } from "react";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form"; import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next"; 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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { FieldLabel, FieldSet } from "@/components/ui/field"; import { Field, FieldLabel, FieldSet } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import {
findPackerEntry,
getPackerDefaultConfig,
getPackerSchemaFields,
normalizePackerCategories,
} from "@/lib/packer-schema";
import type { PackerConfig } from "@/types/memshell"; import type { PackerConfig } from "@/types/memshell";
import type { MemShellFormSchema } from "@/types/schema"; import type { MemShellFormSchema } from "@/types/schema";
@@ -28,25 +35,77 @@ export default function PackageConfigCard({
name: "server", name: "server",
}); });
const options = useMemo(() => { const packingMethod = useWatch({
const filteredOptions = (packerConfig ?? []).filter((name) => { control: form.control,
if (!shellType || shellType === " ") { name: "packingMethod",
return true; });
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"); }, [allOptionNames, form]);
}
if (server.startsWith("XXL")) { useEffect(() => {
return !name.startsWith("Agent"); form.setValue(
} "packerCustomConfig",
return !name.startsWith("Agent") && !name.toLowerCase().startsWith("xxl"); getPackerDefaultConfig(selectedPackerEntry) as any,
}); );
form.setValue("packingMethod", filteredOptions[0]); }, [form, selectedPackerEntry, packingMethod]);
return filteredOptions.map((name) => ({
name: t(name),
value: name,
}));
}, [packerConfig, shellType, server, t, form]);
return ( return (
<Card className="w-full"> <Card className="w-full">
@@ -57,32 +116,30 @@ export default function PackageConfigCard({
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{options.length > 0 ? ( {allOptionNames.length > 0 ? (
<Controller <>
control={form.control} <Controller
name="packingMethod" control={form.control}
render={({ field }) => ( name="packingMethod"
<FieldSet> render={({ field }) => (
<FieldLabel>{t("packerMethod")}</FieldLabel> <Field className="gap-1">
<RadioGroup <FieldLabel>{t("packerMethod")}</FieldLabel>
name={field.name} <PackerCombobox
value={field.value} categories={filteredCategories}
defaultValue={options[0].value} value={field.value}
onValueChange={field.onChange} onValueChange={field.onChange}
className="grid grid-cols-2 md:grid-cols-3" placeholder={t("selectPacker", {
> defaultValue: "Select packer",
{options.map(({ name, value }) => ( })}
<div key={value} className="flex items-center space-x-3"> />
<RadioGroupItem value={value} id={value} /> </Field>
<FieldLabel className="text-xs" htmlFor={value}> )}
{name} />
</FieldLabel> <PackerCustomConfigFields
</div> form={form}
))} fields={selectedSchemaFields}
</RadioGroup> />
</FieldSet> </>
)}
/>
) : ( ) : (
<div className="flex items-center justify-center p-4 gap-4 h-50"> <div className="flex items-center justify-center p-4 gap-4 h-50">
<Spinner /> <Spinner />
@@ -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<string, string> | 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 (
<CodeViewer
code={packResult ?? ""}
header={
<div className="flex items-center justify-between text-xs gap-2">
<Select
onValueChange={(value) => {
setSelectedMethod(value as string);
}}
value={selectedMethod}
>
<SelectTrigger className="h-7 text-xs [&_svg]:h-4 [&_svg]:w-4">
<span className="text-muted-foreground">
{t("common:packerMethod")}:&nbsp;
</span>
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
<SelectContent>
{packMethods.map((method) => (
<SelectItem key={method} value={method} className="text-xs">
{method}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-muted-foreground">({packResult?.length})</span>
</div>
}
button={
packMethod === "JSP" ||
packMethod === "Base64" ||
packMethod === "JavaDeserialize" ||
packMethod.includes("Hessian") ? (
<Button
variant="ghost"
size="icon"
type="button"
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
onClick={handleDownload}
>
<DownloadIcon className="h-4 w-4" />
</Button>
) : null
}
wrapLongLines={!showCode}
showLineNumbers={showCode}
language={showCode ? "java" : "text"}
height={height}
/>
);
}
@@ -3,32 +3,19 @@ import CodeViewer from "@/components/code-viewer";
import type { MemShellResult } from "@/types/memshell"; import type { MemShellResult } from "@/types/memshell";
import { AgentResult } from "./agent"; import { AgentResult } from "./agent";
import { JarResult } from "./jar-result"; import { JarResult } from "./jar-result";
import { MultiPackResult } from "./multi-packer";
export function ResultComponent({ export function ResultComponent({
packResult, packResult,
allPackResults,
packMethod, packMethod,
generateResult, generateResult,
}: Readonly<{ }: Readonly<{
packResult: string | undefined; packResult: string | undefined;
allPackResults: Map<string, string> | undefined;
packMethod: string; packMethod: string;
generateResult?: MemShellResult; generateResult?: MemShellResult;
}>) { }>) {
const showCode = packMethod === "JSP";
const isAgent = packMethod.startsWith("Agent"); const isAgent = packMethod.startsWith("Agent");
const isJar = packMethod.endsWith("Jar"); const isJar = packMethod.endsWith("Jar");
const { t } = useTranslation(); const { t } = useTranslation();
if (allPackResults) {
return (
<MultiPackResult
allPackResults={allPackResults}
packMethod={packMethod}
shellClassName={generateResult?.injectorClassName}
/>
);
}
if (isAgent) { if (isAgent) {
return ( return (
<AgentResult <AgentResult
@@ -52,16 +39,16 @@ export function ResultComponent({
<CodeViewer <CodeViewer
code={packResult ?? ""} code={packResult ?? ""}
header={ header={
<div className="flex items-center justify-between text-xs gap-2"> <div className="flex items-center justify-between text-sm gap-2">
<span> <span>
{t("common:packerMethod")}:{packMethod} {t("common:packerMethod")}:{packMethod}
</span> </span>
<span className="text-muted-foreground">({packResult?.length})</span> <span className="text-muted-foreground">({packResult?.length})</span>
</div> </div>
} }
wrapLongLines={!showCode} wrapLongLines={true}
showLineNumbers={showCode} showLineNumbers={false}
language={showCode ? "java" : "text"} language={"text"}
height={350} height={350}
/> />
); );
+1 -4
View File
@@ -13,12 +13,10 @@ import { ResultComponent } from "./results/result-component";
export default function ShellResult({ export default function ShellResult({
packResult, packResult,
allPackResults,
packMethod, packMethod,
generateResult, generateResult,
}: Readonly<{ }: Readonly<{
packResult: string | undefined; packResult: string | undefined;
allPackResults: Map<string, string> | undefined;
packMethod: string; packMethod: string;
generateResult?: MemShellResult; generateResult?: MemShellResult;
}>) { }>) {
@@ -26,7 +24,7 @@ export default function ShellResult({
if (!generateResult) { if (!generateResult) {
return <QuickUsage />; return <QuickUsage />;
} }
const height = 800; const height = 600;
return ( return (
<Tabs defaultValue="packResult"> <Tabs defaultValue="packResult">
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-3">
@@ -42,7 +40,6 @@ export default function ShellResult({
<BasicInfo generateResult={generateResult} /> <BasicInfo generateResult={generateResult} />
<ResultComponent <ResultComponent
packResult={packResult} packResult={packResult}
allPackResults={allPackResults}
packMethod={packMethod} packMethod={packMethod}
generateResult={generateResult} generateResult={generateResult}
/> />
@@ -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<PackerComboboxProps>) {
const { t } = useTranslation("common");
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const preparedCategories = useMemo<PreparedCategory[]>(
() =>
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 (
<Combobox
value={value ?? null}
open={open}
onOpenChange={(nextOpen) => {
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}
>
<ComboboxInput
className="w-full"
placeholder={resolvedPlaceholder}
disabled={disabled}
showClear={false}
/>
<ComboboxContent>
<ComboboxList>
{filteredCategories.map((category) => (
<ComboboxGroup key={category.name}>
<ComboboxLabel>{category.label}</ComboboxLabel>
{category.packers.map((packer) => (
<ComboboxItem key={packer.name} value={packer.name}>
<span className="truncate" title={packer.label}>
{packer.label}
</span>
</ComboboxItem>
))}
</ComboboxGroup>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
}
@@ -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<T extends FieldValues> = {
form: UseFormReturn<T>;
fields: PackerSchemaField[];
baseName?: string;
};
export function PackerCustomConfigFields<T extends FieldValues>({
form,
fields,
baseName = "packerCustomConfig",
}: Readonly<Props<T>>) {
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 (
<Field className="mt-2 gap-1">
<FieldLabel>
{t("packerParams", { defaultValue: "Packer Params" })}
</FieldLabel>
{supportedFields.map((schemaField) => {
const fieldName = `${baseName}.${schemaField.key}` as any;
const fieldDescription = getFieldDescription(schemaField);
return (
<Controller
key={schemaField.key}
control={form.control}
name={fieldName}
render={({ field }) => {
switch (schemaField.type) {
case "BOOLEAN":
return (
<Field orientation="horizontal">
<Switch
id={fieldName}
checked={Boolean(field.value)}
onCheckedChange={field.onChange}
/>
<FieldContent>
<FieldLabel htmlFor={fieldName}>
{schemaField.key}
</FieldLabel>
{fieldDescription ? (
<FieldDescription>
{fieldDescription}
</FieldDescription>
) : null}
</FieldContent>
</Field>
);
case "ENUM":
return (
<Field orientation="vertical">
<FieldContent>
<FieldLabel htmlFor={fieldName}>
{schemaField.key}
</FieldLabel>
<Select
value={
typeof field.value === "string"
? field.value
: undefined
}
onValueChange={field.onChange}
>
<SelectTrigger id={fieldName}>
<SelectValue
data-placeholder={t("placeholders.select")}
/>
</SelectTrigger>
<SelectContent>
{(schemaField.options ?? []).map((option) => (
<SelectItem
key={option.value}
value={option.value}
>
{option.label || option.value}
</SelectItem>
))}
</SelectContent>
</Select>
{fieldDescription ? (
<FieldDescription>
{fieldDescription}
</FieldDescription>
) : null}
</FieldContent>
</Field>
);
case "INTEGER":
return (
<Field orientation="vertical">
<FieldContent>
<FieldLabel htmlFor={fieldName}>
{schemaField.key}
</FieldLabel>
<Input
id={fieldName}
type="number"
step={1}
value={
typeof field.value === "number"
? String(field.value)
: ""
}
onChange={(event) => {
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>
{fieldDescription}
</FieldDescription>
) : null}
</FieldContent>
</Field>
);
case "STRING":
return (
<Field orientation="vertical">
<FieldContent>
<FieldLabel htmlFor={fieldName}>
{schemaField.key}
</FieldLabel>
<Input
id={fieldName}
type="text"
value={
typeof field.value === "string" ? field.value : ""
}
onChange={field.onChange}
/>
{fieldDescription ? (
<FieldDescription>
{fieldDescription}
</FieldDescription>
) : null}
</FieldContent>
</Field>
);
default:
return <></>;
}
}}
/>
);
})}
</Field>
);
}
@@ -1,18 +1,20 @@
import { PackageIcon } from "lucide-react"; import { PackageIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useMemo } from "react";
import { Controller, type UseFormReturn } from "react-hook-form"; import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next"; 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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { FieldLabel } from "@/components/ui/field"; import { Field, FieldLabel, FieldSet } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import {
findPackerEntry,
getPackerDefaultConfig,
getPackerSchemaFields,
normalizePackerCategories,
} from "@/lib/packer-schema";
import type { PackerConfig } from "@/types/memshell"; import type { PackerConfig } from "@/types/memshell";
import type { ProbeShellFormSchema } from "@/types/schema"; import type { ProbeShellFormSchema } from "@/types/schema";
type Option = {
name: string;
value: string;
};
export default function PackageConfigCard({ export default function PackageConfigCard({
packerConfig, packerConfig,
form, form,
@@ -20,34 +22,73 @@ export default function PackageConfigCard({
packerConfig: PackerConfig | undefined; packerConfig: PackerConfig | undefined;
form: UseFormReturn<ProbeShellFormSchema>; form: UseFormReturn<ProbeShellFormSchema>;
}>) { }>) {
const [options, setOptions] = useState<Array<Option>>([]);
const { t } = useTranslation("common"); 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(() => { 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"); const currentValue = form.getValues("packingMethod");
if ( if (
filteredOptions.length > 0 && allOptionNames.length > 0 &&
(!currentValue || !filteredOptions.includes(currentValue)) (!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 ( return (
<Card className="w-full"> <Card className="w-full">
@@ -58,34 +99,30 @@ export default function PackageConfigCard({
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{options.length > 0 ? ( {allOptionNames.length > 0 ? (
<Controller <>
control={form.control} <Controller
name="packingMethod" control={form.control}
render={({ field }) => ( name="packingMethod"
<div className="space-y-3"> render={({ field }) => (
<FieldLabel>{t("packerMethod")}</FieldLabel> <Field className="gap-1">
<div> <FieldLabel>{t("packerMethod")}</FieldLabel>
<RadioGroup <PackerCombobox
onValueChange={field.onChange} categories={filteredCategories}
value={field.value} value={field.value}
className="grid grid-cols-2 md:grid-cols-3" onValueChange={field.onChange}
> placeholder={t("selectPacker", {
{options.map(({ name, value }) => ( defaultValue: "Select packer",
<div key={value} className="flex items-center space-x-3"> })}
<div> />
<RadioGroupItem value={value} id={value} /> </Field>
</div> )}
<FieldLabel className="text-xs" htmlFor={value}> />
{name} <PackerCustomConfigFields
</FieldLabel> form={form}
</div> fields={selectedSchemaFields}
))} />
</RadioGroup> </>
</div>
</div>
)}
/>
) : ( ) : (
<div className="flex items-center justify-center p-4"> <div className="flex items-center justify-center p-4">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
+3 -15
View File
@@ -3,17 +3,14 @@ import { QuickUsage } from "@/components/probeshell/quick-usage";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type { ProbeShellResult } from "@/types/probeshell"; import type { ProbeShellResult } from "@/types/probeshell";
import CodeViewer from "../code-viewer"; import CodeViewer from "../code-viewer";
import { MultiPackResult } from "../memshell/results/multi-packer";
import { BasicInfo } from "./basic-info"; import { BasicInfo } from "./basic-info";
export default function ShellResult({ export default function ShellResult({
packResult, packResult,
allPackResults,
packMethod, packMethod,
generateResult, generateResult,
}: Readonly<{ }: Readonly<{
packResult: string | undefined; packResult: string | undefined;
allPackResults: Map<string, string> | undefined;
packMethod: string; packMethod: string;
generateResult?: ProbeShellResult; generateResult?: ProbeShellResult;
}>) { }>) {
@@ -21,7 +18,6 @@ export default function ShellResult({
if (!generateResult) { if (!generateResult) {
return <QuickUsage />; return <QuickUsage />;
} }
const showCode = packMethod === "JSP";
const height = 600; const height = 600;
return ( return (
<Tabs defaultValue="packResult"> <Tabs defaultValue="packResult">
@@ -32,14 +28,6 @@ export default function ShellResult({
</TabsList> </TabsList>
<TabsContent value="packResult" className="space-y-2"> <TabsContent value="packResult" className="space-y-2">
<BasicInfo generateResult={generateResult} /> <BasicInfo generateResult={generateResult} />
{allPackResults && (
<MultiPackResult
allPackResults={allPackResults}
shellClassName={generateResult?.shellClassName}
packMethod={packMethod}
height={height}
/>
)}
{packResult && ( {packResult && (
<CodeViewer <CodeViewer
code={packResult} code={packResult}
@@ -53,9 +41,9 @@ export default function ShellResult({
</span> </span>
</div> </div>
} }
wrapLongLines={!showCode} wrapLongLines={true}
showLineNumbers={showCode} showLineNumbers={false}
language={showCode ? "java" : "text"} language={"text"}
height={height} height={height}
/> />
)} )}
+300
View File
@@ -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 <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
}
function ComboboxTrigger({
className,
children,
...props
}: ComboboxPrimitive.Trigger.Props) {
return (
<ComboboxPrimitive.Trigger
data-slot="combobox-trigger"
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" />
</ComboboxPrimitive.Trigger>
);
}
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return (
<ComboboxPrimitive.Clear
data-slot="combobox-clear"
render={<InputGroupButton variant="ghost" size="icon-xs" />}
className={cn(className)}
{...props}
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.Clear>
);
}
function ComboboxInput({
className,
children,
disabled = false,
showTrigger = true,
showClear = false,
...props
}: ComboboxPrimitive.Input.Props & {
showTrigger?: boolean;
showClear?: boolean;
}) {
return (
<InputGroup className={cn("w-auto", className)}>
<ComboboxPrimitive.Input
render={<InputGroupInput disabled={disabled} />}
{...props}
/>
<InputGroupAddon align="inline-end">
{showTrigger && (
<InputGroupButton
size="icon-xs"
variant="ghost"
render={<ComboboxTrigger />}
data-slot="input-group-button"
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
disabled={disabled}
/>
)}
{showClear && <ComboboxClear disabled={disabled} />}
</InputGroupAddon>
{children}
</InputGroup>
);
}
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 (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 overflow-hidden rounded-md shadow-md ring-1 duration-100 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)",
className,
)}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
);
}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return (
<ComboboxPrimitive.List
data-slot="combobox-list"
className={cn(
"no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 p-1 data-empty:p-0 overflow-y-auto overscroll-contain",
className,
)}
{...props}
/>
);
}
function ComboboxItem({
className,
children,
...props
}: ComboboxPrimitive.Item.Props) {
return (
<ComboboxPrimitive.Item
data-slot="combobox-item"
className={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
>
{children}
<ComboboxPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</ComboboxPrimitive.ItemIndicator>
</ComboboxPrimitive.Item>
);
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return (
<ComboboxPrimitive.Group
data-slot="combobox-group"
className={cn(className)}
{...props}
/>
);
}
function ComboboxLabel({
className,
...props
}: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
data-slot="combobox-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return (
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
);
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
data-slot="combobox-empty"
className={cn(
"text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex",
className,
)}
{...props}
/>
);
}
function ComboboxSeparator({
className,
...props
}: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
data-slot="combobox-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function ComboboxChips({
className,
...props
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
ComboboxPrimitive.Chips.Props) {
return (
<ComboboxPrimitive.Chips
data-slot="combobox-chips"
className={cn(
"dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border bg-transparent bg-clip-padding px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1.5",
className,
)}
{...props}
/>
);
}
function ComboboxChip({
className,
children,
showRemove = true,
...props
}: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean;
}) {
return (
<ComboboxPrimitive.Chip
data-slot="combobox-chip"
className={cn(
"bg-muted text-foreground flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-data-[slot=combobox-chip-remove]:pr-0 has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50",
className,
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
render={<Button variant="ghost" size="icon-xs" />}
className="-ml-1 opacity-50 hover:opacity-100"
data-slot="combobox-chip-remove"
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.ChipRemove>
)}
</ComboboxPrimitive.Chip>
);
}
function ComboboxChipsInput({
className,
...props
}: ComboboxPrimitive.Input.Props) {
return (
<ComboboxPrimitive.Input
data-slot="combobox-chip-input"
className={cn("min-w-16 flex-1 outline-none", className)}
{...props}
/>
);
}
function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null);
}
export {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxGroup,
ComboboxLabel,
ComboboxCollection,
ComboboxEmpty,
ComboboxSeparator,
ComboboxChips,
ComboboxChip,
ComboboxChipsInput,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
};
+191
View File
@@ -0,0 +1,191 @@
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group";
import { SearchIcon, CheckIcon } from "lucide-react";
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground rounded-xl! p-1 flex size-full flex-col overflow-hidden",
className,
)}
{...props}
/>
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = false,
...props
}: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
children: React.ReactNode;
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn(
"rounded-xl! top-1/3 translate-y-0 overflow-hidden p-0",
className,
)}
showCloseButton={showCloseButton}
>
{children}
</DialogContent>
</Dialog>
);
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot="command-input-wrapper" className="p-1 pb-0">
<InputGroup className="bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!">
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
<InputGroupAddon>
<SearchIcon className="size-4 shrink-0 opacity-50" />
</InputGroupAddon>
</InputGroup>
</div>
);
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto",
className,
)}
{...props}
/>
);
}
function CommandEmpty({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className={cn("py-6 text-center text-sm", className)}
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium",
className,
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px w-auto", className)}
{...props}
/>
);
}
function CommandItem({
className,
children,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-selected:bg-muted data-selected:text-foreground data-selected:**:[svg]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! [&_svg:not([class*='size-'])]:size-4 group/command-item data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
>
{children}
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
</CommandPrimitive.Item>
);
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
+156
View File
@@ -0,0 +1,156 @@
"use client";
import * as React from "react";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { XIcon } from "lucide-react";
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
render={
<Button
variant="ghost"
className="absolute top-4 right-4"
size="icon-sm"
/>
}
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("gap-2 flex flex-col", className)}
{...props}
/>
);
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean;
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close render={<Button variant="outline" />}>
Close
</DialogPrimitive.Close>
)}
</div>
);
}
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("leading-none font-medium", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: DialogPrimitive.Description.Props) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3",
className,
)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};
+156
View File
@@ -0,0 +1,156 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 h-8 rounded-md border shadow-xs transition-[color,box-shadow] in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto",
className,
)}
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none",
{
variants: {
align: {
"inline-start":
"pl-2 has-[>button]:-ml-1 has-[>kbd]:ml-[-0.15rem] order-first",
"inline-end":
"pr-2 has-[>button]:-mr-1 has-[>kbd]:mr-[-0.15rem] order-last",
"block-start":
"px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start",
"block-end":
"px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start",
},
},
defaultVariants: {
align: "inline-start",
},
},
);
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
/>
);
}
const inputGroupButtonVariants = cva(
"gap-2 text-sm shadow-none flex items-center",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
},
);
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
VariantProps<typeof inputGroupButtonVariants> & {
type?: "button" | "submit" | "reset";
}) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"text-muted-foreground gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
className,
)}
{...props}
/>
);
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1",
className,
)}
{...props}
/>
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1 resize-none",
className,
)}
{...props}
/>
);
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
};
+4
View File
@@ -26,6 +26,7 @@
"paramName.description": "Supports passing values via request parameter (param) or request header (header)", "paramName.description": "Supports passing values via request parameter (param) or request header (header)",
"placeholders.input": "Please input", "placeholders.input": "Please input",
"placeholders.select": "Please select", "placeholders.select": "Please select",
"packerParams": "Package Params",
"ProbeShellGenerator": "ProbeShellGenerator", "ProbeShellGenerator": "ProbeShellGenerator",
"quickUsage.title": "Quick Usage", "quickUsage.title": "Quick Usage",
"server": "Server", "server": "Server",
@@ -49,5 +50,8 @@
"commandTemplate": "Command Template", "commandTemplate": "Command Template",
"commandTemplate.placeholder": "e.g., sh -c \"{command}\" 2>&1", "commandTemplate.placeholder": "e.g., sh -c \"{command}\" 2>&1",
"commandTemplate.description": "Use {command} as placeholder", "commandTemplate.description": "Use {command} as placeholder",
"unicodeEncoded.desc": "Enable Unicode encoding",
"urlEncoded.desc": "Enable URL encoding",
"gzipCompressed.desc": "Enable GZIP compression",
"targetJdkVersion": "JRE Version" "targetJdkVersion": "JRE Version"
} }
+4
View File
@@ -26,6 +26,7 @@
"paramName.description": "支持请求参数 param 或请求头 header 传值", "paramName.description": "支持请求参数 param 或请求头 header 传值",
"placeholders.input": "请输入", "placeholders.input": "请输入",
"placeholders.select": "请选择", "placeholders.select": "请选择",
"packerParams": "打包参数",
"ProbeShellGenerator": "探测马生成器", "ProbeShellGenerator": "探测马生成器",
"quickUsage.title": "快速使用", "quickUsage.title": "快速使用",
"server": "服务类型", "server": "服务类型",
@@ -49,5 +50,8 @@
"commandTemplate": "命令模板", "commandTemplate": "命令模板",
"commandTemplate.placeholder": "例如:sh -c \"{command}\" 2>&1", "commandTemplate.placeholder": "例如:sh -c \"{command}\" 2>&1",
"commandTemplate.description": "使用 {command} 作为占位符", "commandTemplate.description": "使用 {command} 作为占位符",
"unicodeEncoded.desc": "启用 Unicode 编码",
"urlEncoded.desc": "启用 URL 编码",
"gzipCompressed.desc": "启用 GZIP 压缩",
"targetJdkVersion": "JRE 版本" "targetJdkVersion": "JRE 版本"
} }
+101
View File
@@ -0,0 +1,101 @@
import type {
LegacyPackerGroup,
PackerCategory,
PackerConfig,
PackerEntry,
PackerSchemaField,
} from "@/types/memshell";
export type NormalizedPackerEntry = Pick<
PackerEntry,
"name" | "outputKind" | "categoryAnchor" | "schema"
>;
export type NormalizedPackerCategory = {
name: string;
packers: NormalizedPackerEntry[];
};
const isLegacyGroup = (value: unknown): value is LegacyPackerGroup => {
return (
typeof value === "object" &&
value !== null &&
"group" in value &&
"options" in value &&
Array.isArray((value as { options?: unknown[] }).options)
);
};
const isPackerCategory = (value: unknown): value is PackerCategory => {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"packers" in value &&
Array.isArray((value as { packers?: unknown[] }).packers)
);
};
export function normalizePackerCategories(
packerConfig: PackerConfig | undefined,
): NormalizedPackerCategory[] {
return (packerConfig ?? [])
.map((item): NormalizedPackerCategory | null => {
if (typeof item === "string") {
return {
name: item,
packers: [{ name: item, categoryAnchor: false }],
};
}
if (isLegacyGroup(item)) {
return {
name: item.group,
packers: (item.options ?? []).map((name) => ({
name,
categoryAnchor: false,
})),
};
}
if (isPackerCategory(item)) {
return {
name: item.name,
packers: (item.packers ?? []).map((packer) => ({
name: packer.name,
outputKind: packer.outputKind,
categoryAnchor: !!packer.categoryAnchor,
schema: packer.schema,
})),
};
}
return null;
})
.filter((item): item is NormalizedPackerCategory => item !== null);
}
export function findPackerEntry(
categories: NormalizedPackerCategory[],
packerName: string | undefined,
): NormalizedPackerEntry | undefined {
if (!packerName) {
return undefined;
}
for (const category of categories) {
const found = category.packers.find((packer) => packer.name === packerName);
if (found) {
return found;
}
}
return undefined;
}
export function getPackerSchemaFields(
packer: NormalizedPackerEntry | undefined,
): PackerSchemaField[] {
return packer?.schema?.fields ?? [];
}
export function getPackerDefaultConfig(
packer: NormalizedPackerEntry | undefined,
): Record<string, unknown> {
return { ...(packer?.schema?.defaultConfig ?? {}) };
}
+2 -6
View File
@@ -11,6 +11,7 @@ import ShellResult from "@/components/memshell/shell-result";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { env } from "@/config"; import { env } from "@/config";
import { siteConfig } from "@/lib/config"; import { siteConfig } from "@/lib/config";
import { baseOptions } from "@/lib/layout.shared";
import { import {
type APIErrorResponse, type APIErrorResponse,
type MainConfig, type MainConfig,
@@ -26,7 +27,6 @@ import {
useYupValidationResolver, useYupValidationResolver,
} from "@/types/schema"; } from "@/types/schema";
import { transformToPostData } from "@/utils/transformer"; import { transformToPostData } from "@/utils/transformer";
import { baseOptions } from "../lib/layout.shared";
const homeLayoutOptions = baseOptions(); const homeLayoutOptions = baseOptions();
@@ -49,6 +49,7 @@ const defaultValues: MemShellFormSchema = {
headerValue: "", headerValue: "",
injectorClassName: "", injectorClassName: "",
packingMethod: "", packingMethod: "",
packerCustomConfig: {},
shrink: true, shrink: true,
staticInitialize: true, staticInitialize: true,
shellClassBase64: "", shellClassBase64: "",
@@ -95,9 +96,6 @@ export default function MemShellPage() {
}); });
const [packResult, setPackResult] = useState<string | undefined>(); const [packResult, setPackResult] = useState<string | undefined>();
const [allPackResults, setAllPackResults] = useState<
Map<string, string> | undefined
>();
const [generateResult, setGenerateResult] = useState<MemShellResult>(); const [generateResult, setGenerateResult] = useState<MemShellResult>();
const [packMethod, setPackMethod] = useState<string>(""); const [packMethod, setPackMethod] = useState<string>("");
const submitLockRef = useRef(false); const submitLockRef = useRef(false);
@@ -121,7 +119,6 @@ export default function MemShellPage() {
const result = (await response.json()) as MemShellGenerateResponse; const result = (await response.json()) as MemShellGenerateResponse;
setGenerateResult(result.memShellResult); setGenerateResult(result.memShellResult);
setPackResult(result.packResult); setPackResult(result.packResult);
setAllPackResults(result.allPackResults);
setPackMethod(data.packingMethod); setPackMethod(data.packingMethod);
toast.success(t("toast.generateSuccess")); toast.success(t("toast.generateSuccess"));
} catch (error) { } catch (error) {
@@ -179,7 +176,6 @@ export default function MemShellPage() {
packMethod={packMethod} packMethod={packMethod}
generateResult={generateResult} generateResult={generateResult}
packResult={packResult} packResult={packResult}
allPackResults={allPackResults}
/> />
</div> </div>
</form> </form>
+1 -5
View File
@@ -57,15 +57,13 @@ export default function ProbeShellGenerator() {
reqParamName: "", reqParamName: "",
seconds: 5, seconds: 5,
sleepServer: "Tomcat", sleepServer: "Tomcat",
packerCustomConfig: {},
shrink: true, shrink: true,
staticInitialize: true, staticInitialize: true,
}, },
}); });
const [packResult, setPackResult] = useState<string | undefined>(); const [packResult, setPackResult] = useState<string | undefined>();
const [allPackResults, setAllPackResults] = useState<
Map<string, string> | undefined
>();
const [generateResult, setGenerateResult] = useState<ProbeShellResult>(); const [generateResult, setGenerateResult] = useState<ProbeShellResult>();
const [packMethod, setPackMethod] = useState<string>(""); const [packMethod, setPackMethod] = useState<string>("");
const submitLockRef = useRef(false); const submitLockRef = useRef(false);
@@ -93,7 +91,6 @@ export default function ProbeShellGenerator() {
const result = (await response.json()) as ProbeShellGenerateResponse; const result = (await response.json()) as ProbeShellGenerateResponse;
setGenerateResult(result.probeShellResult); setGenerateResult(result.probeShellResult);
setPackResult(result.packResult); setPackResult(result.packResult);
setAllPackResults(result.allPackResults);
setPackMethod(data.packingMethod); setPackMethod(data.packingMethod);
toast.success(t("toast.generateSuccess")); toast.success(t("toast.generateSuccess"));
} catch (error) { } catch (error) {
@@ -132,7 +129,6 @@ export default function ProbeShellGenerator() {
packMethod={packMethod} packMethod={packMethod}
generateResult={generateResult} generateResult={generateResult}
packResult={packResult} packResult={packResult}
allPackResults={allPackResults}
/> />
</div> </div>
</form> </form>
+38 -1
View File
@@ -97,7 +97,44 @@ export interface MainConfig {
}; };
} }
export type PackerConfig = Array<string>; export interface LegacyPackerGroup {
group: string;
options: string[];
}
export interface PackerSchemaFieldOption {
value: string;
label: string;
}
export interface PackerSchemaField {
key: string;
type: string;
required: boolean;
defaultValue?: unknown;
description?: string;
descriptionI18nKey?: string;
options?: PackerSchemaFieldOption[];
}
export interface PackerSchema {
fields?: PackerSchemaField[];
defaultConfig?: Record<string, unknown>;
}
export interface PackerEntry {
name: string;
outputKind?: string;
categoryAnchor?: boolean;
schema?: PackerSchema;
}
export interface PackerCategory {
name: string;
packers: PackerEntry[];
}
export type PackerConfig = Array<LegacyPackerGroup | PackerCategory | string>;
export interface MemShellGenerateResponse { export interface MemShellGenerateResponse {
memShellResult: MemShellResult; memShellResult: MemShellResult;
-1
View File
@@ -61,7 +61,6 @@ export interface PayloadFormValues {
export interface ProbeShellGenerateResponse { export interface ProbeShellGenerateResponse {
probeShellResult: ProbeShellResult; probeShellResult: ProbeShellResult;
packResult?: string; packResult?: string;
allPackResults?: Map<string, string>;
} }
export interface ProbeShellResult { export interface ProbeShellResult {
+2
View File
@@ -26,6 +26,7 @@ export const memShellFormSchema = yup.object({
headerValue: yup.string().optional(), headerValue: yup.string().optional(),
injectorClassName: yup.string().optional(), injectorClassName: yup.string().optional(),
packingMethod: yup.string().required().min(1), packingMethod: yup.string().required().min(1),
packerCustomConfig: yup.object().optional(),
shrink: yup.boolean().optional(), shrink: yup.boolean().optional(),
lambdaSuffix: yup.boolean().optional(), lambdaSuffix: yup.boolean().optional(),
probe: yup.boolean().optional(), probe: yup.boolean().optional(),
@@ -164,6 +165,7 @@ export const probeShellFormSchema = yup.object().shape({
seconds: yup.number().optional(), seconds: yup.number().optional(),
sleepServer: yup.string().optional(), sleepServer: yup.string().optional(),
packingMethod: yup.string().required(), packingMethod: yup.string().required(),
packerCustomConfig: yup.object().optional(),
targetJdkVersion: yup.string().optional(), targetJdkVersion: yup.string().optional(),
debug: yup.boolean().optional(), debug: yup.boolean().optional(),
byPassJavaModule: yup.boolean().optional(), byPassJavaModule: yup.boolean().optional(),
+8 -2
View File
@@ -43,7 +43,10 @@ export function transformToPostData(formValue: MemShellFormSchema) {
shellConfig, shellConfig,
shellToolConfig, shellToolConfig,
injectorConfig, injectorConfig,
packer: formValue.packingMethod, packerSpec: {
name: formValue.packingMethod,
config: formValue.packerCustomConfig ?? {},
},
}; };
} }
@@ -70,7 +73,10 @@ export function transformToProbePostData(formValue: ProbeShellFormSchema) {
return { return {
probeConfig, probeConfig,
probeContentConfig, probeContentConfig,
packer: formValue.packingMethod, packerSpec: {
name: formValue.packingMethod,
config: formValue.packerCustomConfig ?? {},
},
}; };
} }
+17 -4
View File
@@ -5,7 +5,7 @@
"": { "": {
"name": "fumadocs", "name": "fumadocs",
"dependencies": { "dependencies": {
"@base-ui/react": "^1.1.0", "@base-ui/react": "^1.2.0",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@orama/orama": "^3.1.18", "@orama/orama": "^3.1.18",
"@orama/stopwords": "^3.1.18", "@orama/stopwords": "^3.1.18",
@@ -14,6 +14,7 @@
"@tanstack/react-query": "^5.90.20", "@tanstack/react-query": "^5.90.20",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1",
"framer-motion": "^12.33.0", "framer-motion": "^12.33.0",
"fumadocs-core": "^16.5.1", "fumadocs-core": "^16.5.1",
"fumadocs-mdx": "14.2.6", "fumadocs-mdx": "14.2.6",
@@ -106,7 +107,7 @@
"@babel/preset-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], "@babel/preset-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="],
"@babel/runtime": ["@babel/[email protected].4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], "@babel/runtime": ["@babel/[email protected].6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
"@babel/template": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], "@babel/template": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
@@ -114,9 +115,9 @@
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], "@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@base-ui/react": ["@base-ui/react@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@base-ui/utils": "0.2.4", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw=="], "@base-ui/react": ["@base-ui/react@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.5", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw=="],
"@base-ui/utils": ["@base-ui/[email protected].4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng=="], "@base-ui/utils": ["@base-ui/[email protected].5", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw=="],
"@biomejs/biome": ["@biomejs/[email protected]", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.14", "@biomejs/cli-darwin-x64": "2.3.14", "@biomejs/cli-linux-arm64": "2.3.14", "@biomejs/cli-linux-arm64-musl": "2.3.14", "@biomejs/cli-linux-x64": "2.3.14", "@biomejs/cli-linux-x64-musl": "2.3.14", "@biomejs/cli-win32-arm64": "2.3.14", "@biomejs/cli-win32-x64": "2.3.14" }, "bin": { "biome": "bin/biome" } }, "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA=="], "@biomejs/biome": ["@biomejs/[email protected]", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.14", "@biomejs/cli-darwin-x64": "2.3.14", "@biomejs/cli-linux-arm64": "2.3.14", "@biomejs/cli-linux-arm64-musl": "2.3.14", "@biomejs/cli-linux-x64": "2.3.14", "@biomejs/cli-linux-x64-musl": "2.3.14", "@biomejs/cli-win32-arm64": "2.3.14", "@biomejs/cli-win32-x64": "2.3.14" }, "bin": { "biome": "bin/biome" } }, "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA=="],
@@ -544,6 +545,8 @@
"clsx": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "clsx": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cmdk": ["[email protected]", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
"collapse-white-space": ["[email protected]", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "collapse-white-space": ["[email protected]", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
"color-convert": ["[email protected]", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-convert": ["[email protected]", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -1304,6 +1307,8 @@
"ansi-align/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "ansi-align/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"babel-plugin-macros/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"boxen/chalk": ["[email protected]", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "boxen/chalk": ["[email protected]", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"chalk-template/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chalk-template/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -1312,10 +1317,14 @@
"compression/negotiator": ["[email protected]", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], "compression/negotiator": ["[email protected]", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
"dom-helpers/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"fumadocs-mdx/chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "fumadocs-mdx/chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"fumadocs-mdx/unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], "fumadocs-mdx/unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"i18next/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"mdast-util-to-hast/unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], "mdast-util-to-hast/unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"mdast-util-to-markdown/unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], "mdast-util-to-markdown/unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
@@ -1330,10 +1339,14 @@
"react-d3-tree/uuid": ["[email protected]", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "react-d3-tree/uuid": ["[email protected]", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"react-i18next/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"react-router-devtools/@biomejs/cli-darwin-arm64": ["@biomejs/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="], "react-router-devtools/@biomejs/cli-darwin-arm64": ["@biomejs/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="],
"react-router-devtools/framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="], "react-router-devtools/framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="],
"react-syntax-highlighter/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"serve/chalk": ["[email protected]", "", {}, "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w=="], "serve/chalk": ["[email protected]", "", {}, "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w=="],
"serve-handler/bytes": ["[email protected]", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="], "serve-handler/bytes": ["[email protected]", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
+2 -1
View File
@@ -12,7 +12,7 @@
"format": "biome format --write" "format": "biome format --write"
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.1.0", "@base-ui/react": "^1.2.0",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@orama/orama": "^3.1.18", "@orama/orama": "^3.1.18",
"@orama/stopwords": "^3.1.18", "@orama/stopwords": "^3.1.18",
@@ -21,6 +21,7 @@
"@tanstack/react-query": "^5.90.20", "@tanstack/react-query": "^5.90.20",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1",
"framer-motion": "^12.33.0", "framer-motion": "^12.33.0",
"fumadocs-core": "^16.5.1", "fumadocs-core": "^16.5.1",
"fumadocs-mdx": "14.2.6", "fumadocs-mdx": "14.2.6",