diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/loginLog/LoginAttempt.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/loginLog/LoginAttempt.java index 65ac0f1..d8e098b 100644 --- a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/loginLog/LoginAttempt.java +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/loginLog/LoginAttempt.java @@ -19,6 +19,7 @@ record LoginAttempt( ) { static final String STAGE_PRE_LOGIN = "PRE_LOGIN"; static final String STAGE_LOGIN = "LOGIN"; + static final String STAGE_WEBSITE = "WEBSITE"; static final String RESULT_ABORTED = "ABORTED"; static String ipOf(@Nullable InetAddress address) { diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/loginLog/LoginLog.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/loginLog/LoginLog.java index ed39f5f..0535378 100644 --- a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/loginLog/LoginLog.java +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/loginLog/LoginLog.java @@ -1,14 +1,16 @@ package eu.mhsl.craftattack.spawn.common.appliances.tooling.loginLog; +import com.google.common.net.InetAddresses; 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 eu.mhsl.craftattack.spawn.core.database.Database; +import eu.mhsl.craftattack.spawn.core.util.server.Floodgate; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.event.Listener; import org.bukkit.scheduler.BukkitTask; -import org.geysermc.floodgate.api.FloodgateApi; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -20,6 +22,7 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -27,6 +30,7 @@ import java.util.concurrent.ConcurrentHashMap; /** * Records every login attempt (allowed or not) with its IP address in the table login_attempts * (schema: resources/migrations/loginLog). Basis for detecting ban evasion. + * The website reports signups and logins of its users via POST /api/loginlog/website. * Never blocks or delays a login: writes run on the database thread, failures are only logged. */ public class LoginLog extends Appliance { @@ -68,7 +72,7 @@ public class LoginLog extends Appliance { void onPreLogin(UUID uuid, String name, @Nullable InetAddress address, String result, @Nullable Component kickMessage) { if(!this.database.isAvailable()) return; LoginAttempt attempt = new LoginAttempt( - Instant.now(), uuid, name, LoginAttempt.ipOf(address), this.isBedrock(uuid), + Instant.now(), uuid, name, LoginAttempt.ipOf(address), Floodgate.isFloodgateUuid(uuid), LoginAttempt.STAGE_PRE_LOGIN, result, plainText(kickMessage) ); @@ -82,7 +86,7 @@ public class LoginLog extends Appliance { LoginAttempt pending = this.pendingAttempts.remove(uuid); LoginAttempt attempt = pending != null ? pending - : new LoginAttempt(Instant.now(), uuid, name, LoginAttempt.ipOf(address), this.isBedrock(uuid), "", "", null); + : new LoginAttempt(Instant.now(), uuid, name, LoginAttempt.ipOf(address), Floodgate.isFloodgateUuid(uuid), "", "", null); this.write(attempt.withOutcome(LoginAttempt.STAGE_LOGIN, result, plainText(kickMessage))); } @@ -124,12 +128,41 @@ public class LoginLog extends Appliance { }); } - private boolean isBedrock(UUID uuid) { + private record WebsiteEvent(String event, String uuid, String username, String ip) { + } + + private void recordWebsiteEvent(WebsiteEvent request) { + if(request == null) throw new IllegalArgumentException("Request body missing"); + if(!this.database.isAvailable()) throw new IllegalStateException("No database configured"); + + String event = request.event() == null ? "" : request.event().trim().toUpperCase(Locale.ROOT); + if(!event.equals("SIGNUP") && !event.equals("LOGIN")) + throw new IllegalArgumentException("Field 'event' must be 'signup' or 'login'"); + if(request.uuid() == null || request.uuid().isBlank()) + throw new IllegalArgumentException("Field 'uuid' is required"); + UUID uuid; try { - return FloodgateApi.getInstance().isFloodgatePlayer(uuid); - } catch(Throwable e) { - return false; + uuid = UUID.fromString(request.uuid().trim()); + } catch(IllegalArgumentException e) { + throw new IllegalArgumentException("Field 'uuid' is not a valid UUID"); } + if(request.username() == null || request.username().isBlank()) + throw new IllegalArgumentException("Field 'username' is required"); + if(request.ip() == null || !InetAddresses.isInetAddress(request.ip().trim())) + throw new IllegalArgumentException("Field 'ip' is not a valid IP address"); + + this.write(new LoginAttempt( + Instant.now(), uuid, request.username().trim(), request.ip().trim(), Floodgate.isFloodgateUuid(uuid), + LoginAttempt.STAGE_WEBSITE, event, null + )); + } + + @Override + public void httpApi(HttpServer.ApiBuilder apiBuilder) { + apiBuilder.post("website", WebsiteEvent.class, (request, original) -> { + this.recordWebsiteEvent(request); + return HttpServer.nothing; + }); } private static @Nullable String plainText(@Nullable Component component) {