implemented working fingerprinting prototype

This commit is contained in:
2025-10-05 13:24:47 +02:00
parent 7c254707c1
commit 9fca7430a8
32 changed files with 413 additions and 2 deletions

View File

@@ -0,0 +1,218 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.deviceFingerprinting;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import net.kyori.adventure.resource.ResourcePackInfo;
import net.kyori.adventure.resource.ResourcePackRequest;
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;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.*;
public class DeviceFingerprinting extends Appliance {
public record PackInfo(String url, UUID uuid, String hash) {
private static final String failingUrl = "http://127.0.0.1:0";
public PackInfo asFailing() {
return new PackInfo(failingUrl, this.uuid, this.hash);
}
}
public enum PackStatus {
ERROR,
SUCCESS;
public static PackStatus fromBukkitStatus(PlayerResourcePackStatusEvent.Status status) {
return switch(status) {
case DISCARDED -> SUCCESS;
case FAILED_DOWNLOAD -> ERROR;
default -> throw new IllegalStateException("Unexpected value: " + status);
};
}
}
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);
}
}
private List<PackInfo> packs;
private final Map<Player, List<PackStatus>> pendingPacks = new WeakHashMap<>();
@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 onPackReceive(Player player, UUID packId, PlayerResourcePackStatusEvent.Status status) {
if(!this.pendingPacks.containsKey(player)) return;
PackInfo pack = this.packs.stream()
.filter(packInfo -> packInfo.uuid.equals(packId))
.findFirst()
.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;
}
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) {
for (int i = 0; i < this.packs.size(); i++) {
if ((fingerprintId & (1L << i)) != 0) {
PackInfo pack = this.packs.get(i);
this.sendPack(player, pack);
}
}
}
public void sendPack(Player player, PackInfo pack) {
System.out.println("Sending pack: " + pack.url);
player.sendResourcePacks(
ResourcePackRequest.resourcePackRequest()
.required(true)
.packs(ResourcePackInfo.resourcePackInfo(pack.uuid, URI.create(pack.url), pack.hash))
);
}
private List<DeviceFingerprinting.PackInfo> readPacksFromConfig() {
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.");
return packs;
} catch (Exception e) {
throw new IllegalStateException("Failed to parse packs.json.", e);
}
}
@Override
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
apiBuilder.rawGet(
"base.zip",
(request, response) -> this.servePack("base.zip", response)
);
for(int i = 0; i < this.packs.size(); i++) {
int packIndex = i;
apiBuilder.rawGet(
String.format("packs/%d", i),
(request, response) -> this.servePack(String.valueOf(packIndex), response)
);
}
}
private Object servePack(String name, Response response) {
try {
String resourcePath = String.format("/deviceFingerprinting/packs/%s", name);
var inputStream = Main.class.getResourceAsStream(resourcePath);
if (inputStream == null) {
throw new IllegalStateException("Pack file not found: " + resourcePath);
}
response.header("Content-Type", "application/zip");
response.header("Content-Disposition", String.format("attachment; filename=\"pack-%s.zip\"", name));
var outputStream = response.raw().getOutputStream();
inputStream.transferTo(outputStream);
outputStream.close();
return HttpServer.nothing;
} catch (IOException e) {
throw new RuntimeException(String.format("Failed to serve pack '%s'", name), e);
}
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new PlayerJoinListener()
);
}
}

View File

@@ -0,0 +1,27 @@
package eu.mhsl.craftattack.spawn.common.appliances.tooling.deviceFingerprinting;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
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());
}
@EventHandler
public void onResourcePackEvent(PlayerResourcePackStatusEvent event) {
this.getAppliance().onPackReceive(
event.getPlayer(),
event.getID(),
event.getStatus()
);
}
}