Security

Protects shared containers against item duplication — rate limiting, integrity validation, concurrent locking, and admin alerts.

fr.eri.eriapi.security Forge 1.12.2 Java 8

Introduction

The Security module provides four independent utility classes to protect server-side GUIs against item duplication exploits. These classes are generic: they work with any container (ItemStack[]), any player (EntityPlayerMP), and integrate without configuration.

The four classes cover the four main attack vectors:

Class Protection
GuiRateLimiter Limits the number of clicks per second to prevent macros and spam
ItemIntegrityValidator Verifies that the total item count cannot increase after an operation
ContainerLock Serialized concurrent access per key — prevents race conditions between threads
DupeAlertManager Alerts online operators and logs any detected duplication attempt
Package

All classes in this module are located in the fr.eri.eriapi.security package.

GuiRateLimiter

Limits the number of actions per second per player. Uses a sliding one-second window: if a player triggers more than maxPerSecond actions within the same second, check() returns false until the window resets.

Thread-safe: backed by a ConcurrentHashMap internally.

Constructor

Parameter Type Description
maxPerSecond int Maximum number of actions allowed per player per second

Methods

Method Return Description
check(UUID playerId) boolean true if the action is allowed, false if rate-limited
clear(UUID playerId) void Removes the tracker for the player — call on disconnect
clearAll() void Removes all trackers — call on world unload
getMaxPerSecond() int Returns the configured threshold

Usage example

Java — GuiRateLimiter
// Create a limiter of 20 clicks per second (class field)
private final GuiRateLimiter chestRateLimiter = new GuiRateLimiter(20);

// On each player action:
UUID uid = player.getGameProfile().getId();
if (!chestRateLimiter.check(uid)) {
    // Too many clicks — reject silently
    LOGGER.warn("[AntiDupe] Rate-limited {}", player.getName());
    return;
}
// ... process the action

// On player disconnect:
chestRateLimiter.clear(uid);
Multiple distinct limiters

Create a different GuiRateLimiter for each action type if the thresholds should differ. For example: 20/s for chest clicks, 10/s for contract collections.

ItemIntegrityValidator

Validates that the total item count cannot increase during a container ↔ player inventory operation. If the total after exceeds the total before, a duplication has been detected and the operation must be rolled back.

All methods are static — no instance required.

Snapshot methods

Method Description
snapshot(ItemStack[] items) Deep copy of an ItemStack array. Empty stacks are stored as ItemStack.EMPTY.
snapshotPlayerInventory(EntityPlayerMP player) Deep copy of the player's 36 main inventory slots.

Count methods

Method Description
countItems(ItemStack[] items) Sum of getCount() for all non-empty stacks in the array.
countPlayerInventory(EntityPlayerMP player) Counts items currently in the player's 36 main slots (live state).

Validation methods

Signature Description
validate(ItemStack[] containerBefore, ItemStack[] containerAfter,
ItemStack cursorBefore, ItemStack cursorAfter,
int invCountBefore, int invCountAfter)
Returns true if the total has not increased. Numeric overload for both inventory counts.
validate(ItemStack[] containerBefore, ItemStack[] containerAfter,
ItemStack cursorBefore, ItemStack cursorAfter,
ItemStack[] invSnapshotBefore, EntityPlayerMP player)
Convenience overload: passes the BEFORE snapshot and the live player for the AFTER count.

Rollback method

Method Description
rollback(ItemStack[] container, ItemStack[] containerSnapshot,
EntityPlayerMP player, ItemStack[] inventorySnapshot)
Restores the container and player inventory from their snapshots. Also calls player.inventoryContainer.detectAndSendChanges().
Important: if the container is stored externally (NBT, database), persist it after rollback.

Snapshot / validate / rollback workflow

Java — complete before/after pattern
// 1. Snapshot BEFORE the operation
ItemStack[] containerSnap = ItemIntegrityValidator.snapshot(container);
ItemStack   cursorSnap    = cursor.isEmpty() ? ItemStack.EMPTY : cursor.copy();
ItemStack[] invSnap       = ItemIntegrityValidator.snapshotPlayerInventory(player);

// 2. Perform the operation (modify container, cursor, player inventory)
doOperation(container, cursor, player);

// 3. Validate — reject if items were created
if (!ItemIntegrityValidator.validate(containerSnap, container,
        cursorSnap, cursor, invSnap, player)) {
    // Duplication detected — restore state
    ItemIntegrityValidator.rollback(container, containerSnap, player, invSnap);
    // Re-persist the container if necessary
    persistContainer(containerSnap);
    return;
}

// 4. Persist — no duplication
persistContainer(container);
Lost items (negative delta)

validate() returns true if the total after is less than or equal to the total before — meaning item loss is accepted (they may have been dropped). Only an increase in items is detected as a dupe.

ContainerLock

Provides a distinct Object monitor per logical key (e.g. a faction ID, a container UUID). Using these locks inside synchronized blocks prevents two threads from processing the same shared resource simultaneously.

The advantage over a single global lock is granularity: two different factions can be processed in parallel, but two players from the same faction cannot modify the same chest at the same time.

Thread-safe: backed by a ConcurrentHashMap internally.

Methods

