added FingerprintData class and improved device fingerprinting logic

This commit is contained in:
2025-10-05 15:46:45 +02:00
parent 9fca7430a8
commit aad1fcafa6
3 changed files with 140 additions and 96 deletions

View File

@@ -11,7 +11,6 @@ import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerResourcePackStatusEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import spark.Response;
import java.io.IOException;
@@ -21,7 +20,7 @@ import java.net.URI;
import java.util.*;
public class DeviceFingerprinting extends Appliance {
public record PackInfo(String url, UUID uuid, String hash) {
public record PackInfo(@NotNull String url, @NotNull UUID uuid, @NotNull String hash) {
private static final String failingUrl = "http://127.0.0.1:0";
public PackInfo asFailing() {
return new PackInfo(failingUrl, this.uuid, this.hash);
@@ -29,14 +28,15 @@ public class DeviceFingerprinting extends Appliance {
}
public enum PackStatus {
ERROR,
SUCCESS;
UNCACHED,
CACHED,
INVALID;
public static PackStatus fromBukkitStatus(PlayerResourcePackStatusEvent.Status status) {
return switch(status) {
case DISCARDED -> SUCCESS;
case FAILED_DOWNLOAD -> ERROR;
default -> throw new IllegalStateException("Unexpected value: " + status);
case DISCARDED -> CACHED;
case FAILED_DOWNLOAD -> UNCACHED;
default -> INVALID;
};
}
}
@@ -44,102 +44,60 @@ public class DeviceFingerprinting extends Appliance {
public enum PlayerStatus {
PREPARATION,
TESTING,
FINISHED;
}
public record FingerprintData(
Player player,
PlayerStatus status,
@Nullable Long fingerPrint,
@Nullable List<PackStatus> pendingPacks
) {
public FingerprintData(Player player) {
this(player, PlayerStatus.PREPARATION, null, null);
}
FINISHED,
NEW
}
private List<PackInfo> packs;
private final Map<Player, List<PackStatus>> pendingPacks = new WeakHashMap<>();
private final Map<Player, FingerprintData> fingerprints = new WeakHashMap<>();
private final UUID basePackId = UUID.randomUUID();
@Override
public void onEnable() {
this.packs = this.readPacksFromConfig();
}
public void testAllPacks(Player player) {
this.pendingPacks.put(player, Arrays.asList(new PackStatus[this.packs.size()]));
System.out.println("Sending packs...");
this.packs.forEach(pack -> this.sendPack(player, pack.asFailing()));
public void startFingerprinting(Player player) {
this.fingerprints.put(player, FingerprintData.create(player));
Main.logger().info(String.format("Sending base ressource-pack with id '%s' to '%s'%n", this.basePackId, player.getName()));
this.sendPack(player, new PackInfo("http://localhost:8080/api/devicefingerprinting/base.zip", this.basePackId, "3296e8bdd30b4f7cffd11c780a1dc70da2948e71"));
}
public void onPackReceive(Player player, UUID packId, PlayerResourcePackStatusEvent.Status status) {
if(!this.pendingPacks.containsKey(player)) return;
public void onPackUpdate(Player player, UUID packId, PlayerResourcePackStatusEvent.Status status) {
if(!this.fingerprints.containsKey(player)) return;
FingerprintData playerFingerprint = this.fingerprints.get(player);
if(!playerFingerprint.isInTestingOrPreparation()) return;
if(packId.equals(this.basePackId)) {
Main.logger().info(String.format("Base pack for '%s' updated: '%s'", player.getName(), status));
if(status != PlayerResourcePackStatusEvent.Status.ACCEPTED) return;
Main.logger().info(String.format("Base pack loaded successfully, sending now all packs to '%s'...", player.getName()));
playerFingerprint.setTesting();
this.packs.forEach(pack -> this.sendPack(player, pack.asFailing()));
return;
}
PackInfo pack = this.packs.stream()
.filter(packInfo -> packInfo.uuid.equals(packId))
.findFirst()
.filter(packInfo -> Objects.equals(packInfo.uuid, packId))
.findAny()
.orElse(null);
if(pack == null) return;
int packIndex = this.packs.indexOf(pack);
List<PackStatus> currentPendingStatus = this.pendingPacks.get(player);
try {
currentPendingStatus.set(packIndex, PackStatus.fromBukkitStatus(status));
System.out.println(packIndex + " > " + PackStatus.fromBukkitStatus(status));
} catch(IllegalStateException ignored) {
return;
List<PackStatus> pendingPacks = playerFingerprint.getPendingPacks();
PackStatus newPackStatus = PackStatus.fromBukkitStatus(status);
if(newPackStatus == PackStatus.INVALID) return;
pendingPacks.set(packIndex, newPackStatus);
playerFingerprint.updateFingerprint();
if(Objects.requireNonNull(playerFingerprint.getStatus()) == PlayerStatus.NEW) {
Main.logger().info(String.format("Sending fingerprint packs to Player '%s', as it is a unseen Player!", player.getName()));
this.sendNewFingerprint(player, Objects.requireNonNull(playerFingerprint.getFingerPrint()));
}
long fingerPrint = this.calculateFingerprintId(currentPendingStatus);
if(fingerPrint == -1) return;
if(fingerPrint == 0) {
// new Player
long newFingerprintId = this.generateNewFingerprintId();
System.out.println("New Fingerprint: " + newFingerprintId);
this.sendMarkedPacks(player, newFingerprintId);
} else {
System.out.println("Fingerprint: " + fingerPrint);
}
this.pendingPacks.remove(player);
}
private long calculateFingerprintId(List<PackStatus> packStatus) {
long fingerprintId = 0;
for (int i = 0; i < packStatus.size(); i++) {
var status = packStatus.get(i);
if(status == null) return -1;
switch (status) {
case SUCCESS:
fingerprintId |= 1L << i;
break;
case ERROR:
break;
default:
return -1;
}
}
return fingerprintId;
}
private long generateNewFingerprintId() {
long id = 0;
Random random = new Random();
for (int i = 0; i < this.packs.size() / 2; i++) {
while (true) {
int bitIndex = random.nextInt(this.packs.size());
if ((id & (1L << bitIndex)) == 0) {
id |= 1L << bitIndex;
break;
}
}
}
return id;
}
private void sendMarkedPacks(Player player, long fingerprintId) {
private void sendNewFingerprint(Player player, long fingerprintId) {
for (int i = 0; i < this.packs.size(); i++) {
if ((fingerprintId & (1L << i)) != 0) {
PackInfo pack = this.packs.get(i);
@@ -149,7 +107,6 @@ public class DeviceFingerprinting extends Appliance {
}
public void sendPack(Player player, PackInfo pack) {
System.out.println("Sending pack: " + pack.url);
player.sendResourcePacks(
ResourcePackRequest.resourcePackRequest()
.required(true)
@@ -161,12 +118,9 @@ public class DeviceFingerprinting extends Appliance {
try (InputStreamReader reader = new InputStreamReader(Objects.requireNonNull(Main.class.getResourceAsStream("/deviceFingerprinting/packs.json")))) {
Type packListType = new TypeToken<List<DeviceFingerprinting.PackInfo>>(){}.getType();
List<DeviceFingerprinting.PackInfo> packs = new Gson().fromJson(reader, packListType);
if (packs.isEmpty())
throw new IllegalStateException("No resource packs found in packs.json.");
if (packs.isEmpty()) throw new IllegalStateException("No resource packs found in packs.json.");
return packs;
} catch (Exception e) {
} catch (IOException e) {
throw new IllegalStateException("Failed to parse packs.json.", e);
}
}
@@ -209,6 +163,10 @@ public class DeviceFingerprinting extends Appliance {
}
}
public List<PackInfo> getPacks() {
return this.packs;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(

View File

@@ -0,0 +1,91 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.deviceFingerprinting;
import eu.mhsl.craftattack.spawn.core.Main;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
class FingerprintData {
public final Player player;
private DeviceFingerprinting.PlayerStatus status;
private @Nullable Long fingerPrint;
private final List<DeviceFingerprinting.PackStatus> pendingPacks;
int packCount = Main.instance().getAppliance(DeviceFingerprinting.class).getPacks().size();
private FingerprintData(Player player) {
this.player = player;
this.status = DeviceFingerprinting.PlayerStatus.PREPARATION;
this.fingerPrint = null;
this.pendingPacks = Arrays.asList(new DeviceFingerprinting.PackStatus[this.packCount]);
}
public static FingerprintData create(Player player) {
return new FingerprintData(player);
}
public void setTesting() {
this.status = DeviceFingerprinting.PlayerStatus.TESTING;
}
public void updateFingerprint() {
long fingerPrint = 0;
for (int i = 0; i < this.pendingPacks.size(); i++) {
var status = this.pendingPacks.get(i);
if(status == null) return;
switch (status) {
case CACHED:
fingerPrint |= 1L << i;
break;
case UNCACHED:
break;
default:
return;
}
}
if(fingerPrint == 0) {
this.status = DeviceFingerprinting.PlayerStatus.NEW;
this.fingerPrint = this.createNewFingerprint();
Main.logger().info(String.format("Player %s's was marked as a new Player!", this.player.getName()));
} else {
this.status = DeviceFingerprinting.PlayerStatus.FINISHED;
this.fingerPrint = fingerPrint;
}
Main.logger().info(String.format("Player %s's fingerprint is '%s'", this.player.getName(), fingerPrint));
}
private long createNewFingerprint() {
long id = 0;
Random random = new Random();
for (int i = 0; i < this.packCount / 2; i++) {
while (true) {
int bitIndex = random.nextInt(this.packCount);
if ((id & (1L << bitIndex)) == 0) {
id |= 1L << bitIndex;
break;
}
}
}
return id;
}
public List<DeviceFingerprinting.PackStatus> getPendingPacks() {
return this.pendingPacks;
}
public DeviceFingerprinting.PlayerStatus getStatus() {
return this.status;
}
public @Nullable Long getFingerPrint() {
return this.fingerPrint;
}
public boolean isInTestingOrPreparation() {
return this.status == DeviceFingerprinting.PlayerStatus.TESTING || this.status == DeviceFingerprinting.PlayerStatus.PREPARATION;
}
}

View File

@@ -8,17 +8,12 @@ import org.bukkit.event.player.PlayerResourcePackStatusEvent;
class PlayerJoinListener extends ApplianceListener<DeviceFingerprinting> {
@EventHandler
public void onJoin(PlayerJoinEvent event) {
// if(Bukkit.getServer().getServerResourcePack() != null && !event.getPlayer().hasResourcePack()) {
// System.out.println("NO RESSOURCEPACK");
// return;
// }
this.getAppliance().testAllPacks(event.getPlayer());
this.getAppliance().startFingerprinting(event.getPlayer());
}
@EventHandler
public void onResourcePackEvent(PlayerResourcePackStatusEvent event) {
this.getAppliance().onPackReceive(
this.getAppliance().onPackUpdate(
event.getPlayer(),
event.getID(),
event.getStatus()