Sketch of connection refusing system

This commit is contained in:
Tomasz Kuraczyk 2026-02-22 00:30:11 +01:00
parent 49e5051aaf
commit 053bc0a83b
12 changed files with 663 additions and 93 deletions

View file

@ -0,0 +1,61 @@
package net.kuraczyk.banhammer;
import net.kuraczyk.banhammer.events.PlayerPreLogin;
import org.bukkit.ChatColor;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import net.kuraczyk.banhammer.utils.*;
public final class BanHammer extends JavaPlugin {
public FileConfiguration config;
public DBManager db;
@Override
public void onEnable() {
saveDefaultConfig();
config = getConfig();
try {
Class.forName("net.kuraczyk.libs.postgresql.Driver");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
db = new DBManager(
config.getString("database.host"),
config.getInt("database.port"),
config.getString("database.user"),
config.getString("database.pass"),
config.getString("database.database"),
this.getClassLoader()
);
String error = db.connect();
if(error != null){
getLogger().severe(
"Failed to connect to DB!!!!\n"
+error
);
getServer().getPluginManager().disablePlugin(this);
}
getLogger().info(ChatColor.translateAlternateColorCodes('&', "&a&b+&r&8 | &aBan&7Hammer"));
getServer().getPluginManager().registerEvents(new PlayerPreLogin(this), this);
}
@Override
public void onDisable() {
if(db != null){
String error = db.disconnect();
if(error != null) {
getLogger().severe(
"Error occured while disconnecting from from DB!!!!\n"
+error
);
}
}
getLogger().info(ChatColor.translateAlternateColorCodes('&', "&c&b-&r&8 | &aBan&7Hammer"));
}
}

View file

@ -0,0 +1,126 @@
package net.kuraczyk.banhammer.events;
import net.kuraczyk.banhammer.BanHammer;
import net.kyori.adventure.text.Component;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import java.net.InetAddress;
import java.sql.*;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import net.kuraczyk.banhammer.utils.TextUtils;
public class PlayerPreLogin implements Listener {
BanHammer plugin;
Component kickMessage = Component.text("If this message is displayed something went terribly wrong...\nModule: BanHammer");
public PlayerPreLogin(BanHammer m) {
plugin = m;
m.getClass();
}
@EventHandler
public void onPlayerPreLogin(AsyncPlayerPreLoginEvent e) {
if(checkPlayerBanStatus(e.getPlayerProfile().getName(), e.getUniqueId(), e.getAddress()))
e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, kickMessage);
}
private boolean checkPlayerBanStatus(String nickname, UUID uuid, InetAddress ip) {
try {
Connection conn = plugin.db.getConnection();
if(conn!=null){
PreparedStatement ps = conn.prepareStatement("SELECT * from player_bans WHERE (player_name = ? OR player_uuid = ?::uuid) AND (scope = ? OR scope = '*')");
ps.setString(1, nickname);
ps.setObject(2, uuid);
ps.setString(3, plugin.getConfig().getString("serverID"));
ResultSet rs = ps.executeQuery();
while(rs.next()){
String reason = rs.getString("reason");
String parsedDate;
if(rs.getObject("end_time")!=null){
long timeLeft = ((Timestamp)rs.getObject("end_time")).getTime() - new Timestamp(System.currentTimeMillis()).getTime();
if(timeLeft <= 0) {
PreparedStatement archiveBan = conn.prepareStatement("DELETE FROM player_bans WHERE id = ?::uuid");
archiveBan.setString(1, rs.getObject("id").toString());
archiveBan.executeUpdate();
continue;
}
long timeLeftDays = TimeUnit.MILLISECONDS.toDays(timeLeft);
timeLeft-=TimeUnit.DAYS.toMillis(timeLeftDays);
long timeLeftHours = TimeUnit.MILLISECONDS.toHours(timeLeft);
timeLeft-=TimeUnit.HOURS.toMillis(timeLeftHours);
long timeLeftMinutes = TimeUnit.MILLISECONDS.toMinutes(timeLeft);
timeLeft-=TimeUnit.MINUTES.toMillis(timeLeftMinutes);
long timeLeftSeconds = TimeUnit.MILLISECONDS.toSeconds(timeLeft);
timeLeft-=TimeUnit.SECONDS.toMillis(timeLeftSeconds);
parsedDate = TextUtils.parsePolishDate(timeLeftDays, timeLeftHours, timeLeftMinutes, timeLeftSeconds);
kickMessage = TextUtils.colorString(TextUtils.resolvePlaceholders(String.format("Zostałeś zbanowany na %s.\nPowód: %s", parsedDate, reason)));
return true;
}
else {
kickMessage = TextUtils.colorString(TextUtils.resolvePlaceholders(String.format("Zostałeś permanentnie zbanowany.\nPowód: %s", reason)));
return true;
}
}
PreparedStatement ps2 = conn.prepareStatement("SELECT * from ip_bans WHERE (ip_address = ?::inet) AND (scope = ? OR scope = '*')");
ps2.setObject(1, ip.getHostAddress());
ps2.setString(2, plugin.getConfig().getString("serverID"));
ResultSet rs2 = ps2.executeQuery();
while(rs2.next()) {
String reason = rs2.getString("reason");
String parsedDate;
if(rs2.getObject("end_time")!=null){
long timeLeft = ((Timestamp)rs2.getObject("end_time")).getTime() - new Timestamp(System.currentTimeMillis()).getTime();
if(timeLeft <= 0) {
PreparedStatement archiveBan = conn.prepareStatement("DELETE FROM ip_bans WHERE id = ?::uuid");
archiveBan.setString(1, rs2.getObject("id").toString());
archiveBan.executeUpdate();
continue;
}
long timeLeftDays = TimeUnit.MILLISECONDS.toDays(timeLeft);
timeLeft-=TimeUnit.DAYS.toMillis(timeLeftDays);
long timeLeftHours = TimeUnit.MILLISECONDS.toHours(timeLeft);
timeLeft-=TimeUnit.HOURS.toMillis(timeLeftHours);
long timeLeftMinutes = TimeUnit.MILLISECONDS.toMinutes(timeLeft);
timeLeft-=TimeUnit.MINUTES.toMillis(timeLeftMinutes);
long timeLeftSeconds = TimeUnit.MILLISECONDS.toSeconds(timeLeft);
timeLeft-=TimeUnit.SECONDS.toMillis(timeLeftSeconds);
parsedDate = TextUtils.parsePolishDate(timeLeftDays, timeLeftHours, timeLeftMinutes, timeLeftSeconds);
kickMessage = TextUtils.colorString(TextUtils.resolvePlaceholders(String.format("Zostałeś IPzbanowany na %s.\nPowód: %s", parsedDate, reason)));
return true;
}
else {
kickMessage = TextUtils.colorString(TextUtils.resolvePlaceholders(String.format("Zostałeś permanentnie IPzbanowany.\nPowód: %s", reason)));
return true;
}
}
return false;
}
} catch (SQLException e) {
plugin.getLogger().severe(e.toString());
}
return true;
}
}

