From 39961a36834af6f8a9de67ad15f9e68d5057bd25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 04:45:45 +0000 Subject: [PATCH 1/8] Initial plan From ba9ab6fb314be7856b8b2b6fb01694201a54fa24 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 04:49:55 +0000 Subject: [PATCH 2/8] Add webhook functionality for VPN detection Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- .../dev/brighten/antivpn/api/VPNConfig.java | 65 +++++++- .../dev/brighten/antivpn/api/VPNExecutor.java | 4 + .../antivpn/webhook/WebhookNotifier.java | 149 ++++++++++++++++++ Common/Source/src/main/resources/config.yml | 12 ++ 4 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java 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 From 9fac54fe0b8d650afd20fd7817e6b223a697888e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 04:51:31 +0000 Subject: [PATCH 3/8] Address code review feedback: use dedicated executor, improve logging, add security notes Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- .../src/main/java/dev/brighten/antivpn/api/VPNConfig.java | 2 ++ .../java/dev/brighten/antivpn/webhook/WebhookNotifier.java | 4 ++-- Common/Source/src/main/resources/config.yml | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) 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 8becdd8..a679b09 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 @@ -294,6 +294,8 @@ public class VPNConfig { /** * The authentication token to use for webhook requests. + * Note: Token is stored in memory as plaintext. Ensure proper file system + * permissions are set on config.yml to protect sensitive authentication tokens. * @return String */ public String webhookAuthToken() { 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 index 05dcd3e..8e3e0df 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java @@ -44,7 +44,7 @@ public class WebhookNotifier { } catch (Exception e) { AntiVPN.getInstance().getExecutor().logException("Failed to send webhook notification", e); } - }); + }, dev.brighten.antivpn.api.VPNExecutor.threadExecutor); } /** @@ -91,7 +91,7 @@ public class WebhookNotifier { // Check response int responseCode = connection.getResponseCode(); if (responseCode >= 200 && responseCode < 300) { - AntiVPN.getInstance().getExecutor().log(Level.FINE, + AntiVPN.getInstance().getExecutor().log(Level.INFO, "Successfully sent webhook notification for player %s (response: %d)", player.getName(), responseCode); } else { diff --git a/Common/Source/src/main/resources/config.yml b/Common/Source/src/main/resources/config.yml index 2f4e124..9f16093 100644 --- a/Common/Source/src/main/resources/config.yml +++ b/Common/Source/src/main/resources/config.yml @@ -49,6 +49,7 @@ webhooks: # 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 From 6246bce70d6abc84955e7f869ae9aa05b3a9636e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 04:53:36 +0000 Subject: [PATCH 4/8] Add comprehensive webhook integration guide Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- WEBHOOK_GUIDE.md | 199 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 WEBHOOK_GUIDE.md diff --git a/WEBHOOK_GUIDE.md b/WEBHOOK_GUIDE.md new file mode 100644 index 0000000..d869f5a --- /dev/null +++ b/WEBHOOK_GUIDE.md @@ -0,0 +1,199 @@ +# 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. + +## 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: '' + # 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 +- **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 Payload + +When a VPN is detected, AntiVPN sends a JSON payload with the following structure: + +```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" + } +} +``` + +### Payload Fields + +- **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' + useAuthentication: false + authToken: '' + timeout: 5 +``` + +Note: Discord webhooks require a specific format. You may need to use a proxy service to convert the AntiVPN payload format to Discord's webhook format. + +### Custom Backend with Authentication + +```yaml +webhooks: + enabled: true + url: 'https://your-server.com/api/vpn-alerts' + useAuthentication: true + authToken: 'your-secret-token-here' + timeout: 10 +``` + +### Local Development Server + +```yaml +webhooks: + enabled: true + url: 'http://localhost:8080/webhooks/vpn' + 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. From 0f14e68c36ac291e18a3d2bbc890ec88b1bb4d45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:09:14 +0000 Subject: [PATCH 5/8] Add Discord and Slack webhook format support with configurable format option Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- .../dev/brighten/antivpn/api/VPNConfig.java | 88 +++-------- .../dev/brighten/antivpn/api/VPNExecutor.java | 2 - .../antivpn/webhook/WebhookNotifier.java | 149 +++++++++++++++++- Common/Source/src/main/resources/config.yml | 5 + 4 files changed, 166 insertions(+), 78 deletions(-) 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 028e0c6..0c45512 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 @@ -45,10 +45,9 @@ public class VPNConfig { 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()); + defaultWebhookUrl = new ConfigDefault<>("", "webhooks.url", AntiVPN.getInstance()), + defaultWebhookAuthToken = new ConfigDefault<>("", "webhooks.authToken", AntiVPN.getInstance()), + defaultWebhookFormat = new ConfigDefault<>("discord", "webhooks.format", AntiVPN.getInstance()); private final ConfigDefault cacheResultsDefault = new ConfigDefault<>(true, "cachedResults", AntiVPN.getInstance()), defaultUseCredentials = new ConfigDefault<>(true, @@ -62,14 +61,11 @@ public class VPNConfig { 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()); + 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()), - defaultWebhookTimeout = new ConfigDefault<>(5, "webhooks.timeout", - 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", @@ -79,23 +75,6 @@ public class VPNConfig { defCountrylist = new ConfigDefault<>(new ArrayList<>(), "countries.list", AntiVPN.getInstance()); - private String license, kickMessage, databaseType, databaseName, mongoURL, username, password, ip, alertMsg, - countryVanillaKickReason, webhookUrl, webhookAuthToken; - private List prefixWhitelists, commands, countryList, countryKickCommands; - private int port, webhookTimeout; - private boolean cacheResults, databaseEnabled, useCredentials, commandsEnabled, kickPlayers, alertToStaff, - metrics, whitelistCountries, webhookEnabled, webhookUseAuth; - - /** - * License from https://funkemunky.cc/shop to be used for more queries. - * @return String - */ - public String getLicense() { - return license; - } - - /** - * If true, results will be cached to reduce queries to https://funkemunky.cc @Getter private String license; @Getter @@ -133,6 +112,18 @@ 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 ... @@ -220,48 +211,6 @@ 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. - * Note: Token is stored in memory as plaintext. Ensure proper file system - * permissions are set on config.yml to protect sensitive authentication tokens. - * @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 */ @@ -294,6 +243,7 @@ public class VPNConfig { 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 cef988a..3fb3611 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 @@ -80,8 +80,6 @@ public abstract class VPNExecutor { // Send webhook notification if enabled WebhookNotifier.sendWebhookNotification(player, result); - if (AntiVPN.getInstance().getVpnConfig().alertToStaff()) AntiVPN.getInstance().getPlayerExecutor() - //Ensuring kick task is always running if(kickTask == null || kickTask.isDone() || kickTask.isCancelled()) { startKickChecks(); 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 index 8e3e0df..aaae25e 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java @@ -26,11 +26,11 @@ public class WebhookNotifier { * @param result The check result containing VPN information */ public static void sendWebhookNotification(APIPlayer player, CheckResult result) { - if (!AntiVPN.getInstance().getVpnConfig().webhookEnabled()) { + if (!AntiVPN.getInstance().getVpnConfig().isWebhookEnabled()) { return; } - String webhookUrl = AntiVPN.getInstance().getVpnConfig().webhookUrl(); + 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"); @@ -44,7 +44,7 @@ public class WebhookNotifier { } catch (Exception e) { AntiVPN.getInstance().getExecutor().logException("Failed to send webhook notification", e); } - }, dev.brighten.antivpn.api.VPNExecutor.threadExecutor); + }, AntiVPN.getInstance().getExecutor().getThreadExecutor()); } /** @@ -67,16 +67,16 @@ public class WebhookNotifier { 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 (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().webhookTimeout() * 1000); - connection.setReadTimeout(AntiVPN.getInstance().getVpnConfig().webhookTimeout() * 1000); + connection.setConnectTimeout(AntiVPN.getInstance().getVpnConfig().getWebhookTimeout() * 1000); + connection.setReadTimeout(AntiVPN.getInstance().getVpnConfig().getWebhookTimeout() * 1000); // Create JSON payload JSONObject payload = createPayload(player, result); @@ -113,6 +113,141 @@ public class WebhookNotifier { * @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 = new JSONObject[0]; + 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 diff --git a/Common/Source/src/main/resources/config.yml b/Common/Source/src/main/resources/config.yml index 9f16093..55fe5ce 100644 --- a/Common/Source/src/main/resources/config.yml +++ b/Common/Source/src/main/resources/config.yml @@ -46,6 +46,11 @@ webhooks: 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 From faa8bdbb194595c74bbf640a8f4726b3d2a97d44 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:10:21 +0000 Subject: [PATCH 6/8] Update WEBHOOK_GUIDE.md with Discord and Slack format documentation Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- WEBHOOK_GUIDE.md | 128 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 5 deletions(-) diff --git a/WEBHOOK_GUIDE.md b/WEBHOOK_GUIDE.md index d869f5a..439a5bb 100644 --- a/WEBHOOK_GUIDE.md +++ b/WEBHOOK_GUIDE.md @@ -4,7 +4,7 @@ This document explains how to configure and use the webhook feature in AntiVPN t ## 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. +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 @@ -16,6 +16,11 @@ webhooks: 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 @@ -29,13 +34,106 @@ webhooks: - **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 Payload +## Webhook Formats -When a VPN is detected, AntiVPN sends a JSON payload with the following structure: +### 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 { @@ -59,7 +157,12 @@ When a VPN is detected, AntiVPN sends a JSON payload with the following structur } ``` -### Payload Fields +**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 @@ -87,12 +190,25 @@ When a VPN is detected, AntiVPN sends a JSON payload with the following structur webhooks: enabled: true url: 'https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN' + format: 'discord' useAuthentication: false authToken: '' timeout: 5 ``` -Note: Discord webhooks require a specific format. You may need to use a proxy service to convert the AntiVPN payload format to Discord's webhook format. +**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 @@ -100,6 +216,7 @@ Note: Discord webhooks require a specific format. You may need to use a proxy se webhooks: enabled: true url: 'https://your-server.com/api/vpn-alerts' + format: 'generic' useAuthentication: true authToken: 'your-secret-token-here' timeout: 10 @@ -111,6 +228,7 @@ webhooks: webhooks: enabled: true url: 'http://localhost:8080/webhooks/vpn' + format: 'generic' useAuthentication: false authToken: '' timeout: 5 From 51340754e691cc67e1d39e279cc286bf2eefba62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:13:25 +0000 Subject: [PATCH 7/8] Minor optimization: remove unnecessary empty array initialization Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com> --- .../main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index aaae25e..9e22503 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/webhook/WebhookNotifier.java @@ -152,7 +152,7 @@ public class WebhookNotifier { 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 = new JSONObject[0]; + JSONObject[] fields; if (result.response() != null) { fields = new JSONObject[] { createDiscordField("Player", player.getName(), true), From 95d8747bd5f78a6816134030a9d2981ce0a99bf1 Mon Sep 17 00:00:00 2001 From: Dawson Date: Fri, 20 Feb 2026 09:29:52 -0500 Subject: [PATCH 8/8] Fixing webhook spam --- .../src/main/java/dev/brighten/antivpn/api/APIPlayer.java | 3 +++ .../src/main/java/dev/brighten/antivpn/api/VPNExecutor.java | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) 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 0bf5200..f4d6dce 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,6 +20,7 @@ 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; @@ -92,6 +93,7 @@ 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); @@ -135,6 +137,7 @@ 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/VPNExecutor.java b/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNExecutor.java index 3fb3611..a6f173e 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 @@ -23,7 +23,6 @@ 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; @@ -78,7 +77,6 @@ public abstract class VPNExecutor { public void handleKickingOfPlayer(CheckResult result, APIPlayer player) { // Send webhook notification if enabled - WebhookNotifier.sendWebhookNotification(player, result); //Ensuring kick task is always running if(kickTask == null || kickTask.isDone() || kickTask.isCancelled()) {