Complete private authentication, server integrations and activity reporting
Add Fabric/Forge version builds and Paper integration, preserve permanent player identities across renames, harden session authorization, and surface privacy-conscious launcher/server activity in the panel.
This commit is contained in:
commit
a1f19e86c6
74 files changed
+2108
-95
No files matched your search
@@ -0,0 +1,46 @@
|
||||
package net.scopenet.integration;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ActivityTest {
|
||||
@Test void deltasAndFractionalPlaytimeSurviveSnapshots() {
|
||||
AtomicLong time = new AtomicLong();
|
||||
Activity activity = new Activity(time::get);
|
||||
UUID id = UUID.randomUUID();
|
||||
activity.join(id, "Steve");
|
||||
activity.join(id, "Steve");
|
||||
activity.add(id, "Steve", "blocks_broken", 3);
|
||||
time.set(1_500_000_000L);
|
||||
JsonObject first = activity.drain(19.5);
|
||||
JsonObject row = first.getAsJsonArray("stats").get(0).getAsJsonObject();
|
||||
assertEquals(1, row.get("joins").getAsInt());
|
||||
assertEquals(1, row.get("playtime_secs").getAsInt());
|
||||
assertEquals(3, row.get("blocks_broken").getAsInt());
|
||||
time.set(2_000_000_000L);
|
||||
JsonObject second = activity.drain(20);
|
||||
assertEquals(1, second.getAsJsonArray("stats").get(0).getAsJsonObject().get("playtime_secs").getAsInt());
|
||||
assertFalse(second.getAsJsonArray("stats").get(0).getAsJsonObject().has("joins"));
|
||||
assertNotEquals(first.get("batch_id"), second.get("batch_id"));
|
||||
activity.leave(id, "Steve");
|
||||
activity.leave(id, "Steve");
|
||||
JsonObject last = activity.drain(20);
|
||||
assertEquals(0, last.getAsJsonArray("online").size());
|
||||
assertEquals(1, last.getAsJsonArray("events").size());
|
||||
}
|
||||
|
||||
@Test void shutdownClearsOnlineAndIncludesLastSessionTime() {
|
||||
AtomicLong time = new AtomicLong();
|
||||
Activity activity = new Activity(time::get);
|
||||
activity.join(UUID.randomUUID(), "Steve");
|
||||
time.set(3_000_000_000L);
|
||||
activity.leaveAll();
|
||||
JsonObject payload = activity.drain(Double.NaN);
|
||||
assertEquals(0, payload.getAsJsonArray("online").size());
|
||||
assertEquals(3, payload.getAsJsonArray("stats").get(0).getAsJsonObject().get("playtime_secs").getAsInt());
|
||||
assertEquals(20, payload.get("tps").getAsInt());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.scopenet.integration;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.jupiter.api.*;
|
||||
import java.net.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class PanelClientTest {
|
||||
private HttpServer server;
|
||||
private Settings settings;
|
||||
private final String token = "sn_" + "a".repeat(40);
|
||||
|
||||
@BeforeEach void start() throws Exception {
|
||||
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.start();
|
||||
settings = Settings.of("http://127.0.0.1:" + server.getAddress().getPort(), token);
|
||||
}
|
||||
@AfterEach void stop() { server.stop(0); }
|
||||
|
||||
private void endpoint(String path, int status, String response, AtomicReference<JsonObject> received) {
|
||||
server.createContext("/api/server/v1/" + path, exchange -> {
|
||||
assertEquals("Bearer " + token, exchange.getRequestHeaders().getFirst("Authorization"));
|
||||
assertEquals("POST", exchange.getRequestMethod());
|
||||
received.set(JsonParser.parseString(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)).getAsJsonObject());
|
||||
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(status, bytes.length);
|
||||
exchange.getResponseBody().write(bytes);
|
||||
exchange.close();
|
||||
});
|
||||
}
|
||||
|
||||
@Test void postsAuthenticatedIdentityAndPreservesDenialMessage() throws Exception {
|
||||
AtomicReference<JsonObject> received = new AtomicReference<>();
|
||||
endpoint("login", 200, "{\"allowed\":false,\"message\":\"Account disabled\"}", received);
|
||||
Integration integration = integration();
|
||||
UUID id = UUID.randomUUID();
|
||||
assertEquals("Account disabled", integration.login(id, "Steve", "203.0.113.9").get(2, TimeUnit.SECONDS));
|
||||
assertEquals(id.toString(), received.get().get("uuid").getAsString());
|
||||
assertEquals("203.0.113.9", received.get().get("ip").getAsString());
|
||||
integration.close();
|
||||
}
|
||||
|
||||
@Test void malformedAndHttpFailuresDenyAccess() throws Exception {
|
||||
endpoint("login", 200, "{}", new AtomicReference<>());
|
||||
Integration integration = integration();
|
||||
assertEquals(Integration.UNAVAILABLE, integration.login(UUID.randomUUID(), "Steve", null).get(2, TimeUnit.SECONDS));
|
||||
server.removeContext("/api/server/v1/login");
|
||||
endpoint("login", 503, "{\"allowed\":true}", new AtomicReference<>());
|
||||
assertEquals(Integration.UNAVAILABLE, integration.login(UUID.randomUUID(), "Steve", null).get(2, TimeUnit.SECONDS));
|
||||
integration.close();
|
||||
}
|
||||
|
||||
@Test void onlyExplicitBooleanTrueAllowsLogin() throws Exception {
|
||||
assertNull(PanelClient.verdict(JsonParser.parseString("{\"allowed\":true}").getAsJsonObject()));
|
||||
for (String bad : List.of("{}", "{\"allowed\":\"true\"}", "{\"allowed\":null}", "{\"allowed\":1}")) {
|
||||
assertThrows(IOException.class, () -> PanelClient.verdict(JsonParser.parseString(bad).getAsJsonObject()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void redirectsNeverForwardTheServerToken() {
|
||||
server.createContext("/api/server/v1/login", exchange -> {
|
||||
exchange.getResponseHeaders().set("Location", "http://127.0.0.1:1/stolen");
|
||||
exchange.sendResponseHeaders(302, -1);
|
||||
exchange.close();
|
||||
});
|
||||
assertThrows(IOException.class, () -> new PanelClient(settings).post("login", new JsonObject()));
|
||||
}
|
||||
|
||||
@Test void invalidSettingsAndOfflineModeAreRejected() {
|
||||
assertThrows(IllegalArgumentException.class, () -> Settings.of("https://user:pass@panel.example.com", token));
|
||||
assertThrows(IllegalArgumentException.class, () -> Settings.of("file:///tmp/panel", token));
|
||||
assertThrows(IllegalArgumentException.class, () -> Settings.of("https://panel.example.com", ""));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Integration(settings, "test", "1", "1", false, 20, s -> {}, (id, msg) -> {}));
|
||||
}
|
||||
|
||||
private Integration integration() {
|
||||
return new Integration(settings, "test", "1.21.1", "test", true, 20, s -> {}, (id, msg) -> {});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user