From b46760c46b6c77b1f320146f22c184c5263e556f Mon Sep 17 00:00:00 2001
From: iceBear67 <icebear67@sfclub.cc>
Date: Tue, 7 Jan 2025 14:19:27 +0800
Subject: [PATCH] Update: schematic uploader

---
 .../io/ib67/sfcraft/SFCraftInitializer.java   |   6 +
 .../java/io/ib67/sfcraft/config/SFConfig.java |   1 +
 .../ServerHandshakeNetworkHandlerMixin.java   |   1 -
 .../ib67/sfcraft/module/SignatureService.java |   1 +
 .../sfcraft/module/supervisor/WebHandler.java |   8 +
 .../sfcraft/module/supervisor/WebModule.java  | 108 ++----------
 .../supervisor/web/SchematicUploader.java     | 162 ++++++++++++++++++
 .../java/io/ib67/sfcraft/util/Permission.java |   7 -
 .../java/io/ib67/sfcraft/util/SFConsts.java   |   2 +-
 9 files changed, 197 insertions(+), 99 deletions(-)
 create mode 100644 src/main/java/io/ib67/sfcraft/module/supervisor/WebHandler.java
 create mode 100644 src/main/java/io/ib67/sfcraft/module/supervisor/web/SchematicUploader.java

diff --git a/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java b/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java
index 71afd1d..5e698e6 100644
--- a/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java
+++ b/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java
@@ -11,6 +11,7 @@
 import io.ib67.sfcraft.module.randomevt.LongNightModule;
 import io.ib67.sfcraft.module.room.CreativeRoomModule;
 import io.ib67.sfcraft.module.supervisor.WebModule;
+import io.ib67.sfcraft.module.supervisor.web.SchematicUploader;
 import io.ib67.sfcraft.registry.RoomRegistry;
 import io.ib67.sfcraft.registry.chat.SimpleMessageDecorator;
 import io.ib67.sfcraft.registry.event.SFRandomEventRegistry;
@@ -66,7 +67,12 @@ private void registerFeatures() {
         registerFeature(CreativeRoomModule.class);
         registerFeature(ChatPrefixModule.class);
         registerFeature(CustomItemModule.class);
+        registerWebModules();
+    }
+
+    private void registerWebModules() {
         registerFeature(WebModule.class);
+        registerFeature(SchematicUploader.class);
     }
 
     @Override
