added connection key and recorded event to LoginLog
This commit is contained in:
1 parent
095db7e796
commit
86ae261c60
4 files changed
+148
-6
No files matched your search
+21
-6
@@ -5,6 +5,8 @@ import eu.mhsl.craftattack.spawn.core.Main;
|
|||||||
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
|
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
|
||||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||||
import eu.mhsl.craftattack.spawn.core.database.Database;
|
import eu.mhsl.craftattack.spawn.core.database.Database;
|
||||||
|
import eu.mhsl.craftattack.spawn.core.event.LoginAttemptRecordedEvent;
|
||||||
|
import eu.mhsl.craftattack.spawn.core.util.network.Networks;
|
||||||
import eu.mhsl.craftattack.spawn.core.util.server.Floodgate;
|
import eu.mhsl.craftattack.spawn.core.util.server.Floodgate;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||||
@@ -16,6 +18,8 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.Statement;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -30,6 +34,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
/**
|
/**
|
||||||
* Records every login attempt (allowed or not) with its IP address in the table login_attempts
|
* 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.
|
* (schema: resources/migrations/loginLog). Basis for detecting ban evasion.
|
||||||
|
* After each insert a LoginAttemptRecordedEvent is fired on the database thread.
|
||||||
* The website reports signups and logins of its users via POST /api/loginlog/website.
|
* 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.
|
* Never blocks or delays a login: writes run on the database thread, failures are only logged.
|
||||||
*/
|
*/
|
||||||
@@ -101,19 +106,29 @@ public class LoginLog extends Appliance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void write(LoginAttempt attempt) {
|
private void write(LoginAttempt attempt) {
|
||||||
String sql = "INSERT INTO login_attempts (at, uuid, name, ip, bedrock, stage, result, reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
String network = Networks.networkOf(attempt.ip());
|
||||||
|
String sql = "INSERT INTO login_attempts (at, uuid, name, ip, network, bedrock, stage, result, reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
this.database.execute("recording login attempt of " + attempt.name(), connection -> {
|
this.database.execute("recording login attempt of " + attempt.name(), connection -> {
|
||||||
try(PreparedStatement statement = connection.prepareStatement(sql)) {
|
long id;
|
||||||
|
try(PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
statement.setObject(1, LocalDateTime.ofInstant(attempt.at(), ZoneOffset.UTC));
|
statement.setObject(1, LocalDateTime.ofInstant(attempt.at(), ZoneOffset.UTC));
|
||||||
statement.setString(2, attempt.uuid().toString());
|
statement.setString(2, attempt.uuid().toString());
|
||||||
statement.setString(3, attempt.name());
|
statement.setString(3, attempt.name());
|
||||||
statement.setString(4, attempt.ip());
|
statement.setString(4, attempt.ip());
|
||||||
statement.setBoolean(5, attempt.bedrock());
|
statement.setString(5, network);
|
||||||
statement.setString(6, attempt.stage());
|
statement.setBoolean(6, attempt.bedrock());
|
||||||
statement.setString(7, attempt.result());
|
statement.setString(7, attempt.stage());
|
||||||
statement.setString(8, attempt.reason());
|
statement.setString(8, attempt.result());
|
||||||
|
statement.setString(9, attempt.reason());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
|
try(ResultSet keys = statement.getGeneratedKeys()) {
|
||||||
|
if(!keys.next()) return;
|
||||||
|
id = keys.getLong(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Bukkit.getPluginManager().callEvent(new LoginAttemptRecordedEvent(new LoginAttemptRecordedEvent.RecordedAttempt(
|
||||||
|
id, attempt.at(), attempt.uuid(), attempt.name(), attempt.ip(), network, attempt.stage(), attempt.result()
|
||||||
|
)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Connection key per attempt: the IPv4 address or the IPv6 /64 prefix. Groups accounts by internet connection.
|
||||||
|
ALTER TABLE login_attempts
|
||||||
|
ADD COLUMN network VARCHAR(45) NULL COMMENT 'IPv4 address or IPv6 /64 prefix, NULL if unknown' AFTER ip,
|
||||||
|
ADD KEY idx_network_at (network, at);
|
||||||
|
|
||||||
|
UPDATE login_attempts SET network = ip WHERE ip REGEXP '^[0-9]{1,3}([.][0-9]{1,3}){3}$';
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package eu.mhsl.craftattack.spawn.core.event;
|
||||||
|
|
||||||
|
import org.bukkit.event.Event;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired after a login attempt was stored in login_attempts. Called on the database thread:
|
||||||
|
* handlers must return quickly and must never wait for results of the shared Database.
|
||||||
|
*/
|
||||||
|
public class LoginAttemptRecordedEvent extends Event {
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record RecordedAttempt(
|
||||||
|
long id,
|
||||||
|
Instant at,
|
||||||
|
UUID uuid,
|
||||||
|
String name,
|
||||||
|
String ip,
|
||||||
|
@Nullable String network,
|
||||||
|
String stage,
|
||||||
|
String result
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private final RecordedAttempt attempt;
|
||||||
|
|
||||||
|
public LoginAttemptRecordedEvent(RecordedAttempt attempt) {
|
||||||
|
super(true);
|
||||||
|
this.attempt = attempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecordedAttempt getAttempt() {
|
||||||
|
return this.attempt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package eu.mhsl.craftattack.spawn.core.util.network;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups IP addresses by internet connection: an IPv4 address stands for itself,
|
||||||
|
* an IPv6 address for its /64 prefix (one prefix per household line).
|
||||||
|
*/
|
||||||
|
public final class Networks {
|
||||||
|
private static final Pattern IPV4 = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$");
|
||||||
|
private static final Pattern IPV6 = Pattern.compile("^[0-9a-fA-F:.]*:[0-9a-fA-F:.]*$");
|
||||||
|
private static final String IPV6_SUFFIX = "::/64";
|
||||||
|
|
||||||
|
private Networks() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the connection key ("93.204.10.20" or "2003:e1:1f00:ab00::/64"), or null if the text is no IP address
|
||||||
|
*/
|
||||||
|
public static @Nullable String networkOf(@Nullable String ip) {
|
||||||
|
if(ip == null) return null;
|
||||||
|
String text = ip.trim();
|
||||||
|
|
||||||
|
Matcher ipv4 = IPV4.matcher(text);
|
||||||
|
if(ipv4.matches()) return ipv4Of(ipv4);
|
||||||
|
if(!IPV6.matcher(text).matches()) return null;
|
||||||
|
|
||||||
|
InetAddress address;
|
||||||
|
try {
|
||||||
|
// the text contains a colon, so it is parsed as IPv6 literal and never resolved via DNS
|
||||||
|
address = InetAddress.getByName(text);
|
||||||
|
} catch(UnknownHostException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(address instanceof Inet4Address) return address.getHostAddress();
|
||||||
|
byte[] bytes = address.getAddress();
|
||||||
|
return String.format("%x:%x:%x:%x%s", group(bytes, 0), group(bytes, 2), group(bytes, 4), group(bytes, 6), IPV6_SUFFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortened form for display: "93.204.x.x" or "2003:e1:x:x::/64".
|
||||||
|
*/
|
||||||
|
public static String mask(String network) {
|
||||||
|
if(network.endsWith(IPV6_SUFFIX)) {
|
||||||
|
String[] groups = network.substring(0, network.length() - IPV6_SUFFIX.length()).split(":");
|
||||||
|
return groups[0] + ":" + groups[1] + ":x:x" + IPV6_SUFFIX;
|
||||||
|
}
|
||||||
|
String[] octets = network.split("\\.");
|
||||||
|
return octets.length == 4 ? octets[0] + "." + octets[1] + ".x.x" : network;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable String ipv4Of(Matcher matcher) {
|
||||||
|
StringJoiner joiner = new StringJoiner(".");
|
||||||
|
for(int i = 1; i <= 4; i++) {
|
||||||
|
int octet = Integer.parseInt(matcher.group(i));
|
||||||
|
if(octet > 255) return null;
|
||||||
|
joiner.add(Integer.toString(octet));
|
||||||
|
}
|
||||||
|
return joiner.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int group(byte[] bytes, int offset) {
|
||||||
|
return ((bytes[offset] & 0xff) << 8) | (bytes[offset + 1] & 0xff);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user