2 Commits

33 changed files with 457 additions and 2 deletions

View File

@@ -0,0 +1,176 @@
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 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(@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);
}
}
public enum PackStatus {
UNCACHED,
CACHED,
INVALID;
public static PackStatus fromBukkitStatus(PlayerResourcePackStatusEvent.Status status) {
return switch(status) {
case DISCARDED -> CACHED;
case FAILED_DOWNLOAD -> UNCACHED;
default -> INVALID;
};
}
}
public enum PlayerStatus {
PREPARATION,
TESTING,
FINISHED,
NEW
}
private List<PackInfo> packs;
private final Map<Player, FingerprintData> fingerprints = new WeakHashMap<>();
private final UUID basePackId = UUID.randomUUID();
@Override
public void onEnable() {
this.packs = this.readPacksFromConfig();
}
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 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 -> Objects.equals(packInfo.uuid, packId))
.findAny()
.orElse(null);
if(pack == null) return;
int packIndex = this.packs.indexOf(pack);
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()));
}
}
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);
this.sendPack(player, pack);
}
}
}
public void sendPack(Player player, PackInfo pack) {
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 (IOException 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);
}
}
public List<PackInfo> getPacks() {
return this.packs;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(
new PlayerJoinListener()
);
}
}

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

@@ -0,0 +1,22 @@
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) {
this.getAppliance().startFingerprinting(event.getPlayer());
}
@EventHandler
public void onResourcePackEvent(PlayerResourcePackStatusEvent event) {
this.getAppliance().onPackUpdate(
event.getPlayer(),
event.getID(),
event.getStatus()
);
}
}

View File