View file

@ -0,0 +1,92 @@
package net.kuraczyk.banhammer.utils;
import org.apache.commons.lang3.tuple.Pair;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.output.MigrateResult;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Struct;
public class DBManager {
private String host, user, pass, database;
private int port;
private Connection connection;
private ClassLoader classLoader;
public DBManager(String host, int port, String user, String pass, String database, ClassLoader classLoader) {
this.host = host;
this.port = port;
this.user = user;
this.pass = pass;
this.database = database;
this.classLoader = classLoader;
}
public String connect() {
try {
if(connection != null && !connection.isClosed()){
return null;
}
connection = DriverManager.getConnection(String.format("jdbc:postgresql://%s:%d/%s", host, port, database), user, pass);
String migrationSuccess = performMigration();
if(migrationSuccess != null){
return migrationSuccess;
}
return null;
} catch (SQLException e) {
return e.toString();
}
}
public String disconnect() {
try {
if(connection != null && !connection.isClosed()){
connection.close();
return null;
}
return "Error";
} catch (SQLException e) {
return e.toString();
}
}
public Connection getConnection() throws SQLException {
if(connection == null || connection.isClosed()){
return null;
}
return connection;
}
private String performMigration() {
try {
Flyway flyway = Flyway.configure(classLoader)
.driver("net.kuraczyk.libs.postgresql.Driver")
.dataSource(String.format("jdbc:postgresql://%s:%d/%s", host, port, database), user, pass)
.locations("classpath:db/migration")
.baselineOnMigrate(true)
.validateOnMigrate(true)
.cleanDisabled(true)
.table("flyway_schema_history")
.load();
MigrateResult result = flyway.migrate();
if (result.success) {
return null;
} else {
return result.warnings.toString();
}
} catch (Exception e) {
return e.toString();
}
}
}

View file

