Security
Protege les conteneurs partagés contre la duplication d'items — rate limiting, validation d'integrite, verrouillage concurrent et alertes admin.
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 |
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
// 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);
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, |
Retourne true si le total n'a pas augmente. Surcharge numerique pour les deux comptes d'inventaire. |
validate(ItemStack[] containerBefore, ItemStack[] containerAfter, |
Surcharge pratique : passe le snapshot AVANT et le joueur live pour le compte APRES. |
Methode de rollback
| Methode | Description |
|---|---|
rollback(ItemStack[] container, ItemStack[] containerSnapshot, |
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
// 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);
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
// 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);
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
[AntiDupe] NomDuJoueur a tente de dupliquer +3 items (coffre de faction)
Exemple d'utilisation
// 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).
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);
}
}
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.
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).