@@ -0,0 +1,5 @@
## Files originally from "TrackPack"
https://github.com/ALaggyDev/TrackPack/blob/main/README.md
Discovered by: [Laggy](https://github.com/ALaggyDev/) and [NikOverflow](https://github.com/NikOverflow)

View File

@@ -0,0 +1,33 @@
import zipfile
import hashlib
import uuid
import json
SERVER_URL = "http://localhost:8080/api/devicefingerprinting"
packs = []
def file_sha1(path):
h = hashlib.sha1()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
for i in range(0, 24):
path = f"packs/{i}"
with zipfile.ZipFile(path, mode="w") as zf:
zf.writestr(
"pack.mcmeta",
'{"pack":{"pack_format":22,"supported_formats":[22,1000],"description":"pack ' + str(i) + '"}}',
)
hash = file_sha1(path)
packs.append({
"url": f"{SERVER_URL}/packs/{i}",
"uuid": str(uuid.uuid4()),
"hash": hash
})
with open("packs.json", "w") as f:
json.dump(packs, f, indent=4)

View File

@@ -0,0 +1,122 @@
[
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/0",
"uuid": "b35f6e2f-1b50-4493-85be-fb18bd90f9bb",
"hash": "7a39af839ea6484431f7b707759546bea991d435"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/1",
"uuid": "71095b62-d5ef-4ab2-ba3b-3c1b403f5e34",
"hash": "a9192ee73df1c5cff2c188fac6e9e638a1e7b6ce"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/2",
"uuid": "a4dba0a2-f8f2-4a81-bbb2-a9a818820330",
"hash": "6b85b0eb54865dae70bbda89746d83717dc2a214"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/3",
"uuid": "79fa2dc4-8c84-45fc-a09f-d89906f0d900",
"hash": "c7abf7a316f7e8c98985e8317a8b649e824e9f79"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/4",
"uuid": "15702c9b-a22b-426d-b48a-3d65b0368e9a",
"hash": "10cd0e2c46f192deb87ac75c149827d44a713017"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/5",
"uuid": "3d702d41-8e2f-4920-8dd0-1fd2146da9fb",
"hash": "8ad517d259e800b88a38ff00ee6721d5656822f2"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/6",
"uuid": "c20a2e47-ef43-49da-a80d-adf238df3695",
"hash": "798677405a4fd678892e1cf55585c8c91f82e1e2"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/7",
"uuid": "7ce51b81-1263-4919-9f4e-bb749ffe6e2e",
"hash": "af473b8eb7572f35d307bede5f2e20f263c0d804"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/8",
"uuid": "0c70d586-fe48-4ffc-86b0-6b9ec3bfe045",
"hash": "2fb698ff88f2436637641f3b2e6792201feb5144"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/9",
"uuid": "c7af75a8-0b72-495d-a0ff-c1c40e229c13",
"hash": "cf660460798eecf451d639873cc1fedc4661db1b"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/10",
"uuid": "248dbce6-4b2a-44b5-b038-8d718b0ced99",
"hash": "a8ebe708d0f3747c76e4e5e68db5dcb561922706"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/11",
"uuid": "10979174-cb02-40eb-a754-275551ad608d",
"hash": "54961b48db1582a1a0981c8cc9be5ae0f3122cf3"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/12",
"uuid": "a361cfa7-674c-4493-a4cf-4baff851f276",
"hash": "013719dc8da79c96b45a1c5319c20bffe1a56cc9"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/13",
"uuid": "24b39bdb-ada9-40ec-9e3a-132c74b81dc6",
"hash": "206898c6b6600d2648b2d79c61fc6255b19587d9"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/14",
"uuid": "158fc5b4-be2c-4f7a-98cb-af5993adcc90",
"hash": "061b266a7c526fb3a3152a4ea70ca5592e0b503c"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/15",
"uuid": "4f9097a7-be02-48ad-919c-f292307f8490",
"hash": "45a667a0fe06246defabca14ef1271fb6db5a1ac"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/16",
"uuid": "3ce31e60-7e8a-4fb1-8c6d-da9065bea798",
"hash": "75bb12e46203d49e89aa9a826d267552372758bc"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/17",
"uuid": "cd978e5c-3de0-4ada-8ec5-3a88a305eec6",
"hash": "5b20261f7be03e83e9c52307f1408b0c5e58358c"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/18",
"uuid": "75001e58-3999-4779-a1d1-43ab161770ce",
"hash": "544420cffb6c17113c06fb49eeba892c208719d3"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/19",
"uuid": "6a7005a9-c2ca-476d-9a12-07d120ee121a",
"hash": "fcc066a4d3193b60b102e3d906ad8dc0b0fcf65b"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/20",
"uuid": "521c0d84-d82e-49ef-b096-d9b90f15aa19",
"hash": "4545835983ec7f07d02675a69181a80dc396f038"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/21",
"uuid": "c1b590c5-43fc-41e3-83c0-47f35b14f845",
"hash": "8d4c670eaefc0482734e839b72758226dde13bc3"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/22",
"uuid": "43958a18-c087-4f2b-a6ea-066231606eb1",
"hash": "004282602f7bdbb7cd7724f23aae23876f224092"
},
{
"url": "http://localhost:8080/api/devicefingerprinting/packs/23",
"uuid": "4b91ac81-9de4-4c2b-a876-47e621496d10",
"hash": "dae68eae109e08ea4c4c943905502eb331939f64"
}
]

View File

@@ -0,0 +1 @@
!base.zip

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,6 +7,7 @@ import org.bukkit.configuration.ConfigurationSection;
import spark.Request;
import spark.Spark;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -43,12 +44,16 @@ public class HttpServer {
this.applianceName = appliance.getClass().getSimpleName().toLowerCase();
}
public void rawGet(String path, BiFunction<Request, spark.Response, Object> onCall) {
Spark.get(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req, resp)));
}
public void get(String path, Function<Request, Object> onCall) {
Spark.get(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req)));
}
public void rawPost(String path, Function<Request, Object> onCall) {
Spark.post(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req)));
public void rawPost(String path, BiFunction<Request, spark.Response, Object> onCall) {
Spark.post(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req, resp)));
}
public <TRequest> void post(String path, Class<TRequest> clazz, RequestProvider<TRequest, Request, Object> onCall) {