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());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException exception) {
return new ErrorResponse(exception.getMessage());
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Throwable.class)
public ErrorResponse handleThrowable(Throwable throwable) {
@@ -34,10 +34,24 @@ public class ConfigController {
}
@RequestMapping("/packers")
public List<String> getPackers() {
return Arrays.stream(Packers.values())
.filter(packers -> packers.getParentPacker() == null)
.map(Packers::name).toList();
public List<PackerCategoryDTO> getPackers() {
List<PackerCategoryDTO> result = new ArrayList<>();
for (Map.Entry<String, List<Packers>> entry : Packers.groupedPackers().entrySet()) {
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
@@ -66,4 +80,64 @@ public class ConfigController {
commandConfigVO.setImplementationClasses(Arrays.stream(CommandConfig.ImplementationClass.values()).toList());
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.ShellConfig;
import com.reajason.javaweb.memshell.config.ShellToolConfig;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.JarPacker;
import com.reajason.javaweb.packer.JarPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
import org.springframework.web.bind.annotation.*;
import java.util.Base64;
@@ -22,19 +24,24 @@ import java.util.Base64;
@RequestMapping("/api/memshell/generate")
@CrossOrigin("*")
public class MemShellGeneratorController {
@PostMapping
public MemShellGenerateResponse generate(@RequestBody MemShellGenerateRequest request) {
ShellConfig shellConfig = request.getShellConfig();
ShellToolConfig shellToolConfig = request.parseShellToolConfig();
InjectorConfig injectorConfig = request.getInjectorConfig();
MemShellResult generateResult = MemShellGenerator.generate(shellConfig, injectorConfig, shellToolConfig);
Packer packer = request.getPacker().getInstance();
if (packer instanceof AggregatePacker) {
return new MemShellGenerateResponse(generateResult, ((AggregatePacker) packer).packAll(generateResult.toClassPackerConfig()));
if (request.getPackerSpec() == null) {
throw new IllegalArgumentException("packerSpec is required");
}
Packers packers = Packers.fromName(request.getPackerSpec().getName());
Packer<?> packer = packers.getInstance();
if (packer instanceof JarPacker) {
return new MemShellGenerateResponse(generateResult, Base64.getEncoder().encodeToString(((JarPacker) packer).packBytes(generateResult.toJarPackerConfig())));
JarPackerConfig<?> jarPackerConfig = generateResult.toJarPackerConfig();
return new MemShellGenerateResponse(generateResult, Base64.getEncoder().encodeToString(((JarPacker) packer).packBytes(jarPackerConfig)));
}
return new MemShellGenerateResponse(generateResult, packer.pack(generateResult.toClassPackerConfig()));
ClassPackerConfig classPackerConfig = generateResult.toClassPackerConfig();
classPackerConfig.setCustomConfig(packer.resolveCustomConfig(request.getPackerSpec().getConfig()));
return new MemShellGenerateResponse(generateResult, packer.pack(classPackerConfig));
}
}
}
@@ -2,8 +2,9 @@ package com.reajason.javaweb.boot.controller;
import com.reajason.javaweb.boot.dto.ProbeShellGenerateRequest;
import com.reajason.javaweb.boot.dto.ProbeShellGenerateResponse;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.probe.ProbeShellGenerator;
import com.reajason.javaweb.probe.ProbeShellResult;
import com.reajason.javaweb.probe.config.ProbeConfig;
@@ -23,11 +24,13 @@ public class ProbeShellGeneratorController {
ProbeConfig probeConfig = request.getProbeConfig();
ProbeContentConfig probeContentConfig = request.parseProbeContentConfig();
ProbeShellResult generateResult = ProbeShellGenerator.generate(probeConfig, probeContentConfig);
Packer packer = request.getPacker().getInstance();
if (packer instanceof AggregatePacker) {
return new ProbeShellGenerateResponse(generateResult, ((AggregatePacker) packer).packAll(generateResult.toClassPackerConfig()));
} else {
return new ProbeShellGenerateResponse(generateResult, packer.pack(generateResult.toClassPackerConfig()));
if (request.getPackerSpec() == null) {
throw new IllegalArgumentException("packerSpec is required");
}
Packers packers = Packers.fromName(request.getPackerSpec().getName());
Packer packer = packers.getInstance();
ClassPackerConfig classPackerConfig = generateResult.toClassPackerConfig();
classPackerConfig.setCustomConfig(packer.resolveCustomConfig(request.getPackerSpec().getConfig()));
return new ProbeShellGenerateResponse(generateResult, packer.pack(classPackerConfig));
}
}
@@ -1,7 +1,6 @@
package com.reajason.javaweb.boot.dto;
import com.reajason.javaweb.memshell.config.*;
import com.reajason.javaweb.packer.Packers;
import lombok.Data;
import static com.reajason.javaweb.memshell.ShellTool.*;
@@ -15,7 +14,7 @@ public class MemShellGenerateRequest {
private ShellConfig shellConfig;
private ShellToolConfigDTO shellToolConfig;
private InjectorConfig injectorConfig;
private Packers packer;
private PackerRequestSpecDTO packerSpec;
@Data
public static class ShellToolConfigDTO {
@@ -84,4 +83,4 @@ public class MemShellGenerateRequest {
default -> throw new UnsupportedOperationException("unknown shell tool " + shellConfig.getShellTool());
};
}
}
}
@@ -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;
import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.probe.config.*;
import lombok.Data;
@@ -12,7 +11,7 @@ import lombok.Data;
public class ProbeShellGenerateRequest {
private ProbeConfig probeConfig;
private ProbeContentConfigDTO probeContentConfig;
private Packers packer;
private PackerRequestSpecDTO packerSpec;
@Data
static class ProbeContentConfigDTO {
@@ -9,9 +9,12 @@ import org.springframework.http.ResponseEntity;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* @author ReaJason
@@ -37,11 +40,4 @@ public class ConfigControllerIntegrationTest {
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
@Test
public void testConfigPackersEndpoint() {
ResponseEntity<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.boot.dto.MemShellGenerateRequest;
import com.reajason.javaweb.boot.dto.MemShellGenerateResponse;
import com.reajason.javaweb.boot.dto.PackerRequestSpecDTO;
import com.reajason.javaweb.memshell.ShellTool;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.memshell.config.InjectorConfig;
import com.reajason.javaweb.memshell.config.ShellConfig;
import com.reajason.javaweb.packer.Packers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -15,6 +15,8 @@ import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -30,6 +32,23 @@ class MemShellGeneratorControllerTest {
@Test
void generateShell() {
MemShellGenerateRequest request = buildRequest("ScriptEngine", null);
ResponseEntity<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();
request.setShellConfig(ShellConfig.builder()
.server(Server.Tomcat)
@@ -43,16 +62,18 @@ class MemShellGeneratorControllerTest {
request.setInjectorConfig(InjectorConfig.builder()
.urlPattern("/*")
.build());
request.setPacker(Packers.ScriptEngine);
PackerRequestSpecDTO packerRequestSpecDTO = new PackerRequestSpecDTO();
packerRequestSpecDTO.setName(packerName);
if (packerConfig != null) {
packerRequestSpecDTO.setConfig(packerConfig);
}
request.setPackerSpec(packerRequestSpecDTO);
MemShellGenerateRequest.ShellToolConfigDTO shellToolConfigDTO = new MemShellGenerateRequest.ShellToolConfigDTO();
shellToolConfigDTO.setGodzillaKey("key");
shellToolConfigDTO.setGodzillaPass("pass");
shellToolConfigDTO.setHeaderName("User-Agent");
shellToolConfigDTO.setHeaderValue("hello");
request.setShellToolConfig(shellToolConfigDTO);
ResponseEntity<MemShellGenerateResponse> response = restTemplate.postForEntity(
"/api/memshell/generate", request, MemShellGenerateResponse.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
return request;
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ idea {
}
}
version = "2.6.0"
version = "2.7.0-SNAPSHOT"
tasks.register("publishAllToMavenCentral") {
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]
asm = "9.9.1"
asm = "9.9.1" # https://mvnrepository.com/artifact/org.ow2.asm/asm
jna = "5.13.0" # 为适配 JDK6+ 这个不可修改
bcel = "5.2"
javax-servlet-api = "3.0.1"
@@ -12,21 +12,21 @@ reactor-netty = "1.1.25"
jackson = "2.19.0"
jetbrains-annotations = "26.0.2"
byte-buddy = "1.18.4"
commons-io = "2.21.0"
commons-lang3 = "3.20.0"
commons-codec = "1.20.0"
logback = "1.5.24"
okhttp3 = "5.3.2"
fastjson2 = "2.0.60"
java-websocket = "1.6.0"
byte-buddy = "1.18.5" # https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy
commons-io = "2.21.0" # https://mvnrepository.com/artifact/commons-io/commons-io
commons-lang3 = "3.20.0" # https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
commons-codec = "1.21.0" # https://mvnrepository.com/artifact/commons-codec/commons-codec
logback = "1.5.32" # https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
okhttp3 = "5.3.2" # https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
fastjson2 = "2.0.61" # https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2
java-websocket = "1.6.0" # https://mvnrepository.com/artifact/org.java-websocket/Java-WebSocket
mockito = "5.20.0"
mockito = "5.21.0"
hamcrest = "3.0"
junit-jupiter = "5.14.2"
junit-jupiter = "5.14.3" # https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
junit-pioneer = "2.3.0"
junit-platform = "1.14.2"
testcontainers = "2.0.3"
junit-platform = "1.14.3" # https://mvnrepository.com/artifact/org.junit.platform/junit-platform-launcher
testcontainers = "2.0.3" # https://mvnrepository.com/artifact/org.testcontainers/testcontainers
[libraries]
byte-buddy = { module = "net.bytebuddy:byte-buddy", version.ref = "byte-buddy" }
@@ -68,5 +68,5 @@ mockito = ["mockito-core", "mockito-junit-jupiter"]
testcontainers = ["testcontainers", "testcontainers-junit-jupiter"]
[plugins]
lombok = { id = "io.freefair.lombok", version = "9.2.0" }
shadow = { id = "com.gradleup.shadow", version = "9.3.1"}
lombok = { id = "io.freefair.lombok", version = "9.2.0" } # https://plugins.gradle.org/plugin/io.freefair.lombok
shadow = { id = "com.gradleup.shadow", version = "9.3.1"} # https://plugins.gradle.org/plugin/com.gradleup.shadow
@@ -129,7 +129,7 @@ public abstract class AbstractContainerTest {
}
@ParameterizedTest
@EnumSource(names = {"ClassLoaderJSP", "ClassLoaderJSPUnicode", "DefineClassJSP", "DefineClassJSPUnicode", "JSPX", "JSPXUnicode"})
@EnumSource(names = {"ClassLoaderJSP", "DefineClassJSP", "JSPX"})
void testJspPackers(Packers packer) {
ContainerTestConfig config = getConfig();
if (config.isEnableJspPackerTest()) {
@@ -400,14 +400,14 @@ public class ShellAssertion {
public static void injectIsOk(String url, String shellType, String shellTool, String content, Packers packer, GenericContainer<?> container) {
switch (packer) {
case JSP, ClassLoaderJSP, ClassLoaderJSPUnicode, DefineClassJSP, DefineClassJSPUnicode -> {
case JSP, ClassLoaderJSP, DefineClassJSP -> {
String uploadEntry = url + "/upload";
String filename = shellType + shellTool + packer + ".jsp";
String shellUrl = url + "/" + filename;
VulTool.uploadJspFileToServer(uploadEntry, filename, content);
VulTool.urlIsOk(shellUrl);
}
case JSPX, JSPXUnicode -> {
case JSPX -> {
String uploadEntry = url + "/upload";
String filename = shellType + shellTool + packer + ".jspx";
String shellUrl = url + "/" + filename;
+1 -4
View File
@@ -5,7 +5,7 @@ plugins {
}
group = "io.github.reajason"
description = "Java deserialize payload for MemShellParty"
description = "Java payload packer for MemShellParty"
version = rootProject.version
dependencies {
@@ -22,9 +22,6 @@ dependencies {
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
@@ -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
*/
@Data
public class ClassPackerConfig {
public class ClassPackerConfig<T> {
private String className;
private byte[] classBytes;
private String classBytesBase64Str;
private boolean byPassJavaModule;
private T customConfig;
}
@@ -9,7 +9,8 @@ import java.util.Map;
* @since 2025/6/27
*/
@Data
public class JarPackerConfig {
public class JarPackerConfig<T> {
private String mainClassName;
private transient Map<String, byte[]> classBytes;
private T customConfig;
}
@@ -1,10 +1,19 @@
package com.reajason.javaweb.packer;
import com.reajason.javaweb.packer.spec.PackerSchema;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author ReaJason
* @since 2024/11/26
*/
public interface Packer {
public interface Packer<T> {
TypeCache TYPE_CACHE = new TypeCache();
/**
* 将自定义类打包成特定 payload
@@ -12,7 +21,71 @@ public interface Packer {
* @param classPackerConfig 自定义类信息
* @return 字符串 payload
*/
default String pack(ClassPackerConfig classPackerConfig) {
default String pack(ClassPackerConfig<T> classPackerConfig) {
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;
import com.reajason.javaweb.packer.spec.PackerSchema;
import com.reajason.javaweb.packer.aviator.AviatorPacker;
import com.reajason.javaweb.packer.base64.Base64Packer;
import com.reajason.javaweb.packer.base64.Base64URLEncoded;
import com.reajason.javaweb.packer.base64.DefaultBase64Packer;
import com.reajason.javaweb.packer.base64.GzipBase64Packer;
import com.reajason.javaweb.packer.bsh.BeanShellPacker;
import com.reajason.javaweb.packer.deserialize.hessian.Hessian2Packer;
import com.reajason.javaweb.packer.deserialize.hessian.Hessian2XSLTScriptEnginePacker;
@@ -52,7 +50,9 @@ import com.reajason.javaweb.packer.xmldecoder.XMLDecoderPacker;
import com.reajason.javaweb.packer.xmldecoder.XMLDecoderScriptEnginePacker;
import lombok.Getter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -67,20 +67,14 @@ public enum Packers {
* Base64
*/
Base64(new Base64Packer()),
DefaultBase64(new DefaultBase64Packer(), Base64Packer.class),
Base64URLEncoded(new Base64URLEncoded(), Base64Packer.class),
GzipBase64(new GzipBase64Packer(), Base64Packer.class),
/**
* JSP 打包器
*/
JSP(new JspPacker()),
ClassLoaderJSP(new ClassLoaderJspPacker(), JspPacker.class),
ClassLoaderJSPUnicode(new ClassLoaderJspUnicodePacker(), JspPacker.class),
DefineClassJSP(new DefineClassJspPacker(), JspPacker.class),
DefineClassJSPUnicode(new DefineClassJspUnicodePacker(), JspPacker.class),
JSPX(new JspxPacker(), JspPacker.class),
JSPXUnicode(new JspxUnicodePacker(), JspPacker.class),
/**
* BigInteger
@@ -193,7 +187,48 @@ public enum Packers {
this.parentPacker = parentPacker;
}
public static List<Packers> getPackersWithParent(Class<?> parentPacker) {
return Stream.of(Packers.values()).filter(p -> Objects.equals(p.getParentPacker(), parentPacker)).collect(Collectors.toList());
public static Packers fromName(String name) {
if (name == null) {
throw new IllegalArgumentException("packer name is required");
}
try {
return Packers.valueOf(name);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("unknown packer: " + name);
}
}
public String getCategoryName() {
if (parentPacker == null) {
return name();
}
for (Packers packer : Packers.values()) {
if (packer.getParentPacker() == null && packer.getInstance().getClass().equals(parentPacker)) {
return packer.name();
}
}
return name();
}
public boolean hasChildren() {
return Stream.of(Packers.values()).anyMatch(p -> Objects.equals(p.getParentPacker(), this.getInstance().getClass()));
}
public String getOutputKind() {
return instance instanceof JarPacker ? "binary-base64" : "text";
}
public PackerSchema getSchema() {
PackerSchema schema = instance.schema();
return schema == null ? PackerSchema.empty() : schema;
}
public static Map<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 org.apache.commons.io.IOUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Objects;
import java.util.zip.GZIPOutputStream;
/**
* @author ReaJason
@@ -19,4 +22,13 @@ public class Util {
return IOUtils.toString(Objects.requireNonNull(stream), Charset.defaultCharset());
}
}
@SneakyThrows
public static byte[] gzipCompress(byte[] data) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(out)) {
gzip.write(data);
}
return out.toByteArray();
}
}
@@ -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;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Util;
import com.reajason.javaweb.packer.spec.PackerSchema;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Base64;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* @author ReaJason
* @since 2024/12/17
*/
public class Base64Packer implements AggregatePacker {
}
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;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2025/2/20
*/
public class Hessian2Packer implements AggregatePacker {
public class Hessian2Packer implements Packer {
@Override
public String pack(ClassPackerConfig classPackerConfig) {
return Packers.Hessian2XSLTScriptEngine.getInstance().pack(classPackerConfig);
}
}
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.deserialize.hessian;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2025/2/20
*/
public class HessianPacker implements AggregatePacker {
public class HessianPacker implements Packer {
@Override
public String pack(ClassPackerConfig classPackerConfig) {
return Packers.HessianXSLTScriptEngine.getInstance().pack(classPackerConfig);
}
}
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.deserialize.java;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2024/12/10
*/
public class JavaDeserializePacker implements AggregatePacker {
}
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;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2024/12/13
*/
public class GroovyPacker implements AggregatePacker {
public class GroovyPacker implements Packer {
@Override
public String pack(ClassPackerConfig classPackerConfig) {
return Packers.GroovyClassDefiner.getInstance().pack(classPackerConfig);
}
}
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.h2;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2025/6/28
*/
public class H2Packer implements AggregatePacker {
public class H2Packer implements Packer {
@Override
public String pack(ClassPackerConfig classPackerConfig) {
return Packers.H2Javac.getInstance().pack(classPackerConfig);
}
}
@@ -24,7 +24,8 @@ public class DefaultJarPacker implements JarPacker {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
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.write(entry.getValue());
targetJar.closeEntry();
@@ -8,6 +8,7 @@ import lombok.SneakyThrows;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
@@ -21,7 +22,7 @@ public class GroovyTransformJarPacker implements JarPacker {
@SneakyThrows
public byte[] packBytes(JarPackerConfig config) {
String mainClassName = config.getMainClassName();
byte[] mainClassBytes = config.getClassBytes().get(mainClassName);
byte[] mainClassBytes = ((Map<String, byte[]>) config.getClassBytes()).get(mainClassName);
mainClassBytes = ClassInterfaceUtils.addInterface(mainClassBytes, "org.codehaus.groovy.transform.ASTTransformation");
mainClassBytes = ClassAnnotationUtils.setAnnotation(mainClassBytes, "org.codehaus.groovy.transform.GroovyASTTransformation");
@@ -7,6 +7,7 @@ import lombok.SneakyThrows;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
@@ -21,7 +22,7 @@ public class ScriptEngineJarPacker implements JarPacker {
@SneakyThrows
public byte[] packBytes(JarPackerConfig config) {
String mainClassName = config.getMainClassName();
byte[] mainClassBytes = config.getClassBytes().get(mainClassName);
byte[] mainClassBytes = ((Map<String, byte[]>) config.getClassBytes()).get(mainClassName);
byte[] bytes = ClassInterfaceUtils.addInterface(mainClassBytes, "javax.script.ScriptEngineFactory");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
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.Packer;
import com.reajason.javaweb.packer.Util;
import com.reajason.javaweb.packer.spec.PackerSchema;
import lombok.SneakyThrows;
/**
* @author ReaJason
* @since 2024/11/26
*/
public class ClassLoaderJspPacker implements Packer {
public class ClassLoaderJspPacker implements Packer<JspCustomPackerConfig> {
private final String jspTemplate = Util.loadTemplateFromResource("/memshell-party/shell.jsp");
@Override
@SneakyThrows
public String pack(ClassPackerConfig config) {
return jspTemplate.replace("{{className}}", config.getClassName())
public String pack(ClassPackerConfig<JspCustomPackerConfig> config) {
String content = jspTemplate.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", config.getClassBytesBase64Str());
JspCustomPackerConfig customConfig = config.getCustomConfig();
if (customConfig != null && customConfig.isUnicode()) {
return JspUnicoder.encode(content, true);
}
return content;
}
@Override
public PackerSchema schema() {
return JspCustomPackerConfig.schema();
}
}
@@ -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.Packer;
import com.reajason.javaweb.packer.Util;
import com.reajason.javaweb.packer.spec.PackerSchema;
import lombok.SneakyThrows;
/**
* @author ReaJason
* @since 2024/11/26
*/
public class DefineClassJspPacker implements Packer {
public class DefineClassJspPacker implements Packer<JspCustomPackerConfig> {
private final String template = Util.loadTemplateFromResource("/memshell-party/shell1.jsp");
private final String bypassTemplate = Util.loadTemplateFromResource("/memshell-party/shell2.jsp");
@Override
@SneakyThrows
public String pack(ClassPackerConfig config) {
public String pack(ClassPackerConfig<JspCustomPackerConfig> config) {
String injectorBytesBase64Str = config.getClassBytesBase64Str();
String injectorClassName = config.getClassName();
String template = this.template;
if (config.isByPassJavaModule()) {
template = bypassTemplate;
}
return template.replace("{{className}}", injectorClassName)
String content = template.replace("{{className}}", injectorClassName)
.replace("{{base64Str}}", injectorBytesBase64Str);
JspCustomPackerConfig customConfig = config.getCustomConfig();
if (customConfig != null && customConfig.isUnicode()) {
return JspUnicoder.encode(content, true);
}
return content;
}
}
@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;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2024/11/26
*/
public class JspPacker implements AggregatePacker {
}
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.Packer;
import com.reajason.javaweb.packer.Util;
import com.reajason.javaweb.packer.spec.PackerSchema;
import lombok.SneakyThrows;
/**
* @author ReaJason
* @since 2024/11/26
*/
public class JspxPacker implements Packer {
public class JspxPacker implements Packer<JspCustomPackerConfig> {
private final String jspxTemplate = Util.loadTemplateFromResource("/memshell-party/shell.jspx");
@Override
@SneakyThrows
public String pack(ClassPackerConfig config) {
return jspxTemplate.replace("{{className}}", config.getClassName())
public String pack(ClassPackerConfig<JspCustomPackerConfig> config) {
String content = jspxTemplate.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", config.getClassBytesBase64Str());
JspCustomPackerConfig customConfig = config.getCustomConfig();
if (customConfig != null && customConfig.isUnicode()) {
return JspUnicoder.encode(content, false);
}
return content;
}
}
@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;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2024/12/13
*/
public class JXPathPacker implements AggregatePacker {
public class JXPathPacker implements Packer {
@Override
public String pack(ClassPackerConfig classPackerConfig) {
return Packers.JXPathScriptEngine.getInstance().pack(classPackerConfig);
}
}
@@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.jxpath;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.Util;
import org.apache.commons.codec.binary.Base64;
import static com.reajason.javaweb.packer.spel.SpELSpringGzipJDK17Packer.assertClassNameValid;
@@ -18,6 +20,6 @@ public class JXPathSpringGzipJDK17Packer implements Packer {
String className = config.getClassName();
assertClassNameValid(className);
return template.replace("{{className}}", className)
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config));
.replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
}
}
@@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.jxpath;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.Util;
import org.apache.commons.codec.binary.Base64;
/**
* @author ReaJason
@@ -14,6 +16,6 @@ public class JXPathSpringGzipPacker implements Packer {
@Override
public String pack(ClassPackerConfig config) {
return template.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config));
.replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
}
}
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.ognl;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2024/12/14
*/
public class OGNLPacker implements AggregatePacker {
public class OGNLPacker implements Packer {
@Override
public String pack(ClassPackerConfig classPackerConfig) {
return Packers.OGNLScriptEngine.getInstance().pack(classPackerConfig);
}
}
@@ -3,7 +3,9 @@ package com.reajason.javaweb.packer.ognl;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.Util;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Base64;
import static com.reajason.javaweb.packer.spel.SpELSpringGzipJDK17Packer.assertClassNameValid;
@@ -20,6 +22,6 @@ public class OGNLSpringGzipJDK17Packer implements Packer {
String className = config.getClassName();
assertClassNameValid(className);
return template.replace("{{className}}", className)
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config));
.replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
}
}
@@ -3,7 +3,9 @@ package com.reajason.javaweb.packer.ognl;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.Util;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Base64;
/**
* @author ReaJason
@@ -16,6 +18,6 @@ public class OGNLSpringGzipPacker implements Packer {
@SneakyThrows
public String pack(ClassPackerConfig config) {
return template.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config));
.replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
}
}
@@ -1,10 +1,16 @@
package com.reajason.javaweb.packer.scriptengine;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2024/12/3
*/
public class ScriptEnginePacker implements AggregatePacker {
public class ScriptEnginePacker implements Packer {
@Override
public String pack(ClassPackerConfig classPackerConfig) {
return Packers.DefaultScriptEngine.getInstance().pack(classPackerConfig);
}
}
@@ -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;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2025/1/26
*/
public class SpELPacker implements AggregatePacker {
public class SpELPacker implements Packer {
@Override
public String pack(ClassPackerConfig classPackerConfig) {
return Packers.SpELScriptEngine.getInstance().pack(classPackerConfig);
}
}
@@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.spel;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.Util;
import org.apache.commons.codec.binary.Base64;
/**
* @author ReaJason
@@ -16,7 +18,7 @@ public class SpELSpringGzipJDK17Packer implements Packer {
String className = config.getClassName();
assertClassNameValid(className);
return template.replace("{{className}}", className)
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config));
.replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
}
public static void assertClassNameValid(String className) {
@@ -3,6 +3,8 @@ package com.reajason.javaweb.packer.spel;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.Util;
import org.apache.commons.codec.binary.Base64;
/**
@@ -19,6 +21,6 @@ public class SpELSpringGzipPacker implements Packer {
@Override
public String pack(ClassPackerConfig config) {
return template.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config));
.replace("{{base64Str}}", Base64.encodeBase64String(Util.gzipCompress(config.getClassBytes())));
}
}
@@ -1,10 +1,16 @@
package com.reajason.javaweb.packer.translet;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2025/11/19
*/
public class AbstractTransletPacker implements AggregatePacker {
public class AbstractTransletPacker implements Packer {
@Override
public String pack(ClassPackerConfig classPackerConfig) {
return Packers.JDKAbstractTransletPacker.getInstance().pack(classPackerConfig);
}
}
@@ -1,11 +1,16 @@
package com.reajason.javaweb.packer.xmldecoder;
import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2025/7/22
*/
public class XMLDecoderPacker implements AggregatePacker {
public class XMLDecoderPacker implements Packer {
@Override
public String pack(ClassPackerConfig classPackerConfig) {
return Packers.XMLDecoderScriptEngine.getInstance().pack(classPackerConfig);
}
}
@@ -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 { useMemo } from "react";
import { useEffect, useMemo } from "react";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { PackerCombobox } from "@/components/packer/packer-combobox";
import { PackerCustomConfigFields } from "@/components/packer/packer-custom-config-fields";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { FieldLabel, FieldSet } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Field, FieldLabel, FieldSet } from "@/components/ui/field";
import { Spinner } from "@/components/ui/spinner";
import {
findPackerEntry,
getPackerDefaultConfig,
getPackerSchemaFields,
normalizePackerCategories,
} from "@/lib/packer-schema";
import type { PackerConfig } from "@/types/memshell";
import type { MemShellFormSchema } from "@/types/schema";
@@ -28,25 +35,77 @@ export default function PackageConfigCard({
name: "server",
});
const options = useMemo(() => {
const filteredOptions = (packerConfig ?? []).filter((name) => {
if (!shellType || shellType === " ") {
return true;
const packingMethod = useWatch({
control: form.control,
name: "packingMethod",
});
const categories = useMemo(
() => normalizePackerCategories(packerConfig),
[packerConfig],
);
const filteredCategories = useMemo(() => {
return categories
.map((group) => ({
...group,
packers: group.packers.filter((packer) => {
if (packer.categoryAnchor) {
return false;
}
const name = packer.name;
if (!shellType || shellType === " ") {
return true;
}
if (shellType.startsWith("Agent")) {
return name.startsWith("Agent");
}
if ((server ?? "").startsWith("XXL")) {
return !name.startsWith("Agent");
}
return (
!name.startsWith("Agent") && !name.toLowerCase().startsWith("xxl")
);
}),
}))
.filter((group) => group.packers.length > 0);
}, [categories, shellType, server]);
const allOptionNames = useMemo(
() =>
filteredCategories.flatMap((group) =>
group.packers.map((packer) => packer.name),
),
[filteredCategories],
);
const selectedPackerEntry = useMemo(
() =>
findPackerEntry(filteredCategories, packingMethod) ??
findPackerEntry(categories, packingMethod),
[categories, filteredCategories, packingMethod],
);
const selectedSchemaFields = useMemo(
() => getPackerSchemaFields(selectedPackerEntry),
[selectedPackerEntry],
);
useEffect(() => {
if (allOptionNames.length > 0) {
const current = form.getValues("packingMethod");
if (!current || !allOptionNames.includes(current)) {
form.setValue("packingMethod", allOptionNames[0]);
}
if (shellType.startsWith("Agent")) {
return name.startsWith("Agent");
}
if (server.startsWith("XXL")) {
return !name.startsWith("Agent");
}
return !name.startsWith("Agent") && !name.toLowerCase().startsWith("xxl");
});
form.setValue("packingMethod", filteredOptions[0]);
return filteredOptions.map((name) => ({
name: t(name),
value: name,
}));
}, [packerConfig, shellType, server, t, form]);
}
}, [allOptionNames, form]);
useEffect(() => {
form.setValue(
"packerCustomConfig",
getPackerDefaultConfig(selectedPackerEntry) as any,
);
}, [form, selectedPackerEntry, packingMethod]);
return (
<Card className="w-full">
@@ -57,32 +116,30 @@ export default function PackageConfigCard({
</CardTitle>
</CardHeader>
<CardContent>
{options.length > 0 ? (
<Controller
control={form.control}
name="packingMethod"
render={({ field }) => (
<FieldSet>
<FieldLabel>{t("packerMethod")}</FieldLabel>
<RadioGroup
name={field.name}
value={field.value}
defaultValue={options[0].value}
onValueChange={field.onChange}
className="grid grid-cols-2 md:grid-cols-3"
>
{options.map(({ name, value }) => (
<div key={value} className="flex items-center space-x-3">
<RadioGroupItem value={value} id={value} />
<FieldLabel className="text-xs" htmlFor={value}>
{name}
</FieldLabel>
</div>
))}
</RadioGroup>
</FieldSet>
)}
/>
{allOptionNames.length > 0 ? (
<>
<Controller
control={form.control}
name="packingMethod"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("packerMethod")}</FieldLabel>
<PackerCombobox
categories={filteredCategories}
value={field.value}
onValueChange={field.onChange}
placeholder={t("selectPacker", {
defaultValue: "Select packer",
})}
/>
</Field>
)}
/>
<PackerCustomConfigFields
form={form}
fields={selectedSchemaFields}
/>
</>
) : (
<div className="flex items-center justify-center p-4 gap-4 h-50">
<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 { AgentResult } from "./agent";
import { JarResult } from "./jar-result";
import { MultiPackResult } from "./multi-packer";
export function ResultComponent({
packResult,
allPackResults,
packMethod,
generateResult,
}: Readonly<{
packResult: string | undefined;
allPackResults: Map<string, string> | undefined;
packMethod: string;
generateResult?: MemShellResult;
}>) {
const showCode = packMethod === "JSP";
const isAgent = packMethod.startsWith("Agent");
const isJar = packMethod.endsWith("Jar");
const { t } = useTranslation();
if (allPackResults) {
return (
<MultiPackResult
allPackResults={allPackResults}
packMethod={packMethod}
shellClassName={generateResult?.injectorClassName}
/>
);
}
if (isAgent) {
return (
<AgentResult
@@ -52,16 +39,16 @@ export function ResultComponent({
<CodeViewer
code={packResult ?? ""}
header={
<div className="flex items-center justify-between text-xs gap-2">
<div className="flex items-center justify-between text-sm gap-2">
<span>
{t("common:packerMethod")}{packMethod}
</span>
<span className="text-muted-foreground">({packResult?.length})</span>
</div>
}
wrapLongLines={!showCode}
showLineNumbers={showCode}
language={showCode ? "java" : "text"}
wrapLongLines={true}
showLineNumbers={false}
language={"text"}
height={350}
/>
);
+1 -4
View File
@@ -13,12 +13,10 @@ import { ResultComponent } from "./results/result-component";
export default function ShellResult({
packResult,
allPackResults,
packMethod,
generateResult,
}: Readonly<{
packResult: string | undefined;
allPackResults: Map<string, string> | undefined;
packMethod: string;
generateResult?: MemShellResult;
}>) {
@@ -26,7 +24,7 @@ export default function ShellResult({
if (!generateResult) {
return <QuickUsage />;
}
const height = 800;
const height = 600;
return (
<Tabs defaultValue="packResult">
<TabsList className="grid w-full grid-cols-3">
@@ -42,7 +40,6 @@ export default function ShellResult({
<BasicInfo generateResult={generateResult} />
<ResultComponent
packResult={packResult}
allPackResults={allPackResults}
packMethod={packMethod}
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 { useEffect, useState } from "react";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useEffect, useMemo } from "react";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { PackerCombobox } from "@/components/packer/packer-combobox";
import { PackerCustomConfigFields } from "@/components/packer/packer-custom-config-fields";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Field, FieldLabel, FieldSet } from "@/components/ui/field";
import {
findPackerEntry,
getPackerDefaultConfig,
getPackerSchemaFields,
normalizePackerCategories,
} from "@/lib/packer-schema";
import type { PackerConfig } from "@/types/memshell";
import type { ProbeShellFormSchema } from "@/types/schema";
type Option = {
name: string;
value: string;
};
export default function PackageConfigCard({
packerConfig,
form,
@@ -20,34 +22,73 @@ export default function PackageConfigCard({
packerConfig: PackerConfig | undefined;
form: UseFormReturn<ProbeShellFormSchema>;
}>) {
const [options, setOptions] = useState<Array<Option>>([]);
const { t } = useTranslation("common");
const packingMethod = useWatch({
control: form.control,
name: "packingMethod",
});
const categories = useMemo(
() => normalizePackerCategories(packerConfig),
[packerConfig],
);
const filteredCategories = useMemo(() => {
return categories
.map((category) => ({
...category,
packers: category.packers.filter((packer) => {
if (packer.categoryAnchor) {
return false;
}
const name = packer.name;
return (
!name.startsWith("Agent") &&
!name.toLowerCase().startsWith("xxl") &&
!name.toLowerCase().endsWith("jar")
);
}),
}))
.filter((category) => category.packers.length > 0);
}, [categories]);
const allOptionNames = useMemo(
() =>
filteredCategories.flatMap((category) =>
category.packers.map((packer) => packer.name),
),
[filteredCategories],
);
const selectedPackerEntry = useMemo(
() =>
findPackerEntry(filteredCategories, packingMethod) ??
findPackerEntry(categories, packingMethod),
[categories, filteredCategories, packingMethod],
);
const selectedSchemaFields = useMemo(
() => getPackerSchemaFields(selectedPackerEntry),
[selectedPackerEntry],
);
useEffect(() => {
const filteredOptions = (packerConfig ?? []).filter((name) => {
return (
!name.startsWith("Agent") &&
!name.toLowerCase().startsWith("xxl") &&
!name.toLowerCase().endsWith("jar")
);
});
const mappedOptions = filteredOptions.map((name) => {
return {
name: name,
value: name,
};
});
setOptions(mappedOptions);
const currentValue = form.getValues("packingMethod");
if (
filteredOptions.length > 0 &&
(!currentValue || !filteredOptions.includes(currentValue))
allOptionNames.length > 0 &&
(!currentValue ||
!allOptionNames.some((option) => option === currentValue))
) {
form.setValue("packingMethod", filteredOptions[0]);
form.setValue("packingMethod", allOptionNames[0]);
}
}, [form, packerConfig]);
}, [allOptionNames, form]);
useEffect(() => {
form.setValue(
"packerCustomConfig",
getPackerDefaultConfig(selectedPackerEntry) as any,
);
}, [form, selectedPackerEntry, packingMethod]);
return (
<Card className="w-full">
@@ -58,34 +99,30 @@ export default function PackageConfigCard({
</CardTitle>
</CardHeader>
<CardContent>
{options.length > 0 ? (
<Controller
control={form.control}
name="packingMethod"
render={({ field }) => (
<div className="space-y-3">
<FieldLabel>{t("packerMethod")}</FieldLabel>
<div>
<RadioGroup
onValueChange={field.onChange}
{allOptionNames.length > 0 ? (
<>
<Controller
control={form.control}
name="packingMethod"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("packerMethod")}</FieldLabel>
<PackerCombobox
categories={filteredCategories}
value={field.value}
className="grid grid-cols-2 md:grid-cols-3"
>
{options.map(({ name, value }) => (
<div key={value} className="flex items-center space-x-3">
<div>
<RadioGroupItem value={value} id={value} />
</div>
<FieldLabel className="text-xs" htmlFor={value}>
{name}
</FieldLabel>
</div>
))}
</RadioGroup>
</div>
</div>
)}
/>
onValueChange={field.onChange}
placeholder={t("selectPacker", {
defaultValue: "Select packer",
})}
/>
</Field>
)}
/>
<PackerCustomConfigFields
form={form}
fields={selectedSchemaFields}
/>
</>
) : (
<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" />
+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 type { ProbeShellResult } from "@/types/probeshell";
import CodeViewer from "../code-viewer";
import { MultiPackResult } from "../memshell/results/multi-packer";
import { BasicInfo } from "./basic-info";
export default function ShellResult({
packResult,
allPackResults,
packMethod,
generateResult,
}: Readonly<{
packResult: string | undefined;
allPackResults: Map<string, string> | undefined;
packMethod: string;
generateResult?: ProbeShellResult;
}>) {
@@ -21,7 +18,6 @@ export default function ShellResult({
if (!generateResult) {
return <QuickUsage />;
}
const showCode = packMethod === "JSP";
const height = 600;
return (
<Tabs defaultValue="packResult">
@@ -32,14 +28,6 @@ export default function ShellResult({
</TabsList>
<TabsContent value="packResult" className="space-y-2">
<BasicInfo generateResult={generateResult} />
{allPackResults && (
<MultiPackResult
allPackResults={allPackResults}
shellClassName={generateResult?.shellClassName}
packMethod={packMethod}
height={height}
/>
)}
{packResult && (
<CodeViewer
code={packResult}
@@ -53,9 +41,9 @@ export default function ShellResult({
</span>
</div>
}
wrapLongLines={!showCode}
showLineNumbers={showCode}
language={showCode ? "java" : "text"}
wrapLongLines={true}
showLineNumbers={false}
language={"text"}
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)",
"placeholders.input": "Please input",
"placeholders.select": "Please select",
"packerParams": "Package Params",
"ProbeShellGenerator": "ProbeShellGenerator",
"quickUsage.title": "Quick Usage",
"server": "Server",
@@ -49,5 +50,8 @@
"commandTemplate": "Command Template",
"commandTemplate.placeholder": "e.g., sh -c \"{command}\" 2>&1",
"commandTemplate.description": "Use {command} as placeholder",
"unicodeEncoded.desc": "Enable Unicode encoding",
"urlEncoded.desc": "Enable URL encoding",
"gzipCompressed.desc": "Enable GZIP compression",
"targetJdkVersion": "JRE Version"
}
+4
View File
@@ -26,6 +26,7 @@
"paramName.description": "支持请求参数 param 或请求头 header 传值",
"placeholders.input": "请输入",
"placeholders.select": "请选择",
"packerParams": "打包参数",
"ProbeShellGenerator": "探测马生成器",
"quickUsage.title": "快速使用",
"server": "服务类型",
@@ -49,5 +50,8 @@
"commandTemplate": "命令模板",
"commandTemplate.placeholder": "例如:sh -c \"{command}\" 2>&1",
"commandTemplate.description": "使用 {command} 作为占位符",
"unicodeEncoded.desc": "启用 Unicode 编码",
"urlEncoded.desc": "启用 URL 编码",
"gzipCompressed.desc": "启用 GZIP 压缩",
"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 { env } from "@/config";
import { siteConfig } from "@/lib/config";
import { baseOptions } from "@/lib/layout.shared";
import {
type APIErrorResponse,
type MainConfig,
@@ -26,7 +27,6 @@ import {
useYupValidationResolver,
} from "@/types/schema";
import { transformToPostData } from "@/utils/transformer";
import { baseOptions } from "../lib/layout.shared";
const homeLayoutOptions = baseOptions();
@@ -49,6 +49,7 @@ const defaultValues: MemShellFormSchema = {
headerValue: "",
injectorClassName: "",
packingMethod: "",
packerCustomConfig: {},
shrink: true,
staticInitialize: true,
shellClassBase64: "",
@@ -95,9 +96,6 @@ export default function MemShellPage() {
});
const [packResult, setPackResult] = useState<string | undefined>();
const [allPackResults, setAllPackResults] = useState<
Map<string, string> | undefined
>();
const [generateResult, setGenerateResult] = useState<MemShellResult>();
const [packMethod, setPackMethod] = useState<string>("");
const submitLockRef = useRef(false);
@@ -121,7 +119,6 @@ export default function MemShellPage() {
const result = (await response.json()) as MemShellGenerateResponse;
setGenerateResult(result.memShellResult);
setPackResult(result.packResult);
setAllPackResults(result.allPackResults);
setPackMethod(data.packingMethod);
toast.success(t("toast.generateSuccess"));
} catch (error) {
@@ -179,7 +176,6 @@ export default function MemShellPage() {
packMethod={packMethod}
generateResult={generateResult}
packResult={packResult}
allPackResults={allPackResults}
/>
</div>
</form>
+1 -5
View File
@@ -57,15 +57,13 @@ export default function ProbeShellGenerator() {
reqParamName: "",
seconds: 5,
sleepServer: "Tomcat",
packerCustomConfig: {},
shrink: true,
staticInitialize: true,
},
});
const [packResult, setPackResult] = useState<string | undefined>();
const [allPackResults, setAllPackResults] = useState<
Map<string, string> | undefined
>();
const [generateResult, setGenerateResult] = useState<ProbeShellResult>();
const [packMethod, setPackMethod] = useState<string>("");
const submitLockRef = useRef(false);
@@ -93,7 +91,6 @@ export default function ProbeShellGenerator() {
const result = (await response.json()) as ProbeShellGenerateResponse;
setGenerateResult(result.probeShellResult);
setPackResult(result.packResult);
setAllPackResults(result.allPackResults);
setPackMethod(data.packingMethod);
toast.success(t("toast.generateSuccess"));
} catch (error) {
@@ -132,7 +129,6 @@ export default function ProbeShellGenerator() {
packMethod={packMethod}
generateResult={generateResult}
packResult={packResult}
allPackResults={allPackResults}
/>
</div>
</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 {
memShellResult: MemShellResult;
-1
View File
@@ -61,7 +61,6 @@ export interface PayloadFormValues {
export interface ProbeShellGenerateResponse {
probeShellResult: ProbeShellResult;
packResult?: string;
allPackResults?: Map<string, string>;
}
export interface ProbeShellResult {
+2
View File
@@ -26,6 +26,7 @@ export const memShellFormSchema = yup.object({
headerValue: yup.string().optional(),
injectorClassName: yup.string().optional(),
packingMethod: yup.string().required().min(1),
packerCustomConfig: yup.object().optional(),
shrink: yup.boolean().optional(),
lambdaSuffix: yup.boolean().optional(),
probe: yup.boolean().optional(),
@@ -164,6 +165,7 @@ export const probeShellFormSchema = yup.object().shape({
seconds: yup.number().optional(),
sleepServer: yup.string().optional(),
packingMethod: yup.string().required(),
packerCustomConfig: yup.object().optional(),
targetJdkVersion: yup.string().optional(),
debug: yup.boolean().optional(),
byPassJavaModule: yup.boolean().optional(),
+8 -2
View File
@@ -43,7 +43,10 @@ export function transformToPostData(formValue: MemShellFormSchema) {
shellConfig,
shellToolConfig,
injectorConfig,
packer: formValue.packingMethod,
packerSpec: {
name: formValue.packingMethod,
config: formValue.packerCustomConfig ?? {},
},
};
}
@@ -70,7 +73,10 @@ export function transformToProbePostData(formValue: ProbeShellFormSchema) {
return {
probeConfig,
probeContentConfig,
packer: formValue.packingMethod,
packerSpec: {
name: formValue.packingMethod,
config: formValue.packerCustomConfig ?? {},
},
};
}
+17 -4
View File
@@ -5,7 +5,7 @@
"": {
"name": "fumadocs",
"dependencies": {
"@base-ui/react": "^1.1.0",
"@base-ui/react": "^1.2.0",
"@hookform/resolvers": "^5.2.2",
"@orama/orama": "^3.1.18",
"@orama/stopwords": "^3.1.18",
@@ -14,6 +14,7 @@
"@tanstack/react-query": "^5.90.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"framer-motion": "^12.33.0",
"fumadocs-core": "^16.5.1",
"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/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=="],
@@ -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=="],
"@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=="],
@@ -544,6 +545,8 @@
"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=="],
"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=="],
"babel-plugin-macros/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"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=="],
@@ -1312,10 +1317,14 @@
"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/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-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-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/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-handler/bytes": ["[email protected]", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
+2 -1
View File
@@ -12,7 +12,7 @@
"format": "biome format --write"
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"@base-ui/react": "^1.2.0",
"@hookform/resolvers": "^5.2.2",
"@orama/orama": "^3.1.18",
"@orama/stopwords": "^3.1.18",
@@ -21,6 +21,7 @@
"@tanstack/react-query": "^5.90.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"framer-motion": "^12.33.0",
"fumadocs-core": "^16.5.1",
"fumadocs-mdx": "14.2.6",