diff --git a/src/main/java/io/ib67/sfcraft/config/SFConfig.java b/src/main/java/io/ib67/sfcraft/config/SFConfig.java
index 8b73dd2..d2950bc 100644
--- a/src/main/java/io/ib67/sfcraft/config/SFConfig.java
+++ b/src/main/java/io/ib67/sfcraft/config/SFConfig.java
@@ -10,6 +10,7 @@
 public class SFConfig {
     public boolean enableOfflineExempt = true;
     public String domain = "localhost";
+    public String webApiBase = "localhost";
     public String serverSecret = RandomStringUtils.random(32);
     public int httpPort = 8080;
     public long maxSchematicSize = 100000;
diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java
index 7e6c5f7..6f191bb 100644
--- a/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java
+++ b/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java
@@ -14,7 +14,6 @@
 import org.spongepowered.asm.mixin.injection.Redirect;
 
 @Mixin(ServerHandshakeNetworkHandler.class)
-@Debug(export = true)
 public abstract class ServerHandshakeNetworkHandlerMixin {
     @Shadow
     @Final
diff --git a/src/main/java/io/ib67/sfcraft/module/SignatureService.java b/src/main/java/io/ib67/sfcraft/module/SignatureService.java
index 83d4f6a..bf4a474 100644
--- a/src/main/java/io/ib67/sfcraft/module/SignatureService.java
+++ b/src/main/java/io/ib67/sfcraft/module/SignatureService.java
@@ -45,6 +45,7 @@ public Signature readSignature(ByteBuf buf) {
     public byte[] createSignature(Signature signature) {
         var buf = Unpooled.buffer();
         writeSignature(buf, signature);
+        buf = buf.slice(0, buf.readableBytes());
         return buf.array();
     }
 
diff --git a/src/main/java/io/ib67/sfcraft/module/supervisor/WebHandler.java b/src/main/java/io/ib67/sfcraft/module/supervisor/WebHandler.java
new file mode 100644
index 0000000..ba1355f
--- /dev/null
+++ b/src/main/java/io/ib67/sfcraft/module/supervisor/WebHandler.java
@@ -0,0 +1,8 @@
+package io.ib67.sfcraft.module.supervisor;
+
+import io.ib67.sfcraft.ServerModule;
+import io.javalin.Javalin;
+
+public abstract class WebHandler extends ServerModule {
+    protected abstract void register(Javalin javalin);
+}
diff --git a/src/main/java/io/ib67/sfcraft/module/supervisor/WebModule.java b/src/main/java/io/ib67/sfcraft/module/supervisor/WebModule.java
index 1081ef6..f45e0eb 100644
--- a/src/main/java/io/ib67/sfcraft/module/supervisor/WebModule.java
+++ b/src/main/java/io/ib67/sfcraft/module/supervisor/WebModule.java
@@ -1,54 +1,30 @@
 package io.ib67.sfcraft.module.supervisor;
 
 import com.google.inject.Inject;
+import com.google.inject.Provides;
+import io.ib67.sfcraft.SFCraft;
 import io.ib67.sfcraft.ServerModule;
 import io.ib67.sfcraft.config.SFConfig;
-import io.ib67.sfcraft.module.SignatureService;
-import io.ib67.sfcraft.util.Helper;
-import io.ib67.sfcraft.util.litematic.LitematicConverter;
-import io.ib67.sfcraft.util.litematic.LitematicConverterV3;
+import io.ib67.sfcraft.module.manager.ModuleManager;
 import io.javalin.Javalin;
-import io.javalin.http.Context;
-import io.javalin.http.UploadedFile;
-import io.netty.buffer.Unpooled;
-import lombok.SneakyThrows;
 import lombok.extern.log4j.Log4j2;
-import net.minecraft.nbt.NbtIo;
-import net.minecraft.nbt.NbtSizeTracker;
-import org.jetbrains.annotations.NotNull;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Base64;
-import java.util.List;
 
 @Log4j2
 public class WebModule extends ServerModule {
-    private static final int PERMISSION_UPLOAD_SCHEMATICS = 2;
-    private static final Path SCHEMATIC_DIR = Path.of("config/worldedit/schematics/");
+    private Javalin javalin;
     @Inject
     SFConfig config;
-    @Inject
-    SignatureService signatureService;
-    private Javalin javalin;
 
     @Override
-    @SneakyThrows
-    public void onInitialize() {
-        Files.createDirectories(SCHEMATIC_DIR);
-        javalin = Javalin.create(cfg -> cfg.useVirtualThreads = false)
-                .options("/api/schematics/{name}", this::onUploadOptions)
-                .put("/api/schematics/{name}", this::uploadSchematic);
-        Thread.ofVirtual().name("SFCraft API Web").start(() -> {
-            javalin.start(config.httpPort);
-        });
-    }
-
-    private void onUploadOptions(@NotNull Context context) {
-        context.res().addHeader("Access-Control-Allow-Origin", "*");
-        context.res().addHeader("Access-Control-Allow-Methods", "OPTIONS, PUT");
-        context.res().addHeader("Access-Control-Allow-Headers", context.header("Access-Control-Request-Headers"));
-        context.res().addHeader("Access-Control-Max-Age", "86400");
+    public void onEnable() {
+        javalin = Javalin.create(cfg -> cfg.useVirtualThreads = false);
+        var moduleManager = SFCraft.getInjector().getInstance(ModuleManager.class);
+        for (ServerModule module : moduleManager.getModules()) {
+            if(module instanceof WebHandler webHandler) {
+                webHandler.register(javalin);
+            }
+        }
+        Thread.ofVirtual().name("SFCraft API Web").start(() -> javalin.start(config.httpPort));
     }
 
     @Override
@@ -56,61 +32,13 @@ public void onDisable() {
         javalin.stop();
     }
 
-    private void uploadSchematic(@NotNull Context context) {
-        onUploadOptions(context);
-        if (context.contentLength() > config.maxSchematicSize) {
-            context.result("Content is too large!");
-            return;
-        }
-
-        var signRaw = Base64.getUrlDecoder().decode(context.pathParam("sign"));
-        var verifiedSign = signatureService.readSignature(Unpooled.wrappedBuffer(signRaw));
-        if ((verifiedSign.permission() & PERMISSION_UPLOAD_SCHEMATICS) == 0) {
-            context.result("Permission denied!");
-            return;
-        }
-        var files = context.uploadedFileMap();
-        var name = context.pathParam("name");
-        files.forEach((k, v) -> handleUploadSchematic(context, name, v));
+    @Provides
+    public Javalin getJavalin(){
+        return javalin;
     }
 
-    @SneakyThrows
-    private void handleUploadSchematic(@NotNull Context context, String fileName, List<UploadedFile> v) {
-        if (v.size() != 1) {
-            return;
-        }
-        var file = v.getFirst();
-        if (file.size() > config.maxSchematicSize) {
-            context.result("Content is too large!");
-            return;
-        }
-        if (fileName.length() <= 10) return;
-        var baseFileName = Helper.cleanFileName(fileName.substring(0, fileName.length() - 10));
+    @Override
+    public void onInitialize() {
 
-        if (fileName.endsWith(".schematic") || fileName.endsWith(".schem")) {
-            Files.write(SCHEMATIC_DIR.resolve(baseFileName + ".schematic"), file.content().readAllBytes());
-            log.info("Saved " + fileName + " as a schematic.");
-        } else if (fileName.endsWith(".litematic")) {
-            log.error("Handling new {}", fileName);
-            var converter = new LitematicConverterV3(
-                    file.content(),
-                    new NbtSizeTracker(config.maxSchematicSize, 16)
-            );
-            try {
-                converter.read((name, nbt) -> {
-                    name = baseFileName + "-" + name + ".schematic";
-                    try {
-                        NbtIo.writeCompressed(nbt, SCHEMATIC_DIR.resolve(name));
-                        log.info("Schematic " + name + " has been saved!");
-                        context.result("Success!");
-                    } catch (Exception e) {
-                        log.error("Error occurred when serializing .litematic to disk.", e);
-                    }
-                });
-            } catch (Exception e) {
-                log.error("Error occurred when converting .litematic.", e);
-            }
-            converter.close();
-        }
     }
 }
diff --git a/src/main/java/io/ib67/sfcraft/module/supervisor/web/SchematicUploader.java b/src/main/java/io/ib67/sfcraft/module/supervisor/web/SchematicUploader.java
new file mode 100644
index 0000000..961f699
--- /dev/null
+++ b/src/main/java/io/ib67/sfcraft/module/supervisor/web/SchematicUploader.java
@@ -0,0 +1,162 @@
+package io.ib67.sfcraft.module.supervisor.web;
+
+import com.google.inject.Inject;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import io.ib67.sfcraft.config.SFConfig;
+import io.ib67.sfcraft.module.SignatureService;
+import io.ib67.sfcraft.module.supervisor.WebHandler;
+import io.ib67.sfcraft.util.Helper;
+import io.ib67.sfcraft.util.Permission;
+import io.ib67.sfcraft.util.SFConsts;
+import io.ib67.sfcraft.util.litematic.LitematicConverterV3;
+import io.javalin.Javalin;
+import io.javalin.http.Context;
+import io.javalin.http.UploadedFile;
+import io.netty.buffer.Unpooled;
+import lombok.SneakyThrows;
+import lombok.extern.log4j.Log4j2;
+import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.nbt.NbtIo;
+import net.minecraft.nbt.NbtSizeTracker;
+import net.minecraft.server.command.CommandManager;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.text.ClickEvent;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.NotNull;
+
+import java.awt.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.List;
+
+@Log4j2
+public class SchematicUploader extends WebHandler {
+    private static final String TOPIC_SCHEMATIC_UPLOADER = "upload_schematic";
+    private static final int PERMISSION_UPLOAD_SCHEMATICS = 2;
+    private static final Path SCHEMATIC_DIR = Path.of("config/worldedit/schematics/");
+    @Inject
+    SFConfig config;
+    @Inject
+    SignatureService signatureService;
+
+    @Override
+    @SneakyThrows
+    public void onInitialize() {
+        Files.createDirectories(SCHEMATIC_DIR);
+        CommandRegistrationCallback.EVENT.register(this::registerCommand);
+    }
+
+    private void registerCommand(CommandDispatcher<ServerCommandSource> dispatcher, CommandRegistryAccess registry, CommandManager.RegistrationEnvironment env) {
+        dispatcher.register(CommandManager.literal("upload").then(
+                CommandManager.literal("schematic")
+                        .requires(it -> !it.isExecutedByPlayer() || SFConsts.COMMAND_UPLOAD_SCHEMATIC.hasPermission(it.getPlayer()))
+                        .executes(this::onRequestSchematic)
+        ));
+    }
+
+    private int onRequestSchematic(CommandContext<ServerCommandSource> context) {
+        var source = context.getSource();
+        var url = generateSchematicUrl(source.isExecutedByPlayer() ? source.getPlayer().getName().getLiteralString() : "CONSOLE");
+        source.sendMessage(Text.literal("Click this URL to upload schematic files.").withColor(Color.GREEN.getRGB()));
+        source.sendMessage(Text.literal(url).styled(it -> it.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url))));
+        return Command.SINGLE_SUCCESS;
+    }
+
+    @Override
+    public void register(Javalin javalin) {
+        javalin.options("/api/schematics/{name}", this::onUploadOptions).put("/api/schematics/{name}", this::uploadSchematic);
+    }
+
+    public String generateSchematicUrl(String issuer) {
+        var name = issuer;
+        var expireAt = System.currentTimeMillis() + 1000 * 300;
+        var sign = signatureService.createSignature(new SignatureService.Signature(
+                TOPIC_SCHEMATIC_UPLOADER,
+                name,
+                PERMISSION_UPLOAD_SCHEMATICS,
+                expireAt,
+                new byte[0]
+        ));
+        return config.webApiBase + "/schematics/upload?token=" + Base64.getUrlEncoder().encodeToString(sign) + "&expireAt=" + expireAt;
+    }
+
+    private void onUploadOptions(@NotNull Context context) {
+        context.res().addHeader("Access-Control-Allow-Origin", "*");
+        context.res().addHeader("Access-Control-Allow-Methods", "OPTIONS, PUT");
+        context.res().addHeader("Access-Control-Allow-Headers", context.header("Access-Control-Request-Headers"));
+        context.res().addHeader("Access-Control-Max-Age", "86400");
+    }
+
+    private void uploadSchematic(@NotNull Context context) {
+        onUploadOptions(context);
+        if (context.contentLength() > config.maxSchematicSize) {
+            context.result("Content is too large!");
+            context.res().setStatus(400);
+            return;
+        }
+        var signRaw = Base64.getUrlDecoder().decode(context.queryParam("sign"));
+        var verifiedSign = signatureService.readSignature(Unpooled.wrappedBuffer(signRaw));
+        if ((verifiedSign.permission() & PERMISSION_UPLOAD_SCHEMATICS) == 0) {
+            context.result("Permission denied!");
+            return;
+        }
+        context.attribute("sign", verifiedSign);
+        var files = context.uploadedFileMap();
+        var name = context.pathParam("name");
+        files.forEach((k, v) -> handleUploadSchematic(context, name, v));
+    }
+
+    @SneakyThrows
+    private void handleUploadSchematic(@NotNull Context context, String fileName, List<UploadedFile> v) {
+        if (v.size() != 1) {
+            return;
+        }
+        var file = v.getFirst();
+        if (file.size() > config.maxSchematicSize) {
+            context.result("Content is too large!");
+            context.res().setStatus(400);
+            return;
+        }
+        if (fileName.length() <= 10) return;
+        var baseFileName = Helper.cleanFileName(fileName.substring(0, fileName.length() - 10));
+        var sign = context.<SignatureService.Signature>attribute("sign");
+        //var player = serverSupplier.get().getPlayerManager().getPlayer(sign.issuer());
+        ServerPlayerEntity player = null;
+        if (fileName.endsWith(".schematic") || fileName.endsWith(".schem")) {
+            Files.write(SCHEMATIC_DIR.resolve(baseFileName + ".schematic"), file.content().readAllBytes());
+            log.info("Saved " + fileName + " as a schematic.");
+            if (player != null) {
+                player.sendMessage(Text.literal("Schematic " + baseFileName + " has been saved!").withColor(Color.GREEN.getRGB()));
+            }
+        } else if (fileName.endsWith(".litematic")) {
+            log.info("Handling new litematic: {}", fileName);
+            try (var converter = new LitematicConverterV3(file.content(), new NbtSizeTracker(config.maxSchematicSize, 16))) {
+                converter.read((name, nbt) -> {
+                    name = baseFileName + "-" + name + ".schematic";
+                    try {
+                        NbtIo.writeCompressed(nbt, SCHEMATIC_DIR.resolve(name));
+                        log.info("Schematic " + name + " has been saved!");
+                        if (player != null) {
+                            player.sendMessage(Text.literal("Schematic " + name + " has been saved!").withColor(Color.GREEN.getRGB()));
+                        }
+                        context.result("Success!");
+                    } catch (Exception e) {
+                        log.error("Error occurred when serializing .litematic to disk.", e);
+                    }
+                });
+            } catch (Exception e) {
+                log.error("Error occurred when converting .litematic.", e);
+            }
+        } else {
+            context.result("Invalid format");
+            context.res().setStatus(403);
+            return;
+        }
+        context.result("Uploaded!");
+    }
+}
diff --git a/src/main/java/io/ib67/sfcraft/util/Permission.java b/src/main/java/io/ib67/sfcraft/util/Permission.java
index 4b3d8f2..7b299de 100644
--- a/src/main/java/io/ib67/sfcraft/util/Permission.java
+++ b/src/main/java/io/ib67/sfcraft/util/Permission.java
@@ -5,13 +5,6 @@
 
 import java.util.Objects;
 
-/**
- * 基于 Command Tags 实现的权限系统
- * 权限节点通常以 `.` 切割并且总是在 `sfcraft` 分类下,例如 `sfcraft.back`
- * 用 `-权限节点` 表示禁止使用
- *
- * @param <T> 对象
- */
 public record Permission<T extends Entity>(String key, boolean byDefault) {
     /**
      * @param key 权限节点
diff --git a/src/main/java/io/ib67/sfcraft/util/SFConsts.java b/src/main/java/io/ib67/sfcraft/util/SFConsts.java
index 2db8bec..daf10a8 100644
--- a/src/main/java/io/ib67/sfcraft/util/SFConsts.java
+++ b/src/main/java/io/ib67/sfcraft/util/SFConsts.java
@@ -18,7 +18,7 @@ public class SFConsts {
     public static final Permission<PlayerEntity> WORLDEDIT_AT_PLAYGROUND = ofSFCPermission("worldedit", true);
 
     public static final Permission<PlayerEntity> COMMAND_ADDWL = ofSFCPermission("command.addwl", false);
-    public static final Permission<PlayerEntity> COMMAND_UNBLOCKSERVER = ofSFCPermission("command.unblockserver", false);
+    public static final Permission<PlayerEntity> COMMAND_UPLOAD_SCHEMATIC = ofSFCPermission("command.upload.schematic", false);
     public static final Permission<PlayerEntity> COMMAND_LISTOFFLINE = ofSFCPermission("command.listoffline", false);
     public static final Permission<PlayerEntity> COMMAND_LISTPERM = ofSFCPermission("command.listperm", false);
     public static final Permission<PlayerEntity> COMMAND_LISTGEO = ofSFCPermission("command.listgeo", false);