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 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 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 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 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.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 对象 - */ public record Permission(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 WORLDEDIT_AT_PLAYGROUND = ofSFCPermission("worldedit", true); public static final Permission COMMAND_ADDWL = ofSFCPermission("command.addwl", false); - public static final Permission COMMAND_UNBLOCKSERVER = ofSFCPermission("command.unblockserver", false); + public static final Permission COMMAND_UPLOAD_SCHEMATIC = ofSFCPermission("command.upload.schematic", false); public static final Permission COMMAND_LISTOFFLINE = ofSFCPermission("command.listoffline", false); public static final Permission COMMAND_LISTPERM = ofSFCPermission("command.listperm", false); public static final Permission COMMAND_LISTGEO = ofSFCPermission("command.listgeo", false);