mirror of
https://github.com/funkemunky/AntiVPN.git
synced 2026-05-31 01:21:55 +00:00
Merge pull request #73 from funkemunky/copilot/add-allowlist-view-command
Add allowlist `show` and `search` subcommands with pagination
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<Boolean> 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<Integer>
|
||||
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<List<String>> 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 <a href="https://funkemunky.cc">...</a>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
+129
-6
@@ -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", "search"};
|
||||
|
||||
@Override
|
||||
public String permission() {
|
||||
@@ -53,7 +53,7 @@ public class AllowlistCommand extends Command {
|
||||
|
||||
@Override
|
||||
public String usage() {
|
||||
return "<add/remove> <player/uuid/ip>";
|
||||
return "<add <player/uuid/ip> | remove <player/uuid/ip> | show [page] | search <query> [page]>";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -72,6 +72,89 @@ public class AllowlistCommand extends Command {
|
||||
return "&cUsage: /antivpn allowlist " + usage();
|
||||
}
|
||||
|
||||
if(args[0].equalsIgnoreCase("show")) {
|
||||
// args[1] = optional page number (defaults to 1)
|
||||
int page = 1;
|
||||
if (args.length > 1) {
|
||||
try {
|
||||
page = Integer.parseInt(args[1]);
|
||||
if (page < 1) page = 1;
|
||||
} catch (NumberFormatException e) {
|
||||
page = 1;
|
||||
}
|
||||
}
|
||||
|
||||
boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled();
|
||||
|
||||
List<UUID> uuids = databaseEnabled
|
||||
? AntiVPN.getInstance().getDatabase().getAllWhitelisted()
|
||||
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelisted());
|
||||
List<CIDRUtils> ips = databaseEnabled
|
||||
? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps()
|
||||
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps());
|
||||
|
||||
List<String> 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 <query> [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 <query> [page]";
|
||||
}
|
||||
|
||||
boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled();
|
||||
|
||||
List<UUID> uuids = databaseEnabled
|
||||
? AntiVPN.getInstance().getDatabase().getAllWhitelisted()
|
||||
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelisted());
|
||||
List<CIDRUtils> ips = databaseEnabled
|
||||
? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps()
|
||||
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps());
|
||||
|
||||
List<String> entries = new ArrayList<>();
|
||||
for (UUID uuid : uuids) {
|
||||
String entry = uuid.toString();
|
||||
if (entry.toLowerCase().contains(search)) {
|
||||
entries.add("&7- &fUUID: &e" + entry);
|
||||
}
|
||||
}
|
||||
for (CIDRUtils cidr : ips) {
|
||||
String entry = cidr.getCidr();
|
||||
if (entry.toLowerCase().contains(search)) {
|
||||
entries.add("&7- &fIP: &e" + entry);
|
||||
}
|
||||
}
|
||||
|
||||
return buildPage(entries, page, safeSearch, "search " + safeSearch);
|
||||
}
|
||||
|
||||
if(args.length == 1)
|
||||
return "&cYou have to provide a player to allow or deny exemption.";
|
||||
|
||||
@@ -201,11 +284,51 @@ 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") || args[0].equalsIgnoreCase("search")) {
|
||||
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();
|
||||
};
|
||||
}
|
||||
|
||||
private String buildPage(List<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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<VPNDatabase> {
|
||||
@Override
|
||||
public void update(VPNDatabase database) throws DatabaseException {
|
||||
List<CIDRUtils> ipRanges = new ArrayList<>();
|
||||
List<CIDRUtils> rangesToInsert = new ArrayList<>();
|
||||
List<BigInteger[]> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<CIDRUtils> getAllWhitelistedIps() {
|
||||
List<CIDRUtils> ips = new ArrayList<>();
|
||||
settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"),
|
||||
Filters.exists("ip"))).forEach((Consumer<? super Document>) doc -> {
|
||||
Filters.exists("cidr_string"))).forEach((Consumer<? super Document>) doc -> {
|
||||
try {
|
||||
var cidr = new CIDRUtils(doc.getString("cidr_string"));
|
||||
ips.add(cidr);
|
||||
|
||||
+108
@@ -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<MongoVPN> {
|
||||
@Override
|
||||
public void update(MongoVPN database) throws DatabaseException {
|
||||
List<CIDRUtils> ipRanges = new ArrayList<>();
|
||||
List<CIDRUtils> rangesToInsert = new ArrayList<>();
|
||||
List<BigInteger[]> rangesToRemove = new ArrayList<>();
|
||||
database.settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"), Filters.exists("cidr_string")))
|
||||
.forEach((Consumer<? super Document>) 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;
|
||||
}
|
||||
}
|
||||
@@ -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<DB> {
|
||||
int versionNumber();
|
||||
boolean needsUpdate(DB database);
|
||||
|
||||
Version<MongoVPN>[] mongoDbVersions = new Version[] {new MongoFirst(), new MongoSecond()};
|
||||
Version<MySqlVPN>[] mysqlVersions = new Version[] {new MySQLFirst(), new Second()};
|
||||
Version<H2VPN>[] h2Versions = new Version[] {new First(), new Second()};
|
||||
Version<MongoVPN>[] mongoDbVersions = new Version[] {new MongoFirst(), new MongoSecond(), new MongoThird()};
|
||||
Version<MySqlVPN>[] mysqlVersions = new Version[] {new MySQLFirst(), new Second(), new Third()};
|
||||
Version<H2VPN>[] h2Versions = new Version[] {new First(), new Second(), new Third()};
|
||||
}
|
||||
@@ -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<CIDRUtils> rangeToCidrs(BigInteger start, BigInteger end) throws UnknownHostException {
|
||||
List<CIDRUtils> 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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 <token>)
|
||||
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
|
||||
|
||||
@@ -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 <token>)
|
||||
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 <token>` 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 <token>` 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.
|
||||
Reference in New Issue
Block a user