From 0e2468d3fbc974b95b83f37c7937801a55516328 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:11:49 +0000 Subject: [PATCH 1/9] Initial plan From ee7a059b01b96f85263ab80bb38cb2a3ac7b2788 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:17:11 +0000 Subject: [PATCH 2/9] Add allowlist show command with optional search filter Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- .../command/impl/AllowlistCommand.java | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java b/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java index bd22a1d..53174c0 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java @@ -29,7 +29,7 @@ import java.util.stream.Collectors; public class AllowlistCommand extends Command { - private static final String[] secondArgs = new String[] {"add", "remove"}; + private static final String[] secondArgs = new String[] {"add", "remove", "show"}; @Override public String permission() { @@ -53,7 +53,7 @@ public class AllowlistCommand extends Command { @Override public String usage() { - return " "; + return " | remove | show [search]>"; } @Override @@ -72,6 +72,49 @@ public class AllowlistCommand extends Command { return "&cUsage: /antivpn allowlist " + usage(); } + if(args[0].equalsIgnoreCase("show")) { + String search = args.length > 1 ? args[1].toLowerCase() : null; + // Strip color code characters to prevent formatting injection in output + String safeSearch = search != null ? search.replace("&", "") : null; + boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled(); + + List uuids = databaseEnabled + ? AntiVPN.getInstance().getDatabase().getAllWhitelisted() + : new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelisted()); + List ips = databaseEnabled + ? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps() + : new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps()); + + List messages = new ArrayList<>(); + messages.add("&8&m-----------------------------------------------------"); + messages.add("&6&lAllowlist Entries" + (safeSearch != null ? " &7(search: &f" + safeSearch + "&7)" : "")); + messages.add(""); + + boolean any = false; + for (UUID uuid : uuids) { + String entry = uuid.toString(); + if (search == null || entry.toLowerCase().contains(search)) { + messages.add("&7- &fUUID: &e" + entry); + any = true; + } + } + for (CIDRUtils cidr : ips) { + String entry = cidr.getCidr(); + if (search == null || entry.toLowerCase().contains(search)) { + messages.add("&7- &fIP: &e" + entry); + any = true; + } + } + + if (!any) { + messages.add(safeSearch != null + ? "&cNo allowlist entries matching &f\"" + safeSearch + "&c\" were found." + : "&cThe allowlist is empty."); + } + messages.add("&8&m-----------------------------------------------------"); + return String.join("\n", messages); + } + if(args.length == 1) return "&cYou have to provide a player to allow or deny exemption."; @@ -201,10 +244,15 @@ public class AllowlistCommand extends Command { case 1 -> Arrays.stream(secondArgs) .filter(narg -> narg.toLowerCase().startsWith(args[0].toLowerCase())) .collect(Collectors.toList()); - case 2 -> AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream() - .map(APIPlayer::getName) - .filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase())) - .collect(Collectors.toList()); + case 2 -> { + if (args[0].equalsIgnoreCase("show")) { + yield Collections.emptyList(); + } + yield AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream() + .map(APIPlayer::getName) + .filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } default -> Collections.emptyList(); }; } From 0894ef8e3d52006b92965a739676dc7004330952 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:11:49 +0000 Subject: [PATCH 3/9] Initial plan From 38dcfcb1feb0ad8fcd3ff1318bff2ac858bec3b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:17:11 +0000 Subject: [PATCH 4/9] Add allowlist show command with optional search filter Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- .../command/impl/AllowlistCommand.java | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java b/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java index bd22a1d..53174c0 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java @@ -29,7 +29,7 @@ import java.util.stream.Collectors; public class AllowlistCommand extends Command { - private static final String[] secondArgs = new String[] {"add", "remove"}; + private static final String[] secondArgs = new String[] {"add", "remove", "show"}; @Override public String permission() { @@ -53,7 +53,7 @@ public class AllowlistCommand extends Command { @Override public String usage() { - return " "; + return " | remove | show [search]>"; } @Override @@ -72,6 +72,49 @@ public class AllowlistCommand extends Command { return "&cUsage: /antivpn allowlist " + usage(); } + if(args[0].equalsIgnoreCase("show")) { + String search = args.length > 1 ? args[1].toLowerCase() : null; + // Strip color code characters to prevent formatting injection in output + String safeSearch = search != null ? search.replace("&", "") : null; + boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled(); + + List uuids = databaseEnabled + ? AntiVPN.getInstance().getDatabase().getAllWhitelisted() + : new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelisted()); + List ips = databaseEnabled + ? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps() + : new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps()); + + List messages = new ArrayList<>(); + messages.add("&8&m-----------------------------------------------------"); + messages.add("&6&lAllowlist Entries" + (safeSearch != null ? " &7(search: &f" + safeSearch + "&7)" : "")); + messages.add(""); + + boolean any = false; + for (UUID uuid : uuids) { + String entry = uuid.toString(); + if (search == null || entry.toLowerCase().contains(search)) { + messages.add("&7- &fUUID: &e" + entry); + any = true; + } + } + for (CIDRUtils cidr : ips) { + String entry = cidr.getCidr(); + if (search == null || entry.toLowerCase().contains(search)) { + messages.add("&7- &fIP: &e" + entry); + any = true; + } + } + + if (!any) { + messages.add(safeSearch != null + ? "&cNo allowlist entries matching &f\"" + safeSearch + "&c\" were found." + : "&cThe allowlist is empty."); + } + messages.add("&8&m-----------------------------------------------------"); + return String.join("\n", messages); + } + if(args.length == 1) return "&cYou have to provide a player to allow or deny exemption."; @@ -201,10 +244,15 @@ public class AllowlistCommand extends Command { case 1 -> Arrays.stream(secondArgs) .filter(narg -> narg.toLowerCase().startsWith(args[0].toLowerCase())) .collect(Collectors.toList()); - case 2 -> AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream() - .map(APIPlayer::getName) - .filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase())) - .collect(Collectors.toList()); + case 2 -> { + if (args[0].equalsIgnoreCase("show")) { + yield Collections.emptyList(); + } + yield AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream() + .map(APIPlayer::getName) + .filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } default -> Collections.emptyList(); }; } From 11ef7e8d5029107b69158393b5850c6522ab09e6 Mon Sep 17 00:00:00 2001 From: Dawson Date: Fri, 20 Feb 2026 10:15:59 -0500 Subject: [PATCH 5/9] Fixed CIDR notations and the ability for mongo to properly grab CIDRs --- .../command/impl/AllowlistCommand.java | 2 + .../antivpn/database/local/H2VPN.java | 10 +- .../antivpn/database/local/version/Third.java | 115 ++++++++++++++++++ .../antivpn/database/mongo/MongoVPN.java | 4 +- .../database/mongo/version/MongoThird.java | 108 ++++++++++++++++ .../antivpn/database/version/Version.java | 8 +- .../dev/brighten/antivpn/utils/MiscUtils.java | 42 +++++++ 7 files changed, 281 insertions(+), 8 deletions(-) create mode 100644 Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/Third.java create mode 100644 Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/version/MongoThird.java diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java b/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java index 53174c0..c16df14 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java @@ -78,6 +78,8 @@ public class AllowlistCommand extends Command { String safeSearch = search != null ? search.replace("&", "") : null; boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled(); + AntiVPN.getInstance().getExecutor().log("Is Database Enabled: %s", databaseEnabled ? "yes" : "no"); + List uuids = databaseEnabled ? AntiVPN.getInstance().getDatabase().getAllWhitelisted() : new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelisted()); diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/database/local/H2VPN.java b/Common/Source/src/main/java/dev/brighten/antivpn/database/local/H2VPN.java index b0a1971..3235abb 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/database/local/H2VPN.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/database/local/H2VPN.java @@ -207,7 +207,7 @@ public class H2VPN implements VPNDatabase { return; try(var statement = Query.prepare("insert into `whitelisted-ranges` (`cidr_string`, `ip_start`, `ip_end`) values (?, ?, ?)") - .append(cidr.toString()).append(cidr.getStartIpInt()).append(cidr.getEndIpInt())) { + .append(cidr.getCidr()).append(cidr.getStartIpInt()).append(cidr.getEndIpInt())) { statement.execute(); } catch (SQLException e) { @@ -220,7 +220,7 @@ public class H2VPN implements VPNDatabase { if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return; - try(var statement = Query.prepare("delete from `whitelisted-ranges` where `cidr_string` = ?").append(cidr.toString())) { + try(var statement = Query.prepare("delete from `whitelisted-ranges` where `cidr_string` = ?").append(cidr.getCidr())) { statement.execute(); } catch (SQLException e) { @@ -253,7 +253,11 @@ public class H2VPN implements VPNDatabase { try(var statement = Query.prepare("select `cidr_string`, `ip_start`, `ip_end` from `whitelisted-ranges`")) { statement.execute(set -> { try { - ips.add(new CIDRUtils(set.getString("cidr_string"))); + String cidrString = set.getString("cidr_string"); + + AntiVPN.getInstance().getExecutor().log("CIDR String: %s", cidrString); + ips.add(new CIDRUtils(cidrString)); + } catch (UnknownHostException e) { AntiVPN.getInstance().getExecutor() .logException("Could not format ip " diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/Third.java b/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/Third.java new file mode 100644 index 0000000..cd56576 --- /dev/null +++ b/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/Third.java @@ -0,0 +1,115 @@ +/* + * Copyright 2026 Dawson Hessler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.brighten.antivpn.database.local.version; + +import dev.brighten.antivpn.AntiVPN; +import dev.brighten.antivpn.database.DatabaseException; +import dev.brighten.antivpn.database.VPNDatabase; +import dev.brighten.antivpn.database.sql.utils.Query; +import dev.brighten.antivpn.database.version.Version; +import dev.brighten.antivpn.utils.CIDRUtils; +import dev.brighten.antivpn.utils.MiscUtils; + +import java.math.BigInteger; +import java.net.UnknownHostException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +public class Third implements Version { + @Override + public void update(VPNDatabase database) throws DatabaseException { + List ipRanges = new ArrayList<>(); + List rangesToInsert = new ArrayList<>(); + List rangesToRemove = new ArrayList<>(); + try (var preparedQuery = Query.prepare("select ip_start, ip_end from `whitelist-ranges`")) { + preparedQuery.execute(set -> { + BigInteger start = set.getBigDecimal("ip_start").toBigInteger(); + BigInteger end = set.getBigDecimal("ip_end").toBigInteger(); + + try { + var range = MiscUtils.rangeToCidrs(start, end); + + if(range.size() > 1) { + rangesToRemove.add(new BigInteger[]{start, end}); + rangesToInsert.addAll(range); + AntiVPN.getInstance().getExecutor().log(Level.WARNING, "Found multiple CIDR ranges for whitelist range for %s, %s!", start, end); + } else ipRanges.addAll(range); + } catch (UnknownHostException e) { + AntiVPN.getInstance().getExecutor().logException( + String.format("Could not convert ip range to CIDR! %s, %s", start, end), e); + } + }); + } catch (SQLException e) { + AntiVPN.getInstance().getExecutor().logException("Could not get all whitelisted ranges due to SQL error.", e); + } + + AntiVPN.getInstance().getExecutor().log("Inserting %s new ranges into database...", rangesToInsert.size()); + + for (CIDRUtils cidr : rangesToInsert) { + try(var statement = Query.prepare("insert into `whitelisted-ranges` (`cidr_string`, `ip_start`, `ip_end`) values (?, ?, ?)") + .append(cidr.getCidr()).append(cidr.getStartIpInt()).append(cidr.getEndIpInt())) { + statement.execute(); + } catch (SQLException e) { + AntiVPN.getInstance().getExecutor().logException("Could not add cidr '" + cidr + "' to whitelist due to SQL error.", e); + } + } + + AntiVPN.getInstance().getExecutor().log("Removing %s old ranges from database...", rangesToRemove.size()); + + for (BigInteger[] range : rangesToRemove) { + try(var statement = Query.prepare("delete from `whitelisted-ranges` where `ip_start` = ? and `ip_end` = ?")) { + statement.append(range[0]).append(range[1]).execute(); + } catch (SQLException e) { + AntiVPN.getInstance().getExecutor().logException("Could not remove cidr range '" + range[0] + ", " + range[1] + "' from whitelist due to SQL error.", e); + } + } + + AntiVPN.getInstance().getExecutor().log("Updating %s ranges to proper CIDR notation with the database", ipRanges.size()); + + for (CIDRUtils cidr : ipRanges) { + try(var statement = Query.prepare("update `whitelisted-ranges` set `cidr_string` = ? where `ip_start` = ? and `ip_end` = ?")) { + statement.append(cidr.getCidr()).append(cidr.getStartIpInt()).append(cidr.getEndIpInt()).execute(); + } catch (SQLException e) { + AntiVPN.getInstance().getExecutor().logException("Could not update cidr '" + cidr + "' to proper CIDR notation in whitelist due to SQL error.", e); + } + } + + try (var preparedStatement = Query.prepare("INSERT INTO `database_version` (`version`) VALUES (?)").append(versionNumber())) { + preparedStatement.execute(); + } catch (SQLException e) { + AntiVPN.getInstance().getExecutor().logException("Could not update database version to 2 due to SQL error.", e); + } + } + + @Override + public int versionNumber() { + return 2; + } + + @Override + public boolean needsUpdate(VPNDatabase database) { + try (var statement = Query.prepare("select * from `database_version` where version = 2")) { + try(var set = statement.executeQuery()) { + return set.getFetchSize() == 0; + } + } catch (SQLException e) { + return true; + } + } +} diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/MongoVPN.java b/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/MongoVPN.java index cd8fe24..87fb166 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/MongoVPN.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/MongoVPN.java @@ -183,7 +183,7 @@ public class MongoVPN implements VPNDatabase { Document doc = new Document("setting", "whitelist"); doc.append("ip_start", new Decimal128(new BigDecimal(cidr.getStartIpInt()))); doc.append("ip_end", new Decimal128(new BigDecimal(cidr.getEndIpInt()))); - doc.append("cidr_string", cidr.toString()); + doc.append("cidr_string", cidr.getCidr()); settingsDocument.insertOne(doc); } @@ -210,7 +210,7 @@ public class MongoVPN implements VPNDatabase { public List getAllWhitelistedIps() { List ips = new ArrayList<>(); settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"), - Filters.exists("ip"))).forEach((Consumer) doc -> { + Filters.exists("cidr_string"))).forEach((Consumer) doc -> { try { var cidr = new CIDRUtils(doc.getString("cidr_string")); ips.add(cidr); diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/version/MongoThird.java b/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/version/MongoThird.java new file mode 100644 index 0000000..daec1fb --- /dev/null +++ b/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/version/MongoThird.java @@ -0,0 +1,108 @@ +/* + * Copyright 2026 Dawson Hessler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.brighten.antivpn.database.mongo.version; + +import com.mongodb.client.model.Filters; +import dev.brighten.antivpn.AntiVPN; +import dev.brighten.antivpn.database.DatabaseException; +import dev.brighten.antivpn.database.mongo.MongoVPN; +import dev.brighten.antivpn.database.version.Version; +import dev.brighten.antivpn.utils.CIDRUtils; +import dev.brighten.antivpn.utils.MiscUtils; +import org.bson.Document; +import org.bson.types.Decimal128; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Level; + +public class MongoThird implements Version { + @Override + public void update(MongoVPN database) throws DatabaseException { + List ipRanges = new ArrayList<>(); + List rangesToInsert = new ArrayList<>(); + List rangesToRemove = new ArrayList<>(); + database.settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"), Filters.exists("cidr_string"))) + .forEach((Consumer) doc -> { + BigInteger start = doc.get("ip_start", Decimal128.class).bigDecimalValue().toBigInteger(); + BigInteger end = doc.get("ip_end", Decimal128.class).bigDecimalValue().toBigInteger(); + + try { + var range = MiscUtils.rangeToCidrs(start, end); + + if(range.size() > 1) { + rangesToRemove.add(new BigInteger[]{start, end}); + rangesToInsert.addAll(range); + AntiVPN.getInstance().getExecutor().log(Level.WARNING, "Found multiple CIDR ranges for whitelist range for %s, %s!", start, end); + } else ipRanges.addAll(range); + } catch (UnknownHostException e) { + AntiVPN.getInstance().getExecutor().logException( + String.format("Could not convert ip range to CIDR! %s, %s", start, end), e); + } + }); + + if(!rangesToInsert.isEmpty()) { + AntiVPN.getInstance().getExecutor().log("Inserting %s new ranges into database...", rangesToInsert.size()); + var documentsToInsert = rangesToInsert.stream().map(cidr -> { + Document doc = new Document("setting", "whitelist"); + doc.append("ip_start", new Decimal128(new BigDecimal(cidr.getStartIpInt()))); + doc.append("ip_end", new Decimal128(new BigDecimal(cidr.getEndIpInt()))); + doc.append("cidr_string", cidr.getCidr()); + + return doc; + }).toList(); + + database.settingsDocument.insertMany(documentsToInsert); + } + if(!rangesToRemove.isEmpty()) { + AntiVPN.getInstance().getExecutor().log("Removing %s old ranges from database...", rangesToRemove.size()); + rangesToRemove.forEach(range -> database.settingsDocument + .deleteMany(Filters.and( + Filters.gte("ip_start", new Decimal128(new BigDecimal(range[0]))), + Filters.lte("ip_end", new Decimal128(new BigDecimal(range[1])))))); + } + + if(!ipRanges.isEmpty()) { + AntiVPN.getInstance().getExecutor().log("Updating %s CIDRs in database with proper notation...", ipRanges.size()); + + ipRanges.forEach(cidr -> database.settingsDocument + .updateMany(Filters.and(Filters.eq("setting", "whitelist"), + Filters.eq("ip_start", new Decimal128(new BigDecimal(cidr.getStartIpInt()))), + Filters.eq("ip_end", new Decimal128(new BigDecimal(cidr.getEndIpInt())))), + new Document("$set", new Document("cidr_string", cidr.getCidr())))); + } + + var versionCollect = database.antivpnDatabase.getCollection("version"); + versionCollect.insertOne(new Document("version", versionNumber())); + } + + @Override + public int versionNumber() { + return 2; + } + + @Override + public boolean needsUpdate(MongoVPN database) { + var versionCollect = database.antivpnDatabase.getCollection("version"); + + return versionCollect.find(Filters.eq("version", versionNumber())).first() == null; + } +} diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/database/version/Version.java b/Common/Source/src/main/java/dev/brighten/antivpn/database/version/Version.java index f9340b9..2c714b4 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/database/version/Version.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/database/version/Version.java @@ -20,9 +20,11 @@ import dev.brighten.antivpn.database.DatabaseException; import dev.brighten.antivpn.database.local.H2VPN; import dev.brighten.antivpn.database.local.version.First; import dev.brighten.antivpn.database.local.version.Second; +import dev.brighten.antivpn.database.local.version.Third; import dev.brighten.antivpn.database.mongo.MongoVPN; import dev.brighten.antivpn.database.mongo.version.MongoFirst; import dev.brighten.antivpn.database.mongo.version.MongoSecond; +import dev.brighten.antivpn.database.mongo.version.MongoThird; import dev.brighten.antivpn.database.sql.MySqlVPN; import dev.brighten.antivpn.database.sql.version.MySQLFirst; @@ -32,7 +34,7 @@ public interface Version { int versionNumber(); boolean needsUpdate(DB database); - Version[] mongoDbVersions = new Version[] {new MongoFirst(), new MongoSecond()}; - Version[] mysqlVersions = new Version[] {new MySQLFirst(), new Second()}; - Version[] h2Versions = new Version[] {new First(), new Second()}; + Version[] mongoDbVersions = new Version[] {new MongoFirst(), new MongoSecond(), new MongoThird()}; + Version[] mysqlVersions = new Version[] {new MySQLFirst(), new Second(), new Third()}; + Version[] h2Versions = new Version[] {new First(), new Second(), new Third()}; } \ No newline at end of file diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/utils/MiscUtils.java b/Common/Source/src/main/java/dev/brighten/antivpn/utils/MiscUtils.java index 28d0735..4618946 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/utils/MiscUtils.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/utils/MiscUtils.java @@ -22,6 +22,11 @@ import dev.brighten.antivpn.utils.json.JSONObject; import dev.brighten.antivpn.utils.json.JsonReader; import java.io.*; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import java.util.concurrent.ThreadFactory; import java.util.regex.Pattern; @@ -72,6 +77,43 @@ public class MiscUtils { }; } + public static List rangeToCidrs(BigInteger start, BigInteger end) throws UnknownHostException { + List cidrs = new ArrayList<>(); + + while (start.compareTo(end) <= 0) { + // Find the number of trailing zero bits — this determines max block size alignment + int trailingZeros = start.equals(BigInteger.ZERO) + ? 128 // handle the edge case + : start.getLowestSetBit(); + + // Find the largest block that fits + BigInteger remaining = end.subtract(start).add(BigInteger.ONE); + int maxBits = remaining.bitLength() - 1; + + int blockBits = Math.min(trailingZeros, maxBits); + int prefixLen = 32 - blockBits; // use 128 for IPv6 + + // Build the CIDR string + byte[] addrBytes = toFixedLengthBytes(start, 4); // use 16 for IPv6 + String cidr = InetAddress.getByAddress(addrBytes).getHostAddress() + "/" + prefixLen; + cidrs.add(new CIDRUtils(cidr)); + + // Advance past this block + start = start.add(BigInteger.ONE.shiftLeft(blockBits)); + } + + return cidrs; + } + + private static byte[] toFixedLengthBytes(BigInteger value, int length) { + byte[] raw = value.toByteArray(); + byte[] result = new byte[length]; + int srcPos = Math.max(0, raw.length - length); + int destPos = Math.max(0, length - raw.length); + System.arraycopy(raw, srcPos, result, destPos, Math.min(raw.length, length)); + return result; + } + public static UUID lookupUUID(String playername) { try { JSONObject object = JsonReader From 8b5bc6515907c66748ba575ef557000c13e3dbbe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:22:25 +0000 Subject: [PATCH 6/9] Change show search to explicit keyword supporting spaces: show [page] search Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- .../command/impl/AllowlistCommand.java | 80 +++++++++++++++---- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java b/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java index c16df14..68b73f0 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java @@ -53,7 +53,7 @@ public class AllowlistCommand extends Command { @Override public String usage() { - return " | remove | show [search]>"; + return " | remove | show [page] [search ]>"; } @Override @@ -73,12 +73,31 @@ public class AllowlistCommand extends Command { } if(args[0].equalsIgnoreCase("show")) { - String search = args.length > 1 ? args[1].toLowerCase() : null; - // Strip color code characters to prevent formatting injection in output - String safeSearch = search != null ? search.replace("&", "") : null; - boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled(); + // args[1] = optional page number (defaults to 1) + // args[2] = "search" keyword (optional) + // args[3+] = search query joined with spaces (supports spaces in the query) + int page = 1; + if (args.length > 1) { + try { + page = Integer.parseInt(args[1]); + if (page < 1) page = 1; + } catch (NumberFormatException e) { + page = 1; + } + } - AntiVPN.getInstance().getExecutor().log("Is Database Enabled: %s", databaseEnabled ? "yes" : "no"); + String search = null; + String safeSearch = null; + if (args.length > 2 && args[2].equalsIgnoreCase("search")) { + if (args.length <= 3) { + return "&cUsage: /antivpn allowlist show [page] search "; + } + search = String.join(" ", Arrays.copyOfRange(args, 3, args.length)).toLowerCase(); + // Strip color code characters to prevent formatting injection in output + safeSearch = search.replace("&", ""); + } + + boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled(); List uuids = databaseEnabled ? AntiVPN.getInstance().getDatabase().getAllWhitelisted() @@ -87,31 +106,51 @@ public class AllowlistCommand extends Command { ? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps() : new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps()); - List messages = new ArrayList<>(); - messages.add("&8&m-----------------------------------------------------"); - messages.add("&6&lAllowlist Entries" + (safeSearch != null ? " &7(search: &f" + safeSearch + "&7)" : "")); - messages.add(""); - - boolean any = false; + // Build the filtered entry list + List entries = new ArrayList<>(); for (UUID uuid : uuids) { String entry = uuid.toString(); if (search == null || entry.toLowerCase().contains(search)) { - messages.add("&7- &fUUID: &e" + entry); - any = true; + entries.add("&7- &fUUID: &e" + entry); } } for (CIDRUtils cidr : ips) { String entry = cidr.getCidr(); if (search == null || entry.toLowerCase().contains(search)) { - messages.add("&7- &fIP: &e" + entry); - any = true; + entries.add("&7- &fIP: &e" + entry); } } - if (!any) { + int pageSize = 10; + int totalPages = Math.max(1, (int) Math.ceil((double) entries.size() / pageSize)); + if (page > totalPages) page = totalPages; + + List messages = new ArrayList<>(); + messages.add("&8&m-----------------------------------------------------"); + messages.add("&6&lAllowlist Entries &8(&7Page &f" + page + "&7/&f" + totalPages + "&8)" + + (safeSearch != null ? " &7(search: &f" + safeSearch + "&7)" : "")); + messages.add(""); + + if (entries.isEmpty()) { messages.add(safeSearch != null ? "&cNo allowlist entries matching &f\"" + safeSearch + "&c\" were found." : "&cThe allowlist is empty."); + } else { + int start = (page - 1) * pageSize; + int end = Math.min(start + pageSize, entries.size()); + for (int i = start; i < end; i++) { + messages.add(entries.get(i)); + } + if (totalPages > 1) { + messages.add(""); + String searchSuffix = safeSearch != null ? " search " + safeSearch : ""; + if (page > 1) { + messages.add("&7Previous page: &f/antivpn allowlist show " + (page - 1) + searchSuffix); + } + if (page < totalPages) { + messages.add("&7Next page: &f/antivpn allowlist show " + (page + 1) + searchSuffix); + } + } } messages.add("&8&m-----------------------------------------------------"); return String.join("\n", messages); @@ -255,6 +294,13 @@ public class AllowlistCommand extends Command { .filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase())) .collect(Collectors.toList()); } + case 3 -> { + if (args[0].equalsIgnoreCase("show") + && "search".startsWith(args[2].toLowerCase())) { + yield Collections.singletonList("search"); + } + yield Collections.emptyList(); + } default -> Collections.emptyList(); }; } From 3ead4a00934b2f149a34d7ccc40521a3424b92fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:30:57 +0000 Subject: [PATCH 7/9] Make search a standalone subcommand: show [page] | search [page] Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- .../command/impl/AllowlistCommand.java | 145 +++++++++++------- 1 file changed, 86 insertions(+), 59 deletions(-) diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java b/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java index 68b73f0..82642bf 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/command/impl/AllowlistCommand.java @@ -29,7 +29,7 @@ import java.util.stream.Collectors; public class AllowlistCommand extends Command { - private static final String[] secondArgs = new String[] {"add", "remove", "show"}; + private static final String[] secondArgs = new String[] {"add", "remove", "show", "search"}; @Override public String permission() { @@ -53,7 +53,7 @@ public class AllowlistCommand extends Command { @Override public String usage() { - return " | remove | show [page] [search ]>"; + return " | remove | show [page] | search [page]>"; } @Override @@ -74,8 +74,6 @@ public class AllowlistCommand extends Command { if(args[0].equalsIgnoreCase("show")) { // args[1] = optional page number (defaults to 1) - // args[2] = "search" keyword (optional) - // args[3+] = search query joined with spaces (supports spaces in the query) int page = 1; if (args.length > 1) { try { @@ -86,17 +84,6 @@ public class AllowlistCommand extends Command { } } - String search = null; - String safeSearch = null; - if (args.length > 2 && args[2].equalsIgnoreCase("search")) { - if (args.length <= 3) { - return "&cUsage: /antivpn allowlist show [page] search "; - } - search = String.join(" ", Arrays.copyOfRange(args, 3, args.length)).toLowerCase(); - // Strip color code characters to prevent formatting injection in output - safeSearch = search.replace("&", ""); - } - boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled(); List uuids = databaseEnabled @@ -106,54 +93,66 @@ public class AllowlistCommand extends Command { ? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps() : new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps()); - // Build the filtered entry list + List entries = new ArrayList<>(); + for (UUID uuid : uuids) { + entries.add("&7- &fUUID: &e" + uuid); + } + for (CIDRUtils cidr : ips) { + entries.add("&7- &fIP: &e" + cidr.getCidr()); + } + + return buildPage(entries, page, null, "show"); + } + + if(args[0].equalsIgnoreCase("search")) { + // args[1..n-1] = query terms; args[n] = optional page number if last arg is an integer + if (args.length < 2) { + return "&cUsage: /antivpn allowlist search [page]"; + } + + // Detect optional trailing page number + int page = 1; + int queryEnd = args.length; + try { + int candidate = Integer.parseInt(args[args.length - 1]); + if (candidate >= 1 && args.length > 2) { + page = candidate; + queryEnd = args.length - 1; + } + } catch (NumberFormatException ignored) {} + + String search = String.join(" ", Arrays.copyOfRange(args, 1, queryEnd)).toLowerCase(); + // Strip color code characters to prevent formatting injection in output + String safeSearch = search.replace("&", ""); + + if (safeSearch.isEmpty()) { + return "&cUsage: /antivpn allowlist search [page]"; + } + + boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled(); + + List uuids = databaseEnabled + ? AntiVPN.getInstance().getDatabase().getAllWhitelisted() + : new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelisted()); + List ips = databaseEnabled + ? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps() + : new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps()); + List entries = new ArrayList<>(); for (UUID uuid : uuids) { String entry = uuid.toString(); - if (search == null || entry.toLowerCase().contains(search)) { + if (entry.toLowerCase().contains(search)) { entries.add("&7- &fUUID: &e" + entry); } } for (CIDRUtils cidr : ips) { String entry = cidr.getCidr(); - if (search == null || entry.toLowerCase().contains(search)) { + if (entry.toLowerCase().contains(search)) { entries.add("&7- &fIP: &e" + entry); } } - int pageSize = 10; - int totalPages = Math.max(1, (int) Math.ceil((double) entries.size() / pageSize)); - if (page > totalPages) page = totalPages; - - List messages = new ArrayList<>(); - messages.add("&8&m-----------------------------------------------------"); - messages.add("&6&lAllowlist Entries &8(&7Page &f" + page + "&7/&f" + totalPages + "&8)" - + (safeSearch != null ? " &7(search: &f" + safeSearch + "&7)" : "")); - messages.add(""); - - if (entries.isEmpty()) { - messages.add(safeSearch != null - ? "&cNo allowlist entries matching &f\"" + safeSearch + "&c\" were found." - : "&cThe allowlist is empty."); - } else { - int start = (page - 1) * pageSize; - int end = Math.min(start + pageSize, entries.size()); - for (int i = start; i < end; i++) { - messages.add(entries.get(i)); - } - if (totalPages > 1) { - messages.add(""); - String searchSuffix = safeSearch != null ? " search " + safeSearch : ""; - if (page > 1) { - messages.add("&7Previous page: &f/antivpn allowlist show " + (page - 1) + searchSuffix); - } - if (page < totalPages) { - messages.add("&7Next page: &f/antivpn allowlist show " + (page + 1) + searchSuffix); - } - } - } - messages.add("&8&m-----------------------------------------------------"); - return String.join("\n", messages); + return buildPage(entries, page, safeSearch, "search " + safeSearch); } if(args.length == 1) @@ -286,7 +285,7 @@ public class AllowlistCommand extends Command { .filter(narg -> narg.toLowerCase().startsWith(args[0].toLowerCase())) .collect(Collectors.toList()); case 2 -> { - if (args[0].equalsIgnoreCase("show")) { + if (args[0].equalsIgnoreCase("show") || args[0].equalsIgnoreCase("search")) { yield Collections.emptyList(); } yield AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream() @@ -294,14 +293,42 @@ public class AllowlistCommand extends Command { .filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase())) .collect(Collectors.toList()); } - case 3 -> { - if (args[0].equalsIgnoreCase("show") - && "search".startsWith(args[2].toLowerCase())) { - yield Collections.singletonList("search"); - } - yield Collections.emptyList(); - } default -> Collections.emptyList(); }; } + + private String buildPage(List entries, int page, String safeSearch, String subcommandPrefix) { + int pageSize = 10; + int totalPages = Math.max(1, (entries.size() + pageSize - 1) / pageSize); + if (page > totalPages) page = totalPages; + + List messages = new ArrayList<>(); + messages.add("&8&m-----------------------------------------------------"); + messages.add("&6&lAllowlist Entries &8(&7Page &f" + page + "&7/&f" + totalPages + "&8)" + + (safeSearch != null ? " &7(search: &f" + safeSearch + "&7)" : "")); + messages.add(""); + + if (entries.isEmpty()) { + messages.add(safeSearch != null + ? "&cNo allowlist entries matching &f\"" + safeSearch + "&c\" were found." + : "&cThe allowlist is empty."); + } else { + int start = (page - 1) * pageSize; + int end = Math.min(start + pageSize, entries.size()); + for (int i = start; i < end; i++) { + messages.add(entries.get(i)); + } + if (totalPages > 1) { + messages.add(""); + if (page > 1) { + messages.add("&7Previous page: &f/antivpn allowlist " + subcommandPrefix + " " + (page - 1)); + } + if (page < totalPages) { + messages.add("&7Next page: &f/antivpn allowlist " + subcommandPrefix + " " + (page + 1)); + } + } + } + messages.add("&8&m-----------------------------------------------------"); + return String.join("\n", messages); + } } From 0ea8be6ea34a6d84e6b48ccc453b5f7ef21c0573 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:11:49 +0000 Subject: [PATCH 8/9] Initial plan From 3f46db4ad9d53a8789645992a40ebb1ed94ae75c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:43:41 +0000 Subject: [PATCH 9/9] Remove unrelated webhook files from PR branch Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- .../dev/brighten/antivpn/api/APIPlayer.java | 3 - .../dev/brighten/antivpn/api/VPNConfig.java | 30 +- .../dev/brighten/antivpn/api/VPNExecutor.java | 1 - .../antivpn/webhook/WebhookNotifier.java | 284 ---------------- Common/Source/src/main/resources/config.yml | 18 - WEBHOOK_GUIDE.md | 317 ------------------ 6 files changed, 3 insertions(+), 650 deletions(-) delete mode 100644 Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java delete mode 100644 WEBHOOK_GUIDE.md diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/api/APIPlayer.java b/Common/Source/src/main/java/dev/brighten/antivpn/api/APIPlayer.java index f4d6dce..0bf5200 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/api/APIPlayer.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/api/APIPlayer.java @@ -20,7 +20,6 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import dev.brighten.antivpn.AntiVPN; import dev.brighten.antivpn.message.VpnString; -import dev.brighten.antivpn.webhook.WebhookNotifier; import lombok.Getter; import lombok.Setter; @@ -93,7 +92,6 @@ public abstract class APIPlayer { if(cachedResult.response().getIp().equals(ip.getHostAddress())) { AntiVPN.getInstance().getExecutor().log(Level.FINE, "Cached result for " + ip.getHostAddress() + " is " + cachedResult.resultType()); if(cachedResult.resultType().isShouldBlock()) { - WebhookNotifier.sendWebhookNotification(this, cachedResult); AntiVPN.getInstance().getExecutor().handleKickingOfPlayer(cachedResult, this); } onResult.accept(cachedResult); @@ -137,7 +135,6 @@ public abstract class APIPlayer { checkResultCache.put(ip.getHostAddress(), new CheckResult(checkResult.response(), checkResult.resultType(), true)); if(checkResult.resultType().isShouldBlock()) { - WebhookNotifier.sendWebhookNotification(this, checkResult); AntiVPN.getInstance().getExecutor().handleKickingOfPlayer(checkResult, this); } onResult.accept(checkResult); diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNConfig.java b/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNConfig.java index 0c45512..86e84e0 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNConfig.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNConfig.java @@ -44,10 +44,7 @@ public class VPNConfig { defaultIp = new ConfigDefault<>("localhost", "database.ip", AntiVPN.getInstance()), defaultAlertMsg = new ConfigDefault<>("&8[&6KauriVPN&8] &e%player% &7has joined on a VPN/proxy" + " &8(&f%reason%&8) &7in location &8(&f%city%&7, &f%country%&8)", "alerts.message", - AntiVPN.getInstance()), - defaultWebhookUrl = new ConfigDefault<>("", "webhooks.url", AntiVPN.getInstance()), - defaultWebhookAuthToken = new ConfigDefault<>("", "webhooks.authToken", AntiVPN.getInstance()), - defaultWebhookFormat = new ConfigDefault<>("discord", "webhooks.format", AntiVPN.getInstance()); + AntiVPN.getInstance()); private final ConfigDefault cacheResultsDefault = new ConfigDefault<>(true, "cachedResults", AntiVPN.getInstance()), defaultUseCredentials = new ConfigDefault<>(true, @@ -60,12 +57,9 @@ public class VPNConfig { AntiVPN.getInstance()), defaultWhitelistCountries = new ConfigDefault<>(true, "countries.whitelist", AntiVPN.getInstance()), - defaultMetrics = new ConfigDefault<>(true, "bstats", AntiVPN.getInstance()), - defaultWebhookEnabled = new ConfigDefault<>(false, "webhooks.enabled", AntiVPN.getInstance()), - defaultWebhookUseAuth = new ConfigDefault<>(false, "webhooks.useAuthentication", AntiVPN.getInstance()); + defaultMetrics = new ConfigDefault<>(true, "bstats", AntiVPN.getInstance()); private final ConfigDefault - defaultPort = new ConfigDefault<>(-1, "database.port", AntiVPN.getInstance()), - defaultWebhookTimeout = new ConfigDefault<>(5, "webhooks.timeout", AntiVPN.getInstance()); + defaultPort = new ConfigDefault<>(-1, "database.port", AntiVPN.getInstance()); private final ConfigDefault> prefixWhitelistsDefault = new ConfigDefault<>(new ArrayList<>(), "prefixWhitelists", AntiVPN.getInstance()), defaultCommands = new ConfigDefault<>( Collections.singletonList("kick %player% VPNs are not allowed on our server!"), "commands.execute", @@ -112,18 +106,6 @@ public class VPNConfig { private boolean alertToStaff; private boolean metrics; private boolean whitelistCountries; - @Getter - private String webhookUrl; - @Getter - private String webhookAuthToken; - @Getter - private String webhookFormat; - @Getter - private boolean webhookEnabled; - @Getter - private boolean webhookUseAuth; - @Getter - private int webhookTimeout; /** * If true, results will be cached to reduce queries to ... @@ -238,12 +220,6 @@ public class VPNConfig { whitelistCountries = defaultWhitelistCountries.get(); countryKickCommands = defCountryKickCommands.get(); countryVanillaKickReason = defaultCountryKickReason.get(); - webhookEnabled = defaultWebhookEnabled.get(); - webhookUrl = defaultWebhookUrl.get(); - webhookUseAuth = defaultWebhookUseAuth.get(); - webhookAuthToken = defaultWebhookAuthToken.get(); - webhookTimeout = defaultWebhookTimeout.get(); - webhookFormat = defaultWebhookFormat.get(); } } \ No newline at end of file diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNExecutor.java b/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNExecutor.java index a6f173e..6a9f870 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNExecutor.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNExecutor.java @@ -76,7 +76,6 @@ public abstract class VPNExecutor { } public void handleKickingOfPlayer(CheckResult result, APIPlayer player) { - // Send webhook notification if enabled //Ensuring kick task is always running if(kickTask == null || kickTask.isDone() || kickTask.isCancelled()) { diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java b/Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java deleted file mode 100644 index 9e22503..0000000 --- a/Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java +++ /dev/null @@ -1,284 +0,0 @@ -package dev.brighten.antivpn.webhook; - -import dev.brighten.antivpn.AntiVPN; -import dev.brighten.antivpn.api.APIPlayer; -import dev.brighten.antivpn.api.CheckResult; -import dev.brighten.antivpn.utils.json.JSONException; -import dev.brighten.antivpn.utils.json.JSONObject; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.CompletableFuture; -import java.util.logging.Level; - -/** - * Handles sending webhook notifications when a VPN is detected. - */ -public class WebhookNotifier { - - /** - * Sends a webhook notification asynchronously when a player is detected using a VPN. - * - * @param player The player that was detected - * @param result The check result containing VPN information - */ - public static void sendWebhookNotification(APIPlayer player, CheckResult result) { - if (!AntiVPN.getInstance().getVpnConfig().isWebhookEnabled()) { - return; - } - - String webhookUrl = AntiVPN.getInstance().getVpnConfig().getWebhookUrl(); - if (webhookUrl == null || webhookUrl.trim().isEmpty()) { - AntiVPN.getInstance().getExecutor().log(Level.WARNING, - "Webhook is enabled but no URL is configured. Please set webhooks.url in config.yml"); - return; - } - - // Send webhook asynchronously to avoid blocking - CompletableFuture.runAsync(() -> { - try { - sendWebhook(webhookUrl, player, result); - } catch (Exception e) { - AntiVPN.getInstance().getExecutor().logException("Failed to send webhook notification", e); - } - }, AntiVPN.getInstance().getExecutor().getThreadExecutor()); - } - - /** - * Actually sends the HTTP POST request to the webhook URL. - * - * @param webhookUrl The URL to send the webhook to - * @param player The player information - * @param result The check result - * @throws IOException If there's an error sending the request - * @throws JSONException If there's an error creating the JSON payload - */ - private static void sendWebhook(String webhookUrl, APIPlayer player, CheckResult result) - throws IOException, JSONException { - URL url = new URL(webhookUrl); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - - try { - connection.setRequestMethod("POST"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "AntiVPN-Webhook/1.0"); - - // Add authentication header if configured - if (AntiVPN.getInstance().getVpnConfig().isWebhookUseAuth()) { - String token = AntiVPN.getInstance().getVpnConfig().getWebhookAuthToken(); - if (token != null && !token.trim().isEmpty()) { - connection.setRequestProperty("Authorization", "Bearer " + token); - } - } - - connection.setDoOutput(true); - connection.setConnectTimeout(AntiVPN.getInstance().getVpnConfig().getWebhookTimeout() * 1000); - connection.setReadTimeout(AntiVPN.getInstance().getVpnConfig().getWebhookTimeout() * 1000); - - // Create JSON payload - JSONObject payload = createPayload(player, result); - byte[] payloadBytes = payload.toString().getBytes(StandardCharsets.UTF_8); - - // Send request - try (OutputStream os = connection.getOutputStream()) { - os.write(payloadBytes); - os.flush(); - } - - // Check response - int responseCode = connection.getResponseCode(); - if (responseCode >= 200 && responseCode < 300) { - AntiVPN.getInstance().getExecutor().log(Level.INFO, - "Successfully sent webhook notification for player %s (response: %d)", - player.getName(), responseCode); - } else { - AntiVPN.getInstance().getExecutor().log(Level.WARNING, - "Webhook notification returned non-success status code %d for player %s", - responseCode, player.getName()); - } - } finally { - connection.disconnect(); - } - } - - /** - * Creates the JSON payload for the webhook notification. - * - * @param player The player information - * @param result The check result - * @return JSONObject containing the webhook payload - * @throws JSONException If there's an error creating the JSON - */ - private static JSONObject createPayload(APIPlayer player, CheckResult result) throws JSONException { - String format = AntiVPN.getInstance().getVpnConfig().getWebhookFormat().toLowerCase(); - - switch (format) { - case "discord": - return createDiscordPayload(player, result); - case "slack": - return createSlackPayload(player, result); - default: - return createGenericPayload(player, result); - } - } - - /** - * Creates a Discord-formatted webhook payload with rich embeds. - * - * @param player The player information - * @param result The check result - * @return JSONObject containing the Discord-formatted payload - * @throws JSONException If there's an error creating the JSON - */ - private static JSONObject createDiscordPayload(APIPlayer player, CheckResult result) throws JSONException { - JSONObject payload = new JSONObject(); - - // Create embed - JSONObject embed = new JSONObject(); - - // Set title and color based on result type - if (result.resultType().name().equals("DENIED_PROXY")) { - embed.put("title", "🚫 VPN/Proxy Detection"); - embed.put("color", 15158332); // Red color - } else if (result.resultType().name().equals("DENIED_COUNTRY")) { - embed.put("title", "🌍 Country Blocked"); - embed.put("color", 15105570); // Orange color - } - - // Add description - embed.put("description", "A player attempted to join using a VPN/proxy or from a blocked country."); - - // Add fields with player and detection information - JSONObject[] fields; - if (result.response() != null) { - fields = new JSONObject[] { - createDiscordField("Player", player.getName(), true), - createDiscordField("UUID", player.getUuid().toString(), true), - createDiscordField("IP Address", player.getIp().getHostAddress(), true), - createDiscordField("Country", result.response().getCountryName() + " (" + result.response().getCountryCode() + ")", true), - createDiscordField("City", result.response().getCity(), true), - createDiscordField("ISP", result.response().getIsp(), true), - createDiscordField("ASN", result.response().getAsn(), true), - createDiscordField("Detection Method", result.response().getMethod() != null ? result.response().getMethod() : "N/A", true), - createDiscordField("Proxy Status", result.response().isProxy() ? "✓ Detected" : "✗ Not Detected", true) - }; - } else { - fields = new JSONObject[] { - createDiscordField("Player", player.getName(), true), - createDiscordField("UUID", player.getUuid().toString(), true), - createDiscordField("IP Address", player.getIp().getHostAddress(), true) - }; - } - - embed.put("fields", fields); - - // Add timestamp in ISO 8601 format - java.time.Instant instant = java.time.Instant.ofEpochMilli(System.currentTimeMillis()); - embed.put("timestamp", instant.toString()); - - // Add footer - JSONObject footer = new JSONObject(); - footer.put("text", "AntiVPN Detection System"); - embed.put("footer", footer); - - // Add embed to payload - payload.put("embeds", new JSONObject[] { embed }); - - return payload; - } - - /** - * Helper method to create a Discord embed field. - * - * @param name Field name - * @param value Field value - * @param inline Whether the field should be inline - * @return JSONObject containing the field - * @throws JSONException If there's an error creating the JSON - */ - private static JSONObject createDiscordField(String name, String value, boolean inline) throws JSONException { - JSONObject field = new JSONObject(); - field.put("name", name); - field.put("value", value != null ? value : "N/A"); - field.put("inline", inline); - return field; - } - - /** - * Creates a Slack-formatted webhook payload. - * - * @param player The player information - * @param result The check result - * @return JSONObject containing the Slack-formatted payload - * @throws JSONException If there's an error creating the JSON - */ - private static JSONObject createSlackPayload(APIPlayer player, CheckResult result) throws JSONException { - JSONObject payload = new JSONObject(); - - // Build text message - StringBuilder text = new StringBuilder(); - text.append("*VPN/Proxy Detection Alert*\n"); - text.append("Player: ").append(player.getName()).append("\n"); - text.append("IP: ").append(player.getIp().getHostAddress()).append("\n"); - - if (result.response() != null) { - text.append("Country: ").append(result.response().getCountryName()) - .append(" (").append(result.response().getCountryCode()).append(")\n"); - text.append("City: ").append(result.response().getCity()).append("\n"); - text.append("ISP: ").append(result.response().getIsp()).append("\n"); - if (result.response().getMethod() != null) { - text.append("Method: ").append(result.response().getMethod()).append("\n"); - } - } - - payload.put("text", text.toString()); - - return payload; - } - - /** - * Creates a generic JSON payload (original format). - * - * @param player The player information - * @param result The check result - * @return JSONObject containing the generic payload - * @throws JSONException If there's an error creating the JSON - */ - private static JSONObject createGenericPayload(APIPlayer player, CheckResult result) throws JSONException { - JSONObject payload = new JSONObject(); - - // Basic event information - payload.put("event", "vpn_detected"); - payload.put("timestamp", System.currentTimeMillis()); - payload.put("resultType", result.resultType().name()); - - // Player information - JSONObject playerInfo = new JSONObject(); - playerInfo.put("uuid", player.getUuid().toString()); - playerInfo.put("name", player.getName()); - playerInfo.put("ip", player.getIp().getHostAddress()); - payload.put("player", playerInfo); - - // VPN detection information - if (result.response() != null) { - JSONObject detectionInfo = new JSONObject(); - detectionInfo.put("isProxy", result.response().isProxy()); - detectionInfo.put("countryCode", result.response().getCountryCode()); - detectionInfo.put("countryName", result.response().getCountryName()); - detectionInfo.put("city", result.response().getCity()); - detectionInfo.put("isp", result.response().getIsp()); - detectionInfo.put("asn", result.response().getAsn()); - - if (result.response().getMethod() != null) { - detectionInfo.put("method", result.response().getMethod()); - } - - payload.put("detection", detectionInfo); - } - - return payload; - } -} diff --git a/Common/Source/src/main/resources/config.yml b/Common/Source/src/main/resources/config.yml index 55fe5ce..4827f1e 100644 --- a/Common/Source/src/main/resources/config.yml +++ b/Common/Source/src/main/resources/config.yml @@ -40,24 +40,6 @@ commands: - kick %player% VPNs are not allowed on our server! # Enable/disable the default kicking feature of KauriVPN. kickPlayers: true -# Configure webhook notifications when VPN is detected -webhooks: - # Enable/disable webhook notifications - enabled: false - # The webhook URL to send POST requests to when a VPN is detected - url: '' - # Webhook format type: 'discord', 'slack', or 'generic' - # - discord: Formats payload for Discord webhooks with rich embeds - # - slack: Formats payload for Slack webhooks - # - generic: Sends raw JSON payload (for custom integrations) - format: 'discord' - # Optional: Set to true to include authentication header (Authorization: Bearer ) - useAuthentication: false - # The authentication token to use when useAuthentication is true - # Security Note: Token is stored in plaintext. Ensure proper file permissions on this file. - authToken: '' - # Timeout in seconds for webhook requests (default: 5) - timeout: 5 # Configure all alerting functionality alerts: # You may set to 'false' to disable all alerts functionality diff --git a/WEBHOOK_GUIDE.md b/WEBHOOK_GUIDE.md deleted file mode 100644 index 439a5bb..0000000 --- a/WEBHOOK_GUIDE.md +++ /dev/null @@ -1,317 +0,0 @@ -# Webhook Integration Guide - -This document explains how to configure and use the webhook feature in AntiVPN to receive notifications when a player is detected using a VPN. - -## Overview - -When a player is detected using a VPN or connecting from a blocked country, AntiVPN can send an HTTP POST request to a configured webhook URL with detailed information about the detection. AntiVPN supports **Discord**, **Slack**, and **generic** webhook formats. - -## Configuration - -Add the following configuration to your `config.yml`: - -```yaml -webhooks: - # Enable/disable webhook notifications - enabled: false - # The webhook URL to send POST requests to when a VPN is detected - url: '' - # Webhook format type: 'discord', 'slack', or 'generic' - # - discord: Formats payload for Discord webhooks with rich embeds (default) - # - slack: Formats payload for Slack webhooks - # - generic: Sends raw JSON payload (for custom integrations) - format: 'discord' - # Optional: Set to true to include authentication header (Authorization: Bearer ) - useAuthentication: false - # The authentication token to use when useAuthentication is true - # Security Note: Token is stored in plaintext. Ensure proper file permissions on this file. - authToken: '' - # Timeout in seconds for webhook requests (default: 5) - timeout: 5 -``` - -### Configuration Options - -- **enabled**: Set to `true` to enable webhook notifications -- **url**: The complete URL where webhook POST requests will be sent -- **format**: The webhook format type (`discord`, `slack`, or `generic`) -- **useAuthentication**: Set to `true` to include an `Authorization: Bearer ` header -- **authToken**: The authentication token to use (only used when `useAuthentication` is true) -- **timeout**: Connection and read timeout in seconds (default: 5) - -## Webhook Formats - -### Discord Format (format: 'discord') - -Discord webhooks receive rich embeds with color-coded alerts and organized fields. This is the **recommended and default format** for Discord webhooks. - -**Example Discord Payload:** -```json -{ - "embeds": [{ - "title": "🚫 VPN/Proxy Detection", - "description": "A player attempted to join using a VPN/proxy or from a blocked country.", - "color": 15158332, - "fields": [ - { - "name": "Player", - "value": "ExamplePlayer", - "inline": true - }, - { - "name": "UUID", - "value": "550e8400-e29b-41d4-a716-446655440000", - "inline": true - }, - { - "name": "IP Address", - "value": "192.0.2.1", - "inline": true - }, - { - "name": "Country", - "value": "United States (US)", - "inline": true - }, - { - "name": "City", - "value": "New York", - "inline": true - }, - { - "name": "ISP", - "value": "Example ISP", - "inline": true - }, - { - "name": "ASN", - "value": "AS12345", - "inline": true - }, - { - "name": "Detection Method", - "value": "Blacklist", - "inline": true - }, - { - "name": "Proxy Status", - "value": "✓ Detected", - "inline": true - } - ], - "timestamp": "2024-02-04T12:00:00.000Z", - "footer": { - "text": "AntiVPN Detection System" - } - }] -} -``` - -**Features:** -- Color coding: Red for proxy detections, Orange for country blocks -- Rich embeds with organized fields -- Timestamp included -- All detection information in one message - -### Slack Format (format: 'slack') - -Slack webhooks receive simple text messages with Slack markdown formatting. - -**Example Slack Payload:** -```json -{ - "text": "*VPN/Proxy Detection Alert*\nPlayer: ExamplePlayer\nIP: 192.0.2.1\nCountry: United States (US)\nCity: New York\nISP: Example ISP\nMethod: Blacklist\n" -} -``` - -**Features:** -- Simple text format with markdown -- All essential information included -- Works with standard Slack incoming webhooks - -### Generic Format (format: 'generic') - -Generic webhooks receive the raw JSON structure for custom integrations. - -**Example Generic Payload:** - -```json -{ - "event": "vpn_detected", - "timestamp": 1707022000000, - "resultType": "DENIED_PROXY", - "player": { - "uuid": "550e8400-e29b-41d4-a716-446655440000", - "name": "ExamplePlayer", - "ip": "192.0.2.1" - }, - "detection": { - "isProxy": true, - "countryCode": "US", - "countryName": "United States", - "city": "New York", - "isp": "Example ISP", - "asn": "AS12345", - "method": "Blacklist" - } -} -``` - -**Features:** -- Complete JSON structure -- All fields included -- Best for custom backend integrations - -## Payload Field Reference (Generic Format) - -- **event**: Always set to `"vpn_detected"` -- **timestamp**: Unix timestamp in milliseconds when the detection occurred -- **resultType**: The type of detection result. Possible values: - - `"DENIED_PROXY"`: Player is using a VPN/proxy - - `"DENIED_COUNTRY"`: Player is connecting from a blocked country -- **player**: Player information - - **uuid**: Player's Minecraft UUID - - **name**: Player's username - - **ip**: Player's IP address -- **detection**: VPN detection details - - **isProxy**: Boolean indicating if a VPN/proxy was detected - - **countryCode**: ISO country code (e.g., "US", "GB") - - **countryName**: Full country name - - **city**: City name - - **isp**: Internet Service Provider name - - **asn**: Autonomous System Number - - **method**: Detection method used (e.g., "Blacklist", "Datacenter") - -## Example Configurations - -### Discord Webhook - -```yaml -webhooks: - enabled: true - url: 'https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN' - format: 'discord' - useAuthentication: false - authToken: '' - timeout: 5 -``` - -**Note:** With `format: 'discord'`, AntiVPN will automatically format the webhook payload with rich embeds that Discord understands natively. No proxy service is needed! - -### Slack Webhook - -```yaml -webhooks: - enabled: true - url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK' - format: 'slack' - useAuthentication: false - authToken: '' - timeout: 5 -``` - -### Custom Backend with Authentication - -```yaml -webhooks: - enabled: true - url: 'https://your-server.com/api/vpn-alerts' - format: 'generic' - useAuthentication: true - authToken: 'your-secret-token-here' - timeout: 10 -``` - -### Local Development Server - -```yaml -webhooks: - enabled: true - url: 'http://localhost:8080/webhooks/vpn' - format: 'generic' - useAuthentication: false - authToken: '' - timeout: 5 -``` - -## Testing Your Webhook - -### Using the Test Server - -A simple Python test server is available to verify webhook functionality: - -```python -#!/usr/bin/env python3 -import json -from http.server import HTTPServer, BaseHTTPRequestHandler -from datetime import datetime - -class WebhookHandler(BaseHTTPRequestHandler): - def do_POST(self): - content_length = int(self.headers.get('Content-Length', 0)) - body = self.rfile.read(content_length).decode('utf-8') - - print(f"\nWebhook received at {datetime.now().isoformat()}") - print("Payload:", json.dumps(json.loads(body), indent=2)) - - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(b'{"status": "received"}') - -if __name__ == '__main__': - server = HTTPServer(('localhost', 8080), WebhookHandler) - print("Test server running on http://localhost:8080") - server.serve_forever() -``` - -1. Save the above script as `test_server.py` -2. Run it: `python3 test_server.py` -3. Configure AntiVPN to use `http://localhost:8080` as the webhook URL -4. The server will display incoming webhooks in the console - -## Security Considerations - -1. **HTTPS**: Always use HTTPS URLs in production to encrypt webhook data in transit -2. **Authentication**: Enable authentication to prevent unauthorized webhook receivers from impersonating your endpoint -3. **Token Storage**: The authentication token is stored in plaintext in `config.yml`. Ensure proper file system permissions (e.g., `chmod 600 config.yml`) -4. **IP Whitelisting**: Consider implementing IP whitelisting on your webhook endpoint to only accept requests from your Minecraft server -5. **Rate Limiting**: Implement rate limiting on your webhook endpoint to prevent abuse - -## Troubleshooting - -### Webhook Not Firing - -- Verify `enabled: true` in configuration -- Check that `url` is properly set and accessible -- Look for error messages in server logs -- Verify network connectivity from your Minecraft server to the webhook URL - -### Authentication Failures - -- Verify `useAuthentication` and `authToken` are properly configured -- Check that your webhook endpoint expects the `Authorization: Bearer ` header format -- Review webhook endpoint logs for authentication errors - -### Timeout Errors - -- Increase the `timeout` value in configuration -- Verify your webhook endpoint responds quickly (within timeout period) -- Check network latency between Minecraft server and webhook endpoint - -## Common Use Cases - -### Log Aggregation -Send webhook data to a log aggregation service like Splunk, ELK, or Datadog for analysis and alerting. - -### Discord/Slack Notifications -Use a webhook proxy service to format and forward alerts to Discord or Slack channels. - -### Automated Banning -Integrate with your server management system to automatically apply additional penalties to detected players. - -### Analytics -Store webhook data in a database for analytics on VPN usage patterns, geographic distribution, and ISP trends. - -### Compliance Logging -Maintain an audit trail of VPN detections for compliance and security purposes.