@ -0,0 +1,121 @@
package net.kuraczyk.banhammer.utils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import java.util.Arrays;
public class TextUtils {
public static Component colorString(String s){
return colorMarkupFormattedString(
colorLegacyFormattedString(s)
);
}
private static Component colorMarkupFormattedString(String s){
return MiniMessage.miniMessage().deserialize(s);
}
private static String colorLegacyFormattedString(String s){
return ChatColor.translateAlternateColorCodes('&', s);
}
public static String resolvePlaceholders(String s){
return s.replace("{onlinePlayerCount}", String.valueOf(
Bukkit.getOnlinePlayers().size()+Arrays.stream(Bukkit.getOfflinePlayers()).count()
));
}
public static String resolvePlaceholders(String s, Player p){
return resolvePlaceholders(s)
.replace("{playerName}", p.getName())
.replace("{ping}", String.valueOf(p.getPing()));
}
public static String parsePolishDate(long days, long hours, long minutes, long seconds){
String parsedDays, parsedHours, parsedMinutes, parsedSeconds;
// Parse days string block
if(days >= 2){
parsedDays="dni";
}
else if (days==1){
parsedDays="dzień";
}
else{
parsedDays="";
}
if(parsedDays!="") parsedDays=Long.toString(days)+' '+parsedDays+", ";
// Parse hours string block
if(hours >= 20 || (hours < 10 && hours > 1)){
if(hours%10 >=2 && hours%10 <=4){
parsedHours="godziny";
}
else{
parsedHours="godzin";
}
}
else if(hours < 20 && hours >= 10){
parsedHours="godzin";
}
else if (hours==1){
parsedHours="godzina";
}
else{
parsedHours="";
}
if(parsedHours!="") parsedHours=Long.toString(hours)+' '+parsedHours+", ";
// Parse minutes string block
if(minutes >= 20 || (minutes < 10 && minutes > 1)){
if(minutes%10 >=2 && minutes%10 <=4){
parsedMinutes="minuty";
}
else{
parsedMinutes="minut";
}
}
else if(minutes < 20 && minutes >= 10){
parsedMinutes="minut";
}
else if (minutes==1){
parsedMinutes="minuta";
}
else{
parsedMinutes="";
}
if(parsedMinutes!="") parsedMinutes=Long.toString(minutes)+' '+parsedMinutes+", ";
// Parse seconds string block
if(seconds >= 20 || (seconds < 10 && seconds > 1)){
if(seconds%10 >=2 && seconds%10 <=4){
parsedSeconds="sekundy";
}
else{
parsedSeconds="sekund";
}
}
else if(seconds < 20 && seconds >= 10){
parsedSeconds="sekund";
}
else if (seconds==1){
parsedSeconds="sekunda";
}
else{
parsedSeconds="";
}
if(parsedSeconds!="") parsedSeconds=Long.toString(seconds)+' '+parsedSeconds+", ";
// Create final gramatically correct string
String result = String.format("%s%s%s%s", parsedDays, parsedHours, parsedMinutes, parsedSeconds);
if(result.length()>=2) result = result.substring(0, result.length() - 2);
return result;
}
}

View file

@ -0,0 +1,8 @@
database:
host: "localhost"
port: 5432
user: "postgres"
pass: "postgres"
database: "postgres"
serverID: "mcserver"

View file

