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.appliance.Appliance;
|
||||
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 net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
@@ -16,6 +18,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
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
|
||||
* (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.
|
||||
* 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) {
|
||||
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 -> {
|
||||
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.setString(2, attempt.uuid().toString());
|
||||
statement.setString(3, attempt.name());
|
||||
statement.setString(4, attempt.ip());
|
||||
statement.setBoolean(5, attempt.bedrock());
|
||||
statement.setString(6, attempt.stage());
|
||||
statement.setString(7, attempt.result());
|
||||
statement.setString(8, attempt.reason());
|
||||
statement.setString(5, network);
|
||||
statement.setBoolean(6, attempt.bedrock());
|
||||
statement.setString(7, attempt.stage());
|
||||
statement.setString(8, attempt.result());
|
||||
statement.setString(9, attempt.reason());
|
||||
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