Security

Protege les conteneurs partagés contre la duplication d'items — rate limiting, validation d'integrite, verrouillage concurrent et alertes admin.

fr.eri.eriapi.security Forge 1.12.2 Java 8

Introduction

Le module Security fournit quatre classes utilitaires independantes pour proteger les GUI serveur contre les exploits de duplication d'items. Ces classes sont generiques : elles fonctionnent avec n'importe quel conteneur (ItemStack[]), n'importe quel joueur (EntityPlayerMP) et s'integrent sans configuration.

Les quatre classes couvrent les quatre vecteurs d'attaque principaux :

Classe Protection
GuiRateLimiter Limite le nombre de clics par seconde pour empecher les macros et le spam
ItemIntegrityValidator Verifie que le total d'items ne peut pas augmenter apres une operation
ContainerLock Acces concurrent serialise par cle — empeche les race conditions entre threads
DupeAlertManager Alerte les operateurs en ligne et loggue toute tentative de duplication detectee
Package

Toutes les classes de ce module se trouvent dans le package fr.eri.eriapi.security.

GuiRateLimiter

Limite le nombre d'actions par seconde par joueur. Utilise une fenetre glissante d'une seconde : si un joueur declenche plus de maxPerSecond actions dans la meme seconde, check() retourne false jusqu'a ce que la fenetre se reinitialise.

Thread-safe : utilise un ConcurrentHashMap en interne.

Constructeur

Parametre Type Description
maxPerSecond int Nombre maximum d'actions autorisees par joueur par seconde

Methodes

Methode Retour Description
check(UUID playerId) boolean true si l'action est permise, false si rate-limited
clear(UUID playerId) void Supprime le tracker du joueur — appeler a la deconnexion
clearAll() void Supprime tous les trackers — appeler au dechargement du monde
getMaxPerSecond() int Retourne le seuil configure

Exemple d'utilisation

Java — GuiRateLimiter
// Creer un limiteur de 20 clics par seconde (champ de la classe)
private final GuiRateLimiter chestRateLimiter = new GuiRateLimiter(20);

// A chaque action joueur :
UUID uid = player.getGameProfile().getId();
if (!chestRateLimiter.check(uid)) {
    // Trop de clics — rejeter silencieusement
    LOGGER.warn("[AntiDupe] Rate-limited {}", player.getName());
    return;
}
// ... traiter l'action

// A la deconnexion du joueur :
chestRateLimiter.clear(uid);
Plusieurs limiteurs distincts

Cree un GuiRateLimiter different pour chaque type d'action si les seuils doivent etre differents. Par exemple : 20/s pour les clics de coffre, 10/s pour les collectes de contrats.

ItemIntegrityValidator

Valide que le nombre total d'items ne peut pas augmenter pendant une operation conteneur <-> inventaire joueur. Si le total apres depasse le total avant, une duplication a ete detectee et l'operation doit etre annulee.

Toutes les methodes sont statiques — aucune instance requise.

Methodes de snapshot

Methode Description
snapshot(ItemStack[] items) Copie profonde d'un tableau d'ItemStack. Les stacks vides sont stockes comme ItemStack.EMPTY.
snapshotPlayerInventory(EntityPlayerMP player) Copie profonde des 36 slots de l'inventaire principal du joueur.

Methodes de comptage

Methode Description
countItems(ItemStack[] items) Somme des getCount() de tous les stacks non vides du tableau.
countPlayerInventory(EntityPlayerMP player) Compte les items actuels dans les 36 slots principaux du joueur (etat live).

Methodes de validation

Signature Description
validate(ItemStack[] containerBefore, ItemStack[] containerAfter,
ItemStack cursorBefore, ItemStack cursorAfter,
int invCountBefore, int invCountAfter)
Retourne true si le total n'a pas augmente. Surcharge numerique pour les deux comptes d'inventaire.
validate(ItemStack[] containerBefore, ItemStack[] containerAfter,
ItemStack cursorBefore, ItemStack cursorAfter,
ItemStack[] invSnapshotBefore, EntityPlayerMP player)
Surcharge pratique : passe le snapshot AVANT et le joueur live pour le compte APRES.

Methode de rollback

Methode Description
rollback(ItemStack[] container, ItemStack[] containerSnapshot,
EntityPlayerMP player, ItemStack[] inventorySnapshot)
Restaure le conteneur et l'inventaire joueur depuis leurs snapshots. Appelle aussi player.inventoryContainer.detectAndSendChanges().
Important : si le conteneur est stocke en externe (NBT, base de donnees), le persister apres le rollback.

Workflow snapshot / validate / rollback

Java — pattern complet avant/apres
// 1. Snapshot AVANT l'operation
ItemStack[] containerSnap = ItemIntegrityValidator.snapshot(container);
ItemStack   cursorSnap    = cursor.isEmpty() ? ItemStack.EMPTY : cursor.copy();
ItemStack[] invSnap       = ItemIntegrityValidator.snapshotPlayerInventory(player);

// 2. Effectuer l'operation (modifier container, cursor, inventaire joueur)
doOperation(container, cursor, player);

// 3. Valider — rejeter si des items ont ete crees
if (!ItemIntegrityValidator.validate(containerSnap, container,
        cursorSnap, cursor, invSnap, player)) {
    // Duplication detectee — restaurer l'etat
    ItemIntegrityValidator.rollback(container, containerSnap, player, invSnap);
    // Re-persister le conteneur si necessaire
    persistContainer(containerSnap);
    return;
}

