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 6549945..8becdd8 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 @@ -27,6 +27,10 @@ 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()); private final ConfigDefault cacheResultsDefault = new ConfigDefault<>(true, "cachedResults", AntiVPN.getInstance()), @@ -40,9 +44,15 @@ public class VPNConfig { AntiVPN.getInstance()), defaultWhitelistCountries = new ConfigDefault<>(true, "countries.whitelist", AntiVPN.getInstance()), - defaultMetrics = new ConfigDefault<>(true, "bstats", 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()); private final ConfigDefault - defaultPort = new ConfigDefault<>(-1, "database.port", AntiVPN.getInstance()); + defaultPort = new ConfigDefault<>(-1, "database.port", AntiVPN.getInstance()), + defaultWebhookTimeout = new ConfigDefault<>(5, "webhooks.timeout", + 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", @@ -53,11 +63,11 @@ public class VPNConfig { AntiVPN.getInstance()); private String license, kickMessage, databaseType, databaseName, mongoURL, username, password, ip, alertMsg, - countryVanillaKickReason; + countryVanillaKickReason, webhookUrl, webhookAuthToken; private List prefixWhitelists, commands, countryList, countryKickCommands; - private int port; + private int port, webhookTimeout; private boolean cacheResults, databaseEnabled, useCredentials, commandsEnabled, kickPlayers, alertToStaff, - metrics, whitelistCountries; + metrics, whitelistCountries, webhookEnabled, webhookUseAuth; /** * License from https://funkemunky.cc/shop to be used for more queries. @@ -258,6 +268,46 @@ public class VPNConfig { return metrics; } + /** + * If true, webhook notifications will be sent when a VPN is detected. + * @return boolean + */ + public boolean webhookEnabled() { + return webhookEnabled; + } + + /** + * The webhook URL to send POST requests to when a VPN is detected. + * @return String + */ + public String webhookUrl() { + return webhookUrl; + } + + /** + * If true, an authentication header will be included in webhook requests. + * @return boolean + */ + public boolean webhookUseAuth() { + return webhookUseAuth; + } + + /** + * The authentication token to use for webhook requests. + * @return String + */ + public String webhookAuthToken() { + return webhookAuthToken; + } + + /** + * The timeout in seconds for webhook requests. + * @return int + */ + public int webhookTimeout() { + return webhookTimeout; + } + /** * Grabs all information from the config.yml */ @@ -285,6 +335,11 @@ 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(); } } 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 6635f94..5699d59 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 @@ -6,6 +6,7 @@ import dev.brighten.antivpn.utils.Tuple; import dev.brighten.antivpn.utils.json.JSONException; import dev.brighten.antivpn.web.FunkemunkyAPI; import dev.brighten.antivpn.web.objects.VPNResponse; +import dev.brighten.antivpn.webhook.WebhookNotifier; import lombok.Getter; import java.io.IOException; @@ -66,6 +67,9 @@ public abstract class VPNExecutor { } public void handleKickingOfPlayer(CheckResult result, APIPlayer player) { + // Send webhook notification if enabled + WebhookNotifier.sendWebhookNotification(player, result); + if (AntiVPN.getInstance().getVpnConfig().alertToStaff()) AntiVPN.getInstance().getPlayerExecutor() .getOnlinePlayers() .stream() 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 new file mode 100644 index 0000000..05dcd3e --- /dev/null +++ b/Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java @@ -0,0 +1,149 @@ +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().webhookEnabled()) { + return; + } + + String webhookUrl = AntiVPN.getInstance().getVpnConfig().webhookUrl(); + 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); + } + }); + } + + /** + * 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().webhookUseAuth()) { + String token = AntiVPN.getInstance().getVpnConfig().webhookAuthToken(); + if (token != null && !token.trim().isEmpty()) { + connection.setRequestProperty("Authorization", "Bearer " + token); + } + } + + connection.setDoOutput(true); + connection.setConnectTimeout(AntiVPN.getInstance().getVpnConfig().webhookTimeout() * 1000); + connection.setReadTimeout(AntiVPN.getInstance().getVpnConfig().webhookTimeout() * 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.FINE, + "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 { + 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 4827f1e..2f4e124 100644 --- a/Common/Source/src/main/resources/config.yml +++ b/Common/Source/src/main/resources/config.yml @@ -40,6 +40,18 @@ 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: '' + # Optional: Set to true to include authentication header (Authorization: Bearer ) + useAuthentication: false + # The authentication token to use when useAuthentication is true + 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