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.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);
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
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;
|
||||
@Provides
|
||||
public Javalin getJavalin(){
|
||||
return javalin;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
|
||||
@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;
|
||||
|
||||
/**
|
||||
* 基于 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);
|
||||
|
||||
Reference in new issue
Block a user