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