diff --git a/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/ChatBoxPeripheral.java b/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/ChatBoxPeripheral.java index 43405f658..66d50e78f 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/ChatBoxPeripheral.java +++ b/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/ChatBoxPeripheral.java @@ -23,17 +23,28 @@ import de.srendi.advancedperipherals.lib.peripherals.IPeripheralFunction; import de.srendi.advancedperipherals.network.APNetworking; import de.srendi.advancedperipherals.network.toclient.ToastToClientPacket; +import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.level.Level; import net.minecraftforge.server.ServerLifecycleHooks; + import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.function.Predicate; +import static de.srendi.advancedperipherals.common.commands.APCommands.ROOT_SAFE_EXEC_LITERAL; import static de.srendi.advancedperipherals.common.addons.computercraft.operations.SimpleFreeOperation.CHAT_MESSAGE; public class ChatBoxPeripheral extends BasePeripheral { @@ -69,12 +80,102 @@ protected MethodResult withChatOperation(IPeripheralFunction> getChatBoxCommandFilters() { + return APConfig.PERIPHERALS_CONFIG.getChatBoxCommandFilters(); + } + + private boolean shouldWrapCommand(String command) { + return APConfig.PERIPHERALS_CONFIG.chatBoxWrapCommand.get(); + } + + private boolean isCommandBanned(String command) { + for (Predicate pattern : getChatBoxCommandFilters()) { + if (pattern.test(command)) { + return true; + } + } + return false; + } + + private static MutableComponent createFormattedError(String message) { + return Component.literal("[AP] " + message).setStyle(Style.EMPTY.withColor(ChatFormatting.RED).withBold(true)); + } + + @Nullable + protected Style filterComponentStyle(@NotNull Style style) { + ClickEvent click = style.getClickEvent(); + if (click != null) { + if (isChatBoxPreventingRunCommand() && click.getAction() == ClickEvent.Action.RUN_COMMAND) { + style = style + .withClickEvent(null) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, createFormattedError("'run_command' action is banned"))); + } else if (click.getAction() == ClickEvent.Action.RUN_COMMAND || click.getAction() == ClickEvent.Action.SUGGEST_COMMAND) { + String command = click.getValue(); + if (command.length() > 0 && command.charAt(0) == '/') { + if (isCommandBanned(command)) { + style = style + .withClickEvent(null) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, createFormattedError("Command `" + command + "` is banned"))); + } else if (shouldWrapCommand(command)) { + style = style.withClickEvent(new ClickEvent(click.getAction(), "/" + ROOT_SAFE_EXEC_LITERAL + " " + command)); + } + } + } + } + HoverEvent hover = style.getHoverEvent(); + if (hover != null) { + HoverEvent.ItemStackInfo itemInfo = hover.getValue(HoverEvent.Action.SHOW_ITEM); + if (itemInfo != null) { + try { + itemInfo.getItemStack().getTooltipLines(null, TooltipFlag.Default.ADVANCED); + } catch (RuntimeException e) { + style = style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, createFormattedError("Invalid item"))); + } + } + } + return style; + } + + @Nullable + protected MutableComponent filterMessage(@NotNull Component message) { + ComponentContents content = filterComponentContents(message.getContents()); + if (content == null) { + return null; + } + MutableComponent out = MutableComponent.create(content); + if (message instanceof MutableComponent mc) { + Style style = filterComponentStyle(mc.getStyle()); + if (style == null) { + return null; + } + out.setStyle(style); + } + for (Component comp : message.getSiblings()) { + MutableComponent filtered = filterMessage(comp); + if (filtered == null) { + return null; + } + out.append(filtered); + } + return out; + } + + @Nullable private MutableComponent appendPrefix(String prefix, String brackets, String color) { Component prefixComponent = Component.literal(APConfig.PERIPHERALS_CONFIG.defaultChatBoxPrefix.get()); if (!prefix.isEmpty()) { MutableComponent formattablePrefix; try { - formattablePrefix = MutableComponent.Serializer.fromJson(prefix); + formattablePrefix = Component.Serializer.fromJson(prefix); prefixComponent = formattablePrefix; } catch (JsonSyntaxException exception) { AdvancedPeripherals.debug("Non json prefix, using plain text instead."); @@ -83,7 +184,7 @@ private MutableComponent appendPrefix(String prefix, String brackets, String col } if (brackets.isEmpty()) brackets = "[]"; - return Component.literal(color + brackets.charAt(0) + "\u00a7r").append(prefixComponent).append(color + brackets.charAt(1) + "\u00a7r "); + return filterMessage(Component.literal(color + brackets.charAt(0) + "\u00a7r").append(prefixComponent).append(color + brackets.charAt(1) + "\u00a7r ")); } /** @@ -114,22 +215,34 @@ public final MethodResult sendFormattedMessage(@NotNull IArguments arguments) th int range = arguments.optInt(4, -1); ResourceKey dimension = getLevel().dimension(); MutableComponent component = Component.Serializer.fromJson(message); - if (component == null) + if (component == null) { return MethodResult.of(null, "incorrect json"); + } + component = filterMessage(component); + if (component == null) { + return MethodResult.of(null, "illegal message"); + } - if (checkBrackets(arguments.optString(2))) + if (checkBrackets(arguments.optString(2))) { return MethodResult.of(null, "incorrect bracket string (e.g. [], {}, <>, ...)"); + } MutableComponent preparedMessage = appendPrefix( StringUtil.convertAndToSectionMark(arguments.optString(1, APConfig.PERIPHERALS_CONFIG.defaultChatBoxPrefix.get())), arguments.optString(2, "[]"), StringUtil.convertAndToSectionMark(arguments.optString(3, "")) - ).append(component); + ); + if (preparedMessage == null) { + return MethodResult.of(null, "illegal prefix"); + } + preparedMessage.append(component); for (ServerPlayer player : ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayers()) { - if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) + if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) { continue; - if (CoordUtil.isInRange(getPos(), getLevel(), player, range, maxRange)) + } + if (CoordUtil.isInRange(getPos(), getLevel(), player, range, maxRange)) { player.sendSystemMessage(preparedMessage); + } } return MethodResult.of(true); }); @@ -142,19 +255,26 @@ public final MethodResult sendMessage(@NotNull IArguments arguments) throws LuaE int maxRange = APConfig.PERIPHERALS_CONFIG.chatBoxMaxRange.get(); int range = arguments.optInt(4, -1); ResourceKey dimension = getLevel().dimension(); - if (checkBrackets(arguments.optString(2))) + if (checkBrackets(arguments.optString(2))) { return MethodResult.of(null, "incorrect bracket string (e.g. [], {}, <>, ...)"); + } MutableComponent preparedMessage = appendPrefix( StringUtil.convertAndToSectionMark(arguments.optString(1, APConfig.PERIPHERALS_CONFIG.defaultChatBoxPrefix.get())), arguments.optString(2, "[]"), StringUtil.convertAndToSectionMark(arguments.optString(3, "")) - ).append(message); + ); + if (preparedMessage == null) { + return MethodResult.of(null, "illegal prefix"); + } + preparedMessage.append(message); for (ServerPlayer player : ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayers()) { - if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) + if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) { continue; - if (CoordUtil.isInRange(getPos(), getLevel(), player, range, maxRange)) + } + if (CoordUtil.isInRange(getPos(), getLevel(), player, range, maxRange)) { player.sendSystemMessage(preparedMessage); + } } return MethodResult.of(true); }); @@ -169,26 +289,39 @@ public final MethodResult sendFormattedMessageToPlayer(@NotNull IArguments argum int range = arguments.optInt(5, -1); ResourceKey dimension = getLevel().dimension(); ServerPlayer player = getPlayer(playerName); - if (player == null) + if (player == null) { return MethodResult.of(null, "incorrect player name/uuid"); + } MutableComponent component = Component.Serializer.fromJson(message); - if (component == null) + if (component == null) { return MethodResult.of(null, "incorrect json"); + } + component = filterMessage(component); + if (component == null) { + return MethodResult.of(null, "illegal message"); + } - if (checkBrackets(arguments.optString(3))) + if (checkBrackets(arguments.optString(3))) { return MethodResult.of(null, "incorrect bracket string (e.g. [], {}, <>, ...)"); + } MutableComponent preparedMessage = appendPrefix( StringUtil.convertAndToSectionMark(arguments.optString(2, APConfig.PERIPHERALS_CONFIG.defaultChatBoxPrefix.get())), arguments.optString(3, "[]"), StringUtil.convertAndToSectionMark(arguments.optString(4, "")) - ).append(component); - if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) + ); + if (preparedMessage == null) { + return MethodResult.of(null, "illegal prefix"); + } + preparedMessage.append(component); + if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) { return MethodResult.of(false, "NOT_SAME_DIMENSION"); + } - if (CoordUtil.isInRange(getPos(), getLevel(), player, range, maxRange)) + if (CoordUtil.isInRange(getPos(), getLevel(), player, range, maxRange)) { player.sendSystemMessage(preparedMessage); + } return MethodResult.of(true); }); } @@ -204,28 +337,45 @@ public final MethodResult sendFormattedToastToPlayer(@NotNull IArguments argumen int range = arguments.optInt(6, -1); ResourceKey dimension = getLevel().dimension(); ServerPlayer player = getPlayer(playerName); - if (player == null) + if (player == null) { return MethodResult.of(null, "incorrect player name/uuid"); + } MutableComponent messageComponent = Component.Serializer.fromJson(message); - if (messageComponent == null) + if (messageComponent == null) { return MethodResult.of(null, "incorrect json for message"); + } + messageComponent = filterMessage(messageComponent); + if (messageComponent == null) { + return MethodResult.of(null, "illegal message"); + } MutableComponent titleComponent = Component.Serializer.fromJson(title); - if (titleComponent == null) + if (titleComponent == null) { return MethodResult.of(null, "incorrect json for title"); + } + titleComponent = filterMessage(titleComponent); + if (titleComponent == null) { + return MethodResult.of(null, "illegal title"); + } - if (checkBrackets(arguments.optString(4))) + if (checkBrackets(arguments.optString(4))) { return MethodResult.of(null, "incorrect bracket string (e.g. [], {}, <>, ,,,)"); + } MutableComponent preparedMessage = appendPrefix( StringUtil.convertAndToSectionMark(arguments.optString(3, APConfig.PERIPHERALS_CONFIG.defaultChatBoxPrefix.get())), arguments.optString(4, "[]"), StringUtil.convertAndToSectionMark(arguments.optString(5, "")) - ).append(messageComponent); + ); + if (preparedMessage == null) { + return MethodResult.of(null, "illegal prefix"); + } + preparedMessage.append(messageComponent); - if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) + if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) { return MethodResult.of(false, "NOT_SAME_DIMENSION"); + } if (CoordUtil.isInRange(getPos(), getLevel(), player, range, maxRange)) { ToastToClientPacket packet = new ToastToClientPacket(titleComponent, preparedMessage); @@ -245,22 +395,30 @@ public final MethodResult sendMessageToPlayer(@NotNull IArguments arguments) thr int range = arguments.optInt(5, -1); ResourceKey dimension = getLevel().dimension(); ServerPlayer player = getPlayer(playerName); - if (player == null) + if (player == null) { return MethodResult.of(null, "incorrect player name/uuid"); + } - if (checkBrackets(arguments.optString(3))) + if (checkBrackets(arguments.optString(3))) { return MethodResult.of(null, "incorrect bracket string (e.g. [], {}, <>, ...)"); + } MutableComponent preparedMessage = appendPrefix( StringUtil.convertAndToSectionMark(arguments.optString(2, APConfig.PERIPHERALS_CONFIG.defaultChatBoxPrefix.get())), arguments.optString(3, "[]"), StringUtil.convertAndToSectionMark(arguments.optString(4, "")) - ).append(message); - if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) + ); + if (preparedMessage == null) { + return MethodResult.of(null, "illegal prefix"); + } + preparedMessage.append(message); + if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) { return MethodResult.of(false, "NOT_SAME_DIMENSION"); + } - if (CoordUtil.isInRange(getPos(), getLevel(), player, range, maxRange)) + if (CoordUtil.isInRange(getPos(), getLevel(), player, range, maxRange)) { player.sendSystemMessage(preparedMessage, false); + } return MethodResult.of(true); }); } @@ -275,20 +433,27 @@ public final MethodResult sendToastToPlayer(@NotNull IArguments arguments) throw int range = arguments.optInt(6, -1); ResourceKey dimension = getLevel().dimension(); ServerPlayer player = getPlayer(playerName); - if (player == null) + if (player == null) { return MethodResult.of(null, "incorrect player name/uuid"); + } - if (checkBrackets(arguments.optString(4))) + if (checkBrackets(arguments.optString(4))) { return MethodResult.of(null, "incorrect bracket string (e.g. [], {}, <>, ...)"); + } MutableComponent preparedMessage = appendPrefix( StringUtil.convertAndToSectionMark(arguments.optString(3, APConfig.PERIPHERALS_CONFIG.defaultChatBoxPrefix.get())), arguments.optString(4, "[]"), StringUtil.convertAndToSectionMark(arguments.optString(5, "")) - ).append(message); + ); + if (preparedMessage == null) { + return MethodResult.of(null, "illegal prefix"); + } + preparedMessage.append(message); - if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) + if (!APConfig.PERIPHERALS_CONFIG.chatBoxMultiDimensional.get() && player.getLevel().dimension() != dimension) { return MethodResult.of(false, "NOT_SAME_DIMENSION"); + } if (CoordUtil.isInRange(getPos(), getLevel(), player, range, maxRange)) { ToastToClientPacket packet = new ToastToClientPacket(Component.literal(title), preparedMessage); diff --git a/src/main/java/de/srendi/advancedperipherals/common/commands/APCommands.java b/src/main/java/de/srendi/advancedperipherals/common/commands/APCommands.java index 06f8a9aa8..858494f40 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/commands/APCommands.java +++ b/src/main/java/de/srendi/advancedperipherals/common/commands/APCommands.java @@ -1,6 +1,9 @@ package de.srendi.advancedperipherals.common.commands; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.tree.LiteralCommandNode; import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.computer.Environment; @@ -33,9 +36,15 @@ public class APCommands { static final String FORCELOAD_HELP = "/" + ROOT_LITERAL + " " + FORCELOAD_LITERAL + " help" + " - show this help message\n" + "/" + ROOT_LITERAL + " " + FORCELOAD_LITERAL + " dump" + " - show all chunky turtles\n"; + public static final String EXEC_LITERAL = "safe-exec"; + public static final String ROOT_SAFE_EXEC_LITERAL = "ap-safe-exec"; @SubscribeEvent public static void register(RegisterCommandsEvent event) { + LiteralCommandNode safeExecNode = Commands.literal(EXEC_LITERAL) + .then(Commands.argument("command", StringArgumentType.greedyString()) + .executes(APCommands::safeExecute)) + .build(); event.getDispatcher().register(Commands.literal(ROOT_LITERAL) .then(Commands.literal("getHashItem").executes(context -> getHashItem(context.getSource()))) .then(Commands.literal(FORCELOAD_LITERAL) @@ -46,7 +55,9 @@ public static void register(RegisterCommandsEvent event) { .requires(UserLevel.OWNER_OP) .executes(context -> forceloadDump(context.getSource()))) ) + .then(safeExecNode) ); + event.getDispatcher().register(Commands.literal(ROOT_SAFE_EXEC_LITERAL).redirect(safeExecNode)); } private static int getHashItem(CommandSourceStack source) throws CommandSyntaxException { @@ -98,6 +109,17 @@ private static int forceloadDump(CommandSourceStack source) throws CommandSyntax return computers.length; } + private static int safeExecute(CommandContext context) throws CommandSyntaxException { + CommandSourceStack source = context.getSource().withPermission(0); + String command = StringArgumentType.getString(context, "command"); + try { + return source.getServer().getCommands().performPrefixedCommand(source, command); + } catch (RuntimeException e) { + source.sendFailure(Component.literal(e.getMessage())); + return 0; + } + } + private static Component makeComputerDumpCommand(ServerComputer computer) { return ChatHelpers.link( diff --git a/src/main/java/de/srendi/advancedperipherals/common/configuration/PeripheralsConfig.java b/src/main/java/de/srendi/advancedperipherals/common/configuration/PeripheralsConfig.java index a31183cf4..0163339d2 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/configuration/PeripheralsConfig.java +++ b/src/main/java/de/srendi/advancedperipherals/common/configuration/PeripheralsConfig.java @@ -7,6 +7,12 @@ import net.minecraftforge.common.ForgeConfigSpec; import net.minecraftforge.fml.config.ModConfig; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.regex.Pattern; + @FieldsAreNonnullByDefault public class PeripheralsConfig implements IAPConfig { @@ -38,6 +44,10 @@ public class PeripheralsConfig implements IAPConfig { public final ForgeConfigSpec.ConfigValue defaultChatBoxPrefix; public final ForgeConfigSpec.IntValue chatBoxMaxRange; public final ForgeConfigSpec.BooleanValue chatBoxMultiDimensional; + public final ForgeConfigSpec.BooleanValue chatBoxPreventRunCommand; + public final ForgeConfigSpec.BooleanValue chatBoxWrapCommand; + public final ForgeConfigSpec.ConfigValue> chatBoxBannedCommands; + private List> chatBoxCommandFilters = null; // ME Bridge public final ForgeConfigSpec.BooleanValue enableMEBridge; @@ -79,6 +89,26 @@ public class PeripheralsConfig implements IAPConfig { public final ForgeConfigSpec.IntValue poweredPeripheralMaxEnergyStorage; private final ForgeConfigSpec configSpec; + private static final List chatBoxDefaultBannedCommands = Arrays.asList( + "/execute", + "/op", + "/deop", + "/gamemode", + "/gamerule", + "/stop", + + "/give", + "/fill", + "/setblock", + "/summon", + + "/whitelist", + "^/ban-(?:ip)?\\s*", + "^/pardon-(?:ip)?\\s*", + + "^/save-(?:on|off)\\s*" + ); + public PeripheralsConfig() { ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); @@ -117,6 +147,9 @@ public PeripheralsConfig() { defaultChatBoxPrefix = builder.comment("Defines default chatbox prefix").define("defaultChatBoxPrefix", "AP"); chatBoxMaxRange = builder.comment("Defines the maximal range of the chat box in blocks. -1 for infinite. If the range is not -1, players in other dimensions won't able to receive messages").defineInRange("chatBoxMaxRange", -1, -1, 30000000); chatBoxMultiDimensional = builder.comment("If true, the chat box is able to send messages to other dimensions than its own").define("chatBoxMultiDimensional", true); + chatBoxPreventRunCommand = builder.comment("If true, the chat box cannot use 'run_command' action").define("chatBoxPreventRunCommand", false); + chatBoxWrapCommand = builder.comment("If true, the chat box will wrap and execute 'run_command' or 'suggest_command' action with zero permission, in order to prevent operators accidently run dangerous commands.").define("chatBoxWrapCommand", true); + chatBoxBannedCommands = builder.comment("These commands below will not be able to send by 'run_command' or 'suggest_command' action. It will match as prefix if starts with '/', other wise use regex pattern").defineList("chatBoxBannedCommands", chatBoxDefaultBannedCommands, (o) -> o instanceof String value && value.length() > 0); pop("ME_Bridge", builder); @@ -195,4 +228,29 @@ public String getFileName() { public ModConfig.Type getType() { return ModConfig.Type.COMMON; } + + private List> parseChatBoxCommandFilters() { + List> filters = new ArrayList<>(); + for (final String s : chatBoxBannedCommands.get()) { + String p = s; + if (p.charAt(0) == '/') { + p = p.replaceAll("\\s+", "\\\\s+"); + if (p.equals(s)) { + final String prefix = s; + filters.add((v) -> v.startsWith(prefix) && (v.length() == prefix.length() || " \t".indexOf(v.charAt(prefix.length())) != -1)); + continue; + } + p = "^" + p + "\\s*"; + } + filters.add(Pattern.compile(p).asPredicate()); + } + return filters; + } + + public List> getChatBoxCommandFilters() { + if (chatBoxCommandFilters == null) { + chatBoxCommandFilters = parseChatBoxCommandFilters(); + } + return chatBoxCommandFilters; + } }