Skip to content

Commit

Permalink
feat: version 2.0
Browse files Browse the repository at this point in the history
- paper support
- outgoing packet injection
- convert to java
- move to multi module project
  • Loading branch information
radstevee committed Oct 30, 2024
1 parent c81ef11 commit 210c598
Show file tree
Hide file tree
Showing 32 changed files with 732 additions and 443 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ hs_err_*.log
replay_*.log
*.hprof
*.jfr

**/run*/
55 changes: 39 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
```
Expand All @@ -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")
}
```

Expand Down
44 changes: 44 additions & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<MavenPublication>("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
}
}
}
}
}
12 changes: 12 additions & 0 deletions api/src/main/java/net/mcbrawls/inject/api/InjectPlatform.java
Original file line number Diff line number Diff line change
@@ -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);
}
71 changes: 71 additions & 0 deletions api/src/main/java/net/mcbrawls/inject/api/Injector.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
11 changes: 11 additions & 0 deletions api/src/main/java/net/mcbrawls/inject/api/InjectorContext.java
Original file line number Diff line number Diff line change
@@ -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) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.mcbrawls.inject.api;

/**
* The direction in which packets can flow.
*/
public enum PacketDirection {
INBOUND,
OUTBOUND
}
63 changes: 63 additions & 0 deletions api/src/main/java/net/mcbrawls/inject/api/http/HttpByteBuf.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
61 changes: 61 additions & 0 deletions api/src/main/java/net/mcbrawls/inject/api/http/HttpInjector.java
Original file line number Diff line number Diff line change
@@ -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 ");
}
}
Loading

0 comments on commit 210c598

Please sign in to comment.