// 4. Persister — aucune duplication
persistContainer(container);
Items perdus (delta negatif)

validate() retourne true si le total apres est inferieur ou egal au total avant — c'est-a-dire qu'une perte d'items est acceptee (ils ont peut-etre ete droppes). Seul une creation d'items est detectee comme dupe.

ContainerLock

Fournit un verrou Object distinct par cle logique (ex : ID de faction, UUID de conteneur). Utiliser ces verrous dans des blocs synchronized empeche que deux threads traitent simultanement la meme ressource partagee.

L'avantage par rapport a un verrou global unique est la granularite : deux factions differentes peuvent etre traitees en parallele, mais deux joueurs de la meme faction ne peuvent pas modifier le meme coffre en meme temps.

Thread-safe : utilise un ConcurrentHashMap en interne.

Methodes

Methode Description
getLock(String key) Retourne le verrou pour la cle donnee, en le creant s'il n'existe pas encore.
removeLock(String key) Supprime le verrou de la cle — appeler quand la ressource est detruite (ex : faction dissoute).
clearAll() Supprime tous les verrous.
size() Retourne le nombre de cles actuellement tracees.

Exemple d'utilisation

Java — ContainerLock
// Champ de la classe
private final ContainerLock containerLock = new ContainerLock();

// Proteger une operation sur le coffre d'une faction
synchronized (containerLock.getLock(factionInfo.id)) {
    handleChestOperation(player, fm, factionInfo);
    sendChestFullState(player, factionInfo);
}

// Quand une faction est dissoute :
containerLock.removeLock(factionInfo.id);
Pas de timeout

ContainerLock utilise des blocs synchronized standard Java — il n'y a pas de timeout integre. Si le code dans le bloc synchronise peut bloquer indefiniment (ex : acces reseau), envisage un ReentrantLock avec tryLock() a la place.

DupeAlertManager

Envoie des alertes en temps reel a tous les operateurs en ligne (niveau de permission ≥ 2) quand une tentative de duplication est detectee. Loggue egalement l'evenement dans la console serveur sous le logger EriAPI-Security.

Toutes les methodes sont statiques — aucune instance requise.

Methode alert()

Parametre Type Description
exploiter EntityPlayerMP Le joueur qui a declenche la detection de duplication
itemDelta int Nombre d'items qui auraient ete crees (entier positif)
context String Description courte du contexte affichee dans l'alerte (ex : "coffre de faction")

Format du message admin

Apercu — message chat pour les operateurs
[AntiDupe] NomDuJoueur a tente de dupliquer +3 items (coffre de faction)

Exemple d'utilisation

Java — DupeAlertManager.alert()
// Apres avoir detecte une duplication :
int delta = totalApres - totalAvant;
LOGGER.warn("[AntiDupe] Duplication detectee pour {} : +{} items", player.getName(), delta);
DupeAlertManager.alert(player, delta, "coffre de faction");
// Effectuer le rollback...

Exemple complet — proteger un conteneur GUI custom

Voici comment utiliser les quatre classes ensemble pour proteger un conteneur arbitraire (ex : coffre de guilde, marche, stockage de faction).

Java — protection complete d'un conteneur
public class MonGuiHandler {

    // ── Instances des outils EriAPI Security ─────────────────────────────
    // Rate limiter : max 20 clics par seconde
    private final GuiRateLimiter rateLimiter  = new GuiRateLimiter(20);
    // Verrous par conteneur (cle = ID de guilde par exemple)
    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 — rejeter si trop de clics
        if (!rateLimiter.check(uid)) {
            LOGGER.warn("[AntiDupe] Rate-limited {}", player.getName());
            sendFullState(player, container);
            return;
        }

        // 2. Acces concurrent serialise par conteneur
        synchronized (containerLock.getLock(containerId)) {

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

            // 4. Effectuer l'operation metier
            doSlotClick(container, cursor, player, slotIndex, button);

            // 5. Valider l'integrite
            if (!ItemIntegrityValidator.validate(containerSnap, container,
                    cursorSnap, cursor, invSnap, player)) {
                // Duplication detectee
                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 DETECTEE ! Joueur: {} delta: +{}",
                    player.getName(), delta);

                // 6. Alerter les admins
                DupeAlertManager.alert(player, delta, "conteneur-" + containerId);

                // 7. Rollback
                ItemIntegrityValidator.rollback(container, containerSnap, player, invSnap);
                persistContainer(containerId, containerSnap); // re-persister l'etat revenu
                return;
            }

            // 8. Pas de duplication — persister
            persistContainer(containerId, container);
        }

        // Envoyer le nouvel etat au client
        sendFullState(player, container);
    }

    // A appeler sur PlayerLoggedOutEvent
    public void onPlayerDisconnect(EntityPlayerMP player) {
        rateLimiter.clear(player.getGameProfile().getId());
    }

    // A appeler quand le conteneur est supprime definitvement
    public void onContainerDeleted(String containerId) {
        containerLock.removeLock(containerId);
    }
}
Ordre des operations

L'ordre est important : rate limit en premier (rejet rapide sans lock), puis lock concurrent, puis snapshot, puis operation, puis validation. Ne jamais snapshotter apres l'operation — le snapshot doit capturer l'etat d'avant.

Persister APRES rollback

ItemIntegrityValidator.rollback() restaure les tableaux en memoire et synchronise l'inventaire joueur, mais ne persiste pas le conteneur en base de donnees ou en NBT. Si le conteneur est stocke de facon externe, le re-persister manuellement apres le rollback (avec le snapshot original).