From 210c598e8a27ee87162bfe7f7ad1fa34bce6ed39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Wed, 30 Oct 2024 14:54:51 +0100 Subject: [PATCH] feat: version 2.0 - paper support - outgoing packet injection - convert to java - move to multi module project --- .gitignore | 2 + README.md | 55 +++++--- api/build.gradle.kts | 44 +++++++ .../mcbrawls/inject/api/InjectPlatform.java | 12 ++ .../net/mcbrawls/inject/api/Injector.java | 71 +++++++++++ .../mcbrawls/inject/api/InjectorContext.java | 11 ++ .../mcbrawls/inject/api/PacketDirection.java | 9 ++ .../mcbrawls/inject/api/http/HttpByteBuf.java | 63 +++++++++ .../inject/api/http/HttpInjector.java | 61 +++++++++ .../mcbrawls/inject/api/http/HttpRequest.java | 64 ++++++++++ build.gradle.kts | 120 +----------------- fabric/build.gradle.kts | 80 ++++++++++++ .../mcbrawls/inject/fabric/InjectFabric.java | 28 ++++ .../fabric}/mixin/ClientConnectionMixin.java | 8 +- fabric/src/main/resources/fabric.mod.json | 28 ++++ .../src}/main/resources/inject.mixins.json | 5 +- .../inject/fabric/test/InjectTestMod.java | 32 +++++ .../src}/test/resources/fabric.mod.json | 9 +- gradle.properties | 10 +- paper/build.gradle.kts | 94 ++++++++++++++ .../paper/example/InjectPaperExample.java | 31 +++++ paper/src/example/resources/plugin.yml | 4 + .../mcbrawls/inject/paper/InjectPaper.java | 36 ++++++ settings.gradle.kts | 8 ++ .../kotlin/net/mcbrawls/inject/InjectMod.kt | 13 -- .../kotlin/net/mcbrawls/inject/Injector.kt | 48 ------- .../net/mcbrawls/inject/InjectorContext.kt | 6 - .../net/mcbrawls/inject/http/HttpByteBuf.kt | 47 ------- .../net/mcbrawls/inject/http/HttpInjector.kt | 64 ---------- .../net/mcbrawls/inject/http/HttpRequest.kt | 64 ---------- src/main/resources/fabric.mod.json | 27 ---- .../net/mcbrawls/inject/test/InjectTestMod.kt | 21 --- 32 files changed, 732 insertions(+), 443 deletions(-) create mode 100644 api/build.gradle.kts create mode 100644 api/src/main/java/net/mcbrawls/inject/api/InjectPlatform.java create mode 100644 api/src/main/java/net/mcbrawls/inject/api/Injector.java create mode 100644 api/src/main/java/net/mcbrawls/inject/api/InjectorContext.java create mode 100644 api/src/main/java/net/mcbrawls/inject/api/PacketDirection.java create mode 100644 api/src/main/java/net/mcbrawls/inject/api/http/HttpByteBuf.java create mode 100644 api/src/main/java/net/mcbrawls/inject/api/http/HttpInjector.java create mode 100644 api/src/main/java/net/mcbrawls/inject/api/http/HttpRequest.java create mode 100644 fabric/build.gradle.kts create mode 100644 fabric/src/main/java/net/mcbrawls/inject/fabric/InjectFabric.java rename {src/main/java/net/mcbrawls/inject => fabric/src/main/java/net/mcbrawls/inject/fabric}/mixin/ClientConnectionMixin.java (75%) create mode 100644 fabric/src/main/resources/fabric.mod.json rename {src => fabric/src}/main/resources/inject.mixins.json (68%) create mode 100644 fabric/src/test/java/net/mcbrawls/inject/fabric/test/InjectTestMod.java rename {src => fabric/src}/test/resources/fabric.mod.json (53%) create mode 100644 paper/build.gradle.kts create mode 100644 paper/src/example/java/net/mcbrawls/inject/paper/example/InjectPaperExample.java create mode 100644 paper/src/example/resources/plugin.yml create mode 100644 paper/src/main/java/net/mcbrawls/inject/paper/InjectPaper.java delete mode 100644 src/main/kotlin/net/mcbrawls/inject/InjectMod.kt delete mode 100644 src/main/kotlin/net/mcbrawls/inject/Injector.kt delete mode 100644 src/main/kotlin/net/mcbrawls/inject/InjectorContext.kt delete mode 100644 src/main/kotlin/net/mcbrawls/inject/http/HttpByteBuf.kt delete mode 100644 src/main/kotlin/net/mcbrawls/inject/http/HttpInjector.kt delete mode 100644 src/main/kotlin/net/mcbrawls/inject/http/HttpRequest.kt delete mode 100644 src/main/resources/fabric.mod.json delete mode 100644 src/test/kotlin/net/mcbrawls/inject/test/InjectTestMod.kt diff --git a/.gitignore b/.gitignore index d886b0a..87014de 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ hs_err_*.log replay_*.log *.hprof *.jfr + +**/run*/ \ No newline at end of file diff --git a/README.md b/README.md index 218817b..966a630 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,40 @@ Netty easier. This uses the `HttpInjector` class to respond to HTTP requests to the Minecraft server. -```kt -object MyEpicHttpInjector : HttpInjector() { - override fun isRelevant(ctx: InjectorContext, request: HttpRequest) = true - override fun intercept(ctx: ChannelHandlerContext, request: HttpRequest) = ctx.buildHttpBuffer { - writeStatusLine("1.1", 200, "OK") - writeText("Hello, from Minecraft!") +```java +class MyEpicHttpInjector extends HttpInjector { + @Override + public boolean isRelevant(InjectorContext ctx, HttpRequest request) { + return true; + } + + @Override + public HttpByteBuf intercept(ChannelHandlerContext ctx, HttpRequest request) { + HttpByteBuf buf = HttpByteBuf.httpBuf(ctx); + buf.writeStatusLine("1.1", 200, "OK"); + buf.writeText("Hello, from Minecraft!"); + return buf; + } +} +``` + +## Registration +For Fabric, use the `InjectFabric` class: +```java +public class MyMod implements ModInitializer { + @Override + public void onInitialize() { + InjectFabric.INSTANCE.registerInjector(new MyEpicHttpInjector()); } } +``` -object MyMod : DedicatedServerModInitializer { - override fun onInitializeServer() { - MyEpicHttpInjector.register() +For Paper, use the `InjectPaper` class: +```java +public class MyPlugin extends JavaPlugin { + @Override + public void onEnable() { + InjectPaper.INSTANCE.registerInjector(new MyEpicHttpInjector()); } } ``` @@ -32,19 +54,20 @@ Hello, from Minecraft! ## Usage Add the andante repo to gradle: -```groovy +```kt repositories { - maven { - name = "Andante" - url = "https://maven.andante.dev/releases/" - } + maven("https://maven.andante.dev/releases/") } ``` Add the dependency: -```groovy +```kt dependencies { - include modImplementation("net.mcbrawls:inject:VERSION") + // Fabric: + include(modImplementation("net.mcbrawls.inject:fabric:VERSION")!!) + + // Paper: + implementation("net.mcbrawls.inject:paper:VERSION") } ``` diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 0000000..e224ec8 --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + java + `maven-publish` +} + +fun prop(name: String) = project.rootProject.property(name) as String + +group = prop("group") +version = prop("version") + +repositories { + mavenCentral() +} + +dependencies { + compileOnly("io.netty:netty-all:4.1.97.Final") + compileOnly("org.slf4j:slf4j-api:1.7.30") +} + +publishing { + publications { + create("mavenJava") { + artifactId = project.name + from(components["java"]) + } + } + + repositories { + runCatching { // getenv throws if variable doesn't exist + val mavenUser = System.getenv("MAVEN_USERNAME_ANDANTE") + val mavenPass = System.getenv("MAVEN_PASSWORD_ANDANTE") + + maven { + name = "Andante" + url = uri("https://maven.andante.dev/releases/") + + credentials { + username = mavenUser + password = mavenPass + } + } + } + } +} \ No newline at end of file diff --git a/api/src/main/java/net/mcbrawls/inject/api/InjectPlatform.java b/api/src/main/java/net/mcbrawls/inject/api/InjectPlatform.java new file mode 100644 index 0000000..bf0e67e --- /dev/null +++ b/api/src/main/java/net/mcbrawls/inject/api/InjectPlatform.java @@ -0,0 +1,12 @@ +package net.mcbrawls.inject.api; + +/** + * A platform for Inject, like Fabric or Paper. + */ +public interface InjectPlatform { + /** + * Registers the injector. + * @param injector The injector. + */ + void registerInjector(Injector injector); +} \ No newline at end of file diff --git a/api/src/main/java/net/mcbrawls/inject/api/Injector.java b/api/src/main/java/net/mcbrawls/inject/api/Injector.java new file mode 100644 index 0000000..d95cbb6 --- /dev/null +++ b/api/src/main/java/net/mcbrawls/inject/api/Injector.java @@ -0,0 +1,71 @@ +package net.mcbrawls.inject.api; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; + +/** + * A Netty injector. + */ +@Sharable +public abstract class Injector extends ChannelDuplexHandler { + /** + * Predicate for matching if this injector is relevant + * to the given context. + * @param ctx The context. + * @param direction The direction in which this packet goes. + * @return true if it's relevant and should be handled by this injector, false if not. + */ + public boolean isRelevant(InjectorContext ctx, PacketDirection direction) { + return false; + } + + /** + * Gets executed on every channel read. + * @param ctx The context. + * @param buf The read byte buffer. + * @return true if the channel read was handled and should not get + * delegated to the superclass, false if it should be delegated to the superclass. + */ + public boolean onRead(ChannelHandlerContext ctx, ByteBuf buf) throws Exception { + return false; + } + + /** + * Gets executed on every channel write. + * @param ctx The context. + * @param buf The written byte buffer. + * @param promise The mutable channel future. + * @return true if the channel write was handled and should not get + * delegated to the superclass, false if it should be delegated to the superclass. + */ + public boolean onWrite(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise) { + return false; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ByteBuf buf = (ByteBuf) msg; + InjectorContext context = new InjectorContext(ctx.pipeline(), buf); + if (!isRelevant(context, PacketDirection.INBOUND)) { + super.channelRead(ctx, msg); + return; + } + + if (!onRead(ctx, buf)) super.channelRead(ctx, msg); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + ByteBuf buf = (ByteBuf) msg; + InjectorContext context = new InjectorContext(ctx.pipeline(), buf); + if (!isRelevant(context, PacketDirection.OUTBOUND)) { + super.write(ctx, msg, promise); + return; + } + + if (!onWrite(ctx, buf, promise)) super.write(ctx, msg, promise); + } +} \ No newline at end of file diff --git a/api/src/main/java/net/mcbrawls/inject/api/InjectorContext.java b/api/src/main/java/net/mcbrawls/inject/api/InjectorContext.java new file mode 100644 index 0000000..2db8cf1 --- /dev/null +++ b/api/src/main/java/net/mcbrawls/inject/api/InjectorContext.java @@ -0,0 +1,11 @@ +package net.mcbrawls.inject.api; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelPipeline; + +/** + * Context of an injector. + * @param pipeline The channel pipeline. + * @param message The read byte buffer. + */ +public record InjectorContext(ChannelPipeline pipeline, ByteBuf message) {} \ No newline at end of file diff --git a/api/src/main/java/net/mcbrawls/inject/api/PacketDirection.java b/api/src/main/java/net/mcbrawls/inject/api/PacketDirection.java new file mode 100644 index 0000000..1d5cb3a --- /dev/null +++ b/api/src/main/java/net/mcbrawls/inject/api/PacketDirection.java @@ -0,0 +1,9 @@ +package net.mcbrawls.inject.api; + +/** + * The direction in which packets can flow. + */ +public enum PacketDirection { + INBOUND, + OUTBOUND +} \ No newline at end of file diff --git a/api/src/main/java/net/mcbrawls/inject/api/http/HttpByteBuf.java b/api/src/main/java/net/mcbrawls/inject/api/http/HttpByteBuf.java new file mode 100644 index 0000000..88e5174 --- /dev/null +++ b/api/src/main/java/net/mcbrawls/inject/api/http/HttpByteBuf.java @@ -0,0 +1,63 @@ +package net.mcbrawls.inject.api.http; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import java.nio.charset.StandardCharsets; + +/** + * A custom ByteBuf used for sending HTTP responses using HttpInterceptor. + */ +public class HttpByteBuf { + private final ByteBuf inner; + + public HttpByteBuf(ByteBuf inner) { + this.inner = inner; + } + + /** + * Writes the status line to the buffer. + * Format is as following: + * HTTP/{protocolVersion} {statusCode} {statusMessage} \n + */ + public void writeStatusLine(String protocolVersion, int statusCode, String statusMessage) { + inner.writeCharSequence("HTTP/" + protocolVersion + " " + statusCode + " " + statusMessage + "\n", StandardCharsets.US_ASCII); + } + + /** + * Writes an HTTP header to the buffer. + */ + public void writeHeader(String header, String value) { + inner.writeCharSequence(header + ": " + value + "\n", StandardCharsets.US_ASCII); + } + + /** + * Writes text to the buffer. + */ + public void writeText(String text) { + inner.writeCharSequence("\n" + text, StandardCharsets.US_ASCII); + } + + /** + * Writes a byte array to the buffer. + */ + public void writeBytes(byte[] bytes) { + inner.writeCharSequence("\n", StandardCharsets.US_ASCII); + inner.writeBytes(bytes); + } + + /** + * @return the inner byte buf + */ + public ByteBuf inner() { + return inner; + } + + /** + * Creates a new {@link HttpByteBuf} based off a {@link ChannelHandlerContext}'s allocator. + * @param ctx the context + * @return the HTTP byte buf + */ + public static HttpByteBuf httpBuf(ChannelHandlerContext ctx) { + return new HttpByteBuf(ctx.alloc().buffer()); + } +} \ No newline at end of file diff --git a/api/src/main/java/net/mcbrawls/inject/api/http/HttpInjector.java b/api/src/main/java/net/mcbrawls/inject/api/http/HttpInjector.java new file mode 100644 index 0000000..7a35726 --- /dev/null +++ b/api/src/main/java/net/mcbrawls/inject/api/http/HttpInjector.java @@ -0,0 +1,61 @@ +package net.mcbrawls.inject.api.http; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import net.mcbrawls.inject.api.Injector; +import net.mcbrawls.inject.api.InjectorContext; +import net.mcbrawls.inject.api.PacketDirection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Sharable +public abstract class HttpInjector extends Injector { + private final Logger logger = LoggerFactory.getLogger("HttpInjector " + hashCode()); + + public abstract boolean isRelevant(InjectorContext ctx, HttpRequest request); + + public abstract HttpByteBuf intercept(ChannelHandlerContext ctx, HttpRequest request); + + @Override + public boolean isRelevant(InjectorContext ctx, PacketDirection direction) { + return isRequestGet(ctx.message()); + } + + @Override + public boolean onRead(ChannelHandlerContext ctx, ByteBuf buf) throws Exception { + HttpRequest request = HttpRequest.parse(buf); + HttpByteBuf response = intercept(ctx, request); + + ctx.writeAndFlush(response.inner()) + .addListener(ChannelFutureListener.CLOSE) + .addListener(future -> { + Throwable cause = future.cause(); + if (cause == null) { + logger.debug("Write successful"); + } else { + logger.error("Write failed: {}", String.valueOf(cause)); + //noinspection CallToPrintStackTrace + cause.printStackTrace(); + } + }); + + return true; + } + + private boolean isRequestMethod(ByteBuf buf, @SuppressWarnings("SameParameterValue") String method) { + for (int i = 0; i < method.length(); i++) { + char charAt = method.charAt(i); + int byteAt = buf.getUnsignedByte(i); + if (charAt != byteAt) { + return false; + } + } + return true; + } + + private boolean isRequestGet(ByteBuf buf) { + return isRequestMethod(buf, "GET "); + } +} \ No newline at end of file diff --git a/api/src/main/java/net/mcbrawls/inject/api/http/HttpRequest.java b/api/src/main/java/net/mcbrawls/inject/api/http/HttpRequest.java new file mode 100644 index 0000000..054e83c --- /dev/null +++ b/api/src/main/java/net/mcbrawls/inject/api/http/HttpRequest.java @@ -0,0 +1,64 @@ +package net.mcbrawls.inject.api.http; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; + +public class HttpRequest { + private final String request; + private final Map headers; + private final String[] requestParts; + + public HttpRequest(String request, Map headers) { + this.request = request; + this.headers = headers; + this.requestParts = request.split(" "); + } + + public String getRequestMethod() { + return requestParts[0]; + } + + public String getRequestURI() { + return requestParts[1]; + } + + public String getProtocolVersion() { + return requestParts[2]; + } + + public String getHeader(String header) { + return headers.get(header); + } + + public static HttpRequest parse(ByteBuf buf) throws Exception { + try (ByteBufInputStream stream = new ByteBufInputStream(buf)) { + return parse(stream); + } + } + + private static HttpRequest parse(InputStream stream) throws Exception { + try (InputStreamReader reader = new InputStreamReader(stream); + BufferedReader bufferedReader = new BufferedReader(reader)) { + String request = bufferedReader.readLine(); + Map headers = readHeaders(bufferedReader); + return new HttpRequest(request, headers); + } + } + + private static Map readHeaders(BufferedReader reader) throws Exception { + Map headers = new HashMap<>(); + String header = reader.readLine(); + while (header != null && !header.isEmpty()) { + int split = header.indexOf(':'); + headers.put(header.substring(0, split), header.substring(split + 1).trim()); + header = reader.readLine(); + } + return headers; + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f1c0db6..0b09012 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,118 +1,2 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - kotlin("jvm") version "2.0.21" - id("fabric-loom") version "1.7.1" - id("maven-publish") -} - -version = project.property("mod_version") as String -group = project.property("maven_group") as String - -val modId = project.property("mod_id") as String - -base { - archivesName.set(modId) -} - -val targetJavaVersion = 21 -java { - toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) - withSourcesJar() -} - -repositories { -} - -dependencies { - minecraft("com.mojang:minecraft:${project.property("minecraft_version")}") - mappings("net.fabricmc:yarn:${project.property("yarn_mappings")}:v2") - modImplementation("net.fabricmc:fabric-loader:${project.property("loader_version")}") - modImplementation("net.fabricmc:fabric-language-kotlin:${project.property("kotlin_loader_version")}") - - modImplementation("net.fabricmc.fabric-api:fabric-api:${project.property("fabric_version")}") - - testImplementation(sourceSets.main.get().output) -} - -sourceSets { - val main = main.get() - - test { - compileClasspath += main.compileClasspath - runtimeClasspath += main.runtimeClasspath - } -} - -loom { - splitEnvironmentSourceSets() - - runs { - create("test") { - inherit(runs["server"]) - name("Minecraft Server (Test)") - source(sourceSets.test.get()) - ideConfigGenerated(true) - } - } - - mods { - register(modId) { - sourceSet("main") - sourceSet("client") - } - } -} - -tasks.processResources { - inputs.property("version", project.version) - filteringCharset = "UTF-8" - - filesMatching("fabric.mod.json") { - expand( - "version" to project.version, - ) - } -} - -tasks.withType().configureEach { - options.encoding = "UTF-8" - options.release.set(targetJavaVersion) -} - -tasks.withType().configureEach { - compilerOptions.jvmTarget.set(JvmTarget.fromTarget(targetJavaVersion.toString())) -} - -tasks.jar { - from("LICENSE") { - rename { "${it}_${project.base.archivesName}" } - } -} - -publishing { - publications { - create("mavenJava") { - artifactId = modId - from(components["java"]) - } - } - - repositories { - val mavenUser = runCatching { System.getenv("MAVEN_USERNAME_ANDANTE") }.getOrNull() - val mavenPass = runCatching { System.getenv("MAVEN_PASSWORD_ANDANTE") }.getOrNull() - - if (mavenUser == null || mavenPass == null) return@repositories - - maven { - name = "Andante" - url = uri("https://maven.andante.dev/releases/") - - credentials { - username = mavenUser - password = mavenPass - } - } - } -} +version = project.property("version") as String +group = project.property("group") as String diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts new file mode 100644 index 0000000..9a609f0 --- /dev/null +++ b/fabric/build.gradle.kts @@ -0,0 +1,80 @@ +plugins { + id("fabric-loom") version "1.8-SNAPSHOT" + id("maven-publish") + java +} + +fun prop(name: String) = project.rootProject.property(name) as String + +version = prop("version") +group = prop("group") + +base { + archivesName.set(project.name) +} + +dependencies { + minecraft("com.mojang:minecraft:${prop("minecraft_version")}") + mappings("net.fabricmc:yarn:${prop("yarn_mappings")}:v2") + modImplementation("net.fabricmc:fabric-loader:${prop("loader_version")}") + + implementation(project(":api")) +} + +tasks.processResources { + inputs.property("version", project.version) + + filesMatching("fabric.mod.json") { + expand("version" to project.version) + } +} + +tasks.withType { + options.release.set(21) +} + +java { + withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +loom { + splitEnvironmentSourceSets() + + runs { + create("test") { + inherit(runs["server"]) + name("Minecraft Server (Test)") + source(sourceSets.test.get()) + ideConfigGenerated(true) + } + } +} + +publishing { + publications { + create("mavenJava") { + artifactId = project.name + from(components["java"]) + } + } + + repositories { + runCatching { // getenv throws if variable doesn't exist + val mavenUser = System.getenv("MAVEN_USERNAME_ANDANTE") + val mavenPass = System.getenv("MAVEN_PASSWORD_ANDANTE") + + maven { + name = "Andante" + url = uri("https://maven.andante.dev/releases/") + + credentials { + username = mavenUser + password = mavenPass + } + } + } + } +} \ No newline at end of file diff --git a/fabric/src/main/java/net/mcbrawls/inject/fabric/InjectFabric.java b/fabric/src/main/java/net/mcbrawls/inject/fabric/InjectFabric.java new file mode 100644 index 0000000..c90d5cf --- /dev/null +++ b/fabric/src/main/java/net/mcbrawls/inject/fabric/InjectFabric.java @@ -0,0 +1,28 @@ +package net.mcbrawls.inject.fabric; + +import net.fabricmc.api.ModInitializer; +import net.mcbrawls.inject.api.InjectPlatform; +import net.mcbrawls.inject.api.Injector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class InjectFabric implements ModInitializer, InjectPlatform { + private static final Logger LOGGER = LoggerFactory.getLogger("inject"); + public static InjectFabric INSTANCE; + public final List injectors = new ArrayList<>(); + + @Override + public void onInitialize() { + INSTANCE = this; + + LOGGER.info("Inject initialising"); + } + + @Override + public void registerInjector(Injector injector) { + injectors.add(injector); + } +} diff --git a/src/main/java/net/mcbrawls/inject/mixin/ClientConnectionMixin.java b/fabric/src/main/java/net/mcbrawls/inject/fabric/mixin/ClientConnectionMixin.java similarity index 75% rename from src/main/java/net/mcbrawls/inject/mixin/ClientConnectionMixin.java rename to fabric/src/main/java/net/mcbrawls/inject/fabric/mixin/ClientConnectionMixin.java index 52577db..9be566f 100644 --- a/src/main/java/net/mcbrawls/inject/mixin/ClientConnectionMixin.java +++ b/fabric/src/main/java/net/mcbrawls/inject/fabric/mixin/ClientConnectionMixin.java @@ -1,7 +1,7 @@ -package net.mcbrawls.inject.mixin; +package net.mcbrawls.inject.fabric.mixin; import io.netty.channel.ChannelPipeline; -import net.mcbrawls.inject.InjectMod; +import net.mcbrawls.inject.fabric.InjectFabric; import net.minecraft.network.ClientConnection; import net.minecraft.network.NetworkSide; import net.minecraft.network.handler.PacketSizeLogger; @@ -14,8 +14,6 @@ public class ClientConnectionMixin { @Inject(method = "addHandlers", at = @At("TAIL")) private static void addHandlers(ChannelPipeline pipeline, NetworkSide side, boolean local, PacketSizeLogger packetSizeLogger, CallbackInfo ci) { - var channel = pipeline.channel(); - - InjectMod.INSTANCE.getInjectors$inject().forEach(injector -> channel.pipeline().addFirst(injector)); + InjectFabric.INSTANCE.injectors.forEach(pipeline::addFirst); } } diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..64033b7 --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "id": "inject", + "version": "${version}", + "name": "inject", + "description": "A library for making injecting into Netty easier!", + "authors": [ + "radstevee", + "andantet" + ], + "contact": { + "sources": "https://github.com/mcbrawls/inject" + }, + "license": "MIT", + "icon": "assets/inject/icon.png", + "environment": "*", + "entrypoints": { + "main": ["net.mcbrawls.inject.fabric.InjectFabric"] + }, + "mixins": [ + "inject.mixins.json" + ], + "depends": { + "fabricloader": ">=0.16.7", + "minecraft": "~1.21.1", + "java": ">=21" + } +} \ No newline at end of file diff --git a/src/main/resources/inject.mixins.json b/fabric/src/main/resources/inject.mixins.json similarity index 68% rename from src/main/resources/inject.mixins.json rename to fabric/src/main/resources/inject.mixins.json index 9ac9e75..090662e 100644 --- a/src/main/resources/inject.mixins.json +++ b/fabric/src/main/resources/inject.mixins.json @@ -1,7 +1,6 @@ { "required": true, - "minVersion": "0.8", - "package": "net.mcbrawls.inject.mixin", + "package": "net.mcbrawls.inject.fabric.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ "ClientConnectionMixin" @@ -9,4 +8,4 @@ "injectors": { "defaultRequire": 1 } -} +} \ No newline at end of file diff --git a/fabric/src/test/java/net/mcbrawls/inject/fabric/test/InjectTestMod.java b/fabric/src/test/java/net/mcbrawls/inject/fabric/test/InjectTestMod.java new file mode 100644 index 0000000..53071f7 --- /dev/null +++ b/fabric/src/test/java/net/mcbrawls/inject/fabric/test/InjectTestMod.java @@ -0,0 +1,32 @@ +package net.mcbrawls.inject.fabric.test; + +import io.netty.channel.ChannelHandlerContext; +import net.fabricmc.api.DedicatedServerModInitializer; +import net.fabricmc.api.ModInitializer; +import net.mcbrawls.inject.api.InjectorContext; +import net.mcbrawls.inject.api.http.HttpByteBuf; +import net.mcbrawls.inject.api.http.HttpInjector; +import net.mcbrawls.inject.api.http.HttpRequest; +import net.mcbrawls.inject.fabric.InjectFabric; + +public class InjectTestMod implements DedicatedServerModInitializer { + static class MyEpicHttpInjector extends HttpInjector { + @Override + public boolean isRelevant(InjectorContext ctx, HttpRequest request) { + return true; + } + + @Override + public HttpByteBuf intercept(ChannelHandlerContext ctx, HttpRequest request) { + HttpByteBuf buf = HttpByteBuf.httpBuf(ctx); + buf.writeStatusLine("1.1", 200, "OK"); + buf.writeText("Hello, from Minecraft!"); + return buf; + } + } + + @Override + public void onInitializeServer() { + InjectFabric.INSTANCE.registerInjector(new MyEpicHttpInjector()); + } +} diff --git a/src/test/resources/fabric.mod.json b/fabric/src/test/resources/fabric.mod.json similarity index 53% rename from src/test/resources/fabric.mod.json rename to fabric/src/test/resources/fabric.mod.json index db8fe78..e7c60b5 100644 --- a/src/test/resources/fabric.mod.json +++ b/fabric/src/test/resources/fabric.mod.json @@ -7,11 +7,6 @@ "environment": "server", "entrypoints": { - "server": [ - { - "adapter": "kotlin", - "value": "net.mcbrawls.inject.test.InjectTestMod" - } - ] + "server": ["net.mcbrawls.inject.fabric.test.InjectTestMod"] } -} +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 216f3b8..73acaa6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,15 +1,17 @@ # Done to increase the memory available to gradle. org.gradle.jvmargs=-Xmx1G + # Fabric Properties # check these on https://modmuss50.me/fabric.html minecraft_version=1.21.1 yarn_mappings=1.21.1+build.3 loader_version=0.16.7 -kotlin_loader_version=1.12.3+kotlin.2.0.21 + # Mod Properties -mod_version=1.3.2 -maven_group=net.mcbrawls -mod_id=inject +version=2.0.0 +group=net.mcbrawls.inject +id=inject + # Dependencies # check this on https://modmuss50.me/fabric.html fabric_version=0.106.0+1.21.1 diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts new file mode 100644 index 0000000..c98c852 --- /dev/null +++ b/paper/build.gradle.kts @@ -0,0 +1,94 @@ +plugins { + java + `maven-publish` + id("io.papermc.paperweight.userdev") version "1.7.4" + id("xyz.jpenilla.run-paper") version "2.3.1" +} + +fun prop(name: String) = project.rootProject.property(name) as String + +group = prop("group") +version = prop("version") + +repositories { + mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") +} + +sourceSets { + create("example") { + compileClasspath += sourceSets.main.get().output + runtimeClasspath += sourceSets.main.get().output + } +} + +val exampleImplementation by configurations.getting { + extendsFrom(configurations.implementation.get()) + isCanBeResolved = true +} + +val exampleCompileOnly by configurations.getting { + extendsFrom(configurations.compileOnly.get()) +} + +dependencies { + val version = prop("minecraft_version") + "-R0.1-SNAPSHOT" + + implementation(project(":api")) + paperweight.paperDevBundle(version) + + exampleCompileOnly("io.papermc.paper:paper-api:$version") + exampleImplementation(project(":api")) +} + +tasks { + val jarExample by creating(Jar::class) { + archiveClassifier.set("example") + from(sourceSets["main"].output) + from(sourceSets["example"].output) + from(project(":api").sourceSets.main.get().output) // Cursed. But it works (I spent 2 hours on this) + + group = "build" + } + + task("runExample") { + dependsOn(runDevBundleServer) + dependsOn(jarExample) + + group = "run paper" + } + + runDevBundleServer { + pluginJars(jarExample.archiveFile) + } +} + +runPaper { + disablePluginJarDetection() +} + +publishing { + publications { + create("mavenJava") { + artifactId = project.name + from(components["java"]) + } + } + + repositories { + runCatching { // getenv throws if variable doesn't exist + val mavenUser = System.getenv("MAVEN_USERNAME_ANDANTE") + val mavenPass = System.getenv("MAVEN_PASSWORD_ANDANTE") + + maven { + name = "Andante" + url = uri("https://maven.andante.dev/releases/") + + credentials { + username = mavenUser + password = mavenPass + } + } + } + } +} \ No newline at end of file diff --git a/paper/src/example/java/net/mcbrawls/inject/paper/example/InjectPaperExample.java b/paper/src/example/java/net/mcbrawls/inject/paper/example/InjectPaperExample.java new file mode 100644 index 0000000..99bbaea --- /dev/null +++ b/paper/src/example/java/net/mcbrawls/inject/paper/example/InjectPaperExample.java @@ -0,0 +1,31 @@ +package net.mcbrawls.inject.paper.example; + +import io.netty.channel.ChannelHandlerContext; +import net.mcbrawls.inject.api.InjectorContext; +import net.mcbrawls.inject.api.http.HttpByteBuf; +import net.mcbrawls.inject.api.http.HttpInjector; +import net.mcbrawls.inject.api.http.HttpRequest; +import net.mcbrawls.inject.paper.InjectPaper; +import org.bukkit.plugin.java.JavaPlugin; + +public class InjectPaperExample extends JavaPlugin { + static class MyEpicHttpInjector extends HttpInjector { + @Override + public boolean isRelevant(InjectorContext ctx, HttpRequest request) { + return true; + } + + @Override + public HttpByteBuf intercept(ChannelHandlerContext ctx, HttpRequest request) { + HttpByteBuf buf = HttpByteBuf.httpBuf(ctx); + buf.writeStatusLine("1.1", 200, "OK"); + buf.writeText("Hello, from Minecraft!"); + return buf; + } + } + + @Override + public void onEnable() { + InjectPaper.INSTANCE.registerInjector(new MyEpicHttpInjector()); + } +} \ No newline at end of file diff --git a/paper/src/example/resources/plugin.yml b/paper/src/example/resources/plugin.yml new file mode 100644 index 0000000..4845942 --- /dev/null +++ b/paper/src/example/resources/plugin.yml @@ -0,0 +1,4 @@ +name: inject-paper-example +version: 0.0 +main: net.mcbrawls.inject.paper.example.InjectPaperExample +api-version: 1.21 \ No newline at end of file diff --git a/paper/src/main/java/net/mcbrawls/inject/paper/InjectPaper.java b/paper/src/main/java/net/mcbrawls/inject/paper/InjectPaper.java new file mode 100644 index 0000000..4b43422 --- /dev/null +++ b/paper/src/main/java/net/mcbrawls/inject/paper/InjectPaper.java @@ -0,0 +1,36 @@ +package net.mcbrawls.inject.paper; + +import io.papermc.paper.network.ChannelInitializeListenerHolder; +import net.kyori.adventure.key.Key; +import net.mcbrawls.inject.api.InjectPlatform; +import net.mcbrawls.inject.api.Injector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class InjectPaper implements InjectPlatform { + private static final Logger LOGGER = LoggerFactory.getLogger("inject"); + public static InjectPaper INSTANCE = new InjectPaper(); + private static final List injectors = new ArrayList<>(); + private static boolean hasInitialised = false; + + private InjectPaper() { + } + + @Override + public void registerInjector(Injector injector) { + injectors.add(injector); + + if (!hasInitialised) { + ChannelInitializeListenerHolder.addListener(Key.key("inject", "injector"), (channel) -> { + var pipeline = channel.pipeline(); + injectors.forEach(pipeline::addFirst); + }); + + hasInitialised = true; + LOGGER.info("Inject initialised"); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 05eb23d..205a73c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,3 +6,11 @@ pluginManagement { gradlePluginPortal() } } + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} + +include(":api") +include(":fabric") +include("paper") diff --git a/src/main/kotlin/net/mcbrawls/inject/InjectMod.kt b/src/main/kotlin/net/mcbrawls/inject/InjectMod.kt deleted file mode 100644 index 0f92f5f..0000000 --- a/src/main/kotlin/net/mcbrawls/inject/InjectMod.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.mcbrawls.inject - -import net.fabricmc.api.DedicatedServerModInitializer -import org.slf4j.LoggerFactory - -object InjectMod : DedicatedServerModInitializer { - internal val injectors = mutableListOf() - val LOGGER = LoggerFactory.getLogger("Inject") - - override fun onInitializeServer() { - LOGGER.info("Inject initialising") - } -} diff --git a/src/main/kotlin/net/mcbrawls/inject/Injector.kt b/src/main/kotlin/net/mcbrawls/inject/Injector.kt deleted file mode 100644 index 9dfc28a..0000000 --- a/src/main/kotlin/net/mcbrawls/inject/Injector.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.mcbrawls.inject - -import io.netty.buffer.ByteBuf -import io.netty.channel.ChannelDuplexHandler -import io.netty.channel.ChannelHandler.Sharable -import io.netty.channel.ChannelHandlerContext - -/** - * A Netty injector. - */ -@Sharable -abstract class Injector : ChannelDuplexHandler() { - /** - * Predicate for matching if this injector is relevant - * to the given context. - * @param ctx The context. - * @return true if it's relevant, false if not. - */ - abstract fun isRelevant(ctx: InjectorContext): Boolean - - /** - * Gets executed on every channel read. - * @param ctx The context. - * @return true if the channel read was handled and should not get - * delegated to the superclass, false if it should be delegated to the superclass. - */ - abstract fun onRead(ctx: ChannelHandlerContext, buf: ByteBuf): Boolean - - override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { - val buf = msg as ByteBuf - val context = InjectorContext(ctx.pipeline(), buf) - if (!isRelevant(context)) { - super.channelRead(ctx, msg) - return - } - - val shouldDelegate = !onRead(ctx, buf) - - if (shouldDelegate) super.channelRead(ctx, msg) - } - - /** - * Registers this injector. - */ - fun register() { - InjectMod.injectors.add(this) - } -} diff --git a/src/main/kotlin/net/mcbrawls/inject/InjectorContext.kt b/src/main/kotlin/net/mcbrawls/inject/InjectorContext.kt deleted file mode 100644 index abb9285..0000000 --- a/src/main/kotlin/net/mcbrawls/inject/InjectorContext.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mcbrawls.inject - -import io.netty.buffer.ByteBuf -import io.netty.channel.ChannelPipeline - -data class InjectorContext(val pipeline: ChannelPipeline, val message: ByteBuf) \ No newline at end of file diff --git a/src/main/kotlin/net/mcbrawls/inject/http/HttpByteBuf.kt b/src/main/kotlin/net/mcbrawls/inject/http/HttpByteBuf.kt deleted file mode 100644 index 36f3b70..0000000 --- a/src/main/kotlin/net/mcbrawls/inject/http/HttpByteBuf.kt +++ /dev/null @@ -1,47 +0,0 @@ -package net.mcbrawls.inject.http - -import io.netty.buffer.ByteBuf -import io.netty.channel.ChannelHandlerContext -import java.nio.charset.StandardCharsets - -/** - * A custom [ByteBuf] used for sending HTTP responses using [HttpInterceptor]. - * @param inner The inner byte buffer. - */ -class HttpByteBuf(val inner: ByteBuf) { - /** - * Writes the status line to the buffer. - * Format is as following: - * ``` - * HTTP/{protocolVersion} {statusCode} {statusMessage} \n - * ``` - */ - fun writeStatusLine(protocolVersion: String, statusCode: Int, statusMessage: String) { - inner.writeCharSequence("HTTP/$protocolVersion $statusCode $statusMessage\n", StandardCharsets.US_ASCII) - } - - /** - * Writes an HTTP header to the buffer. - */ - fun writeHeader(header: String, value: String) { - inner.writeCharSequence("$header: $value\n", StandardCharsets.US_ASCII) - } - - /** - * Writes text to the buffer. - */ - fun writeText(text: String) { - inner.writeCharSequence("\n" + text, StandardCharsets.US_ASCII) - } - - /** - * Writes a byte array to the buffer. - */ - fun writeBytes(bytes: ByteArray) { - inner.writeCharSequence("\n", StandardCharsets.US_ASCII) - inner.writeBytes(bytes) - } -} - -fun ChannelHandlerContext.httpBuffer() = HttpByteBuf(alloc().buffer()) -inline fun ChannelHandlerContext.buildHttpBuffer(block: HttpByteBuf.() -> Unit) = httpBuffer().apply(block) \ No newline at end of file diff --git a/src/main/kotlin/net/mcbrawls/inject/http/HttpInjector.kt b/src/main/kotlin/net/mcbrawls/inject/http/HttpInjector.kt deleted file mode 100644 index febacb9..0000000 --- a/src/main/kotlin/net/mcbrawls/inject/http/HttpInjector.kt +++ /dev/null @@ -1,64 +0,0 @@ -package net.mcbrawls.inject.http - -import io.netty.buffer.ByteBuf -import io.netty.channel.ChannelFutureListener -import io.netty.channel.ChannelHandler.Sharable -import io.netty.channel.ChannelHandlerContext -import net.mcbrawls.inject.Injector -import net.mcbrawls.inject.InjectorContext -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -/** - * An [Injector] for HTTP requests. - */ -@Sharable -abstract class HttpInjector : Injector() { - private val logger: Logger = LoggerFactory.getLogger("HttpInjector ${hashCode()}") - - abstract fun intercept(ctx: ChannelHandlerContext, request: HttpRequest): HttpByteBuf - - override fun isRelevant(ctx: InjectorContext): Boolean { - return ctx.message.isRequestGet() - } - - final override fun onRead(ctx: ChannelHandlerContext, buf: ByteBuf): Boolean { - val request = HttpRequest.parse(buf) - val response = intercept(ctx, request) - - ctx.writeAndFlush(response.inner) - .addListener(ChannelFutureListener.CLOSE) - .addListener { future -> - val cause = future.cause() - if (cause == null) { - logger.debug("Write successful") - } else { - logger.error("Write failed: $cause") - cause.printStackTrace() - } - } - - return true - } - - private companion object { - /** - * Checks the first buffer bytes according to the method string length. - * @return whether the buffer matches the method - */ - fun ByteBuf.isRequestMethod(method: String): Boolean { - method.forEachIndexed { index, char -> - val byte = getUnsignedByte(index).toInt() - if (byte.toChar() != char) { - return false - } - } - - return true - } - - fun ByteBuf.isRequestGet(): Boolean { - return isRequestMethod("GET ") - } - } -} diff --git a/src/main/kotlin/net/mcbrawls/inject/http/HttpRequest.kt b/src/main/kotlin/net/mcbrawls/inject/http/HttpRequest.kt deleted file mode 100644 index 13c8d03..0000000 --- a/src/main/kotlin/net/mcbrawls/inject/http/HttpRequest.kt +++ /dev/null @@ -1,64 +0,0 @@ -package net.mcbrawls.inject.http - -import io.netty.buffer.ByteBuf -import io.netty.buffer.ByteBufInputStream -import java.io.BufferedReader -import java.io.InputStream -import java.io.InputStreamReader - -/** - * An HTTP request. - * @param request The request body, as a string. - * @param headers The header map. - */ -data class HttpRequest(val request: String, val headers: Map) { - private val requestParts = request.split(" ") - - /** - * The HTTP request method used by this request. - */ - val requestMethod = requestParts[0] - - /** - * The URI this request was sent to. - */ - val requestURI = requestParts[1] - - /** - * The HTTP protocol version used by the connection. - */ - val protocolVersion = requestParts[2] - - /** - * Gets a header. - * @param header The headers name. - * @return The value, if it exists. - */ - operator fun get(header: String) = headers[header] - - companion object { - /** - * Parses an HTTP request from a [ByteBuf]. - */ - fun parse(buf: ByteBuf) = ByteBufInputStream(buf).use(::parse) - - private fun parse(stream: InputStream) = InputStreamReader(stream).use { reader -> - val bufferedReader = BufferedReader(reader) - val request = bufferedReader.readLine() - val headers = readHeaders(bufferedReader) - bufferedReader.close() - - HttpRequest(request, headers) - } - - private fun readHeaders(reader: BufferedReader) = buildMap { - var header = reader.readLine() - while (header != null && header.isNotEmpty()) { - val split = header.indexOf(':') - this[header.substring(0, split)] = header.substring(split + 1).trim() - - header = reader.readLine() - } - } - } -} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json deleted file mode 100644 index 9fabe66..0000000 --- a/src/main/resources/fabric.mod.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "schemaVersion": 1, - "id": "inject", - "version": "${version}", - - "name": "inject", - "description": "Inject into netty easily!", - "authors": ["radstevee"], - "contact": {}, - - "license": "MIT", - "icon": "assets/inject/icon.png", - - "environment": "server", - "entrypoints": { - "server": [ - { - "adapter": "kotlin", - "value": "net.mcbrawls.inject.InjectMod" - } - ] - }, - - "mixins": [ - "inject.mixins.json" - ] -} diff --git a/src/test/kotlin/net/mcbrawls/inject/test/InjectTestMod.kt b/src/test/kotlin/net/mcbrawls/inject/test/InjectTestMod.kt deleted file mode 100644 index c35428d..0000000 --- a/src/test/kotlin/net/mcbrawls/inject/test/InjectTestMod.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.mcbrawls.inject.test - -import io.netty.channel.ChannelHandlerContext -import net.fabricmc.api.DedicatedServerModInitializer -import net.mcbrawls.inject.http.HttpByteBuf -import net.mcbrawls.inject.http.HttpInjector -import net.mcbrawls.inject.http.HttpRequest -import net.mcbrawls.inject.http.httpBuffer - -class InjectTestMod : DedicatedServerModInitializer { - override fun onInitializeServer() { - object : HttpInjector() { - override fun intercept(ctx: ChannelHandlerContext, request: HttpRequest): HttpByteBuf { - val buffer = ctx.httpBuffer() - buffer.writeStatusLine("1.1", 200, "OK") - buffer.writeText("You have successfully received a response.") - return buffer - } - }.register() - } -}