Method Description
getLock(String key) Returns the lock for the given key, creating it if it does not yet exist.
removeLock(String key) Removes the lock for the key — call when the resource is destroyed (e.g. faction disbanded).
clearAll() Removes all locks.
size() Returns the current number of tracked keys.

Usage example

Java — ContainerLock
// Class field
private final ContainerLock containerLock = new ContainerLock();

// Protect a faction chest operation
synchronized (containerLock.getLock(factionInfo.id)) {
    handleChestOperation(player, fm, factionInfo);
    sendChestFullState(player, factionInfo);
}

// When a faction is disbanded:
containerLock.removeLock(factionInfo.id);
No timeout

ContainerLock uses standard Java synchronized blocks — there is no built-in timeout. If the code inside the synchronized block can block indefinitely (e.g. network access), consider a ReentrantLock with tryLock() instead.

DupeAlertManager

Sends real-time alerts to all online operators (permission level ≥ 2) when a duplication attempt is detected. Also logs the event to the server console under the EriAPI-Security logger.

All methods are static — no instance required.

Method alert()

Parameter Type Description
exploiter EntityPlayerMP The player who triggered the duplication detection
itemDelta int Number of items that would have been created (positive integer)
context String Short description of the context displayed in the alert (e.g. "faction chest")

Admin message format

Preview — chat message for operators
[AntiDupe] PlayerName attempted to duplicate +3 items (faction chest)

Usage example

Java — DupeAlertManager.alert()
// After detecting a duplication:
int delta = totalAfter - totalBefore;
LOGGER.warn("[AntiDupe] Duplication detected for {} : +{} items", player.getName(), delta);
DupeAlertManager.alert(player, delta, "faction chest");
// Perform the rollback...

Full Example — Protecting a Custom GUI Container

Here is how to use all four classes together to protect an arbitrary container (e.g. guild chest, marketplace, faction storage).

Java — complete container protection
public class MyGuiHandler {

    // ── EriAPI Security instances ─────────────────────────────────────────
    // Rate limiter: max 20 clicks per second
    private final GuiRateLimiter rateLimiter   = new GuiRateLimiter(20);
    // Per-container locks (key = guild ID for example)
    private final ContainerLock  containerLock = new ContainerLock();

    public void handleSlotClick(EntityPlayerMP player, String containerId,
                                 ItemStack[] container, ItemStack cursor,
                                 int slotIndex, int button) {

        UUID uid = player.getGameProfile().getId();

        // 1. Rate limit — reject if too many clicks
        if (!rateLimiter.check(uid)) {
            LOGGER.warn("[AntiDupe] Rate-limited {}", player.getName());
            sendFullState(player, container);
            return;
        }

        // 2. Serialized concurrent access per container
        synchronized (containerLock.getLock(containerId)) {

            // 3. Snapshot BEFORE the operation
            ItemStack[] containerSnap = ItemIntegrityValidator.snapshot(container);
            ItemStack   cursorSnap    = cursor.isEmpty() ? ItemStack.EMPTY : cursor.copy();
            ItemStack[] invSnap       = ItemIntegrityValidator.snapshotPlayerInventory(player);

            // 4. Perform the business operation
            doSlotClick(container, cursor, player, slotIndex, button);

            // 5. Validate integrity
            if (!ItemIntegrityValidator.validate(containerSnap, container,
                    cursorSnap, cursor, invSnap, player)) {
                // Duplication detected
                int totalBefore = ItemIntegrityValidator.countItems(containerSnap)
                        + (cursorSnap.isEmpty() ? 0 : cursorSnap.getCount())
                        + ItemIntegrityValidator.countItems(invSnap);
                int totalAfter  = ItemIntegrityValidator.countItems(container)
                        + (cursor.isEmpty() ? 0 : cursor.getCount())
                        + ItemIntegrityValidator.countPlayerInventory(player);
                int delta = totalAfter - totalBefore;

                LOGGER.warn("[AntiDupe] DUPE DETECTED! Player: {} delta: +{}",
                    player.getName(), delta);

                // 6. Alert admins
                DupeAlertManager.alert(player, delta, "container-" + containerId);

                // 7. Rollback
                ItemIntegrityValidator.rollback(container, containerSnap, player, invSnap);
                persistContainer(containerId, containerSnap); // re-persist restored state
                return;
            }

            // 8. No duplication — persist
            persistContainer(containerId, container);
        }

        // Send new state to client
        sendFullState(player, container);
    }

    // Call on PlayerLoggedOutEvent
    public void onPlayerDisconnect(EntityPlayerMP player) {
        rateLimiter.clear(player.getGameProfile().getId());
    }

    // Call when the container is permanently deleted
    public void onContainerDeleted(String containerId) {
        containerLock.removeLock(containerId);
    }
}
Operation order

Order matters: rate limit first (fast rejection without locking), then concurrent lock, then snapshot, then operation, then validation. Never snapshot after the operation — the snapshot must capture the state before it.

Persist AFTER rollback

ItemIntegrityValidator.rollback() restores the in-memory arrays and synchronizes the player inventory, but does not persist the container to the database or NBT. If the container is stored externally, re-persist it manually after the rollback (using the original snapshot).