@ -0,0 +1,129 @@
CREATE TABLE player_bans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_name VARCHAR(16) NOT NULL,
player_uuid UUID NOT NULL,
reason TEXT,
scope VARCHAR(20) NOT NULL DEFAULT '*',
operator_name VARCHAR(16) NOT NULL,
operator_uuid UUID,
start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP
);
CREATE TABLE ip_bans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ip_address INET NOT NULL,
reason TEXT,
scope VARCHAR(20) NOT NULL DEFAULT '*',
operator_name VARCHAR(16) NOT NULL,
operator_uuid UUID,
start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP
);
CREATE TABLE ban_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
target JSONB NOT NULL,
reason TEXT,
scope VARCHAR(20) NOT NULL DEFAULT '*',
operator_name VARCHAR(16) NOT NULL,
operator_uuid UUID,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP
);
CREATE TABLE appeals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ban_id UUID NOT NULL,
target JSONB NOT NULL, --so appealing player could use email, account, discord
sustained BOOLEAN, -- null is pending
response TEXT,
operator_name VARCHAR(16),
operator_uuid UUID,
appeal_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
resolve_date TIMESTAMP
);
CREATE INDEX idx_player_bans_uuid ON player_bans(player_uuid);
CREATE INDEX idx_player_bans_name ON player_bans(player_name);
CREATE INDEX idx_player_bans_scope ON player_bans(scope);
CREATE INDEX idx_player_bans_end_time ON player_bans(end_time) WHERE end_time IS NOT NULL;
CREATE INDEX idx_player_bans_active_lookup
ON player_bans(player_uuid, scope);
CREATE INDEX idx_ip_bans_ip ON ip_bans(ip_address);
CREATE INDEX idx_ip_bans_scope ON ip_bans(scope);
CREATE INDEX idx_ip_bans_end_time ON ip_bans(end_time) WHERE end_time IS NOT NULL;
CREATE INDEX idx_ip_bans_active_lookup
ON ip_bans(ip_address, scope);
CREATE INDEX idx_ban_history_target ON ban_history USING GIN (target);
CREATE INDEX idx_ban_history_scope ON ban_history(scope);
CREATE INDEX idx_ban_history_start_time ON ban_history(start_time DESC);
CREATE INDEX idx_appeals_ban_id ON appeals(ban_id);
CREATE INDEX idx_appeals_sustained ON appeals(sustained);
CREATE INDEX idx_appeals_appeal_date ON appeals(appeal_date DESC);
CREATE INDEX idx_appeals_target ON appeals USING GIN (target);
CREATE UNIQUE INDEX idx_unique_active_player_ban
ON player_bans(player_uuid, scope);
CREATE UNIQUE INDEX idx_unique_active_ip_ban
ON ip_bans(ip_address, scope);
CREATE OR REPLACE FUNCTION archive_ban_on_delete()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO ban_history (target, reason, scope, operator_name, operator_uuid, start_time, end_time)
VALUES (
CASE TG_TABLE_NAME
WHEN 'player_bans' THEN jsonb_build_object(
'player_uuid', OLD.player_uuid,
'player_name', OLD.player_name
)
WHEN 'ip_bans' THEN jsonb_build_object(
'ip_address', host(OLD.ip_address)
)
END,
OLD.reason,
OLD.scope,
OLD.operator_name,
OLD.operator_uuid,
OLD.start_time,
OLD.end_time
);
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER archive_player_ban_on_delete
BEFORE DELETE ON player_bans
FOR EACH ROW
EXECUTE FUNCTION archive_ban_on_delete();
CREATE TRIGGER archive_ip_ban_on_delete
BEFORE DELETE ON ip_bans
FOR EACH ROW
EXECUTE FUNCTION archive_ban_on_delete();
-- Comments for documentation (written by sonnet 4.5)
COMMENT ON TABLE player_bans IS 'Active player bans - deleted bans are moved to ban_history';
COMMENT ON TABLE ip_bans IS 'Active IP bans - deleted bans are moved to ban_history';
COMMENT ON TABLE ban_history IS 'Historical record of expired or revoked bans';
COMMENT ON TABLE appeals IS 'Ban appeal requests and their outcomes';
COMMENT ON COLUMN player_bans.end_time IS 'NULL indicates permanent ban';
COMMENT ON COLUMN player_bans.scope IS 'Ban scope: * for global, server name for specific server';
COMMENT ON COLUMN ip_bans.end_time IS 'NULL indicates permanent ban';
COMMENT ON COLUMN ip_bans.scope IS 'Ban scope: * for global, server name for specific server';
COMMENT ON COLUMN ban_history.target IS 'JSONB containing player info (player_uuid, player_name) or IP info (ip_address)';
COMMENT ON COLUMN ban_history.end_time IS 'NULL if ban was permanent and manually revoked';
COMMENT ON COLUMN appeals.sustained IS 'NULL = pending review, TRUE = ban kept (appeal rejected), FALSE = ban removed (appeal approved)';
COMMENT ON COLUMN appeals.response IS 'Operator response to the appeal';
COMMENT ON COLUMN appeals.target IS 'JSONB containing appellant contact info (player_uuid, player_name, email, discord, etc.)';

View file

@ -0,0 +1,53 @@
DROP TRIGGER IF EXISTS archive_player_ban_on_delete ON player_bans;
DROP TRIGGER IF EXISTS archive_ip_ban_on_delete ON ip_bans;
DROP FUNCTION IF EXISTS archive_ban_on_delete();
CREATE OR REPLACE FUNCTION archive_player_ban_on_delete()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO ban_history (target, reason, scope, operator_name, operator_uuid, start_time, end_time)
VALUES (
jsonb_build_object(
'player_uuid', OLD.player_uuid,
'player_name', OLD.player_name
),
OLD.reason,
OLD.scope,
OLD.operator_name,
OLD.operator_uuid,
OLD.start_time,
OLD.end_time
);
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION archive_ip_ban_on_delete()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO ban_history (target, reason, scope, operator_name, operator_uuid, start_time, end_time)
VALUES (
jsonb_build_object(
'ip_address', host(OLD.ip_address)
),
OLD.reason,
OLD.scope,
OLD.operator_name,
OLD.operator_uuid,
OLD.start_time,
OLD.end_time
);
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER archive_player_ban_on_delete
BEFORE DELETE ON player_bans
FOR EACH ROW
EXECUTE FUNCTION archive_player_ban_on_delete();
CREATE TRIGGER archive_ip_ban_on_delete
BEFORE DELETE ON ip_bans
FOR EACH ROW
EXECUTE FUNCTION archive_ip_ban_on_delete();

View file

@ -0,0 +1,4 @@
name: BanHammer
version: '0.1.0-SNAPSHOT'
main: net.kuraczyk.banhammer.BanHammer
api-version: '1.21'