feat(web): move main port hooking to lightweight separate library for interoperability
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/jfmod Pipeline was successful Details

This commit is contained in:
Johannes Frohnmeyer 2023-08-30 23:15:43 +02:00
parent 3acc4b2420
commit 915f60b6b4
Signed by: Johannes
GPG Key ID: E76429612C2929F4
18 changed files with 199 additions and 56 deletions

View File

@ -0,0 +1,12 @@
plugins {
id("jfmod.module")
}
base {
archivesName.set("libjf-mainhttp-v0")
}
dependencies {
val fabricVersion: String by rootProject.extra
implementation(fabricApi.module("fabric-api-base", fabricVersion))
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.libjf.mainhttp.api.v0;
import org.jetbrains.annotations.Nullable;
public interface MainHttpHandler {
default boolean isActive() {
return true;
}
byte @Nullable [] handle(byte[] request);
}

View File

@ -0,0 +1,17 @@
package io.gitlab.jfronny.libjf.mainhttp.api.v0;
import io.gitlab.jfronny.libjf.mainhttp.impl.MainHttp;
public interface ServerState {
static void onActivate(Runnable listener) {
MainHttp.ON_ACTIVATE.register(listener);
}
static boolean isActive() {
return !MainHttp.GAME_PORT.isEmpty();
}
static int getPort() {
return MainHttp.GAME_PORT.getTopmost();
}
}

View File

@ -1,16 +1,11 @@
package io.gitlab.jfronny.libjf.web.impl.variant.shared;
package io.gitlab.jfronny.libjf.mainhttp.impl;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.web.api.v1.HttpRequest;
import io.gitlab.jfronny.libjf.web.api.v1.HttpResponse;
import io.gitlab.jfronny.libjf.web.impl.JfWeb;
import io.gitlab.jfronny.libjf.mainhttp.impl.util.Trie;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.List;
public class HttpDecoder extends ChannelInboundHandlerAdapter {
@ -45,20 +40,13 @@ public class HttpDecoder extends ChannelInboundHandlerAdapter {
byte[] data = new byte[buf.readableBytes()];
buf.readBytes(data);
buf.release();
// Parse and process request
try (ByteArrayInputStream is = new ByteArrayInputStream(data);
HttpResponse response = JfWeb.getHandler().handle(HttpRequest.read(is));
ByteArrayOutputStream os = new ByteArrayOutputStream()) {
// Write and send response
response.write(os);
os.flush();
ctx.pipeline()
.firstContext()
.writeAndFlush(Unpooled.wrappedBuffer(os.toByteArray()))
.addListener(ChannelFutureListener.CLOSE);
}
// Process request
ctx.pipeline()
.firstContext()
.writeAndFlush(Unpooled.wrappedBuffer(MainHttp.handle(data)))
.addListener(ChannelFutureListener.CLOSE);
} catch (RuntimeException re) {
LibJf.LOGGER.error("Could not process HTTP", re);
MainHttp.LOGGER.error("Could not process HTTP", re);
} finally {
if (passOn) {
buf.resetReaderIndex();

View File

@ -0,0 +1,42 @@
package io.gitlab.jfronny.libjf.mainhttp.impl;
import io.gitlab.jfronny.libjf.mainhttp.api.v0.MainHttpHandler;
import io.gitlab.jfronny.libjf.mainhttp.impl.util.ClaimPool;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.fabricmc.loader.api.FabricLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class MainHttp {
public static final Event<Runnable> ON_ACTIVATE = EventFactory.createArrayBacked(Runnable.class, listeners -> () -> {
for (Runnable listener : listeners) listener.run();
});
public static final ClaimPool<Integer> GAME_PORT = new ClaimPool<>();
public static final String MOD_ID = "libjf-mainhttp";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
private static final List<MainHttpHandler> activeHandlers = FabricLoader.getInstance()
.getEntrypoints(MOD_ID + ":v0", MainHttpHandler.class)
.stream()
.filter(MainHttpHandler::isActive)
.toList();
private static final byte[] NOT_FOUND = """
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Length: 0
""".getBytes();
public static boolean isEnabled() {
return !activeHandlers.isEmpty();
}
public static byte[] handle(byte[] request) {
for (MainHttpHandler handler : activeHandlers) {
byte[] option = handler.handle(request);
if (option != null) return option;
}
return NOT_FOUND;
}
}

View File

@ -1,6 +1,6 @@
package io.gitlab.jfronny.libjf.web.impl.mixin;
package io.gitlab.jfronny.libjf.mainhttp.impl.mixin;
import io.gitlab.jfronny.libjf.web.impl.JfWebConfig;
import io.gitlab.jfronny.libjf.mainhttp.impl.MainHttp;
import org.objectweb.asm.tree.ClassNode;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
@ -8,8 +8,8 @@ import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import java.util.List;
import java.util.Set;
public class JfWebMixinPlugin implements IMixinConfigPlugin {
private static final String MIXIN_PACKAGE = "io.gitlab.jfronny.libjf.web.impl.mixin.";
public class JfMainHTTPMixinPlugin implements IMixinConfigPlugin {
private static final String MIXIN_PACKAGE = "io.gitlab.jfronny.libjf.mainhttp.impl.mixin.";
@Override
public void onLoad(String mixinPackage) {
@ -23,7 +23,7 @@ public class JfWebMixinPlugin implements IMixinConfigPlugin {
@Override
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
return switch (mixinClassName) {
case MIXIN_PACKAGE + "ServerNetworkIoMixin", MIXIN_PACKAGE + "ServerNetworkIo$1Mixin" -> JfWebConfig.port == -1;
case MIXIN_PACKAGE + "ServerNetworkIoMixin", MIXIN_PACKAGE + "ServerNetworkIo$1Mixin" -> MainHttp.isEnabled();
default -> throw new IllegalArgumentException("Unexpected mixin: " + mixinClassName);
};
}

View File

@ -1,6 +1,6 @@
package io.gitlab.jfronny.libjf.web.impl.mixin;
package io.gitlab.jfronny.libjf.mainhttp.impl.mixin;
import io.gitlab.jfronny.libjf.web.impl.variant.shared.HttpDecoder;
import io.gitlab.jfronny.libjf.mainhttp.impl.HttpDecoder;
import io.netty.channel.Channel;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;

View File

@ -1,7 +1,7 @@
package io.gitlab.jfronny.libjf.web.impl.mixin;
package io.gitlab.jfronny.libjf.mainhttp.impl.mixin;
import io.gitlab.jfronny.libjf.web.impl.util.ClaimPool;
import io.gitlab.jfronny.libjf.web.impl.variant.shared.SharedWebServer;
import io.gitlab.jfronny.libjf.mainhttp.impl.MainHttp;
import io.gitlab.jfronny.libjf.mainhttp.impl.util.ClaimPool;
import net.minecraft.server.ServerNetworkIo;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@ -15,12 +15,13 @@ import java.util.Set;
@Mixin(ServerNetworkIo.class)
public class ServerNetworkIoMixin {
@Unique private final Set<ClaimPool<Integer>.Claim> libjf$portClaim = new HashSet<>();
@Unique
private final Set<ClaimPool<Integer>.Claim> libjf$portClaim = new HashSet<>();
@Inject(method = "bind(Ljava/net/InetAddress;I)V", at = @At("HEAD"))
void onBind(InetAddress address, int port, CallbackInfo ci) {
libjf$portClaim.add(SharedWebServer.gamePort.claim(port));
SharedWebServer.emitActive();
libjf$portClaim.add(MainHttp.GAME_PORT.claim(port));
MainHttp.ON_ACTIVATE.invoker().run();
}
@Inject(method = "stop()V", at = @At("HEAD"))

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.web.impl.util;
package io.gitlab.jfronny.libjf.mainhttp.impl.util;
import java.util.LinkedList;
import java.util.List;

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.web.impl.variant.shared;
package io.gitlab.jfronny.libjf.mainhttp.impl.util;
import it.unimi.dsi.fastutil.chars.Char2ObjectArrayMap;
import it.unimi.dsi.fastutil.chars.Char2ObjectMap;

View File

@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "libjf-mainhttp-v0",
"name": "LibJF MainHTTP",
"version": "${version}",
"authors": [
"JFronny"
],
"contact": {
"email": "projects.contact@frohnmeyer-wds.de",
"homepage": "https://jfronny.gitlab.io",
"issues": "https://git.frohnmeyer-wds.de/JfMods/LibJF/issues",
"sources": "https://git.frohnmeyer-wds.de/JfMods/LibJF"
},
"license": "MIT",
"environment": "*",
"mixins": ["libjf-mainhttp-v0.mixins.json"],
"depends": {
"fabricloader": ">=0.12.0",
"minecraft": "*"
},
"custom": {
"modmenu": {
"parent": "libjf",
"badges": ["library"]
}
}
}

View File

@ -1,9 +1,9 @@
{
"required": true,
"minVersion": "0.8",
"package": "io.gitlab.jfronny.libjf.web.impl.mixin",
"package": "io.gitlab.jfronny.libjf.mainhttp.impl.mixin",
"compatibilityLevel": "JAVA_16",
"plugin": "io.gitlab.jfronny.libjf.web.impl.mixin.JfWebMixinPlugin",
"plugin": "io.gitlab.jfronny.libjf.mainhttp.impl.mixin.JfMainHTTPMixinPlugin",
"server": [
"ServerNetworkIo$1Mixin",
"ServerNetworkIoMixin"

View File

@ -12,6 +12,7 @@ dependencies {
val fabricVersion: String by rootProject.extra
api(devProject(":libjf-base"))
api(devProject(":libjf-config-core-v2"))
api(devProject(":libjf-mainhttp-v0"))
include(modImplementation(fabricApi.module("fabric-command-api-v2", fabricVersion))!!)
annotationProcessor(project(":libjf-config-compiler-plugin-v2"))

View File

@ -44,7 +44,7 @@ public class HttpResponseImpl implements HttpResponse {
this.version = "HTTP/1.1";
this.statusCode = statusCode;
this.header = new HashMap<>();
this.header = new LinkedHashMap<>();
addHeader("Connection", "keep-alive");
}
@ -52,7 +52,7 @@ public class HttpResponseImpl implements HttpResponse {
@Override
public HttpResponseImpl addHeader(String key, String value) {
ensureOpen();
Set<String> valueSet = header.computeIfAbsent(key, k -> new HashSet<>());
Set<String> valueSet = header.computeIfAbsent(key, k -> new LinkedHashSet<>());
valueSet.add(value);
return this;
}
@ -60,7 +60,7 @@ public class HttpResponseImpl implements HttpResponse {
@Override
public HttpResponseImpl removeHeader(String key, String value) {
ensureOpen();
Set<String> valueSet = header.computeIfAbsent(key, k -> new HashSet<>());
Set<String> valueSet = header.computeIfAbsent(key, k -> new LinkedHashSet<>());
valueSet.remove(value);
return this;
}
@ -88,14 +88,15 @@ public class HttpResponseImpl implements HttpResponse {
public void write(OutputStream out) throws IOException {
OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
Map<String, Set<String>> finalHeaders = new LinkedHashMap<>(header);
if (data != null) {
addHeader("Transfer-Encoding", "chunked");
finalHeaders.computeIfAbsent("Transfer-Encoding", k -> new LinkedHashSet<>()).add("chunked");
} else {
addHeader("Content-Length", "0");
finalHeaders.computeIfAbsent("Content-Length", k -> new LinkedHashSet<>()).add("0");
}
writeLine(writer, version + " " + statusCode.getCode() + " " + statusCode.getMessage());
for (Entry<String, Set<String>> e : header.entrySet()) {
for (Entry<String, Set<String>> e : finalHeaders.entrySet()) {
if (e.getValue().isEmpty()) continue;
writeLine(writer, e.getKey() + ": " + StringUtils.join(e.getValue(), ", "));
}

View File

@ -0,0 +1,38 @@
package io.gitlab.jfronny.libjf.web.impl.variant.shared;
import io.gitlab.jfronny.libjf.mainhttp.api.v0.MainHttpHandler;
import io.gitlab.jfronny.libjf.mainhttp.api.v0.ServerState;
import io.gitlab.jfronny.libjf.web.api.v1.*;
import io.gitlab.jfronny.libjf.web.impl.JfWeb;
import io.gitlab.jfronny.libjf.web.impl.JfWebConfig;
import org.jetbrains.annotations.Nullable;
import java.io.*;
public class MainHttpHandlerImpl implements MainHttpHandler {
public MainHttpHandlerImpl() {
ServerState.onActivate(SharedWebServer::emitActive);
}
@Override
public boolean isActive() {
return JfWebConfig.port == -1;
}
@Override
public byte @Nullable [] handle(byte[] request) {
// Parse and process request
try (ByteArrayInputStream is = new ByteArrayInputStream(request);
HttpResponse response = JfWeb.getHandler().handle(HttpRequest.read(is))) {
if (response.getStatusCode() == HttpStatusCode.NOT_FOUND) return null;
// Write and send response
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
response.write(os);
os.flush();
return os.toByteArray();
}
} catch (IOException e) {
return null;
}
}
}

View File

@ -1,13 +1,12 @@
package io.gitlab.jfronny.libjf.web.impl.variant.shared;
import io.gitlab.jfronny.libjf.mainhttp.api.v0.ServerState;
import io.gitlab.jfronny.libjf.web.impl.host.RequestHandler;
import io.gitlab.jfronny.libjf.web.impl.util.ClaimPool;
import io.gitlab.jfronny.libjf.web.impl.variant.AbstractWebServer;
import java.util.*;
public class SharedWebServer extends AbstractWebServer {
public static final ClaimPool<Integer> gamePort = new ClaimPool<>();
public static final Set<Runnable> onActive = new LinkedHashSet<>();
public static void emitActive() {
@ -23,9 +22,8 @@ public class SharedWebServer extends AbstractWebServer {
@Override
public String getServerRoot() {
Integer gamePort = this.gamePort.getTopmost();
if (gamePort == null) throw new UnsupportedOperationException("Attempted to get server root on unhosted server");
else return getServerRoot(gamePort);
if (!ServerState.isActive()) throw new UnsupportedOperationException("Attempted to get server root on unhosted server");
else return getServerRoot(ServerState.getPort());
}
@Override
@ -47,6 +45,6 @@ public class SharedWebServer extends AbstractWebServer {
@Override
public boolean isActive() {
return !gamePort.isEmpty();
return ServerState.isActive();
}
}

View File

@ -15,17 +15,18 @@
},
"license": "MIT",
"environment": "*",
"mixins": ["libjf-web-v1.mixins.json"],
"entrypoints": {
"main": ["io.gitlab.jfronny.libjf.web.impl.JfWeb"],
"libjf:coprocess": ["io.gitlab.jfronny.libjf.web.impl.JfWeb"],
"libjf:config": ["io.gitlab.jfronny.libjf.web.impl.JFC_JfWebConfig"]
"libjf:config": ["io.gitlab.jfronny.libjf.web.impl.JFC_JfWebConfig"],
"libjf-mainhttp:v0": ["io.gitlab.jfronny.libjf.web.impl.variant.shared.MainHttpHandlerImpl"]
},
"depends": {
"fabricloader": ">=0.12.0",
"minecraft": "*",
"libjf-base": ">=${version}",
"libjf-config-core-v2": ">=${version}",
"libjf-mainhttp-v0": ">=${version}",
"fabric-command-api-v2": "*"
},
"custom": {

View File

@ -17,11 +17,16 @@ include("libjf-base")
include("libjf-config-core-v2")
include("libjf-config-commands")
include("libjf-config-ui-tiny")
include("libjf-config-compiler-plugin-v2")
include("libjf-data-v0")
include("libjf-data-manipulation-v0")
include("libjf-devutil")
include("libjf-translate-v1")
include("libjf-unsafe-v0")
include("libjf-web-v1")
include("libjf-config-compiler-plugin-v2")
include("libjf-devutil")
include("libjf-translate-v1")
include("libjf-unsafe-v0")
include("libjf-mainhttp-v0")
include("libjf-web-v1")