Update: schematic uploader

This commit is contained in:
iceBear67 committed 2025-01-07 14:19:27 +08:00
1 parent 9ea173c8a4
commit b46760c46b
9 files changed
+197 -99

No files matched your search

@@ -11,6 +11,7 @@ import io.ib67.sfcraft.module.compat.ModCompatModule;
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 @@ public class SFCraftInitializer extends GuiceModInitializer {
registerFeature(CreativeRoomModule.class);
registerFeature(ChatPrefixModule.class);
registerFeature(CustomItemModule.class);
registerWebModules();
}
private void registerWebModules() {
registerFeature(WebModule.class);
registerFeature(SchematicUploader.class);
}
@Override
@@ -10,6 +10,7 @@ import java.util.concurrent.ThreadLocalRandom;
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;
@@ -14,7 +14,6 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ServerHandshakeNetworkHandler.class)
@Debug(export = true)
public abstract class ServerHandshakeNetworkHandlerMixin {
@Shadow
@Final
@@ -45,6 +45,7 @@ public class SignatureService {
public byte[] createSignature(Signature signature) {
var buf = Unpooled.buffer();
writeSignature(buf, signature);
buf = buf.slice(0, buf.readableBytes());
return buf.array();
}
@@ -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);
}
@@ -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 class WebModule extends ServerModule {
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();
}
}
}
@@ -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!");
}
}
@@ -5,13 +5,6 @@ import net.minecraft.entity.Entity;
import java.util.Objects;
/**
* 基于 Command Tags 实现的权限系统
* 权限节点通常以 `.` 切割并且总是在 `sfcraft` 分类下,例如 `sfcraft.back`
* 用 `-权限节点` 表示禁止使用
*
* @param <T> 对象
*/
public record Permission<T extends Entity>(String key, boolean byDefault) {
/**
* @param key 权限节点
@@ -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);