Skip to content

Commit

Permalink
Update: schematic uploader
Browse files Browse the repository at this point in the history
  • Loading branch information
iceBear67 committed Jan 7, 2025
1 parent 9ea173c commit b46760c
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 99 deletions.
6 changes: 6 additions & 0 deletions src/main/java/io/ib67/sfcraft/SFCraftInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/ib67/sfcraft/config/SFConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import org.spongepowered.asm.mixin.injection.Redirect;

@Mixin(ServerHandshakeNetworkHandler.class)
@Debug(export = true)
public abstract class ServerHandshakeNetworkHandlerMixin {
@Shadow
@Final
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/ib67/sfcraft/module/SignatureService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
108 changes: 18 additions & 90 deletions src/main/java/io/ib67/sfcraft/module/supervisor/WebModule.java
Original file line number Diff line number Diff line change
@@ -1,116 +1,44 @@
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
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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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!");
}
}
7 changes: 0 additions & 7 deletions src/main/java/io/ib67/sfcraft/util/Permission.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 权限节点
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/ib67/sfcraft/util/SFConsts.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit b46760c

Please sign in to comment.