Update: schematic uploader
This commit is contained in:
1 parent
9ea173c8a4
commit
b46760c46b
9 files changed
+196
-98
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.randomevt.LongNightModule;
|
||||||
import io.ib67.sfcraft.module.room.CreativeRoomModule;
|
import io.ib67.sfcraft.module.room.CreativeRoomModule;
|
||||||
import io.ib67.sfcraft.module.supervisor.WebModule;
|
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.RoomRegistry;
|
||||||
import io.ib67.sfcraft.registry.chat.SimpleMessageDecorator;
|
import io.ib67.sfcraft.registry.chat.SimpleMessageDecorator;
|
||||||
import io.ib67.sfcraft.registry.event.SFRandomEventRegistry;
|
import io.ib67.sfcraft.registry.event.SFRandomEventRegistry;
|
||||||
@@ -66,7 +67,12 @@ public class SFCraftInitializer extends GuiceModInitializer {
|
|||||||
registerFeature(CreativeRoomModule.class);
|
registerFeature(CreativeRoomModule.class);
|
||||||
registerFeature(ChatPrefixModule.class);
|
registerFeature(ChatPrefixModule.class);
|
||||||
registerFeature(CustomItemModule.class);
|
registerFeature(CustomItemModule.class);
|
||||||
|
registerWebModules();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerWebModules() {
|
||||||
registerFeature(WebModule.class);
|
registerFeature(WebModule.class);
|
||||||
|
registerFeature(SchematicUploader.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import java.util.concurrent.ThreadLocalRandom;
|
|||||||
public class SFConfig {
|
public class SFConfig {
|
||||||
public boolean enableOfflineExempt = true;
|
public boolean enableOfflineExempt = true;
|
||||||
public String domain = "localhost";
|
public String domain = "localhost";
|
||||||
|
public String webApiBase = "localhost";
|
||||||
public String serverSecret = RandomStringUtils.random(32);
|
public String serverSecret = RandomStringUtils.random(32);
|
||||||
public int httpPort = 8080;
|
public int httpPort = 8080;
|
||||||
public long maxSchematicSize = 100000;
|
public long maxSchematicSize = 100000;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
|
|
||||||
@Mixin(ServerHandshakeNetworkHandler.class)
|
@Mixin(ServerHandshakeNetworkHandler.class)
|
||||||
@Debug(export = true)
|
|
||||||
public abstract class ServerHandshakeNetworkHandlerMixin {
|
public abstract class ServerHandshakeNetworkHandlerMixin {
|
||||||
@Shadow
|
@Shadow
|
||||||
@Final
|
@Final
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public class SignatureService {
|
|||||||
public byte[] createSignature(Signature signature) {
|
public byte[] createSignature(Signature signature) {
|
||||||
var buf = Unpooled.buffer();
|
var buf = Unpooled.buffer();
|
||||||
writeSignature(buf, signature);
|
writeSignature(buf, signature);
|
||||||
|
buf = buf.slice(0, buf.readableBytes());
|
||||||
return buf.array();
|
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;
|
package io.ib67.sfcraft.module.supervisor;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provides;
|
||||||
|
import io.ib67.sfcraft.SFCraft;
|
||||||
import io.ib67.sfcraft.ServerModule;
|
import io.ib67.sfcraft.ServerModule;
|
||||||
import io.ib67.sfcraft.config.SFConfig;
|
import io.ib67.sfcraft.config.SFConfig;
|
||||||
import io.ib67.sfcraft.module.SignatureService;
|
import io.ib67.sfcraft.module.manager.ModuleManager;
|
||||||
import io.ib67.sfcraft.util.Helper;
|
|
||||||
import io.ib67.sfcraft.util.litematic.LitematicConverter;
|
|
||||||
import io.ib67.sfcraft.util.litematic.LitematicConverterV3;
|
|
||||||
import io.javalin.Javalin;
|
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 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
|
@Log4j2
|
||||||
public class WebModule extends ServerModule {
|
public class WebModule extends ServerModule {
|
||||||
private static final int PERMISSION_UPLOAD_SCHEMATICS = 2;
|
private Javalin javalin;
|
||||||
private static final Path SCHEMATIC_DIR = Path.of("config/worldedit/schematics/");
|
|
||||||
@Inject
|
@Inject
|
||||||
SFConfig config;
|
SFConfig config;
|
||||||
@Inject
|
|
||||||
SignatureService signatureService;
|
|
||||||
private Javalin javalin;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
public void onEnable() {
|
||||||
public void onInitialize() {
|
javalin = Javalin.create(cfg -> cfg.useVirtualThreads = false);
|
||||||
Files.createDirectories(SCHEMATIC_DIR);
|
var moduleManager = SFCraft.getInjector().getInstance(ModuleManager.class);
|
||||||
javalin = Javalin.create(cfg -> cfg.useVirtualThreads = false)
|
for (ServerModule module : moduleManager.getModules()) {
|
||||||
.options("/api/schematics/{name}", this::onUploadOptions)
|
if(module instanceof WebHandler webHandler) {
|
||||||
.put("/api/schematics/{name}", this::uploadSchematic);
|
webHandler.register(javalin);
|
||||||
Thread.ofVirtual().name("SFCraft API Web").start(() -> {
|
|
||||||
javalin.start(config.httpPort);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private void onUploadOptions(@NotNull Context context) {
|
Thread.ofVirtual().name("SFCraft API Web").start(() -> javalin.start(config.httpPort));
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -56,61 +32,13 @@ public class WebModule extends ServerModule {
|
|||||||
javalin.stop();
|
javalin.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void uploadSchematic(@NotNull Context context) {
|
@Provides
|
||||||
onUploadOptions(context);
|
public Javalin getJavalin(){
|
||||||
if (context.contentLength() > config.maxSchematicSize) {
|
return javalin;
|
||||||
context.result("Content is too large!");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var signRaw = Base64.getUrlDecoder().decode(context.pathParam("sign"));
|
@Override
|
||||||
var verifiedSign = signatureService.readSignature(Unpooled.wrappedBuffer(signRaw));
|
public void onInitialize() {
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
@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));
|
|
||||||
|
|
||||||
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;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于 Command Tags 实现的权限系统
|
|
||||||
* 权限节点通常以 `.` 切割并且总是在 `sfcraft` 分类下,例如 `sfcraft.back`
|
|
||||||
* 用 `-权限节点` 表示禁止使用
|
|
||||||
*
|
|
||||||
* @param <T> 对象
|
|
||||||
*/
|
|
||||||
public record Permission<T extends Entity>(String key, boolean byDefault) {
|
public record Permission<T extends Entity>(String key, boolean byDefault) {
|
||||||
/**
|
/**
|
||||||
* @param key 权限节点
|
* @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> WORLDEDIT_AT_PLAYGROUND = ofSFCPermission("worldedit", true);
|
||||||
|
|
||||||
public static final Permission<PlayerEntity> COMMAND_ADDWL = ofSFCPermission("command.addwl", false);
|
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_LISTOFFLINE = ofSFCPermission("command.listoffline", false);
|
||||||
public static final Permission<PlayerEntity> COMMAND_LISTPERM = ofSFCPermission("command.listperm", false);
|
public static final Permission<PlayerEntity> COMMAND_LISTPERM = ofSFCPermission("command.listperm", false);
|
||||||
public static final Permission<PlayerEntity> COMMAND_LISTGEO = ofSFCPermission("command.listgeo", false);
|
public static final Permission<PlayerEntity> COMMAND_LISTGEO = ofSFCPermission("command.listgeo", false);
|
||||||
|
|||||||
Reference in new issue
Block a user