More item checks, new item storage.

This commit is contained in:
2025-03-21 15:43:52 -05:00
parent 6d6891925b
commit 5082e6ff6a
31 changed files with 496 additions and 357 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -17,6 +17,7 @@ public class NBTConfig implements JsonSerializable<NBTConfig> {
public RateLimit rateLimit = new RateLimit();
public class RateLimit {
public int maxOverhead = 32768;
public int rateLimitBytes = 16348;
public int byteDecay = 1024; // Every Minute
public int rateLimitItems = 10;

View File

@@ -1,39 +1,79 @@
package me.trouper.sentinel.data.storage;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.github.itzispyder.pdk.utils.misc.config.JsonSerializable;
import me.trouper.sentinel.Sentinel;
import org.bukkit.inventory.ItemStack;
import java.io.*;
import java.util.*;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class NBTStorage implements JsonSerializable<NBTStorage> {
@Override
public File getFile() {
File file = new File(Sentinel.getInstance().getDirector().io.getDataFolder(), "/storage/nbt.json");
file.getParentFile().mkdirs();
return file;
}
// Mapping from file name to owner UUID (as a String)
public Map<String, String> caughtItems = new HashMap<>();
public static ItemStack toItem(String serializedString) {
if (serializedString.equals("null")) return null;
byte[] decodedBytes = Base64.getDecoder().decode(serializedString);
String mapString = new String(decodedBytes);
// Remove the curly braces and split by commas to get key-value pairs
String[] keyValuePairs = mapString.substring(1, mapString.length() - 1).split(", ");
Map<String, Object> deserializedMap = new HashMap<>();
for (String pair : keyValuePairs) {
String[] keyValue = pair.split("=");
deserializedMap.put(keyValue[0], keyValue[1]);
private final File mappingFile;
private final File storageDir;
public NBTStorage() {
// Create the storage directory: /storage/nbt/ inside the plugin data folder
File dataFolder = Sentinel.getInstance().getDirector().io.getDataFolder();
storageDir = new File(dataFolder, "storage/nbt");
if (!storageDir.exists()) {
storageDir.mkdirs();
}
ItemStack item = ItemStack.deserialize(deserializedMap);
return item;
// The mapping file that stores the file-name to owner UUID mapping
mappingFile = new File(dataFolder, "storage/nbt.json");
mappingFile.getParentFile().mkdirs();
}
/**
* Stores an ItemStack's serialized NBT to a unique file
* and maps the generated file name to the owner UUID.
*
* @param item the ItemStack to store
* @param owner the owner's UUID
*/
public void storeItem(ItemStack item, UUID owner) {
// Generate a unique file name with a .nbt extension
String fileName = UUID.randomUUID().toString() + ".nbt";
File file = new File(storageDir, fileName);
try (FileOutputStream fos = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(fos)) {
String nbt = serializeItem(item);
writer.write(nbt);
} catch (IOException e) {
e.printStackTrace();
}
// Add mapping: file name -> owner UUID (as string)
caughtItems.put(fileName, owner.toString());
save();
}
/**
* Placeholder for item serialization.
* Replace this with an actual NBT serialization logic.
*
* @param item the ItemStack to serialize
* @return a String representing the NBT data of the item
*/
private String serializeItem(ItemStack item) {
return item.toString();
}
public static String toB64(ItemStack itemStack) {
Map<String, Object> serializedMap = itemStack.serialize();
return Base64.getEncoder().encodeToString(serializedMap.toString().getBytes());
// Make a deserialize method too.
@Override
public File getFile() {
return mappingFile;
}
}

View File

@@ -6,8 +6,8 @@ import me.trouper.sentinel.Sentinel;
import me.trouper.sentinel.data.storage.NBTStorage;
import me.trouper.sentinel.server.events.violations.AbstractViolation;
import me.trouper.sentinel.server.functions.helpers.ActionConfiguration;
import me.trouper.sentinel.server.functions.itemchecks.ItemCheck;
import me.trouper.sentinel.server.functions.itemchecks.RateLimitCheck;
import me.trouper.sentinel.server.functions.hotbar.items.ItemCheck;
import me.trouper.sentinel.server.functions.hotbar.items.RateLimitCheck;
import me.trouper.sentinel.server.gui.Items;
import me.trouper.sentinel.server.gui.MainGUI;
import me.trouper.sentinel.server.gui.config.AntiNukeGUI;
@@ -45,7 +45,7 @@ public class CreativeHotbar extends AbstractViolation {
if (!new RateLimitCheck().passes(new Pair<>(p,i))) {
List<String> punishmentCommands = new ArrayList<>();
for (String punishmentCommand : Sentinel.getInstance().getDirector().io.nbtConfig.rateLimit.punishmentCommands) {
punishmentCommands.add(punishmentCommand.formatted());
punishmentCommands.add(punishmentCommand.formatted(RateLimitCheck.dataUsed.get(p.getUniqueId()),Sentinel.getInstance().getDirector().io.nbtConfig.rateLimit.rateLimitBytes));
}
ServerUtils.verbose("Player flags rate limit, performing action");
@@ -55,7 +55,7 @@ public class CreativeHotbar extends AbstractViolation {
.cancel(true)
.punish(true)
.deop(Sentinel.getInstance().getDirector().io.violationConfig.creativeHotbarAction.deop)
.setPunishmentCommands();
.setPunishmentCommands(punishmentCommands);
runActions(
Sentinel.getInstance().getDirector().io.lang.violations.protections.rootName.rootNameFormatPlayer.formatted(p.getName(), Sentinel.getInstance().getDirector().io.lang.violations.protections.rootName.grab, Sentinel.getInstance().getDirector().io.lang.violations.protections.rootName.nbtItem),

View File

@@ -0,0 +1,9 @@
package me.trouper.sentinel.server.functions.hotbar;
import me.trouper.sentinel.Sentinel;
import me.trouper.sentinel.data.config.NBTConfig;
public abstract class AbstractCheck<T> {
public NBTConfig config = Sentinel.getInstance().getDirector().io.nbtConfig;
public abstract boolean passes(T input);
}

View File

@@ -1,7 +1,9 @@
package me.trouper.sentinel.server.functions.itemchecks;
package me.trouper.sentinel.server.functions.hotbar.entities;
import de.tr7zw.changeme.nbtapi.NBT;
import me.trouper.sentinel.Sentinel;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.server.functions.hotbar.misc.InventoryCheck;
import me.trouper.sentinel.server.functions.hotbar.items.ItemCheck;
import me.trouper.sentinel.utils.InventoryUtils;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.entity.Entity;
@@ -43,7 +45,7 @@ public class EntityCheck extends AbstractCheck<Entity> {
}
}
if (!entity.getPassengers().isEmpty()) {
if (!Sentinel.getInstance().getDirector().io.nbtConfig.allowRecursion) {
if (!config.allowRecursion) {
ServerUtils.verbose("Entity recursion not allowed.");
return false;
}
@@ -60,7 +62,7 @@ public class EntityCheck extends AbstractCheck<Entity> {
ServerUtils.verbose("Entity death time check failed.");
failsTiming.set(true);
}
if (nbt.hasTag("Hurttime") && nbt.getInteger("Hurttime") < 1) {
if (nbt.hasTag("HurtTime") && nbt.getInteger("HurtTime") < 1) {
ServerUtils.verbose("Entity hurt time check failed.");
failsTiming.set(true);
}

View File

@@ -1,5 +1,6 @@
package me.trouper.sentinel.server.functions.itemchecks;
package me.trouper.sentinel.server.functions.hotbar.entities;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;

View File

@@ -1,5 +1,7 @@
package me.trouper.sentinel.server.functions.itemchecks;
package me.trouper.sentinel.server.functions.hotbar.entities;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.server.functions.hotbar.items.ItemCheck;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.entity.Mob;
import org.bukkit.inventory.ItemStack;
@@ -11,8 +13,10 @@ public class EquipmentCheck extends AbstractCheck<Mob> {
public boolean passes(Mob mob) {
ServerUtils.verbose("Running mob check.");
for (EquipmentSlot slot : EquipmentSlot.values()) {
if (mob.getEquipment().getItem(slot).isEmpty()) continue;
ItemStack item = mob.getEquipment().getItem(slot);
if (item != null && !new ItemCheck().passes(item)) {
if (!new ItemCheck().passes(item)) {
ServerUtils.verbose("Equipment slot did not pass.");
return false;
}
}

View File

@@ -1,6 +1,7 @@
package me.trouper.sentinel.server.functions.itemchecks;
package me.trouper.sentinel.server.functions.hotbar.items;
import me.trouper.sentinel.Sentinel;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
@@ -10,7 +11,12 @@ import java.util.Map;
import static org.bukkit.enchantments.Enchantment.MENDING;
public class EnchantmentCheck {
public class EnchantmentCheck extends AbstractCheck<ItemStack> {
@Override
public boolean passes(ItemStack input) {
return !hasIllegalEnchants(input);
}
public boolean hasIllegalEnchants(ItemStack item) {
ServerUtils.verbose("Checking item for illegal enchants: ", item.getType().name());
@@ -119,4 +125,6 @@ public class EnchantmentCheck {
return level > maxLevel;
}
}

View File

@@ -0,0 +1,81 @@
package me.trouper.sentinel.server.functions.hotbar.items;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import me.trouper.sentinel.Sentinel;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.server.functions.hotbar.misc.BlockStateCheck;
import me.trouper.sentinel.server.functions.hotbar.misc.InventoryCheck;
import me.trouper.sentinel.server.functions.hotbar.nbt.ComponentCheck;
import me.trouper.sentinel.utils.InventoryUtils;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.Arrays;
public class ItemCheck extends AbstractCheck<ItemStack> {
@Override
public boolean passes(ItemStack item) {
try {
return scan(item);
} catch (Exception ex) {
Sentinel.getInstance().getLogger().warning("Caught an exception while handling an item check: " + Arrays.toString(ex.getStackTrace()));
return false;
}
}
private boolean scan(ItemStack item) {
ServerUtils.verbose("Checking item: " + item.getType().name());
// No metadata? Nothing to check.
if (item.getItemMeta() == null) {
ServerUtils.verbose("Item passes because it has no metadata.");
return true;
}
if (!new MetaCheck().passes(item)) {
ServerUtils.verbose("Item failed metadata check.");
return false;
}
// NBT-based checks
ReadWriteNBT nbt = NBT.itemStackToNBT(item);
ReadWriteNBT components = nbt.getCompound("components");
if (components != null) {
if (!new ComponentCheck().passes(components)) {
ServerUtils.verbose("Components check failed.");
return false;
}
}
// Spawn egg checks.
if (!new SpawnEggCheck().passes(item)) {
ServerUtils.verbose("Spawn egg check failed.");
return false;
}
if (!new BlockStateCheck().passes(item)) {
ServerUtils.verbose("Block State check failed.");
return false;
}
// Check for an inventory inside the item.
Inventory inv = InventoryUtils.getInventory(item);
if (inv != null) {
ServerUtils.verbose("Item contains an inventory: " + inv);
if (!new InventoryCheck().passes(inv)) {
ServerUtils.verbose("Item failed inventory check.");
return false;
}
}
ServerUtils.verbose("Item passed all checks.");
return true;
}
}

View File

@@ -0,0 +1,65 @@
package me.trouper.sentinel.server.functions.hotbar.items;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.BundleMeta;
import org.bukkit.inventory.meta.ItemMeta;
public class MetaCheck extends AbstractCheck<ItemStack> {
@Override
public boolean passes(ItemStack item) {
ItemMeta meta = item.getItemMeta();
// Name, lore, potion, attribute and enchantment checks.
if (!config.allowName && meta.hasDisplayName()) {
ServerUtils.verbose("Custom names not allowed.");
return false;
}
if (!config.allowLore && meta.hasLore()) {
ServerUtils.verbose("Custom lore not allowed.");
return false;
}
if (!config.allowBooks && meta instanceof BookMeta) {
ServerUtils.verbose("Item failed book check.");
return false;
}
if (!config.allowPotions &&
(item.getType().equals(Material.POTION) ||
item.getType().equals(Material.SPLASH_POTION) ||
item.getType().equals(Material.LINGERING_POTION))) {
ServerUtils.verbose("Potions not allowed.");
return false;
}
if (!config.allowAttributes && meta.hasAttributeModifiers()) {
ServerUtils.verbose("Attribute modifiers not allowed.");
return false;
}
if (config.globalMaxEnchant != 0 && new EnchantmentCheck().hasIllegalEnchants(item)) {
ServerUtils.verbose("Illegal enchantments found.");
return false;
}
// Recursion check for use-remainder items.
if (meta.hasUseRemainder()) {
if (!config.allowRecursion) {
ServerUtils.verbose("Recursion not allowed.");
return false;
}
if (meta.getUseRemainder() != null && !passes(meta.getUseRemainder())) {
ServerUtils.verbose("Use remainder item failed check.");
return false;
}
}
// Bundle check recursively check the contained items.
if (item.getType().name().contains("_BUNDLE") && meta instanceof BundleMeta bm) {
for (ItemStack bundleItem : bm.getItems()) {
if (!passes(bundleItem)) return false;
}
}
return true;
}
}

View File

@@ -1,16 +1,13 @@
package me.trouper.sentinel.server.functions.itemchecks;
package me.trouper.sentinel.server.functions.hotbar.items;
import de.tr7zw.changeme.nbtapi.NBTItem;
import io.github.itzispyder.pdk.utils.misc.Pair;
import me.trouper.sentinel.Sentinel;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitTask;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@@ -36,20 +33,19 @@ public class RateLimitCheck extends AbstractCheck<Pair<Player,ItemStack>> {
ServerUtils.verbose("Current Player used items: " + currentUsed);
currentUsed++;
itemsUsed.put(uuid,currentUsed);
return currentUsed <= Sentinel.getInstance().getDirector().io.nbtConfig.rateLimit.rateLimitItems;
return currentUsed <= config.rateLimit.rateLimitItems;
}
private boolean dataLimit(Player player, UUID uuid, ItemStack item) {
int itemData = 0;
int currentData = dataUsed.getOrDefault(uuid,0);
ServerUtils.verbose("Current Player used data: " + currentData);
try {
NBTItem nbt = new NBTItem(item);
itemData = nbt.toString().length();
int itemData = nbt.toString().length();
ServerUtils.verbose("Item data: " + itemData);
currentData += itemData;
if (currentData < config.rateLimit.maxOverhead) currentData += itemData;
} catch (Exception e) {
Sentinel.getInstance().getLogger().warning("Could not determine size of item. Blocking.");
Sentinel.getInstance().getLogger().warning(Arrays.toString(e.getStackTrace()));
@@ -60,24 +56,24 @@ public class RateLimitCheck extends AbstractCheck<Pair<Player,ItemStack>> {
ServerUtils.verbose("New Player used data: " + currentData);
return currentData <= Sentinel.getInstance().getDirector().io.nbtConfig.rateLimit.rateLimitBytes;
return currentData <= config.rateLimit.rateLimitBytes;
}
public static void decayData() {
public void decayData() {
for (UUID uuid : dataUsed.keySet()) {
int currentData = dataUsed.get(uuid);
if (currentData > 0) {
currentData -= Sentinel.getInstance().getDirector().io.nbtConfig.rateLimit.byteDecay;
currentData -= config.rateLimit.byteDecay;
dataUsed.put(uuid, Math.max(0, currentData));
}
}
}
public static void decayItems() {
public void decayItems() {
for (UUID uuid : itemsUsed.keySet()) {
int currentItems = itemsUsed.get(uuid);
if (currentItems > 0) {
currentItems -= Sentinel.getInstance().getDirector().io.nbtConfig.rateLimit.itemDecay;
currentItems -= config.rateLimit.itemDecay;
itemsUsed.put(uuid, Math.max(0, currentItems));
}
}

View File

@@ -0,0 +1,50 @@
package me.trouper.sentinel.server.functions.hotbar.items;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.server.functions.hotbar.entities.EntitySnapshotCheck;
import me.trouper.sentinel.server.functions.hotbar.nbt.EntityDataCheck;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SpawnEggMeta;
public class SpawnEggCheck extends AbstractCheck<ItemStack> {
@Override
public boolean passes(ItemStack item) {
ServerUtils.verbose("Running spawn egg checks on item: ",item.getType().name());
if (!item.getType().name().toLowerCase().contains("spawn_egg")) return true;
if (!SpawnEggCheck.entityMatches(item)) {
ServerUtils.verbose("Spawn egg entity doesn't match item type.");
return false;
}
ReadWriteNBT nbt = NBT.itemStackToNBT(item);
ReadWriteNBT components = nbt.getCompound("components");
if (components != null) {
var entityData = components.getCompound("minecraft:entity_data");
if (!new EntityDataCheck().passes(entityData)) {
ServerUtils.verbose("Spawn egg entity data check failed.");
return false;
}
}
if (item.hasItemMeta() && item.getItemMeta() instanceof SpawnEggMeta sem) {
if (sem.getSpawnedEntity() != null && !new EntitySnapshotCheck().passes(sem.getSpawnedEntity())) {
ServerUtils.verbose("Spawn egg entity snapshot check failed.");
return false;
}
}
return true;
}
public static boolean entityMatches(ItemStack item) {
if (item.hasItemMeta() && item.getItemMeta() instanceof SpawnEggMeta sem) {
String eggEntityName = item.getType().name().replace("_SPAWN_EGG", "");
return sem.getSpawnedEntity() != null &&
sem.getSpawnedEntity().getEntityType().name().equals(eggEntityName);
}
return false;
}
}

View File

@@ -0,0 +1,100 @@
package me.trouper.sentinel.server.functions.hotbar.misc;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.server.functions.hotbar.entities.EntitySnapshotCheck;
import me.trouper.sentinel.server.functions.hotbar.items.ItemCheck;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.Material;
import org.bukkit.block.*;
import org.bukkit.entity.EntityType;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.spawner.TrialSpawnerConfiguration;
public class BlockStateCheck extends AbstractCheck<ItemStack> {
@Override
public boolean passes(ItemStack item) {
ItemMeta meta = item.getItemMeta();
if (!(meta instanceof BlockStateMeta blockStateMeta)) {
ServerUtils.verbose("Item passes due to not being a block state meta");
return true;
}
if (item.getType().name().contains("CAMPFIRE") ) {
BlockState bs = blockStateMeta.getBlockState();
if (bs instanceof Campfire campfire) {
for (int slot = 0; slot < 4; slot++) {
org.bukkit.inventory.ItemStack campfireItem = campfire.getItem(slot);
if (campfireItem != null && !new ItemCheck().passes(campfireItem)) {
ServerUtils.verbose("Campfire item failed check.");
return false;
}
}
}
}
// Lectern and Chiseled Bookshelf check (by validating their inventories).
if (item.getType().equals(Material.LECTERN)) {
BlockState bs = blockStateMeta.getBlockState();
if (bs instanceof Lectern lectern) {
if (!new InventoryCheck().passes(lectern.getInventory())) {
ServerUtils.verbose("Lectern inventory failed check.");
return false;
}
}
}
if (item.getType().equals(Material.CHISELED_BOOKSHELF)) {
BlockState bs = blockStateMeta.getBlockState();
if (bs instanceof ChiseledBookshelf bookshelf) {
if (!new InventoryCheck().passes(bookshelf.getInventory())) {
ServerUtils.verbose("Chiseled bookshelf inventory failed check.");
return false;
}
}
}
// Spawner check.
if (item.getType().equals(Material.SPAWNER)) {
BlockState bs = blockStateMeta.getBlockState();
if (bs instanceof CreatureSpawner spawner) {
if (spawner.getSpawnedEntity() != null) {
if (spawner.getSpawnedEntity().getEntityType().equals(EntityType.FALLING_BLOCK) ||
spawner.getSpawnedEntity().getEntityType().equals(EntityType.COMMAND_BLOCK_MINECART)) {
ServerUtils.verbose("Spawner contains disallowed entity type.");
return false;
}
if (!new EntitySnapshotCheck().passes(spawner.getSpawnedEntity())) {
ServerUtils.verbose("Spawner entity snapshot check failed.");
return false;
}
}
}
}
// Trial Spawner check.
if (item.getType() == Material.TRIAL_SPAWNER) {
BlockState bs = blockStateMeta.getBlockState();
if (bs instanceof TrialSpawner spawner) {
ServerUtils.verbose("Running trial spawner check.");
if (spawner.getNormalConfiguration() != null) {
TrialSpawnerConfiguration config = spawner.getNormalConfiguration();
if (config.getSpawnedEntity() != null && !new EntitySnapshotCheck().passes(config.getSpawnedEntity())) {
ServerUtils.verbose("Trial Spawner failed check: Normal entity snapshot not allowed.");
return false;
}
}
if (spawner.getOminousConfiguration() != null) {
TrialSpawnerConfiguration config = spawner.getOminousConfiguration();
if (config.getSpawnedEntity() != null && !new EntitySnapshotCheck().passes(config.getSpawnedEntity())) {
ServerUtils.verbose("Trial Spawner failed check: Ominous entity snapshot not allowed.");
return false;
}
}
}
}
return true;
}
}

View File

@@ -1,8 +1,7 @@
package me.trouper.sentinel.server.functions.itemchecks;
package me.trouper.sentinel.server.functions.hotbar.misc;
import me.trouper.sentinel.Sentinel;
import me.trouper.sentinel.server.functions.itemchecks.AbstractCheck;
import me.trouper.sentinel.server.functions.itemchecks.ItemCheck;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.server.functions.hotbar.items.ItemCheck;
import me.trouper.sentinel.utils.InventoryUtils;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.inventory.Inventory;
@@ -11,16 +10,20 @@ import org.bukkit.inventory.ItemStack;
public class InventoryCheck extends AbstractCheck<Inventory> {
@Override
public boolean passes(Inventory inventory) {
public boolean passes(Inventory inv) {
ServerUtils.verbose("Running Inventory Check");
for (ItemStack item : inventory.getContents()) {
if (item == null || item.getType().isAir()) continue;
if (!new ItemCheck().passes(item)) {
for (ItemStack i : inv.getContents()) {
if (i == null || i.getType().isAir()) continue;
if (!new ItemCheck().passes(i)) {
ServerUtils.verbose("Inventory item failed check.");
return false;
}
Inventory subInventory = InventoryUtils.getInventory(item);
if (subInventory != null && !Sentinel.getInstance().getDirector().io.nbtConfig.allowRecursion) return false;
Inventory subInventory = InventoryUtils.getInventory(i);
if (subInventory != null && !config.allowRecursion) {
ServerUtils.verbose("Recursion is disabled. Failing check.");
return false;
}
if (subInventory != null && !passes(subInventory)) {
ServerUtils.verbose("Sub-inventory failed check.");
return false;
@@ -28,5 +31,5 @@ public class InventoryCheck extends AbstractCheck<Inventory> {
}
ServerUtils.verbose("Inventory passed all checks.");
return true;
}
}
}

View File

@@ -0,0 +1,32 @@
package me.trouper.sentinel.server.functions.hotbar.nbt;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.utils.ServerUtils;
public class ComponentCheck extends AbstractCheck<ReadWriteNBT> {
@Override
public boolean passes(ReadWriteNBT components) {
ServerUtils.verbose("Checking Consumable & tool");
if (!config.allowCustomConsumables && components.getCompound("minecraft:consumable") != null) {
ServerUtils.verbose("Item is consumable and not allowed.");
return false;
}
if (!config.allowCustomTools && components.getCompound("minecraft:tool") != null) {
ServerUtils.verbose("Item is custom tool and not allowed.");
return false;
}
ServerUtils.verbose("Checking Entity data");
ReadWriteNBT entityData = components.getCompound("minecraft:entity_data");
if (!new EntityDataCheck().passes(entityData)) {
ServerUtils.verbose("Entity Data Check Failed.");
return false;
}
return true;
}
}

View File

@@ -0,0 +1,40 @@
package me.trouper.sentinel.server.functions.hotbar.nbt;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import me.trouper.sentinel.server.functions.hotbar.AbstractCheck;
import me.trouper.sentinel.server.functions.hotbar.items.ItemCheck;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.inventory.ItemStack;
public class EntityDataCheck extends AbstractCheck<ReadWriteNBT> {
@Override
public boolean passes(ReadWriteNBT entityData) {
if (entityData == null) {
ServerUtils.verbose("Entity Data check passed. There was no data.");
return true;
}
ReadWriteNBT itemData = entityData.getCompound("Item");
if (itemData != null) {
ServerUtils.verbose("Entity data holds an item");
ItemStack heldItem = NBT.itemStackFromNBT(itemData);
if (heldItem != null && !new ItemCheck().passes(heldItem)) {
ServerUtils.verbose("Item contents failed check.");
return false;
}
}
if (entityData.hasTag("DeathTime") && entityData.getInteger("DeathTime") < 1) {
ServerUtils.verbose("Death time check failed.");
return false;
}
if (entityData.hasTag("HurtTime") && entityData.getInteger("HurtTime") < 1) {
ServerUtils.verbose("Hurt time check failed.");
return false;
}
ServerUtils.verbose("Entity Data check passed. There was no flagging.");
return true;
}
}

View File

@@ -1,10 +0,0 @@
package me.trouper.sentinel.server.functions.itemchecks;
import me.trouper.sentinel.Sentinel;
import me.trouper.sentinel.utils.ServerUtils;
import java.util.Arrays;
public abstract class AbstractCheck<T> {
public abstract boolean passes(T input);
}

View File

@@ -1,218 +0,0 @@
package me.trouper.sentinel.server.functions.itemchecks;
import de.tr7zw.changeme.nbtapi.NBT;
import me.trouper.sentinel.Sentinel;
import me.trouper.sentinel.data.config.NBTConfig;
import me.trouper.sentinel.utils.InventoryUtils;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.Material;
import org.bukkit.block.*;
import org.bukkit.entity.EntityType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
import org.bukkit.inventory.meta.BundleMeta;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.Arrays;
import java.util.List;
public class ItemCheck extends AbstractCheck<ItemStack> {
public List<AbstractCheck<ItemStack>> checks;
public ItemCheck() {
enchantmentCheck = new EnchantmentCheck();
}
@Override
public boolean passes(ItemStack item) {
try {
return scan(item);
} catch (Exception ex) {
Sentinel.getInstance().getLogger().warning("Caught an exception while handling an item check: " + Arrays.toString(ex.getStackTrace()));
return false;
}
}
private boolean checksPass(ItemStack item) {
}
private boolean scan(ItemStack item) {
ServerUtils.verbose("Checking item: " + item.getType().name());
NBTConfig config = Sentinel.getInstance().getDirector().io.nbtConfig;
// No metadata? Nothing to check.
if (item.getItemMeta() == null) {
ServerUtils.verbose("Item passes because it has no metadata.");
return true;
}
ItemMeta meta = item.getItemMeta();
// Check for an inventory inside the item.
Inventory inv = InventoryUtils.getInventory(item);
if (inv != null) {
ServerUtils.verbose("Item contains an inventory: " + inv);
if (!new InventoryCheck().passes(inv)) {
ServerUtils.verbose("Item failed inventory check.");
return false;
}
}
// NBT-based checks (e.g. custom consumables/tools).
var nbt = NBT.itemStackToNBT(item);
var components = nbt.getCompound("components");
if (!config.allowCustomConsumables && components.getCompound("minecraft:consumable") != null) {
ServerUtils.verbose("Item is consumable and not allowed.");
return false;
}
if (!config.allowCustomTools && components.getCompound("minecraft:tool") != null) {
ServerUtils.verbose("Item is custom tool and not allowed.");
return false;
}
var entityData = components.getCompound("minecraft:entity_data");
if (entityData != null) {
if (item.getType().name().contains("ITEM_FRAME")) {
var itemData = entityData.getCompound("Item");
ItemStack heldItem = NBT.itemStackFromNBT(itemData);
if (heldItem != null && !new ItemCheck().passes(heldItem)) {
ServerUtils.verbose("Item frame contents failed check.");
return false;
}
}
if (isSpawnEgg(item)) {
if (entityData.hasTag("DeathTime") && entityData.getInteger("DeathTime") < 1) {
ServerUtils.verbose("Egg death time check failed.");
return false;
}
if (entityData.hasTag("Hurttime") && entityData.getInteger("HurtTime") < 1) {
ServerUtils.verbose("Egg hurt time check failed.");
return false;
}
}
}
// Bundle check recursively check the contained items.
if (item.getType().name().contains("_BUNDLE") && meta instanceof BundleMeta bm) {
for (ItemStack bundleItem : bm.getItems()) {
if (!passes(bundleItem)) return false;
}
}
// Campfire check.
if (item.getType().name().contains("CAMPFIRE") && meta instanceof BlockStateMeta blockStateMeta) {
BlockState bs = blockStateMeta.getBlockState();
if (bs instanceof Campfire campfire) {
for (int slot = 0; slot < 4; slot++) {
ItemStack campfireItem = campfire.getItem(slot);
if (campfireItem != null && !passes(campfireItem)) {
ServerUtils.verbose("Campfire item failed check.");
return false;
}
}
}
}
// Lectern and Chiseled Bookshelf check (by validating their inventories).
if (item.getType().equals(Material.LECTERN) && meta instanceof BlockStateMeta blockStateMeta) {
BlockState bs = blockStateMeta.getBlockState();
if (bs instanceof Lectern lectern) {
if (!new InventoryCheck().passes(lectern.getInventory())) {
ServerUtils.verbose("Lectern inventory failed check.");
return false;
}
}
}
if (item.getType().equals(Material.CHISELED_BOOKSHELF) && meta instanceof BlockStateMeta blockStateMeta) {
BlockState bs = blockStateMeta.getBlockState();
if (bs instanceof ChiseledBookshelf bookshelf) {
if (!new InventoryCheck().passes(bookshelf.getInventory())) {
ServerUtils.verbose("Chiseled bookshelf inventory failed check.");
return false;
}
}
}
// Spawner check.
if (item.getType().equals(Material.SPAWNER) && meta instanceof BlockStateMeta blockStateMeta) {
BlockState bs = blockStateMeta.getBlockState();
if (bs instanceof CreatureSpawner spawner) {
if (spawner.getSpawnedEntity() != null) {
if (spawner.getSpawnedEntity().getEntityType().equals(EntityType.FALLING_BLOCK) ||
spawner.getSpawnedEntity().getEntityType().equals(EntityType.COMMAND_BLOCK_MINECART)) {
ServerUtils.verbose("Spawner contains disallowed entity type.");
return false;
}
if (!new EntitySnapshotCheck().passes(spawner.getSpawnedEntity())) {
ServerUtils.verbose("Spawner entity snapshot check failed.");
return false;
}
}
}
}
// Trial Spawner check.
if (item.getType() == Material.TRIAL_SPAWNER && meta instanceof BlockStateMeta blockStateMeta) {
BlockState bs = blockStateMeta.getBlockState();
if (bs instanceof TrialSpawner trialSpawner) {
if (!new TrialSpawnerCheck().passes(trialSpawner)) return false;
}
}
// Spawn egg checks.
if (isSpawnEgg(item)) {
if (!SpawnEggCheck.matches(item)) {
ServerUtils.verbose("Spawn egg match check failed.");
return false;
}
if (!new SpawnEggCheck().passes(item)) {
ServerUtils.verbose("Spawn egg check failed.");
return false;
}
}
// Name, lore, potion, attribute and enchantment checks.
if (!config.allowName && meta.hasDisplayName()) {
ServerUtils.verbose("Custom names not allowed.");
return false;
}
if (!config.allowLore && meta.hasLore()) {
ServerUtils.verbose("Custom lore not allowed.");
return false;
}
if (!config.allowPotions &&
(item.getType().equals(Material.POTION) ||
item.getType().equals(Material.SPLASH_POTION) ||
item.getType().equals(Material.LINGERING_POTION))) {
ServerUtils.verbose("Potions not allowed.");
return false;
}
if (!config.allowAttributes && meta.hasAttributeModifiers()) {
ServerUtils.verbose("Attribute modifiers not allowed.");
return false;
}
if (config.globalMaxEnchant != 0 && new EnchantmentCheck().hasIllegalEnchants(item)) {
ServerUtils.verbose("Illegal enchantments found.");
return false;
}
// Recursion check for use-remainder items.
if (meta.hasUseRemainder()) {
if (!config.allowRecursion) {
ServerUtils.verbose("Recursion not allowed.");
return false;
}
if (meta.getUseRemainder() != null && !passes(meta.getUseRemainder())) {
ServerUtils.verbose("Use remainder item failed check.");
return false;
}
}
ServerUtils.verbose("Item passed all checks.");
return true;
}
private boolean isSpawnEgg(ItemStack item) {
return item.getType().name().toLowerCase().contains("spawn_egg");
}
}

View File

@@ -1,31 +0,0 @@
package me.trouper.sentinel.server.functions.itemchecks;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SpawnEggMeta;
public class SpawnEggCheck extends AbstractCheck<ItemStack> {
@Override
public boolean passes(ItemStack item) {
ServerUtils.verbose("Running spawn egg checks on item: ",item.getType().name());
if (item.hasItemMeta() && item.getItemMeta() instanceof SpawnEggMeta sem) {
if (sem.getSpawnedEntity() != null && !new EntitySnapshotCheck().passes(sem.getSpawnedEntity())) {
return false;
}
}
return true;
}
public static boolean matches(ItemStack item) {
if (item.hasItemMeta() && item.getItemMeta() instanceof SpawnEggMeta sem) {
String eggEntityName = item.getType().name().replace("_SPAWN_EGG", "");
return sem.getSpawnedEntity() != null &&
sem.getSpawnedEntity().getEntityType().name().equals(eggEntityName);
}
return false;
}
}

View File

@@ -1,33 +0,0 @@
package me.trouper.sentinel.server.functions.itemchecks;
import me.trouper.sentinel.utils.ServerUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.TrialSpawner;
import org.bukkit.block.spawner.SpawnerEntry;
import org.bukkit.entity.Entity;
import org.bukkit.spawner.TrialSpawnerConfiguration;
public class TrialSpawnerCheck extends AbstractCheck<TrialSpawner> {
@Override
public boolean passes(TrialSpawner spawner) {
ServerUtils.verbose("Running trial spawner check.");
if (spawner.getNormalConfiguration() != null) {
TrialSpawnerConfiguration config = spawner.getNormalConfiguration();
if (config.getSpawnedEntity() != null && !new EntitySnapshotCheck().passes(config.getSpawnedEntity())) {
ServerUtils.verbose("Trial Spawner failed check: Normal entity snapshot not allowed.");
return false;
}
}
if (spawner.getOminousConfiguration() != null) {
TrialSpawnerConfiguration config = spawner.getOminousConfiguration();
if (config.getSpawnedEntity() != null && !new EntitySnapshotCheck().passes(config.getSpawnedEntity())) {
ServerUtils.verbose("Trial Spawner failed check: Ominous entity snapshot not allowed.");
return false;
}
}
return true;
}
}

View File

@@ -4,7 +4,6 @@ import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import io.github.itzispyder.pdk.utils.SchedulerUtils;
import me.trouper.sentinel.Sentinel;
import me.trouper.sentinel.data.config.MainConfig;
import me.trouper.sentinel.server.commands.*;
import me.trouper.sentinel.server.events.admin.AntiBanEvents;
import me.trouper.sentinel.server.events.admin.BlockDisplayHideEvent;
@@ -28,7 +27,7 @@ import me.trouper.sentinel.server.events.violations.entities.CommandMinecartPlac
import me.trouper.sentinel.server.events.violations.entities.CommandMinecartUse;
import me.trouper.sentinel.server.functions.chatfilter.profanity.ProfanityFilter;
import me.trouper.sentinel.server.functions.chatfilter.spam.SpamFilter;
import me.trouper.sentinel.server.functions.itemchecks.RateLimitCheck;
import me.trouper.sentinel.server.functions.hotbar.items.RateLimitCheck;
import me.trouper.sentinel.utils.Text;
import org.bukkit.Bukkit;
@@ -184,8 +183,8 @@ public final class Loader {
Bukkit.getScheduler().runTaskTimer(Sentinel.getInstance(), SpamFilter::decayHeat,0, 20);
Bukkit.getScheduler().runTaskTimer(Sentinel.getInstance(), ProfanityFilter::decayScore,0,1200);
Bukkit.getScheduler().runTaskTimer(Sentinel.getInstance(), WandEvents::handleDisplay,0,1);
Bukkit.getScheduler().runTaskTimer(Sentinel.getInstance(), RateLimitCheck::decayData,0,1200);
Bukkit.getScheduler().runTaskTimer(Sentinel.getInstance(), RateLimitCheck::decayItems,0,200);
Bukkit.getScheduler().runTaskTimer(Sentinel.getInstance(), new RateLimitCheck()::decayData,0,1200);
Bukkit.getScheduler().runTaskTimer(Sentinel.getInstance(), new RateLimitCheck()::decayItems,0,200);
if (Sentinel.getInstance().getDirector().io.mainConfig.backdoorDetection.enabled) Sentinel.getInstance().getDirector().backdoorDetection.init();