Compare commits

...

125 Commits

Author SHA1 Message Date
Dawson 107bf56892 Merge pull request #92 from funkemunky/bugfix/spam-kick
Fixing player spam kick
2026-05-03 14:30:47 -04:00
funkemunky d0ce303b26 Bumping version to 1.10.1.2 2026-05-03 14:29:14 -04:00
funkemunky 54e0cd4c9b Fixing gradle build 2026-05-03 14:25:10 -04:00
funkemunky c224e4af5e Applying spotless 2026-05-03 14:22:32 -04:00
funkemunky 51be97c922 parallel testing if possible 2026-05-03 14:08:15 -04:00
funkemunky c5766b6fb0 tests now pass 2026-05-03 14:03:31 -04:00
funkemunky 54403c02ee Fixing player spam kick 2026-05-03 13:16:21 -04:00
funkemunky bc6828f8af Refactor of checkPlayer logic to fix potential concurrency issues. This was a poor design. Also implemented gradle tooling for debugging velocity plugins 2026-04-30 20:54:37 -04:00
funkemunky 8637cd5e8b Ensuring classloader is isolated 2026-04-30 10:24:04 -04:00
funkemunky 91888a12f1 Updating cache settings 2026-04-30 08:12:52 -04:00
funkemunky f350824123 Adding build caching 2026-04-30 08:05:22 -04:00
funkemunky bafcc2ca33 Making create release build a little faster 2026-04-30 07:44:33 -04:00
funkemunky bacec8c58f Fixing version in plugin manifests 2026-04-30 07:42:58 -04:00
funkemunky 018e466565 Fixing project version 2026-04-30 07:38:52 -04:00
funkemunky 826b3cc567 Fixing create release 2026-04-30 07:21:28 -04:00
funkemunky 4706b62698 Updating changelog 2026-04-30 07:07:49 -04:00
funkemunky fdde7a2884 Bumping h2database version to 2.4.240 2026-04-30 07:04:45 -04:00
funkemunky 89c80e4b50 Bumping h2database version to 2.2.240 2026-04-30 06:58:28 -04:00
funkemunky 493ce27b5b Fixing changelog 2026-04-29 13:28:00 -04:00
funkemunky 00449f0006 Updating changelog 2026-04-29 13:21:28 -04:00
funkemunky 4ac0e57f5d Fixing build issues causing startup problems on velocity 2026-04-29 12:53:56 -04:00
Copilot 980d0c4af8 Add 1.10.1 entry to CHANGELOG.md (#86)
* Add 1.10.1 entry to CHANGELOG.md for 04/28/2026 release

Agent-Logs-Url: https://github.com/funkemunky/AntiVPN/sessions/7d8f588b-2395-4174-9998-fcdb4cba6154

Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>

* Update CHANGELOG.md

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-04-28 21:39:31 -04:00
Dawson c95c7b37a1 Correcting Kick Errors on Bukkit-based platforms (#84)
* Forcing BukkitPlayer#kickPlayer to always run within a main thread context using BukkitRunnable. Bumping version to 1.10.1

* Fixing async kick error, adding condition that allows players to be whitelisted even while they are offline (assuming this is not a cracked server).

* Updating gradle piplines and files

* correcting startup bug as a result of a packaging issue

* Fixing asynchronous run command

* Adds a regression test to ensure this doesnt happen again
2026-04-28 21:30:35 -04:00
funkemunky 775d29114a Revert "Adding postgres"
This reverts commit ee07deb68f.
2026-04-17 09:43:56 -04:00
funkemunky ee07deb68f Adding postgres 2026-04-10 13:46:57 -04:00
Dawson 1acdfe8c4b Added automated testing (#82)
* Using built in pipeline

* Added testing for databases, and fixed some potential database bugs

* Fixing MySQL issues

* Adding central test suite

* Updating test workflow to build without tests and one with tests
2026-04-08 21:29:37 -04:00
Dawson 57109e4c97 Feature/gradle refactor (#80)
* Gradle project refactor

* Updating workflows for github

* Attempting to fix workflows

* Fixing maven central proxy

* Fixing missing dependency in velocity

* Fixing build

* Getting rid of universal submodule

* Updating workflows to handle new build structure, removing maven workflows

* Fixing permissions issues caused by gradle files being handled on Windows

* Revert "Fixing permissions issues caused by gradle files being handled on Windows"

This reverts commit 3e4d8be955.

* replacing instances of gradlew

* Adding gradlew wrapper

* Attempting to fix gradle issues

* Attempting to fix gradle issues

* Adding back ssl args

* Using built in pipeline
2026-04-08 15:27:06 -04:00
funkemunky a483a90851 Updated changelog 2026-04-07 09:50:12 -04:00
funkemunky c6d282e3cc Correcting release workflow to handle current project structure 2026-04-07 09:38:09 -04:00
Dawson ce6e040d3d Merge pull request #74 from funkemunky/31-folia-support-for-kaurivpn
Adding folia support
2026-04-07 09:31:00 -04:00
funkemunky 45d2a7eaa0 Fixing MySQL Lib injection 2026-04-06 15:06:02 -04:00
funkemunky 9e68524bd7 Fixing SQL load errors 2026-04-06 12:52:42 -04:00
Dawson 756b3b04c7 Merge pull request #78 from funkemunky/bugfix/typo-sql-thrid
Fixing typo in SQL that prevents plugin from loading
2026-03-16 08:54:07 -04:00
funkemunky 8bbc964f0d Fixed CIDR notations and the ability for mongo to properly grab CIDRs 2026-03-16 08:51:33 -04:00
funkemunky 1ebaa64d6d Added folia supported to plugin.yml since we do not use things that would be affected by their thread model. 2026-02-26 09:30:36 -05:00
Dawson 7983172423 Merge pull request #73 from funkemunky/copilot/add-allowlist-view-command
Add allowlist `show` and `search` subcommands with pagination
2026-02-20 10:47:26 -05:00
copilot-swe-agent[bot] 3f46db4ad9 Remove unrelated webhook files from PR branch
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-20 15:43:41 +00:00
copilot-swe-agent[bot] 0ea8be6ea3 Initial plan 2026-02-20 10:35:25 -05:00
copilot-swe-agent[bot] 3ead4a0093 Make search a standalone subcommand: show [page] | search <query> [page]
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-20 15:30:57 +00:00
copilot-swe-agent[bot] 8b5bc65159 Change show search to explicit keyword supporting spaces: show [page] search <query...>
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-20 15:22:25 +00:00
funkemunky 11ef7e8d50 Fixed CIDR notations and the ability for mongo to properly grab CIDRs 2026-02-20 10:15:59 -05:00
funkemunky ea4fe063b2 Merge remote-tracking branch 'origin/copilot/add-allowlist-view-command' into copilot/add-allowlist-view-command 2026-02-20 09:32:19 -05:00
copilot-swe-agent[bot] 38dcfcb1fe Add allowlist show command with optional search filter
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-20 09:32:07 -05:00
copilot-swe-agent[bot] 0894ef8e3d Initial plan 2026-02-20 09:32:07 -05:00
Dawson 3892cf43b3 Merge pull request #72 from funkemunky/copilot/add-vpn-detection-webhooks
Add webhook notifications for VPN detection events with Discord and Slack support
2026-02-20 09:30:39 -05:00
funkemunky 95d8747bd5 Fixing webhook spam 2026-02-20 09:29:52 -05:00
copilot-swe-agent[bot] ee7a059b01 Add allowlist show command with optional search filter
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-20 14:17:11 +00:00
copilot-swe-agent[bot] 51340754e6 Minor optimization: remove unnecessary empty array initialization
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-20 14:13:25 +00:00
copilot-swe-agent[bot] 0e2468d3fb Initial plan 2026-02-20 14:11:49 +00:00
copilot-swe-agent[bot] faa8bdbb19 Update WEBHOOK_GUIDE.md with Discord and Slack format documentation
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-20 14:10:21 +00:00
copilot-swe-agent[bot] 0f14e68c36 Add Discord and Slack webhook format support with configurable format option
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-20 14:09:14 +00:00
Dawson 772e924303 Merge branch 'master' into copilot/add-vpn-detection-webhooks 2026-02-20 09:01:22 -05:00
Dawson a195106708 Merge pull request #67 from funkemunky/57-feature-allow-subnets-to-be-whitelisted
WIP: Subnet Whitelisting
2026-02-20 08:59:34 -05:00
funkemunky de6dc8aa7b Adding mojang API backup 2026-02-20 08:57:54 -05:00
copilot-swe-agent[bot] 6246bce70d Add comprehensive webhook integration guide
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-04 04:53:36 +00:00
copilot-swe-agent[bot] 9fac54fe0b Address code review feedback: use dedicated executor, improve logging, add security notes
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-04 04:51:31 +00:00
copilot-swe-agent[bot] ba9ab6fb31 Add webhook functionality for VPN detection
Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-02-04 04:49:55 +00:00
copilot-swe-agent[bot] 39961a3683 Initial plan 2026-02-04 04:45:45 +00:00
funkemunky 5792b81cb1 Correcting logic bug with getDatabaseType function, removing unnecessary function in StringUtil 2026-01-27 10:27:56 -05:00
funkemunky e044d27e27 Removed redundant "Disabling AntiVPN..." log message on Velocity 2026-01-27 10:26:07 -05:00
funkemunky 17cbed8bda Standarized logging calls across platforms 2026-01-27 10:25:47 -05:00
funkemunky 50c357f508 Standarized isWhitelisted to make sure it only accepts cidr strings 2026-01-27 10:25:36 -05:00
funkemunky dddd860c15 Delay command execution to prevent messaging errors 2026-01-27 10:25:21 -05:00
funkemunky e09217877c Update platform listeners to ensure consistent kick functionality 2026-01-27 10:25:03 -05:00
funkemunky 7ffba38992 Improved performance of kicking players, will not allow players to remain on if API tells us we should block them. 2026-01-20 09:44:30 -05:00
funkemunky 8a4b86c9ef Allowlist functionality is now working, corrected sql errors 2026-01-14 09:42:20 -05:00
funkemunky ac57a540c2 Adding copyright headers 2026-01-05 10:27:59 -05:00
funkemunky 18d9bcea39 Implemented MongoDB version and cleared compile errors 2026-01-05 10:27:20 -05:00
funkemunky 9f7f4b40f0 Implementing database versioning for CIDR whitelisting 2026-01-05 09:24:21 -05:00
funkemunky 7913676323 Start of versioning database functions and handling errors 2025-12-30 07:24:35 -08:00
funkemunky 9ea3141ae7 Merge branch 'refs/heads/master' into 57-feature-allow-subnets-to-be-whitelisted 2025-12-30 06:27:43 -08:00
funkemunky cd7008ee09 Merge branch '69-bug-sql-error-on-plugin-load-v194' 2025-12-14 11:43:35 -05:00
funkemunky c743404069 Correcting some package formatting 2025-12-14 11:43:07 -05:00
funkemunky 58395bb705 Merge branch 'master' into 57-feature-allow-subnets-to-be-whitelisted
# Conflicts:
#	Bukkit/Plugin/src/main/java/dev/brighten/antivpn/bukkit/BukkitPlugin.java
#	Bukkit/pom.xml
#	Bungee/BungeePlugin/src/main/java/dev/brighten/antivpn/bungee/BungeePlugin.java
#	Bungee/pom.xml
#	Common/Source/src/main/java/dev/brighten/antivpn/database/Database.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/VPNDatabase.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/local/H2VPN.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/MongoVPN.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/mongodb/MongoDatabase.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/mongodb/records/AlertsUser.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/mongodb/records/CidrWhitelist.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/mongodb/records/UserIpResponse.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/mongodb/records/UserWhitelist.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/postgres/PostgresDatabase.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/sql/MySqlVPN.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/sql/utils/ExecutableStatement.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/sql/utils/MySQL.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/sql/utils/NonClosableConnection.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/sql/utils/Query.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/sql/utils/ResultSetIterator.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/sqllite/LiteDatabase.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/sqllite/version/Version.java
#	Common/Source/src/main/java/dev/brighten/antivpn/database/sqllite/version/impl/First.java
#	Common/Source/src/main/java/dev/brighten/antivpn/utils/CIDRUtils.java
#	Common/Source/src/main/java/dev/brighten/antivpn/utils/IpUtils.java
#	Common/pom.xml
#	Sponge/pom.xml
#	Universal/pom.xml
#	Velocity/VelocityPlugin/src/main/java/dev/brighten/antivpn/velocity/VelocityListener.java
#	Velocity/VelocityPlugin/src/main/java/dev/brighten/antivpn/velocity/VelocityPlugin.java
#	Velocity/pom.xml
#	pom.xml
2025-12-12 06:56:23 -08:00
Dawson bd8bd943bd Merge pull request #70 from funkemunky/69-bug-sql-error-on-plugin-load-v194
Start of project restructure to use a JarInJarClassLoader
2025-12-12 08:50:21 -05:00
funkemunky c95c9bb8f3 Near final staged for refactor 2025-12-04 14:19:02 -05:00
funkemunky c7734b2294 Refactored so in the future I can combine everything into one jar 2025-12-04 09:28:21 -05:00
funkemunky 8faa59f161 Updating to use proxy instead 2025-12-04 08:49:52 -05:00
funkemunky c09269cc20 Implemented Velocity refactor 2025-12-03 20:57:21 -05:00
funkemunky 011d18ad46 fixing class path for bootstrap class in loader bukkit plugin 2025-12-03 10:30:31 -05:00
funkemunky f4d1c7a62c Correcting build process for the rest of the downstream maven poms 2025-12-03 10:11:35 -05:00
funkemunky 6dc0f13c2c Undoing the local change I made for dev purposes 2025-12-03 10:06:57 -05:00
funkemunky 606144d404 Start of project restructure to use a JarInJarClassLoader 2025-12-02 10:24:38 -05:00
funkemunky fe2f9271d8 Changing path...again 2025-09-30 11:47:14 -04:00
funkemunky 43aefc1aed Fixed release 2025-09-30 11:30:10 -04:00
funkemunky ae605a32a5 This should do it 2025-09-30 10:03:35 -04:00
funkemunky e1e7b375c8 Fixing path 2025-09-30 10:00:49 -04:00
funkemunky ef15d4750f Checking download files 2025-09-30 09:52:13 -04:00
funkemunky d8b48e8c9b Checking out files 2025-09-30 09:46:08 -04:00
funkemunky 7b3174eaae Updating to 1.9.4 2025-09-30 08:38:23 -04:00
funkemunky 1c3e653dda Updating test workflow jdk version to Java 21 2025-06-05 11:38:45 -04:00
funkemunky 1930a86a0d Added task for clearing outdated database responses 2025-06-05 11:37:13 -04:00
funkemunky edd255e29d Added mongodb and option to remove spoiled cached responses 2025-06-05 11:35:15 -04:00
funkemunky c191fbccfa Correcting sponge compile 2025-06-05 10:33:16 -04:00
funkemunky 9697150465 Added postgresql 2025-06-05 10:24:45 -04:00
funkemunky a5ea502f17 Added postgres 2025-06-05 09:50:30 -04:00
funkemunky bb3a31e100 CIDR Support setup and implemeneted for adding/removing via command, and checking updates for CIDR implemented. Also fixed problem with SHA-algor not being present 2025-05-30 18:10:08 -04:00
funkemunky 83f06b29c8 Merge branch 'master' into 57-feature-allow-subnets-to-be-whitelisted
# Conflicts:
#	Bukkit/pom.xml
#	Bungee/pom.xml
#	Bungee/src/main/java/dev/brighten/antivpn/bungee/BungeeListener.java
#	Common/pom.xml
#	Common/src/main/java/dev/brighten/antivpn/AntiVPN.java
#	Common/src/main/java/dev/brighten/antivpn/api/VPNExecutor.java
#	Common/src/main/java/dev/brighten/antivpn/database/local/H2VPN.java
#	Common/src/main/java/dev/brighten/antivpn/database/mongo/MongoVPN.java
#	Common/src/main/java/dev/brighten/antivpn/database/sql/MySqlVPN.java
#	Common/src/main/java/dev/brighten/antivpn/database/sql/utils/MySQL.java
#	Common/src/main/java/dev/brighten/antivpn/database/sql/utils/Query.java
#	Common/src/main/java/dev/brighten/antivpn/utils/StringUtil.java
#	Sponge/pom.xml
#	Universal/pom.xml
#	Velocity/pom.xml
#	Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityListener.java
#	pom.xml
2025-05-28 21:19:55 -04:00
funkemunky da5511fc33 Sponge compile target to Java 21 2025-05-28 16:47:06 -04:00
funkemunky f28badf949 Adding maven cache instead of using the setup-java job 2025-05-28 16:44:34 -04:00
Dawson 00124cddf2 Merge pull request #66 from funkemunky/feature/spongepowered
feature/spongepowered
2025-05-28 16:42:44 -04:00
funkemunky 103fdf74da Using JDK 21 instead 2025-05-28 16:40:28 -04:00
funkemunky 4f43028ec0 Attempting to fix cache dependency path 2025-05-28 16:38:48 -04:00
funkemunky 866217ff08 Fixing repositories 2025-05-28 16:36:28 -04:00
funkemunky f271275bfa Sponge is loading, other plugins load, fixed some bugs that were introduced, added sponge plugin uploader in maven workflow 2025-05-28 16:33:29 -04:00
funkemunky 68a2acce00 Got Sponge support working, need to ensure other plugins are still working tho 2025-05-28 15:42:43 -04:00
funkemunky 7247341693 Got initial sponge powered loading working, Im not sure the consequences of it though 2025-05-27 19:39:27 -04:00
funkemunky 3f6bb4a0e6 Refactor, code cleanup, sponge impl.
BungeeCord still has problems, bad API, not my fault technically. still need to fix though
2025-05-27 13:13:53 -04:00
funkemunky de31d837b9 Velocity dependency fix 2025-05-22 14:29:05 -04:00
funkemunky 6967246edb Fixing loading on velocity and removing sneakythrows from Query class 2025-05-19 13:23:38 -04:00
funkemunky 52efc7de3f Ensuring queries auto-close 2025-05-19 10:31:54 -04:00
Dawson ea33a34b3d Shrinking Jar File Size Again (#64)
* Adding back dynamic library support and adding some metrics, velocity first

* Removing guava, using caffiene instead

* Merge cleanup

* Maybe this will get caches working properly now?

* Refactored to be more clean and reliable

* Fixing bungee compile

---------

Co-authored-by: Dawson <dawson@funkemunky.cc>
2025-05-12 11:20:23 -04:00
funkemunky 3a0419cbac Merge branch 'bugfix/h2-load' 2025-04-22 12:09:04 -04:00
funkemunky 24257e4f42 Merge branch 'bugfix/bukkit-kick'
# Conflicts:
#	Bukkit/src/main/java/dev/brighten/antivpn/bukkit/BukkitListener.java
2025-04-22 12:08:53 -04:00
Dawson 2b7f043eb9 Improving shutdown and error handling (#63)
Co-authored-by: Dawson <dawson@funkemunky.cc>
2025-04-22 12:07:30 -04:00
Dawson 2dbe465b9e Fixing Bukkit player kicking not functioning properly (#62)
* Fixing bug where player is not kicked when using proxy on Bukkit servers

* Removing from PlayerLoginEvent and doing PlayerJoinEvent only instead.

---------

Co-authored-by: Dawson <dawson@funkemunky.cc>
2025-04-22 09:58:46 -04:00
funkemunky ac628811cc Merge branch 'master' into bugfix/bukkit-kick 2025-04-22 09:55:08 -04:00
Dawson 3aae8d8f49 Backing up and resetting database on versioning compatibility (#61)
Co-authored-by: Dawson <dawson@funkemunky.cc>
2025-04-22 09:54:43 -04:00
funkemunky f0c37c6ff0 Merge branch 'master' into bugfix/bukkit-kick
# Conflicts:
#	Assembly/pom.xml
#	Bukkit/pom.xml
#	Bungee/pom.xml
#	Common/pom.xml
#	Sponge/pom.xml
#	Velocity/pom.xml
#	pom.xml
2025-04-22 09:53:47 -04:00
funkemunky 311f1e198b Backing up and resetting database on versioning compatibility 2025-04-21 13:20:13 -04:00
Dawson 9cbeed1df3 58-bug-plugin-fails-to-load-after-upgrading-to-193-on-velocity (#60)
* Moving to previous H2 version that was in 1.9.2 that somehow got downgraded

* Reverting back to Java 8 compile target

* removing minimize on shade for Common

* Clearly something here changed something, cause reverting it this way seems to have fixed it

* Cleaning up code here

---------

Co-authored-by: Dawson <dawson@funkemunky.cc>
2025-03-27 10:44:34 -04:00
funkemunky 6453898ca4 Removing from PlayerLoginEvent and doing PlayerJoinEvent only instead. 2025-03-20 10:29:21 -04:00
funkemunky 6243727ebf Fixing bug where player is not kicked when using proxy on Bukkit servers 2025-03-17 11:24:56 -04:00
funkemunky 247b329280 Added test workflow 2025-02-01 10:46:35 -05:00
funkemunky 069142a06b Version 2.0.0-SNAPSHOT: Updated to use SQLLite with versioning. Needed a recode of the database system to allow for future updates. 2025-02-01 10:45:53 -05:00
236 changed files with 19765 additions and 11682 deletions
+1288
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
name: create-release.yml
on:
workflow_dispatch:
jobs:
build-and-release:
name: Build and Release
runs-on: ubuntu-latest
env:
JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStoreType=JKS -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit
steps:
- uses: actions/checkout@v5
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'zulu'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '9.4.1'
cache-read-only: true
cache-cleanup: on-success
- name: Build
run: gradle build -x test --no-daemon
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version Number from Gradle
id: get_version
run: |
VERSION=$(gradle properties -q | awk -F': ' '/^version:/ {print $2; exit}')
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Extract latest CHANGELOG entry
id: changelog
run: |
CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
echo "Extracted latest release notes from CHANGELOG.md:"
echo "$CHANGELOG_CONTENT"
{
echo 'content<<EOF'
echo "$CHANGELOG_CONTENT"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Create Release
uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ env.VERSION }}
release_name: Release v${{ env.VERSION }}
draft: false
prerelease: false
body: ${{ steps.changelog.outputs.content }}
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./build/libs/AntiVPN-${{ env.VERSION }}-universal.jar
asset_name: AntiVPN-v${{ env.VERSION }}.jar
asset_content_type: application/java-archive
+37
View File
@@ -0,0 +1,37 @@
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
env:
JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStoreType=JKS -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit
steps:
- uses: actions/checkout@v5
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'zulu'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '9.4.1'
cache-overwrite-existing: 'true'
cache-read-only: ${{ github.event_name == 'pull_request' }}
cache-cleanup: on-success
- name: Build
run: gradle build -x test --no-daemon
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload AntiVPN
uses: actions/upload-artifact@v4
with:
name: AntiVPN-Universal
path: build/libs/AntiVPN-*-universal.jar
-28
View File
@@ -1,28 +0,0 @@
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17.0.2
uses: actions/setup-java@v4
with:
java-version: 17.0
distribution: 'zulu'
cache: 'maven'
- name: Compile
run: mvn -B package --file pom.xml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload AntiVPN
uses: actions/upload-artifact@v4
with:
name: AntiVPN
path: Assembly/target/Assembly-*.jar
+34
View File
@@ -0,0 +1,34 @@
on:
push:
jobs:
build-and-test:
runs-on: ubuntu-latest
env:
JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStoreType=JKS -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit
steps:
- uses: actions/checkout@v5
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'zulu'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '9.4.1'
cache-overwrite-existing: 'true'
cache-read-only: ${{ github.event_name == 'pull_request' }}
cache-cleanup: on-success
- name: Build
run: gradle build -x test --no-daemon
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload AntiVPN
uses: actions/upload-artifact@v4
with:
name: AntiVPN-Universal
path: build/libs/AntiVPN-*-universal.jar
- name: Test
run: gradle test --no-daemon --parallel
+4
View File
@@ -188,6 +188,7 @@ fabric.properties
# Package Files #
*.jar
!gradle/wrapper/gradle-wrapper.jar
*.war
*.nar
*.ear
@@ -293,3 +294,6 @@ $RECYCLE.BIN/
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,maven,java,intellij,eclipse,netbeans
/.gradle/
.grade/**
/run/
-76
View File
@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>AntiVPN</artifactId>
<groupId>dev.brighten.antivpn</groupId>
<version>1.9.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Assembly</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.yaml.snakeyaml</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.org.yaml.snakeyaml</shadedPattern>
</relocation>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.google.common</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Bungee</artifactId>
<version>${version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Velocity</artifactId>
<version>${version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Common</artifactId>
<version>${version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Bukkit</artifactId>
<version>${version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
+37
View File
@@ -0,0 +1,37 @@
plugins {
id 'com.gradleup.shadow'
}
evaluationDependsOn(':Bukkit:Plugin')
dependencies {
compileOnly 'org.spigotmc:spigot-api:1.20.2-R0.1-SNAPSHOT'
compileOnly project(':Bukkit:Plugin')
implementation project(':Common:loader-utils')
}
tasks.processResources {
filesMatching('plugin.yml') {
expand(project: project, projectVersion: project.version)
}
}
shadowJar {
archiveClassifier.set('')
// Include the shaded plugin jar as a single resource file
from(project(':Bukkit:Plugin').tasks.shadowJar) {
rename { 'antivpn-bukkit.jarinjar' }
}
}
// Ensure the plugin is built before packaging the loader
tasks.named('shadowJar') {
dependsOn(':Bukkit:Plugin:shadowJar')
}
tasks.named('compileJava') {
dependsOn(':Bukkit:Plugin:shadowJar')
}
tasks.build.dependsOn shadowJar
@@ -0,0 +1,51 @@
/*
* 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.bukkit.loader;
import dev.brighten.antivpn.loader.JarInJarClassLoader;
import dev.brighten.antivpn.loader.LoaderBootstrap;
import org.bukkit.plugin.java.JavaPlugin;
public class BukkitLoaderPlugin extends JavaPlugin {
private static final String JAR_NAME = "antivpn-bukkit.jarinjar";
private static final String SOURCE_NAME = "antivpn-source.jarinjar";
private static final String BOOTSTRAP_CLASS = "dev.brighten.antivpn.bukkit.BukkitPlugin";
private final LoaderBootstrap plugin;
public BukkitLoaderPlugin() {
JarInJarClassLoader loader =
new JarInJarClassLoader(getClass().getClassLoader(), JAR_NAME, SOURCE_NAME);
this.plugin = loader.instantiatePlugin(BOOTSTRAP_CLASS, JavaPlugin.class, this);
}
@Override
public void onLoad() {
this.plugin.onLoad(getDataFolder());
}
@Override
public void onEnable() {
this.plugin.onEnable();
}
@Override
public void onDisable() {
this.plugin.onDisable();
}
}
@@ -0,0 +1,6 @@
name: KauriVPN
main: dev.brighten.antivpn.bukkit.loader.BukkitLoaderPlugin
version: ${project.version}
author: funkemunky
api-version: 1.13
folia-supported: true
+28
View File
@@ -0,0 +1,28 @@
plugins {
id 'com.gradleup.shadow'
}
dependencies {
compileOnly 'org.spigotmc:spigot-api:1.20.2-R0.1-SNAPSHOT'
implementation project(':Common:Source')
implementation project(':Common:loader-utils')
implementation 'org.bstats:bstats-bukkit:2.2.1'
testImplementation 'com.github.seeseemelk:MockBukkit-v1.20:3.84.0'
testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation 'org.mockito:mockito-subclass:5.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'
testImplementation testFixtures(project(':Common:Source'))
}
shadowJar {
archiveClassifier.set('')
relocate 'org.bstats', 'dev.brighten.antivpn.bukkit.org.bstats'
}
test {
useJUnitPlatform()
systemProperty 'mockito.mockmaker', 'subclass'
}
tasks.build.dependsOn shadowJar
@@ -0,0 +1,55 @@
/*
* 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.bukkit;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.command.CommandExecutor;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@RequiredArgsConstructor
public class BukkitCommandExecutor implements CommandExecutor {
private final CommandSender sender;
@Override
public void sendMessage(String message, Object... objects) {
sender.sendMessage(
ChatColor.translateAlternateColorCodes('&', String.format(message, objects)));
}
@Override
public boolean hasPermission(String permission) {
return sender.hasPermission(permission);
}
@Override
public Optional<APIPlayer> getPlayer() {
if (!isPlayer()) return Optional.empty();
return AntiVPN.getInstance().getPlayerExecutor().getPlayer(((Player) sender).getUniqueId());
}
@Override
public boolean isPlayer() {
return sender instanceof Player;
}
}
@@ -0,0 +1,123 @@
/*
* 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.bukkit;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.CheckResult;
import dev.brighten.antivpn.api.OfflinePlayer;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.utils.StringUtil;
import java.util.logging.Level;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitRunnable;
public class BukkitListener extends VPNExecutor implements Listener {
@Override
public void registerListeners() {
Bukkit.getPluginManager().registerEvents(this, BukkitPlugin.pluginInstance.getPlugin());
}
@Override
public void log(Level level, String log, Object... objects) {
Bukkit.getLogger().log(level, String.format(log, objects));
}
@Override
public void log(String log, Object... objects) {
log(Level.INFO, String.format(log, objects));
}
@Override
public void logException(String message, Throwable ex) {
Bukkit.getLogger().log(Level.SEVERE, message, ex);
}
@Override
public void runCommand(String command) {
new BukkitRunnable() {
public void run() {
Bukkit.getServer()
.dispatchCommand(
Bukkit.getConsoleSender(), ChatColor.translateAlternateColorCodes('&', command));
}
}.runTask(BukkitPlugin.pluginInstance.getPlugin());
}
@Override
public void disablePlugin() {
HandlerList.unregisterAll(this);
Bukkit.getPluginManager().disablePlugin(BukkitPlugin.pluginInstance.getPlugin());
}
@EventHandler(priority = EventPriority.HIGH)
public void onLogin(final PlayerLoginEvent event) {
APIPlayer player =
AntiVPN.getInstance()
.getPlayerExecutor()
.getPlayer(event.getPlayer().getUniqueId())
.orElse(
new OfflinePlayer(
event.getPlayer().getUniqueId(),
event.getPlayer().getName(),
event.getAddress()));
CheckResult result = player.checkPlayer();
if (!result.resultType().isShouldBlock()) return;
if (!AntiVPN.getInstance().getVpnConfig().isKickPlayers()) {
return;
}
event.setResult(PlayerLoginEvent.Result.KICK_BANNED);
event.setKickMessage(
switch (result.resultType()) {
case DENIED_COUNTRY ->
StringUtil.varReplace(
AntiVPN.getInstance().getVpnConfig().getCountryVanillaKickReason(),
player,
result.response());
case DENIED_PROXY ->
StringUtil.varReplace(
AntiVPN.getInstance().getVpnConfig().getKickMessage(), player, result.response());
default -> "You were kicked by KauriVPN for an unknown reason!";
});
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onJoin(final PlayerJoinEvent event) {
AntiVPN.getInstance()
.getPlayerExecutor()
.getPlayer(event.getPlayer().getUniqueId())
.ifPresent(APIPlayer::checkAlertsState);
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
AntiVPN.getInstance().getPlayerExecutor().unloadPlayer(event.getPlayer().getUniqueId());
}
}
@@ -0,0 +1,55 @@
/*
* 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.bukkit;
import dev.brighten.antivpn.api.APIPlayer;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
public class BukkitPlayer extends APIPlayer {
private final Player player;
public BukkitPlayer(Player player) {
super(
player.getUniqueId(),
player.getName(),
player.getAddress() != null ? player.getAddress().getAddress() : null);
this.player = player;
}
@Override
public void sendMessage(String message) {
player.sendMessage(ChatColor.translateAlternateColorCodes('&', message));
}
@Override
public void kickPlayer(String reason) {
new BukkitRunnable() {
public void run() {
player.kickPlayer(ChatColor.translateAlternateColorCodes('&', reason));
}
}.runTask(BukkitPlugin.pluginInstance.getPlugin());
}
@Override
public boolean hasPermission(String permission) {
return player.hasPermission(permission);
}
}
@@ -0,0 +1,65 @@
/*
* 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.bukkit;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.PlayerExecutor;
import java.util.*;
import java.util.stream.Collectors;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
public class BukkitPlayerExecutor implements PlayerExecutor {
private final Map<UUID, BukkitPlayer> cachedPlayers = new HashMap<>();
@Override
public Optional<APIPlayer> getPlayer(String name) {
final Player player = Bukkit.getPlayer(name);
if (player == null) {
return Optional.empty();
}
return Optional.of(
cachedPlayers.computeIfAbsent(player.getUniqueId(), k -> new BukkitPlayer(player)));
}
@Override
public Optional<APIPlayer> getPlayer(UUID uuid) {
final Player player = Bukkit.getPlayer(uuid);
if (player == null) {
return Optional.empty();
}
return Optional.of(
cachedPlayers.computeIfAbsent(player.getUniqueId(), k -> new BukkitPlayer(player)));
}
@Override
public void unloadPlayer(UUID uuid) {
cachedPlayers.remove(uuid);
}
@Override
public List<APIPlayer> getOnlinePlayers() {
return Bukkit.getOnlinePlayers().stream()
.map(pl -> cachedPlayers.computeIfAbsent(pl.getUniqueId(), k -> new BukkitPlayer(pl)))
.collect(Collectors.toList());
}
}
@@ -0,0 +1,162 @@
/*
* 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.bukkit;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.bukkit.command.BukkitCommand;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.local.H2VPN;
import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.sql.MySqlVPN;
import dev.brighten.antivpn.loader.LoaderBootstrap;
import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie;
import org.bukkit.Bukkit;
import org.bukkit.command.SimpleCommandMap;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.SimplePluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
public class BukkitPlugin implements LoaderBootstrap {
public static BukkitPlugin pluginInstance;
private SimpleCommandMap commandMap;
@Getter private File dataFolder;
private final List<org.bukkit.command.Command> registeredCommands = new ArrayList<>();
@Getter private final JavaPlugin plugin;
public BukkitPlugin(JavaPlugin plugin) {
this.plugin = plugin;
}
@Getter private PlayerCommandRunner playerCommandRunner;
@Override
public void onLoad(File dataFolder) {
this.dataFolder = dataFolder;
}
public void onEnable() {
pluginInstance = this;
Bukkit.getLogger().info("Starting AntiVPN services...");
AntiVPN.start(new BukkitListener(), new BukkitPlayerExecutor(), getDataFolder());
playerCommandRunner = new PlayerCommandRunner();
playerCommandRunner.start();
// Loading our bStats metrics to be pushed to https://bstats.org
if (AntiVPN.getInstance().getVpnConfig().metrics()) {
Bukkit.getLogger().info("Starting bStats metrics...");
Metrics metrics = new Metrics(plugin, 12615);
metrics.addCustomChart(new SimplePie("database_used", this::getDatabaseType));
new BukkitRunnable() {
public void run() {
AntiVPN.getInstance().checked = AntiVPN.getInstance().detections = 0;
}
}.runTaskTimerAsynchronously(plugin, 12000, 12000);
}
Bukkit.getLogger().info("Setting up and registering commands...");
// We need access to the commandMap to register our commands without using the "proper" method
if (Bukkit.getServer().getPluginManager() instanceof SimplePluginManager manager) {
try {
Field field = SimplePluginManager.class.getDeclaredField("commandMap");
field.setAccessible(true);
commandMap = (SimpleCommandMap) field.get(manager);
} catch (IllegalArgumentException
| SecurityException
| NoSuchFieldException
| IllegalAccessException e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
// Registering commands
for (Command command : AntiVPN.getInstance().getCommands()) {
// Wraps our general command API to Bukkit specific calls
BukkitCommand newCommand = new BukkitCommand(command);
// Adding to our own list for later referencing
registeredCommands.add(newCommand);
// This tells Bukkit to register our command for use.
commandMap.register(plugin.getName(), newCommand);
}
// TODO Finish system before implementing on startup
/*Bukkit.getLogger().info("Getting strings...");
AntiVPN.getInstance().getMessageHandler().initStrings(vpnString -> new ConfigDefault<>
(vpnString.getDefaultMessage(), "messages." + vpnString.getKey(), BukkitPlugin.pluginInstance)
.get());
AntiVPN.getInstance().getMessageHandler().reloadStrings();*/
plugin.reloadConfig();
}
@Override
@SuppressWarnings("unchecked")
public void onDisable() {
Bukkit.getLogger().info("Stopping plugin services...");
AntiVPN.getInstance().stop();
playerCommandRunner.stop();
Bukkit.getLogger().info("Unregistering commands...");
try {
Field field = SimpleCommandMap.class.getDeclaredField("knownCommands");
field.setAccessible(true);
if (field.get(commandMap) instanceof Map<?, ?> knownCommands) {
Map<String, org.bukkit.command.Command> casted =
(Map<String, org.bukkit.command.Command>) knownCommands;
casted.values().removeAll(registeredCommands);
registeredCommands.clear();
}
} catch (IllegalAccessException | NoSuchFieldException e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
Bukkit.getLogger().info("Unregistering listeners...");
HandlerList.unregisterAll(plugin);
Bukkit.getLogger().info("Cancelling any running tasks...");
Bukkit.getScheduler().cancelTasks(plugin);
}
private String getDatabaseType() {
VPNDatabase database = AntiVPN.getInstance().getDatabase();
if (database instanceof MySqlVPN) {
return "MySQL";
} else if (database instanceof H2VPN) {
return "H2";
} else if (database instanceof MongoVPN) {
return "MongoDB";
} else {
return "No-Database";
}
}
}
@@ -0,0 +1,80 @@
/*
* 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.bukkit;
import dev.brighten.antivpn.utils.MiscUtils;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import lombok.Data;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitRunnable;
public class PlayerCommandRunner {
private final ScheduledExecutorService executorService;
private final Queue<PlayerAction> playerActions = new ArrayBlockingQueue<>(10000);
public PlayerCommandRunner() {
executorService =
Executors.newSingleThreadScheduledExecutor(
MiscUtils.createThreadFactory("AntiVPN:PlayerCommandRunner"));
}
void start() {
executorService.scheduleAtFixedRate(
() -> {
long currentTime = System.currentTimeMillis();
while (!playerActions.isEmpty()) {
PlayerAction action = playerActions.peek();
if (action == null) continue;
if (currentTime - action.start > 2000L || Bukkit.getPlayer(action.getUuid()) != null) {
new BukkitRunnable() {
public void run() {
action.getAction().run();
}
}.runTask(BukkitPlugin.pluginInstance.getPlugin());
playerActions.poll();
}
}
},
1000,
100,
TimeUnit.MILLISECONDS);
}
void stop() {
executorService.shutdown();
playerActions.clear();
}
void addAction(UUID uuid, Runnable action) {
playerActions.add(new PlayerAction(uuid, System.currentTimeMillis(), action));
}
@Data
static class PlayerAction {
private final UUID uuid;
private final long start;
private final Runnable action;
}
}
@@ -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.bukkit.command;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.bukkit.BukkitCommandExecutor;
import dev.brighten.antivpn.command.Command;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import lombok.val;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandSender;
public class BukkitCommand extends org.bukkit.command.Command {
private final Command command;
public BukkitCommand(Command command) {
super(command.name(), command.description(), command.usage(), Arrays.asList(command.aliases()));
this.command = command;
}
@Override
public List<String> tabComplete(CommandSender sender, String alias, String[] args)
throws IllegalArgumentException {
val children = command.children();
if (children.length > 0 && args.length > 0) {
for (Command child : children) {
if (child.name().equalsIgnoreCase(args[0])
|| Arrays.stream(child.aliases())
.anyMatch(alias2 -> alias2.equalsIgnoreCase(args[0]))) {
return child.tabComplete(
new BukkitCommandExecutor(sender),
alias,
IntStream.range(0, args.length - 1)
.mapToObj(i -> args[i + 1])
.toArray(String[]::new));
}
}
}
return command.tabComplete(new BukkitCommandExecutor(sender), alias, args);
}
@Override
public boolean execute(CommandSender sender, String s, String[] args) {
if (!sender.hasPermission("antivpn.command.*") && !sender.hasPermission(command.permission())) {
sender.sendMessage(
ChatColor.translateAlternateColorCodes(
'&',
AntiVPN.getInstance().getMessageHandler().getString("no-permission").getMessage()));
return true;
}
val children = command.children();
if (children.length > 0 && args.length > 0) {
for (Command child : children) {
if (child.name().equalsIgnoreCase(args[0])
|| Arrays.stream(child.aliases()).anyMatch(alias -> alias.equalsIgnoreCase(args[0]))) {
if (!sender.hasPermission("antivpn.command.*")
&& !sender.hasPermission(child.permission())) {
sender.sendMessage(
ChatColor.translateAlternateColorCodes(
'&',
AntiVPN.getInstance()
.getMessageHandler()
.getString("no-permission")
.getMessage()));
return true;
}
sender.sendMessage(
ChatColor.translateAlternateColorCodes(
'&',
child.execute(
new BukkitCommandExecutor(sender),
IntStream.range(0, args.length - 1)
.mapToObj(i -> args[i + 1])
.toArray(String[]::new))));
return true;
}
}
}
sender.sendMessage(
ChatColor.translateAlternateColorCodes(
'&', command.execute(new BukkitCommandExecutor(sender), args)));
return true;
}
}
@@ -0,0 +1,203 @@
package dev.brighten.antivpn.bukkit;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;
import be.seeseemelk.mockbukkit.MockBukkit;
import be.seeseemelk.mockbukkit.ServerMock;
import be.seeseemelk.mockbukkit.entity.PlayerMock;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.StandardTest;
import dev.brighten.antivpn.api.*;
import dev.brighten.antivpn.message.MessageHandler;
import dev.brighten.antivpn.message.VpnString;
import dev.brighten.antivpn.web.objects.VPNResponse;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.bukkit.command.CommandSender;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class BukkitListenerTest extends StandardTest {
private ServerMock server;
private BukkitListener listener;
private VPNExecutor vpnExecutor;
@BeforeEach
public void setUp() throws Exception {
server = MockBukkit.mock(new RecordingServerMock());
JavaPlugin plugin =
MockBukkit.loadWith(
TestPlugin.class,
new PluginDescriptionFile("AntiVPNTest", "1.0.0", TestPlugin.class.getName()));
BukkitPlugin.pluginInstance = new BukkitPlugin(plugin);
AntiVPN antiVPN = mock(AntiVPN.class);
VPNConfig config = mock(VPNConfig.class);
PlayerExecutor playerExecutor = mock(PlayerExecutor.class);
vpnExecutor = mock(VPNExecutor.class);
MessageHandler messageHandler = mock(MessageHandler.class);
when(antiVPN.getVpnConfig()).thenReturn(config);
when(antiVPN.getPlayerExecutor()).thenReturn(playerExecutor);
when(antiVPN.getExecutor()).thenReturn(vpnExecutor);
when(antiVPN.getMessageHandler()).thenReturn(messageHandler);
when(playerExecutor.getPlayer(any(UUID.class))).thenReturn(Optional.empty());
when(config.getPrefixWhitelists()).thenReturn(java.util.Collections.emptyList());
when(config.getCountryList()).thenReturn(java.util.Collections.emptyList());
when(config.isKickPlayers()).thenReturn(true);
when(config.getKickMessage()).thenReturn("Blocked!");
VpnString mockVpnString = mock(VpnString.class);
when(mockVpnString.getFormattedMessage(any())).thenReturn("Blocked!");
when(messageHandler.getString(anyString())).thenReturn(mockVpnString);
when(vpnExecutor.checkIp(anyString()))
.thenReturn(
CompletableFuture.completedFuture(
VPNResponse.builder()
.success(true)
.proxy(false)
.ip("127.0.0.1")
.method("N/A")
.countryName("N/A")
.city("N/A")
.build()));
// Use reflection to set the private static INSTANCE field
Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE");
instanceField.setAccessible(true);
instanceField.set(null, antiVPN);
listener = new BukkitListener();
}
@AfterEach
public void tearDown() throws Exception {
// Reset the singleton
Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE");
instanceField.setAccessible(true);
instanceField.set(null, null);
BukkitPlugin.pluginInstance = null;
MockBukkit.unmock();
}
@Test
public void testLoginEventAllowed() throws Exception {
PlayerMock player = server.addPlayer("TestPlayer");
InetAddress address = InetAddress.getByName("127.0.0.1");
mockCache(
"127.0.0.1",
new CheckResult(
VPNResponse.builder()
.success(true)
.proxy(false)
.ip("127.0.0.1")
.method("N/A")
.countryName("N/A")
.countryCode("N/A")
.city("N/A")
.build(),
ResultType.ALLOWED,
true));
PlayerLoginEvent event = new PlayerLoginEvent(player, "localhost", address);
listener.onLogin(event);
assertEquals(PlayerLoginEvent.Result.ALLOWED, event.getResult());
}
@Test
public void testLoginPipelineProxyPlayerIsKickedWithoutErrors() throws Exception {
PlayerMock player = server.addPlayer("PipelineProxyPlayer");
InetAddress address = InetAddress.getByName("1.1.1.1");
mockCache();
PlayerLoginEvent event = new PlayerLoginEvent(player, "localhost", address);
assertDoesNotThrow(() -> listener.onLogin(event));
assertEquals(PlayerLoginEvent.Result.KICK_BANNED, event.getResult());
assertEquals(
"Blocked!",
net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection()
.serialize(event.kickMessage()));
}
@Test
public void testRunCommandDispatchesOnPrimaryThreadWhenCalledAsynchronously() {
RecordingServerMock recordingServer = (RecordingServerMock) server;
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
CompletableFuture<Void> asyncCall =
CompletableFuture.runAsync(() -> listener.runCommand("antivpn-test &aok"), executor);
assertDoesNotThrow(() -> asyncCall.get(5, TimeUnit.SECONDS));
assertFalse(
recordingServer.commandDispatched(),
"Command should be scheduled, not dispatched asynchronously");
server.getScheduler().performOneTick();
assertTrue(
recordingServer.commandDispatched(),
"Scheduled command should be dispatched on the next server tick");
assertTrue(
recordingServer.dispatchedOnPrimaryThread(),
"Command dispatch must happen on Bukkit's primary thread");
assertEquals("antivpn-test §aok", recordingServer.dispatchedCommand());
} finally {
executor.shutdownNow();
}
}
public static class TestPlugin extends JavaPlugin {}
private static class RecordingServerMock extends ServerMock {
private final AtomicBoolean commandDispatched = new AtomicBoolean();
private final AtomicBoolean dispatchedOnPrimaryThread = new AtomicBoolean();
private final AtomicReference<String> dispatchedCommand = new AtomicReference<>();
@Override
public boolean dispatchCommand(@NotNull CommandSender sender, @NotNull String commandLine) {
commandDispatched.set(true);
dispatchedOnPrimaryThread.set(isPrimaryThread());
dispatchedCommand.set(commandLine);
return super.dispatchCommand(sender, commandLine);
}
private boolean commandDispatched() {
return commandDispatched.get();
}
private boolean dispatchedOnPrimaryThread() {
return dispatchedOnPrimaryThread.get();
}
private String dispatchedCommand() {
return dispatchedCommand.get();
}
}
}
@@ -0,0 +1,56 @@
package dev.brighten.antivpn.bukkit;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import be.seeseemelk.mockbukkit.MockBukkit;
import be.seeseemelk.mockbukkit.ServerMock;
import be.seeseemelk.mockbukkit.entity.PlayerMock;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.bukkit.plugin.java.JavaPlugin;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class BukkitPlayerTest {
private ServerMock server;
@BeforeEach
void setUp() {
server = MockBukkit.mock();
BukkitPlugin pluginBootstrap = mock(BukkitPlugin.class);
JavaPlugin plugin = MockBukkit.createMockPlugin();
when(pluginBootstrap.getPlugin()).thenReturn(plugin);
BukkitPlugin.pluginInstance = pluginBootstrap;
}
@AfterEach
void tearDown() {
BukkitPlugin.pluginInstance = null;
MockBukkit.unmock();
}
@Test
void kickPlayerCalledFromAsyncContext_isScheduledAndExecutedOnMainThread() {
PlayerMock player = server.addPlayer("AsyncKickPlayer");
BukkitPlayer bukkitPlayer = new BukkitPlayer(player);
assertTrue(player.isOnline());
CompletableFuture<Void> asyncKick =
CompletableFuture.runAsync(() -> bukkitPlayer.kickPlayer("&cBlocked!"));
assertDoesNotThrow(() -> asyncKick.get(1, TimeUnit.SECONDS));
assertTrue(player.isOnline(), "Kick should be deferred to the server scheduler");
server.getScheduler().performTicks(1);
assertFalse(player.isOnline(), "Player should be kicked when scheduled task runs");
}
}
-92
View File
@@ -1,92 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>AntiVPN</artifactId>
<groupId>dev.brighten.antivpn</groupId>
<version>1.9.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Bukkit</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>8</source>
<target>8</target>
<compilerArgument>-XDignore.symbol.file</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<!-- Replace this with your package! -->
<shadedPattern>dev.brighten.antivpn.bukkit.org.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>org.yaml.snakeyaml</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.org.yaml.snakeyaml</shadedPattern>
</relocation>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.google.common</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Common</artifactId>
<version>1.9.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>2.2.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -1,40 +0,0 @@
package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.command.CommandExecutor;
import lombok.RequiredArgsConstructor;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Optional;
@RequiredArgsConstructor
public class BukkitCommandExecutor implements CommandExecutor {
private final CommandSender sender;
@Override
public void sendMessage(String message, Object... objects) {
sender.sendMessage(ChatColor.translateAlternateColorCodes('&',
String.format(message, objects)));
}
@Override
public boolean hasPermission(String permission) {
return sender.hasPermission(permission);
}
@Override
public Optional<APIPlayer> getPlayer() {
if(!isPlayer()) return Optional.empty();
return AntiVPN.getInstance().getPlayerExecutor().getPlayer(((Player)sender).getUniqueId());
}
@Override
public boolean isPlayer() {
return sender instanceof Player;
}
}
@@ -1,231 +0,0 @@
package dev.brighten.antivpn.bukkit;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.message.VpnString;
import dev.brighten.antivpn.web.objects.VPNResponse;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitRunnable;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
@SuppressWarnings("unchecked")
public class BukkitListener extends VPNExecutor implements Listener {
private final Cache<UUID, VPNResponse> responseCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(2000)
.build();
@Override
public void registerListeners() {
BukkitPlugin.pluginInstance.getServer().getPluginManager()
.registerEvents(this, BukkitPlugin.pluginInstance);
}
@Override
public void shutdown() {
}
@Override
public void log(Level level, String log, Object... objects) {
Bukkit.getLogger().log(level, String.format(log, objects));
}
@Override
public void log(String log, Object... objects) {
log(Level.INFO, String.format(log, objects));
}
@Override
public void logException(String message, Exception ex) {
Bukkit.getLogger().log(Level.SEVERE, message, ex);
}
@EventHandler
public void onJoin(final PlayerJoinEvent event) {
AntiVPN.getInstance().getPlayerExecutor().getPlayer(event.getPlayer().getUniqueId())
.ifPresent(player -> AntiVPN.getInstance().getDatabase().alertsState(player.getUuid(), enabled -> {
if(enabled) {
player.setAlertsEnabled(true);
player.sendMessage(AntiVPN.getInstance().getMessageHandler()
.getString("command-alerts-toggled")
.getFormattedMessage(new VpnString.Var<>("state", true)));
}
}));
}
@EventHandler
public void onListener(final PlayerLoginEvent event) {
//If they're exempt, don't check.
if(event.getPlayer().hasPermission("antivpn.bypass") //Has bypass permission
|| AntiVPN.getInstance().getExecutor().isWhitelisted(event.getPlayer().getUniqueId()) //Is exempt
//Or has a name that starts with a certain prefix. This is for Bedrock exempting.
|| AntiVPN.getInstance().getExecutor().isWhitelisted(event.getAddress().getHostAddress())
|| AntiVPN.getInstance().getVpnConfig().getPrefixWhitelists().stream()
.anyMatch(prefix -> event.getPlayer().getName().startsWith(prefix))) return;
if(responseCache.asMap().containsKey(event.getPlayer().getUniqueId())) {
VPNResponse cached = responseCache.getIfPresent(event.getPlayer().getUniqueId());
if (cached != null && cached.isProxy()) {
event.setResult(PlayerLoginEvent.Result.KICK_BANNED);
event.setKickMessage(org.bukkit.ChatColor.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString()));
return;
}
}
final Player player = event.getPlayer();
checkIp(event.getAddress().getHostAddress(),
AntiVPN.getInstance().getVpnConfig().cachedResults(), result -> {
if(result.isSuccess()) {
//We need to run on main thread or kicking and running commands will cause errors
//If the player is whitelisted, we don't want to kick them
if(AntiVPN.getInstance().getExecutor().isWhitelisted(event.getPlayer().getUniqueId())) {
log("UUID is whitelisted: %s",
event.getPlayer().getUniqueId().toString());
return;
}
//If the IP is whitelisted, we don't want to kick them
InetSocketAddress address = event.getPlayer().getAddress();
if (address != null){
InetAddress address1 = address.getAddress();
if (address1 != null && AntiVPN.getInstance().getExecutor()
.isWhitelisted(address1.getHostAddress())) {
log("IP is whitelisted: %s",
address1.getHostAddress());
return;
}
}
// If the countryList() size is zero, no need to check.
// Running country check first
if(!AntiVPN.getInstance().getVpnConfig().countryList().isEmpty()
// This bit of code will decide whether or not to kick the player
// If it contains the code and it is set to whitelist, it will not kick as they are equal
// and vise versa. However, if the contains does not match the state, it will kick.
&& AntiVPN.getInstance().getVpnConfig().countryList()
.contains(result.getCountryCode())
!= AntiVPN.getInstance().getVpnConfig().whitelistCountries()) {
final String kickReason = AntiVPN.getInstance().getVpnConfig()
.countryVanillaKickReason();
// Start "online" fix
// In case the response was so fast from API the player wouldn't be "online".
event.setResult(PlayerLoginEvent.Result.KICK_BANNED);
event.setKickMessage(ChatColor
.translateAlternateColorCodes('&',
kickReason
.replace("%player%", event.getPlayer().getName())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode())));
// End "online" fix
//Using our built in kicking system if no commands are configured
if(AntiVPN.getInstance().getVpnConfig().countryKickCommands().isEmpty()) {
// Kicking our player
new BukkitRunnable() {
public void run() {
event.getPlayer().kickPlayer(ChatColor
.translateAlternateColorCodes('&',
kickReason
.replace("%player%", event.getPlayer().getName())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode())));
}
}.runTask(BukkitPlugin.pluginInstance);
} else {
final String playerName = event.getPlayer().getName();
BukkitPlugin.pluginInstance.getPlayerCommandRunner()
.addAction(event.getPlayer().getUniqueId(), () -> {
for (String cmd : AntiVPN.getInstance().getVpnConfig().countryKickCommands()) {
final String formattedCommand = ChatColor.translateAlternateColorCodes('&',
cmd.replace("%player%", playerName)
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode()));
// Runs our command from console
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), formattedCommand);
}
});
}
} else if(result.isProxy()) {
// Start "online" fix
// In case the response was so fast from API the player wouldn't be "online".
event.setResult(PlayerLoginEvent.Result.KICK_BANNED);
event.setKickMessage(ChatColor
.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString()
.replace("%player%", event.getPlayer().getName())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode())));
// End "online" fix
if(AntiVPN.getInstance().getVpnConfig().kickPlayersOnDetect()) {
new BukkitRunnable() {
public void run() {
player.kickPlayer(org.bukkit.ChatColor.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString()));
}
}.runTask(BukkitPlugin.pluginInstance);
}
log(Level.INFO, event.getPlayer().getName()
+ " joined on a VPN/Proxy (" + result.getMethod() + ")");
//Ensuring the user wishes to alert to staff
if(AntiVPN.getInstance().getVpnConfig().alertToStaff())
AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream()
.filter(APIPlayer::isAlertsEnabled)
.forEach(pl -> pl.sendMessage(AntiVPN.getInstance().getVpnConfig().alertMessage()
.replace("%player%", event.getPlayer().getName())
.replace("%reason%", result.getMethod())
.replace("%country%", result.getCountryName())
.replace("%city%", result.getCity())));
//In case the user wants to run their own commands instead of using the built in kicking
if(AntiVPN.getInstance().getVpnConfig().runCommands()) {
String playerName = event.getPlayer().getName();
BukkitPlugin.pluginInstance.getPlayerCommandRunner()
.addAction(event.getPlayer().getUniqueId(), () -> {
for (String command : AntiVPN.getInstance().getVpnConfig().commands()) {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
ChatColor.translateAlternateColorCodes('&',
command.replace("%player%",
playerName)));
}
});
}
AntiVPN.getInstance().detections++;
}
} else {
log(Level.WARNING,
"The API query was not a success! " +
"You may need to upgrade your license on https://funkemunky.cc/shop");
}
AntiVPN.getInstance().checked++;
});
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
AntiVPN.getInstance().getPlayerExecutor().unloadPlayer(event.getPlayer().getUniqueId());
}
}
@@ -1,38 +0,0 @@
package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.api.APIPlayer;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
public class BukkitPlayer extends APIPlayer {
private final Player player;
public BukkitPlayer(Player player) {
super(player.getUniqueId(), player.getName(), player.getAddress().getAddress());
this.player = player;
}
@Override
public void sendMessage(String message) {
player.sendMessage(ChatColor.translateAlternateColorCodes('&', message));
}
@Override
public void kickPlayer(String reason) {
if(!Bukkit.isPrimaryThread()) {
new BukkitRunnable() {
public void run() {
player.kickPlayer(ChatColor.translateAlternateColorCodes('&', reason));
}
}.runTask(BukkitPlugin.pluginInstance);
} else player.kickPlayer(ChatColor.translateAlternateColorCodes('&', reason));
}
@Override
public boolean hasPermission(String permission) {
return player.hasPermission(permission);
}
}
@@ -1,50 +0,0 @@
package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.PlayerExecutor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.stream.Collectors;
public class BukkitPlayerExecutor implements PlayerExecutor {
private final Map<UUID, BukkitPlayer> cachedPlayers = new HashMap<>();
@Override
public Optional<APIPlayer> getPlayer(String name) {
final Player player = Bukkit.getPlayer(name);
if(player == null) {
return Optional.empty();
}
return Optional.of(cachedPlayers.computeIfAbsent(player.getUniqueId(), k -> new BukkitPlayer(player)));
}
@Override
public Optional<APIPlayer> getPlayer(UUID uuid) {
final Player player = Bukkit.getPlayer(uuid);
if(player == null) {
return Optional.empty();
}
return Optional.of(cachedPlayers.computeIfAbsent(player.getUniqueId(), k -> new BukkitPlayer(player)));
}
@Override
public void unloadPlayer(UUID uuid) {
cachedPlayers.remove(uuid);
}
@Override
public List<APIPlayer> getOnlinePlayers() {
return Bukkit.getOnlinePlayers().stream()
.map(pl -> cachedPlayers.computeIfAbsent(pl.getUniqueId(), k -> new BukkitPlayer(pl)))
.collect(Collectors.toList());
}
}
@@ -1,116 +0,0 @@
package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.bukkit.command.BukkitCommand;
import dev.brighten.antivpn.command.Command;
import lombok.Getter;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SingleLineChart;
import org.bukkit.Bukkit;
import org.bukkit.command.SimpleCommandMap;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.SimplePluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class BukkitPlugin extends JavaPlugin {
public static BukkitPlugin pluginInstance;
private SimpleCommandMap commandMap;
private final List<org.bukkit.command.Command> registeredCommands = new ArrayList<>();
@Getter
private SingleLineChart vpnDetections, ipsChecked;
@Getter
private PlayerCommandRunner playerCommandRunner;
public void onEnable() {
pluginInstance = this;
Bukkit.getLogger().info("Starting AntiVPN services...");
AntiVPN.start(new BukkitListener(), new BukkitPlayerExecutor(), getDataFolder());
playerCommandRunner = new PlayerCommandRunner();
playerCommandRunner.start();
// Loading our bStats metrics to be pushed to https://bstats.org
if(AntiVPN.getInstance().getVpnConfig().metrics()) {
Bukkit.getLogger().info("Starting bStats metrics...");
Metrics metrics = new Metrics(this, 12615);
metrics.addCustomChart(vpnDetections = new SingleLineChart("vpn_detections",
() -> AntiVPN.getInstance().detections));
metrics.addCustomChart(ipsChecked = new SingleLineChart("ips_checked",
() -> AntiVPN.getInstance().checked));
new BukkitRunnable() {
public void run() {
AntiVPN.getInstance().checked = AntiVPN.getInstance().detections = 0;
}
}.runTaskTimerAsynchronously(this, 12000, 12000);
}
Bukkit.getLogger().info("Setting up and registering commands...");
// We need access to the commandMap to register our commands without using the "proper" method
if (pluginInstance.getServer().getPluginManager() instanceof SimplePluginManager) {
SimplePluginManager manager = (SimplePluginManager) pluginInstance.getServer().getPluginManager();
try {
Field field = SimplePluginManager.class.getDeclaredField("commandMap");
field.setAccessible(true);
commandMap = (SimpleCommandMap) field.get(manager);
} catch (IllegalArgumentException | SecurityException | NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
// Registering commands
for (Command command : AntiVPN.getInstance().getCommands()) {
// Wraps our general command API to Bukkit specific calls
BukkitCommand newCommand = new BukkitCommand(command);
// Adding to our own list for later referencing
registeredCommands.add(newCommand);
// This tells Bukkit to register our command for use.
commandMap.register(pluginInstance.getName(), newCommand);
}
//TODO Finish system before implementing on startup
/*Bukkit.getLogger().info("Getting strings...");
AntiVPN.getInstance().getMessageHandler().initStrings(vpnString -> new ConfigDefault<>
(vpnString.getDefaultMessage(), "messages." + vpnString.getKey(), BukkitPlugin.pluginInstance)
.get());
AntiVPN.getInstance().getMessageHandler().reloadStrings();*/
reloadConfig();
}
@Override
public void onDisable() {
Bukkit.getLogger().info("Stopping plugin services...");
AntiVPN.getInstance().stop();
playerCommandRunner.stop();
Bukkit.getLogger().info("Unregistering commands...");
try {
Field field = SimpleCommandMap.class.getDeclaredField("knownCommands");
field.setAccessible(true);
Map<String, org.bukkit.command.Command> knownCommands =
(Map<String, org.bukkit.command.Command>) field.get(commandMap);
knownCommands.values().removeAll(registeredCommands);
registeredCommands.clear();
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
Bukkit.getLogger().info("Unregistering listeners...");
HandlerList.unregisterAll(this);
Bukkit.getLogger().info("Cancelling any running tasks...");
Bukkit.getScheduler().cancelTasks(this);
}
}
@@ -1,61 +0,0 @@
package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.utils.MiscUtils;
import lombok.Data;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class PlayerCommandRunner {
private final ScheduledExecutorService executorService;
private final Queue<PlayerAction> playerActions = new ArrayBlockingQueue<>(10000);
public PlayerCommandRunner() {
executorService = Executors.newSingleThreadScheduledExecutor(
MiscUtils.createThreadFactory("AntiVPN:PlayerCommandRunner")
);
}
void start() {
executorService.scheduleAtFixedRate(() -> {
long currentTime = System.currentTimeMillis();
while(!playerActions.isEmpty()) {
PlayerAction action = playerActions.peek();
if(action == null) continue;
if(currentTime - action.start > 2000L || Bukkit.getPlayer(action.getUuid()) != null) {
new BukkitRunnable() {
public void run() {
action.getAction().run();
}
}.runTask(BukkitPlugin.pluginInstance);
playerActions.poll();
}
}
}, 1000, 100, TimeUnit.MILLISECONDS);
}
void stop() {
executorService.shutdown();
playerActions.clear();
}
void addAction(UUID uuid, Runnable action) {
playerActions.add(new PlayerAction(uuid, System.currentTimeMillis(), action));
}
@Data
static class PlayerAction {
private final UUID uuid;
private final long start;
private final Runnable action;
}
}
@@ -1,77 +0,0 @@
package dev.brighten.antivpn.bukkit.command;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.bukkit.BukkitCommandExecutor;
import dev.brighten.antivpn.command.Command;
import lombok.val;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandSender;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
public class BukkitCommand extends org.bukkit.command.Command {
private final Command command;
public BukkitCommand(Command command) {
super(command.name(), command.description(), command.usage(), Arrays.asList(command.aliases()));
this.command = command;
}
@Override
public List<String> tabComplete(CommandSender sender, String alias, String[] args)
throws IllegalArgumentException {
val children = command.children();
if(children.length > 0 && args.length > 0) {
for (Command child : children) {
if(child.name().equalsIgnoreCase(args[0]) || Arrays.stream(child.aliases())
.anyMatch(alias2 -> alias2.equalsIgnoreCase(args[0]))) {
return child.tabComplete(new BukkitCommandExecutor(sender), alias, IntStream
.range(0, args.length - 1)
.mapToObj(i -> args[i + 1]).toArray(String[]::new));
}
}
}
return command.tabComplete(new BukkitCommandExecutor(sender), alias, args);
}
@Override
public boolean execute(CommandSender sender, String s, String[] args) {
if(!sender.hasPermission("antivpn.command.*")
&& !sender.hasPermission(command.permission())) {
sender.sendMessage(ChatColor.translateAlternateColorCodes('&',
AntiVPN.getInstance().getMessageHandler().getString("no-permission").getMessage()));
return true;
}
val children = command.children();
if(children.length > 0 && args.length > 0) {
for (Command child : children) {
if(child.name().equalsIgnoreCase(args[0]) || Arrays.stream(child.aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0]))) {
if(!sender.hasPermission("antivpn.command.*")
&& !sender.hasPermission(child.permission())) {
sender.sendMessage(ChatColor.translateAlternateColorCodes('&',
AntiVPN.getInstance().getMessageHandler().getString("no-permission").getMessage()));
return true;
}
sender.sendMessage(ChatColor.translateAlternateColorCodes('&',
child.execute(new BukkitCommandExecutor(sender), IntStream
.range(0, args.length - 1)
.mapToObj(i -> args[i + 1]).toArray(String[]::new))));
return true;
}
}
}
sender.sendMessage(ChatColor.translateAlternateColorCodes('&',
command.execute(new BukkitCommandExecutor(sender), args)));
return true;
}
}
-5
View File
@@ -1,5 +0,0 @@
name: KauriVPN
main: dev.brighten.antivpn.bukkit.BukkitPlugin
version: ${project.version}
author: funkemunky
api-version: 1.13
+36
View File
@@ -0,0 +1,36 @@
plugins {
id 'com.gradleup.shadow'
}
evaluationDependsOn(':Bungee:BungeePlugin')
dependencies {
compileOnly 'net.md-5:bungeecord-api:1.21-R0.2'
compileOnly project(':Bungee:BungeePlugin')
implementation project(':Common:loader-utils')
}
tasks.processResources {
filesMatching('bungee.yml') {
expand(project: project, projectVersion: project.version)
}
}
shadowJar {
archiveClassifier.set('')
// Include the shaded plugin jar as a single resource file
from(project(':Bungee:BungeePlugin').tasks.shadowJar) {
rename { 'antivpn-bungee.jarinjar' }
}
}
tasks.named('shadowJar') {
dependsOn(':Bungee:BungeePlugin:shadowJar')
}
tasks.named('compileJava') {
dependsOn(':Bungee:BungeePlugin:shadowJar')
}
tasks.build.dependsOn shadowJar
@@ -0,0 +1,51 @@
/*
* 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.bungee;
import dev.brighten.antivpn.loader.JarInJarClassLoader;
import dev.brighten.antivpn.loader.LoaderBootstrap;
import net.md_5.bungee.api.plugin.Plugin;
public class BungeeLoaderPlugin extends Plugin {
private static final String JAR_NAME = "antivpn-bungee.jarinjar";
private static final String SOURCE_NAME = "antivpn-source.jarinjar";
private static final String BOOTSTRAP_CLASS = "dev.brighten.antivpn.bungee.BungeePlugin";
private final LoaderBootstrap plugin;
public BungeeLoaderPlugin() {
JarInJarClassLoader loader =
new JarInJarClassLoader(getClass().getClassLoader(), JAR_NAME, SOURCE_NAME);
this.plugin = loader.instantiatePlugin(BOOTSTRAP_CLASS, Plugin.class, this);
}
@Override
public void onLoad() {
this.plugin.onLoad(getDataFolder());
}
@Override
public void onEnable() {
this.plugin.onEnable();
}
@Override
public void onDisable() {
this.plugin.onDisable();
}
}
@@ -1,5 +1,5 @@
name: KauriVPN
main: dev.brighten.antivpn.bungee.BungeePlugin
main: dev.brighten.antivpn.bungee.BungeeLoaderPlugin
description: A simple and fast antivpn plugin.
version: ${project.version}
author: funkemunky
author: funkemunky
+30
View File
@@ -0,0 +1,30 @@
plugins {
id 'com.gradleup.shadow'
}
dependencies {
compileOnly 'net.md-5:bungeecord-api:1.21-R0.2'
testImplementation 'net.md-5:bungeecord-api:1.21-R0.2'
implementation project(':Common:Source')
implementation project(':Common:loader-utils')
implementation 'org.bstats:bstats-bungeecord:2.2.1'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation 'org.mockito:mockito-subclass:5.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'
testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
testImplementation testFixtures(project(':Common:Source'))
}
tasks.compileJava.dependsOn(':Common:Source:jar')
test {
useJUnitPlatform()
systemProperty 'mockito.mockmaker', 'subclass'
}
shadowJar {
archiveClassifier.set('')
relocate 'org.bstats', 'dev.brighten.antivpn.bungee.org.bstats'
}
tasks.build.dependsOn shadowJar
@@ -0,0 +1,154 @@
/*
* 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.bungee;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.*;
import dev.brighten.antivpn.utils.MiscUtils;
import dev.brighten.antivpn.utils.StringUtil;
import java.net.InetSocketAddress;
import java.util.UUID;
import java.util.logging.Level;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.event.LoginEvent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.scheduler.ScheduledTask;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
public class BungeeListener extends VPNExecutor implements Listener {
private ScheduledTask cacheResetTask;
@Override
public void registerListeners() {
BungeePlugin.pluginInstance
.getProxy()
.getPluginManager()
.registerListener(BungeePlugin.pluginInstance.getPlugin(), this);
}
@Override
public void log(Level level, String log, Object... objects) {
BungeePlugin.pluginInstance.getProxy().getLogger().log(Level.INFO, String.format(log, objects));
}
@Override
public void log(String log, Object... objects) {
log(Level.INFO, log, objects);
}
@Override
public void logException(String message, Throwable ex) {
BungeePlugin.pluginInstance.getProxy().getLogger().log(Level.SEVERE, message, ex);
}
@Override
public void runCommand(String command) {
BungeePlugin.pluginInstance
.getProxy()
.getPluginManager()
.dispatchCommand(BungeePlugin.pluginInstance.getProxy().getConsole(), command);
}
@Override
public void disablePlugin() {
BungeePlugin.pluginInstance
.getProxy()
.getPluginManager()
.unregisterListeners(BungeePlugin.pluginInstance.getPlugin());
if (cacheResetTask != null) {
cacheResetTask.cancel();
cacheResetTask = null;
}
BungeePlugin.pluginInstance
.getProxy()
.getPluginManager()
.unregisterCommands(BungeePlugin.pluginInstance.getPlugin());
BungeePlugin.pluginInstance.onDisable();
}
@EventHandler(priority = EventPriority.HIGH)
public void onListener(final PreLoginEvent event) {
APIPlayer player =
AntiVPN.getInstance()
.getPlayerExecutor()
.getPlayer(event.getConnection().getUniqueId())
.orElseGet(
() -> {
UUID uuid = MiscUtils.lookupUUID(event.getConnection().getName());
AntiVPN.getInstance()
.getExecutor()
.log(
Level.INFO,
"Getting offline player for %s with name %s",
event.getConnection().getUniqueId(),
uuid);
return new OfflinePlayer(
uuid,
event.getConnection().getName(),
((InetSocketAddress) event.getConnection().getSocketAddress()).getAddress());
});
CheckResult result = player.checkPlayer();
if (!result.resultType().isShouldBlock()) return;
if (!AntiVPN.getInstance().getVpnConfig().isKickPlayers()) {
return;
}
event.setCancelled(true);
event.setReason(
TextComponent.fromLegacy(
StringUtil.varReplace(
switch (result.resultType()) {
case DENIED_PROXY ->
StringUtil.varReplace(
AntiVPN.getInstance().getVpnConfig().getKickMessage(),
player,
result.response());
case DENIED_COUNTRY ->
StringUtil.varReplace(
AntiVPN.getInstance().getVpnConfig().getCountryVanillaKickReason(),
player,
result.response());
default -> "You were kicked by KauriVPN for an unknown reason!";
},
player,
result.response())));
}
@EventHandler(priority = EventPriority.HIGH)
public void onJoin(LoginEvent event) {
if (event.isCancelled()) return;
// Handling player alerts on join
AntiVPN.getInstance()
.getPlayerExecutor()
.getPlayer(event.getConnection().getUniqueId())
.ifPresent(APIPlayer::checkAlertsState);
}
@EventHandler
public void onLeave(PlayerDisconnectEvent event) {
AntiVPN.getInstance().getPlayerExecutor().unloadPlayer(event.getPlayer().getUniqueId());
}
}
@@ -0,0 +1,50 @@
/*
* 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.bungee;
import dev.brighten.antivpn.api.APIPlayer;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
public class BungeePlayer extends APIPlayer {
private final ProxiedPlayer player;
public BungeePlayer(ProxiedPlayer player) {
super(player.getUniqueId(), player.getName(), player.getAddress().getAddress());
this.player = player;
}
@Override
public void sendMessage(String message) {
player.sendMessage(
TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', message)));
}
@Override
public void kickPlayer(String reason) {
player.disconnect(
TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', reason)));
}
@Override
public boolean hasPermission(String permission) {
return player.hasPermission(permission);
}
}
@@ -0,0 +1,59 @@
/*
* 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.bungee;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.PlayerExecutor;
import java.util.*;
import java.util.stream.Collectors;
import net.md_5.bungee.api.connection.ProxiedPlayer;
public class BungeePlayerExecutor implements PlayerExecutor {
private final Map<UUID, BungeePlayer> cachedPlayers = new HashMap<>();
@Override
public Optional<APIPlayer> getPlayer(String name) {
ProxiedPlayer player = BungeePlugin.pluginInstance.getProxy().getPlayer(name);
if (player == null) return Optional.empty();
return Optional.of(
cachedPlayers.computeIfAbsent(player.getUniqueId(), key -> new BungeePlayer(player)));
}
@Override
public Optional<APIPlayer> getPlayer(UUID uuid) {
ProxiedPlayer player = BungeePlugin.pluginInstance.getProxy().getPlayer(uuid);
if (player == null) return Optional.empty();
return Optional.of(cachedPlayers.computeIfAbsent(uuid, key -> new BungeePlayer(player)));
}
@Override
public void unloadPlayer(UUID uuid) {
this.cachedPlayers.remove(uuid);
}
@Override
public List<APIPlayer> getOnlinePlayers() {
return BungeePlugin.pluginInstance.getProxy().getPlayers().stream()
.map(pl -> cachedPlayers.computeIfAbsent(pl.getUniqueId(), key -> new BungeePlayer(pl)))
.collect(Collectors.toList());
}
}
@@ -0,0 +1,105 @@
/*
* 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.bungee;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.bungee.command.BungeeCommand;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.local.H2VPN;
import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.sql.MySqlVPN;
import dev.brighten.antivpn.loader.LoaderBootstrap;
import java.io.File;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Plugin;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
public class BungeePlugin implements LoaderBootstrap {
public static BungeePlugin pluginInstance;
@Getter private File dataFolder;
@Getter private final Plugin plugin;
public BungeePlugin(Plugin plugin) {
this.plugin = plugin;
}
@Override
public void onLoad(File dataFolder) {
this.dataFolder = dataFolder;
}
@Override
public void onEnable() {
pluginInstance = this;
// Setting up config
ProxyServer.getInstance().getLogger().info("Loading config...");
// Loading plugin
ProxyServer.getInstance().getLogger().info("Starting AntiVPN services...");
AntiVPN.start(new BungeeListener(), new BungeePlayerExecutor(), getDataFolder());
if (AntiVPN.getInstance().getVpnConfig().metrics()) {
ProxyServer.getInstance().getLogger().info("Starting bStats metrics...");
Metrics metrics = new Metrics(getPlugin(), 12616);
metrics.addCustomChart(new SimplePie("database_used", this::getDatabaseType));
ProxyServer.getInstance()
.getScheduler()
.schedule(
getPlugin(),
() -> AntiVPN.getInstance().checked = AntiVPN.getInstance().detections = 0,
10,
10,
TimeUnit.MINUTES);
}
for (Command command : AntiVPN.getInstance().getCommands()) {
ProxyServer.getInstance()
.getPluginManager()
.registerCommand(getPlugin(), new BungeeCommand(command));
}
}
@Override
public void onDisable() {
AntiVPN.getInstance().stop();
}
private String getDatabaseType() {
VPNDatabase database = AntiVPN.getInstance().getDatabase();
if (database instanceof MySqlVPN) {
return "MySQL";
} else if (database instanceof H2VPN) {
return "H2";
} else if (database instanceof MongoVPN) {
return "MongoDB";
} else {
return "No-Database";
}
}
public ProxyServer getProxy() {
return ProxyServer.getInstance();
}
}
@@ -0,0 +1,112 @@
/*
* 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.bungee.command;
import dev.brighten.antivpn.AntiVPN;
import java.util.Arrays;
import java.util.stream.IntStream;
import lombok.val;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
public class BungeeCommand extends Command implements TabExecutor {
private final dev.brighten.antivpn.command.Command command;
public BungeeCommand(dev.brighten.antivpn.command.Command command) {
super(command.name(), command.permission(), command.aliases());
this.command = command;
}
@Override
public void execute(CommandSender sender, String[] args) {
if (!sender.hasPermission("antivpn.command.*") && !sender.hasPermission(command.permission())) {
sender.sendMessage(
TextComponent.fromLegacyText(
ChatColor.translateAlternateColorCodes(
'&',
AntiVPN.getInstance()
.getMessageHandler()
.getString("no-permission")
.getMessage())));
return;
}
val children = command.children();
if (children.length > 0 && args.length > 0) {
for (dev.brighten.antivpn.command.Command child : children) {
if (child.name().equalsIgnoreCase(args[0])
|| Arrays.stream(child.aliases()).anyMatch(alias -> alias.equalsIgnoreCase(args[0]))) {
if (!sender.hasPermission("antivpn.command.*")
&& !sender.hasPermission(child.permission())) {
sender.sendMessage(
TextComponent.fromLegacyText(
ChatColor.translateAlternateColorCodes(
'&',
AntiVPN.getInstance()
.getMessageHandler()
.getString("no-permission")
.getMessage())));
return;
}
sender.sendMessage(
TextComponent.fromLegacyText(
ChatColor.translateAlternateColorCodes(
'&',
child.execute(
new BungeeCommandExecutor(sender),
IntStream.range(0, args.length - 1)
.mapToObj(i -> args[i + 1])
.toArray(String[]::new)))));
return;
}
}
}
sender.sendMessage(
TextComponent.fromLegacyText(
ChatColor.translateAlternateColorCodes(
'&', command.execute(new BungeeCommandExecutor(sender), args))));
}
@Override
public Iterable<String> onTabComplete(CommandSender sender, String[] args) {
val children = command.children();
if (children.length > 0 && args.length > 0) {
for (dev.brighten.antivpn.command.Command child : children) {
if (child.name().equalsIgnoreCase(args[0])
|| Arrays.stream(child.aliases())
.anyMatch(alias2 -> alias2.equalsIgnoreCase(args[0]))) {
return child.tabComplete(
new BungeeCommandExecutor(sender),
"alias",
IntStream.range(0, args.length - 1)
.mapToObj(i -> args[i + 1])
.toArray(String[]::new));
}
}
}
return command.tabComplete(new BungeeCommandExecutor(sender), "alias", args);
}
}
@@ -0,0 +1,59 @@
/*
* 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.bungee.command;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.command.CommandExecutor;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
@RequiredArgsConstructor
public class BungeeCommandExecutor implements CommandExecutor {
private final CommandSender sender;
@Override
public void sendMessage(String message, Object... objects) {
sender.sendMessage(
TextComponent.fromLegacyText(
ChatColor.translateAlternateColorCodes('&', String.format(message, objects))));
}
@Override
public boolean hasPermission(String permission) {
return sender.hasPermission(permission);
}
@Override
public Optional<APIPlayer> getPlayer() {
if (!isPlayer()) return Optional.empty();
return AntiVPN.getInstance()
.getPlayerExecutor()
.getPlayer(((ProxiedPlayer) sender).getUniqueId());
}
@Override
public boolean isPlayer() {
return sender instanceof ProxiedPlayer;
}
}
@@ -0,0 +1,114 @@
package dev.brighten.antivpn.bungee;
import static org.mockito.Mockito.*;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.StandardTest;
import dev.brighten.antivpn.api.PlayerExecutor;
import dev.brighten.antivpn.api.VPNConfig;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.message.MessageHandler;
import dev.brighten.antivpn.message.VpnString;
import dev.brighten.antivpn.web.objects.VPNResponse;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.PreLoginEvent;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class BungeeListenerTest extends StandardTest {
private BungeeListener listener;
private VPNExecutor vpnExecutor;
@BeforeEach
public void setUp() throws Exception {
AntiVPN antiVPN = mock(AntiVPN.class);
VPNConfig config = mock(VPNConfig.class);
PlayerExecutor playerExecutor = mock(PlayerExecutor.class);
vpnExecutor = mock(VPNExecutor.class);
MessageHandler messageHandler = mock(MessageHandler.class);
when(antiVPN.getVpnConfig()).thenReturn(config);
when(antiVPN.getPlayerExecutor()).thenReturn(playerExecutor);
when(antiVPN.getExecutor()).thenReturn(vpnExecutor);
when(antiVPN.getMessageHandler()).thenReturn(messageHandler);
when(playerExecutor.getPlayer(any(UUID.class))).thenReturn(Optional.empty());
when(config.getPrefixWhitelists()).thenReturn(java.util.Collections.emptyList());
when(config.getCountryList()).thenReturn(java.util.Collections.emptyList());
when(config.isKickPlayers()).thenReturn(true);
when(config.getKickMessage()).thenReturn("Blocked!");
VpnString mockVpnString = mock(VpnString.class);
when(mockVpnString.getFormattedMessage(any())).thenReturn("Blocked!");
when(messageHandler.getString(anyString())).thenReturn(mockVpnString);
when(vpnExecutor.checkIp(anyString()))
.thenReturn(
CompletableFuture.completedFuture(
VPNResponse.builder()
.success(true)
.proxy(false)
.ip("127.0.0.1")
.method("N/A")
.countryName("N/A")
.city("N/A")
.build()));
// Use reflection to set the private static INSTANCE field
Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE");
instanceField.setAccessible(true);
instanceField.set(null, antiVPN);
listener = new BungeeListener();
}
@AfterEach
public void tearDown() throws Exception {
// Reset the singleton
Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE");
instanceField.setAccessible(true);
instanceField.set(null, null);
}
@Test
public void testPreLoginEventAllowed() {
PreLoginEvent event = mock(PreLoginEvent.class);
PendingConnection connection = mock(PendingConnection.class);
when(event.getConnection()).thenReturn(connection);
when(connection.getUniqueId()).thenReturn(UUID.randomUUID());
when(connection.getName()).thenReturn("TestPlayer");
when(connection.getSocketAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 12345));
listener.onListener(event);
verify(event, never()).setCancelled(true);
}
@Test
public void testPreLoginEventBlocked() throws NoSuchFieldException, IllegalAccessException {
PreLoginEvent event = mock(PreLoginEvent.class);
PendingConnection connection = mock(PendingConnection.class);
UUID uuid = UUID.randomUUID();
when(event.getConnection()).thenReturn(connection);
when(connection.getUniqueId()).thenReturn(uuid);
when(connection.getName()).thenReturn("ProxyPlayer");
when(connection.getSocketAddress()).thenReturn(new InetSocketAddress("1.1.1.1", 12345));
// Mock proxy response
mockCache();
listener.onListener(event);
verify(event).setCancelled(true);
verify(event).setReason(any());
}
}
-91
View File
@@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>AntiVPN</artifactId>
<groupId>dev.brighten.antivpn</groupId>
<version>1.9.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Bungee</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>8</source>
<target>8</target>
<compilerArgument>-XDignore.symbol.file</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<!-- Replace this with your package! -->
<shadedPattern>dev.brighten.antivpn.bungee.org.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>org.yaml.snakeyaml</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.org.yaml.snakeyaml</shadedPattern>
</relocation>
<relocation>
<pattern>com.google</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.google</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Common</artifactId>
<version>1.9.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.github.bungee</groupId>
<artifactId>BungeeCord-1.8</artifactId>
<version>1.8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bungeecord</artifactId>
<version>2.2.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -1,179 +0,0 @@
package dev.brighten.antivpn.bungee;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.web.objects.VPNResponse;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.scheduler.ScheduledTask;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class BungeeListener extends VPNExecutor implements Listener {
private ScheduledTask cacheResetTask;
private final Cache<UUID, VPNResponse> responseCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(2000)
.build();
@Override
public void registerListeners() {
BungeePlugin.pluginInstance.getProxy().getPluginManager()
.registerListener(BungeePlugin.pluginInstance, this);
}
@Override
public void shutdown() {
if(cacheResetTask != null) {
cacheResetTask.cancel();
cacheResetTask = null;
}
threadExecutor.shutdown();
BungeePlugin.pluginInstance.getProxy().getPluginManager().unregisterListener(this);
}
@Override
public void log(Level level, String log, Object... objects) {
BungeeCord.getInstance().getLogger().log(Level.INFO, String.format(log, objects));
}
@Override
public void log(String log, Object... objects) {
log(Level.INFO, String.format(log, objects));
}
@Override
public void logException(String message, Exception ex) {
BungeeCord.getInstance().getLogger().log(Level.SEVERE, message, ex);
}
@EventHandler(priority = EventPriority.LOWEST)
public void onListener(final PreLoginEvent event) {
if(!responseCache.asMap().containsKey(event.getConnection().getUniqueId())) return;
VPNResponse cached = responseCache.getIfPresent(event.getConnection().getUniqueId());
if(cached != null && cached.isProxy()) {
event.setCancelled(true);
event.setCancelReason(TextComponent.fromLegacyText(ChatColor
.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString())));
AntiVPN.getInstance().getExecutor().log(Level.INFO,
"%s was kicked from pre-login proxy cache.",
event.getConnection().getName());
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onListener(final PostLoginEvent event) {
if(event.getPlayer().hasPermission("antivpn.bypass") //Has bypass permission
|| AntiVPN.getInstance().getVpnConfig().getPrefixWhitelists().stream()
.anyMatch(prefix -> event.getPlayer().getName().startsWith(prefix))) return;
checkIp(event.getPlayer().getAddress().getAddress().getHostAddress(),
AntiVPN.getInstance().getVpnConfig().cachedResults(), result -> {
if(result.isSuccess()) {
//If the player is whitelisted, we don't want to kick them
if(AntiVPN.getInstance().getExecutor().isWhitelisted(event.getPlayer().getUniqueId())) {
AntiVPN.getInstance().getExecutor().log("UUID is whitelisted: %s",
event.getPlayer().getUniqueId().toString());
return;
}
//If the IP is whitelisted, we don't want to kick them
if(AntiVPN.getInstance().getExecutor().isWhitelisted(event.getPlayer().getAddress().getAddress()
.getHostAddress())) {
AntiVPN.getInstance().getExecutor().log("IP is whitelisted: %s",
event.getPlayer().getAddress().getAddress().getHostAddress());
return;
}
responseCache.put(event.getPlayer().getUniqueId(), result);
if(!AntiVPN.getInstance().getVpnConfig().countryList().isEmpty()
// This bit of code will decide whether or not to kick the player
// If it contains the code and it is set to whitelist, it will not kick as they are equal
// and vise versa. However, if the contains does not match the state, it will kick.
&& AntiVPN.getInstance().getVpnConfig().countryList()
.contains(result.getCountryCode()) != AntiVPN.getInstance().getVpnConfig().whitelistCountries()) {
//Using our built in kicking system if no commands are configured
if(AntiVPN.getInstance().getVpnConfig().countryKickCommands().isEmpty()) {
final String kickReason = AntiVPN.getInstance().getVpnConfig()
.countryVanillaKickReason();
// Kicking our player
event.getPlayer().disconnect(TextComponent.fromLegacyText(ChatColor
.translateAlternateColorCodes('&',
kickReason
.replace("%player%", event.getPlayer().getName())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode()))));
} else {
for (String cmd : AntiVPN.getInstance().getVpnConfig().countryKickCommands()) {
final String formattedCommand = ChatColor.translateAlternateColorCodes('&',
cmd.replace("%player%", event.getPlayer().getName())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode()));
// Runs our command from console
BungeeCord.getInstance().getPluginManager().dispatchCommand(
BungeeCord.getInstance().getConsole(), formattedCommand);
}
}
} else if(result.isProxy()) {
if(AntiVPN.getInstance().getVpnConfig().kickPlayersOnDetect())
event.getPlayer().disconnect(TextComponent.fromLegacyText(ChatColor
.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString())));
BungeeCord.getInstance().getLogger().info(event.getPlayer().getName()
+ " joined on a VPN/Proxy (" + result.getMethod() + ")");
if(AntiVPN.getInstance().getVpnConfig().alertToStaff()) //Ensuring the user wishes to alert to staff
AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream()
.filter(APIPlayer::isAlertsEnabled)
.forEach(pl -> pl.sendMessage(AntiVPN.getInstance().getVpnConfig().alertMessage()
.replace("%player%", event.getPlayer().getName())
.replace("%reason%", result.getMethod())
.replace("%country%", result.getCountryName())
.replace("%city%", result.getCity())));
//In case the user wants to run their own commands instead of using the built in kicking
if(AntiVPN.getInstance().getVpnConfig().runCommands()) {
for (String command : AntiVPN.getInstance().getVpnConfig().commands()) {
BungeeCord.getInstance().getPluginManager()
.dispatchCommand(BungeeCord.getInstance().getConsole(),
ChatColor.translateAlternateColorCodes('&',
command.replace("%player%", event.getPlayer().getName())));
}
}
AntiVPN.getInstance().detections++;
}
} else {
BungeeCord.getInstance().getLogger()
.log(Level.WARNING,
"The API query was not a success! " +
"You may need to upgrade your license on https://funkemunky.cc/shop");
}
AntiVPN.getInstance().checked++;
});
}
@EventHandler
public void onLeave(PlayerDisconnectEvent event) {
AntiVPN.getInstance().getPlayerExecutor().unloadPlayer(event.getPlayer().getUniqueId());
}
}
@@ -1,34 +0,0 @@
package dev.brighten.antivpn.bungee;
import dev.brighten.antivpn.api.APIPlayer;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
public class BungeePlayer extends APIPlayer {
private final ProxiedPlayer player;
public BungeePlayer(ProxiedPlayer player) {
super(player.getUniqueId(), player.getName(), player.getAddress().getAddress());
this.player = player;
}
@Override
public void sendMessage(String message) {
player.sendMessage(TextComponent.fromLegacyText(ChatColor
.translateAlternateColorCodes('&', message)));
}
@Override
public void kickPlayer(String reason) {
player.disconnect(TextComponent.fromLegacyText(ChatColor
.translateAlternateColorCodes('&', reason)));
}
@Override
public boolean hasPermission(String permission) {
return player.hasPermission(permission);
}
}
@@ -1,44 +0,0 @@
package dev.brighten.antivpn.bungee;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.PlayerExecutor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import java.util.*;
import java.util.stream.Collectors;
public class BungeePlayerExecutor implements PlayerExecutor {
private final Map<UUID, BungeePlayer> cachedPlayers = new HashMap<>();
@Override
public Optional<APIPlayer> getPlayer(String name) {
ProxiedPlayer player = BungeeCord.getInstance().getPlayer(name);
if(player == null) return Optional.empty();
return Optional.of(cachedPlayers.computeIfAbsent(player.getUniqueId(), key -> new BungeePlayer(player)));
}
@Override
public Optional<APIPlayer> getPlayer(UUID uuid) {
ProxiedPlayer player = BungeeCord.getInstance().getPlayer(uuid);
if(player == null) return Optional.empty();
return Optional.of(cachedPlayers.computeIfAbsent(uuid, key -> new BungeePlayer(player)));
}
@Override
public void unloadPlayer(UUID uuid) {
this.cachedPlayers.remove(uuid);
}
@Override
public List<APIPlayer> getOnlinePlayers() {
return BungeeCord.getInstance().getPlayers().stream()
.map(pl -> cachedPlayers.computeIfAbsent(pl.getUniqueId(), key -> new BungeePlayer(pl)))
.collect(Collectors.toList());
}
}
@@ -1,52 +0,0 @@
package dev.brighten.antivpn.bungee;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.bungee.command.BungeeCommand;
import dev.brighten.antivpn.command.Command;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.plugin.Plugin;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SingleLineChart;
import java.util.concurrent.TimeUnit;
public class BungeePlugin extends Plugin {
public static BungeePlugin pluginInstance;
private SingleLineChart vpnDetections, ipsChecked;
@Override
public void onEnable() {
pluginInstance = this;
//Setting up config
BungeeCord.getInstance().getLogger().info("Loading config...");
//Loading plugin
BungeeCord.getInstance().getLogger().info("Starting AntiVPN services...");
AntiVPN.start(new BungeeListener(), new BungeePlayerExecutor(), getDataFolder());
if(AntiVPN.getInstance().getVpnConfig().metrics()) {
BungeeCord.getInstance().getLogger().info("Starting bStats metrics...");
Metrics metrics = new Metrics(this, 12616);
metrics.addCustomChart(vpnDetections = new SingleLineChart("vpn_detections",
() -> AntiVPN.getInstance().detections));
metrics.addCustomChart(ipsChecked = new SingleLineChart("ips_checked",
() -> AntiVPN.getInstance().checked));
BungeeCord.getInstance().getScheduler().schedule(this,
() -> AntiVPN.getInstance().checked = AntiVPN.getInstance().detections = 0,
10, 10, TimeUnit.MINUTES);
}
for (Command command : AntiVPN.getInstance().getCommands()) {
BungeeCord.getInstance().getPluginManager().registerCommand(pluginInstance, new BungeeCommand(command));
}
}
@Override
public void onDisable() {
AntiVPN.getInstance().stop();
}
}
@@ -1,79 +0,0 @@
package dev.brighten.antivpn.bungee.command;
import dev.brighten.antivpn.AntiVPN;
import lombok.val;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import java.util.Arrays;
import java.util.stream.IntStream;
public class BungeeCommand extends Command implements TabExecutor {
private final dev.brighten.antivpn.command.Command command;
public BungeeCommand(dev.brighten.antivpn.command.Command command) {
super(command.name(), command.permission(), command.aliases());
this.command = command;
}
@Override
public void execute(CommandSender sender, String[] args) {
if(!sender.hasPermission("antivpn.command.*")
&& !sender.hasPermission(command.permission())) {
sender.sendMessage(TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&',
AntiVPN.getInstance().getMessageHandler().getString("no-permission").getMessage())));
return;
}
val children = command.children();
if(children.length > 0 && args.length > 0) {
for (dev.brighten.antivpn.command.Command child : children) {
if(child.name().equalsIgnoreCase(args[0]) || Arrays.stream(child.aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0]))) {
if(!sender.hasPermission("antivpn.command.*")
&& !sender.hasPermission(child.permission())) {
sender.sendMessage(TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&',
AntiVPN.getInstance().getMessageHandler().getString("no-permission").getMessage())));
return;
}
sender.sendMessage(TextComponent
.fromLegacyText(ChatColor
.translateAlternateColorCodes('&',
child.execute(new BungeeCommandExecutor(sender), IntStream
.range(0, args.length - 1)
.mapToObj(i -> args[i + 1]).toArray(String[]::new)))));
return;
}
}
}
sender.sendMessage(TextComponent
.fromLegacyText(ChatColor
.translateAlternateColorCodes('&',
command.execute(new BungeeCommandExecutor(sender), args))));
}
@Override
public Iterable<String> onTabComplete(CommandSender sender, String[] args) {
val children = command.children();
if(children.length > 0 && args.length > 0) {
for (dev.brighten.antivpn.command.Command child : children) {
if(child.name().equalsIgnoreCase(args[0]) || Arrays.stream(child.aliases())
.anyMatch(alias2 -> alias2.equalsIgnoreCase(args[0]))) {
return child.tabComplete(new BungeeCommandExecutor(sender), "alias", IntStream
.range(0, args.length - 1)
.mapToObj(i -> args[i + 1]).toArray(String[]::new));
}
}
}
return command.tabComplete(new BungeeCommandExecutor(sender), "alias", args);
}
}
@@ -1,41 +0,0 @@
package dev.brighten.antivpn.bungee.command;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.command.CommandExecutor;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import java.util.Optional;
@RequiredArgsConstructor
public class BungeeCommandExecutor implements CommandExecutor {
private final CommandSender sender;
@Override
public void sendMessage(String message, Object... objects) {
sender.sendMessage(TextComponent.fromLegacyText(ChatColor
.translateAlternateColorCodes('&', String.format(message, objects))));
}
@Override
public boolean hasPermission(String permission) {
return sender.hasPermission(permission);
}
@Override
public Optional<APIPlayer> getPlayer() {
if(!isPlayer()) return Optional.empty();
return AntiVPN.getInstance().getPlayerExecutor().getPlayer(((ProxiedPlayer) sender).getUniqueId());
}
@Override
public boolean isPlayer() {
return sender instanceof ProxiedPlayer;
}
}
+71
View File
@@ -0,0 +1,71 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.10.1.2] 2026-05-03
### Fixed
- Kick spam on non-cached VPN detection events.
### Changed
- Project cleanup and performance improvements.
## [1.10.1.1] 2026-04-29
### Fixed
- Startup error on velocity instances is now corrected.
### Changed
- Bumped dependency versions for H2 database.
## [1.10.1] - 2026-04-28
### Fixed
- Async kick error that caused issues during player removal when running commands.
- Startup error caused by a packaging issue is now corrected.
## [1.10.0] - 2026-04-07
### Added
- CIDR allowlisting, including commands to add, remove, view, and search entries
- MongoDB support for CIDR allowlist storage
- VPN detection webhooks with Discord and Slack formatting options
- Mojang API fallback support for player lookups
- Folia support
### Changed
- Improved player blocking so flagged users are removed more reliably across platforms
- Updated allowlist handling to validate CIDR entries more consistently
- Improved database cleanup for outdated cached responses
### Fixed
- SQL startup and loading issues, including MySQL library injection problems
- CIDR parsing issues and MongoDB CIDR lookup failures
- Allowlist-related SQL errors
- Repeated webhook spam from duplicate VPN detection events
### Documentation
- Expanded webhook setup documentation for Discord and Slack
## [1.9.4] - 2025-09-30
### Added
- Sponge platform support
- UUID lookup support for player validation
- Better scheduled kick checking
- Java 17 and Java 21 support
- Database metrics tracking for bStats
### Changed
- **BREAKING**: Minimum Java version upgraded from 8 to 17
- Replaced the old cache implementation with Caffeine for better performance
- Improved asynchronous player checking and VPN detection handling
- Improved database connection management and error handling
### Fixed
- H2 database compatibility issues with automatic backup and recovery
- Memory leaks and resource cleanup problems in database handling
- Thread safety issues in player cache management
- Command registration issues during plugin startup and shutdown
+47
View File
@@ -0,0 +1,47 @@
plugins {
id 'com.gradleup.shadow'
id 'java-test-fixtures'
}
dependencies {
implementation 'org.ow2.asm:asm:9.8'
implementation 'org.ow2.asm:asm-commons:9.8'
implementation 'org.yaml:snakeyaml:2.2'
implementation 'org.jetbrains:annotations:26.0.2'
compileOnly 'com.mysql:mysql-connector-j:9.3.0'
compileOnly 'com.h2database:h2:2.4.240'
implementation'com.github.ben-manes.caffeine:caffeine:3.1.8'
compileOnly 'org.mongodb:mongo-java-driver:3.12.14'
testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation "org.junit.jupiter:junit-jupiter:5.8.1"
testImplementation "org.testcontainers:testcontainers:2.0.4"
testImplementation "org.testcontainers:testcontainers-junit-jupiter:2.0.4"
testImplementation 'org.testcontainers:mysql:1.20.4'
testImplementation 'org.testcontainers:mongodb:1.20.4'
testRuntimeOnly 'org.slf4j:slf4j-simple:2.0.16'
testImplementation 'com.mysql:mysql-connector-j:9.3.0'
testImplementation 'com.h2database:h2:2.4.240'
testImplementation 'org.mongodb:mongo-java-driver:3.12.14'
testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
testFixturesImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
}
shadowJar {
archiveClassifier.set('')
}
tasks.build.dependsOn shadowJar
components.java.withVariantsFromConfiguration(configurations.shadowRuntimeElements) {
skip()
}
test {
useJUnitPlatform()
}
jar {
archiveClassifier.set('raw')
}
@@ -0,0 +1,292 @@
/*
* 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;
import dev.brighten.antivpn.api.PlayerExecutor;
import dev.brighten.antivpn.api.VPNConfig;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.impl.AntiVPNCommand;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.local.H2VPN;
import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.sql.MySqlVPN;
import dev.brighten.antivpn.depends.LibraryLoader;
import dev.brighten.antivpn.depends.MavenLibrary;
import dev.brighten.antivpn.message.MessageHandler;
import dev.brighten.antivpn.utils.ConfigDefault;
import dev.brighten.antivpn.utils.MiscUtils;
import dev.brighten.antivpn.utils.config.Configuration;
import dev.brighten.antivpn.utils.config.ConfigurationProvider;
import dev.brighten.antivpn.utils.config.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter(AccessLevel.PRIVATE)
@MavenLibrary(groupId = "com.h2database", artifactId = "h2", version = "2.4.240")
@MavenLibrary(groupId = "org.mongodb", artifactId = "mongo-java-driver", version = "3.12.14")
@MavenLibrary(groupId = "com.mysql", artifactId = "mysql-connector-j", version = "9.3.0")
public class AntiVPN {
private static AntiVPN INSTANCE;
private VPNConfig vpnConfig;
private VPNExecutor executor;
private PlayerExecutor playerExecutor;
private VPNDatabase database;
private MessageHandler messageHandler;
private Configuration config;
private List<Command> commands = new ArrayList<>();
public int detections, checked;
private File pluginFolder;
public static void start(VPNExecutor executor, PlayerExecutor playerExecutor, File pluginFolder) {
// Initializing
INSTANCE = new AntiVPN();
INSTANCE.pluginFolder = pluginFolder;
INSTANCE.executor = executor;
INSTANCE.playerExecutor = playerExecutor;
LibraryLoader.loadAll(INSTANCE);
try {
File configFile = new File(pluginFolder, "config.yml");
if (!configFile.exists()) {
if (configFile.getParentFile().mkdirs()) {
AntiVPN.getInstance().getExecutor().log("Created plugin folder!");
}
MiscUtils.copy(INSTANCE.getResource("config.yml"), configFile);
}
INSTANCE.config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
} catch (IOException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not load config.yml, plugin disabling...", e);
executor.disablePlugin();
return;
}
INSTANCE.vpnConfig = new VPNConfig();
INSTANCE.executor.registerListeners();
INSTANCE.vpnConfig.update();
INSTANCE.messageHandler = new MessageHandler();
try {
switch (INSTANCE.vpnConfig.getDatabaseType().toLowerCase()) {
case "h2":
case "local":
case "flatfile":
{
AntiVPN.getInstance().getExecutor().log("Using databaseType H2...");
INSTANCE.database = new H2VPN();
INSTANCE.database.init();
break;
}
case "mysql":
case "sql":
{
AntiVPN.getInstance().getExecutor().log("Using databaseType MySQL...");
INSTANCE.database = new MySqlVPN();
INSTANCE.database.init();
break;
}
case "mongo":
case "mongodb":
case "mongod":
{
INSTANCE.database = new MongoVPN();
INSTANCE.database.init();
break;
}
default:
{
AntiVPN.getInstance()
.getExecutor()
.log(
"Could not find database type \""
+ INSTANCE.vpnConfig.getDatabaseType()
+ "\". "
+ "Options: [MySQL]");
break;
}
}
} catch (Exception e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not initialize database, plugin disabling...", e);
executor.disablePlugin();
return;
}
// Registering commands
INSTANCE.registerCommands();
// Turning on alerts of players who are already online.
playerExecutor
.getOnlinePlayers()
.forEach(
player -> {
// We want to make sure they even have permission to see alerts before we make a bunch
// of unnecessary database queries.
if (player.hasPermission("antivpn.command.alerts")) {
// Running database check for enabled alerts.
INSTANCE.database.alertsState(player.getUuid(), player::setAlertsEnabled);
}
});
AntiVPN.getInstance()
.getMessageHandler()
.initStrings(
vpnString ->
new ConfigDefault<>(
vpnString.getDefaultMessage(),
"messages." + vpnString.getKey(),
AntiVPN.getInstance())
.get());
AntiVPN.getInstance().getMessageHandler().reloadStrings();
// Starting kick checks
AntiVPN.getInstance().getExecutor().startKickChecks();
}
public InputStream getResource(String filename) {
if (filename == null) {
throw new IllegalArgumentException("Filename cannot be null");
} else {
try {
URL url = executor.getClass().getClassLoader().getResource(filename);
if (url == null) {
return null;
} else {
URLConnection connection = url.openConnection();
connection.setUseCaches(false);
return connection.getInputStream();
}
} catch (IOException var4) {
return null;
}
}
}
public void stop() {
if (database instanceof H2VPN) {
database.shutdown();
// Try to deregister driver
try {
java.sql.Driver driver = java.sql.DriverManager.getDriver("jdbc:h2:");
if (driver != null) {
java.sql.DriverManager.deregisterDriver(driver);
}
} catch (Exception e) {
// Log but don't throw
executor.log("Failed to deregister H2 driver: " + e.getMessage());
}
}
if (executor != null && executor.getThreadExecutor() != null) {
executor.getThreadExecutor().shutdown();
}
if (database != null) database.shutdown();
INSTANCE = null;
}
public void reloadDatabase() {
database.shutdown();
switch (AntiVPN.getInstance().getVpnConfig().getDatabaseType().toLowerCase()) {
case "h2":
case "local":
case "flatfile":
{
AntiVPN.getInstance().getExecutor().log("Using databaseType H2...");
INSTANCE.database = new H2VPN();
INSTANCE.database.init();
break;
}
case "mysql":
case "sql":
{
AntiVPN.getInstance().getExecutor().log("Using databaseType MySQL...");
INSTANCE.database = new MySqlVPN();
INSTANCE.database.init();
break;
}
case "mongo":
case "mongodb":
case "mongod":
{
INSTANCE.database = new MongoVPN();
INSTANCE.database.init();
break;
}
default:
{
AntiVPN.getInstance()
.getExecutor()
.log(
"Could not find database type \""
+ INSTANCE.vpnConfig.getDatabaseType()
+ "\". "
+ "Options: [MySQL]");
break;
}
}
}
public static AntiVPN getInstance() {
assert INSTANCE != null : "AntiVPN has not been initialized!";
return INSTANCE;
}
public void saveConfig() {
try {
ConfigurationProvider.getProvider(YamlConfiguration.class)
.save(getConfig(), new File(pluginFolder.getPath() + File.separator + "config.yml"));
} catch (IOException e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
public void reloadConfig() {
try {
config =
ConfigurationProvider.getProvider(YamlConfiguration.class)
.load(new File(pluginFolder.getPath() + File.separator + "config.yml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void registerCommands() {
commands.add(new AntiVPNCommand());
}
}
@@ -0,0 +1,171 @@
/*
* 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.api;
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 java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import lombok.Getter;
import lombok.Setter;
@Getter
public abstract class APIPlayer {
private final UUID uuid;
private final String name;
private final InetAddress ip;
@Setter private boolean alertsEnabled;
public static final Cache<String, CheckResult> checkResultCache =
Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).maximumSize(2000).build();
public APIPlayer(UUID uuid, String name, InetAddress ip) {
this.uuid = uuid;
this.name = name;
this.ip = ip;
}
public abstract void sendMessage(String message);
public abstract void kickPlayer(String reason);
public abstract boolean hasPermission(String permission);
public void updateAlertsState() {
// Updating into database so its synced across servers and saved on logout.
AntiVPN.getInstance().getDatabase().updateAlertsState(uuid, alertsEnabled);
sendMessage(
AntiVPN.getInstance()
.getMessageHandler()
.getString("command-alerts-toggled")
.getFormattedMessage(new VpnString.Var<>("state", alertsEnabled)));
}
public void checkAlertsState() {
AntiVPN.getInstance()
.getExecutor()
.getThreadExecutor()
.execute(
() ->
AntiVPN.getInstance()
.getDatabase()
.alertsState(
uuid,
state -> {
if (state) {
alertsEnabled = true;
updateAlertsState();
}
}));
}
/***
* The result is only returned if it is cached. Otherwise, there is an asynchronous call to the API
* and will handle kicking the player if necessary.
* @return CheckResult - The cached result of the check if it exists.
*/
public CheckResult checkPlayer() {
if (hasPermission("antivpn.bypass") // Has bypass permission
// Is exempt
|| (uuid != null && AntiVPN.getInstance().getExecutor().isWhitelisted(uuid))
// Or has a name that starts with a certain prefix. This is for Bedrock exempting.
|| AntiVPN.getInstance().getExecutor().isWhitelisted(ip.getHostAddress() + "/32")
|| AntiVPN.getInstance().getVpnConfig().getPrefixWhitelists().stream()
.anyMatch(name::startsWith)) {
return new CheckResult(null, ResultType.WHITELISTED, false);
}
CheckResult cachedResult = checkResultCache.getIfPresent(ip.getHostAddress());
if (cachedResult != null) {
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()) {
AntiVPN.getInstance().getExecutor().handleKickingOfPlayer(cachedResult, this);
}
return cachedResult;
}
}
AntiVPN.getInstance()
.getExecutor()
.checkIp(ip.getHostAddress())
.thenAccept(
result -> {
if (!result.isSuccess()) {
AntiVPN.getInstance()
.getExecutor()
.log(
Level.WARNING,
"The API query was not a success! "
+ "You may need to upgrade your license on "
+ "https://funkemunky.cc/shop");
return;
}
// If the countryList() size is zero, no need to check.
// Running country check first
CheckResult checkResult;
if (!AntiVPN.getInstance().getVpnConfig().getCountryList().isEmpty()
&& !((uuid != null && AntiVPN.getInstance().getExecutor().isWhitelisted(uuid))
// Or has a name that starts with a certain prefix. This is for Bedrock
// exempting.
|| AntiVPN.getInstance()
.getExecutor()
.isWhitelisted(ip.getHostAddress() + "/32"))
// This bit of code will decide whether or not to kick the player
// If it contains the code and it is set to whitelist, it will not kick
// as they are equal and vise versa. However, if the contains does not match
// the state, it will kick.
&& AntiVPN.getInstance()
.getVpnConfig()
.getCountryList()
.contains(result.getCountryCode())
!= AntiVPN.getInstance().getVpnConfig().getWhitelistCountries()) {
// Using our built in kicking system if no commands are configured
checkResult = new CheckResult(result, ResultType.DENIED_COUNTRY, false);
} else if (result.isProxy()) {
checkResult = new CheckResult(result, ResultType.DENIED_PROXY, false);
} else {
checkResult = new CheckResult(result, ResultType.ALLOWED, false);
}
AntiVPN.getInstance()
.getExecutor()
.log(
Level.FINE,
"Result for " + ip.getHostAddress() + " is " + checkResult.resultType());
checkResultCache.put(
ip.getHostAddress(),
new CheckResult(checkResult.response(), checkResult.resultType(), true));
if (checkResult.resultType().isShouldBlock()) {
AntiVPN.getInstance().getExecutor().handleKickingOfPlayer(checkResult, this);
}
AntiVPN.getInstance().checked++;
});
return new CheckResult(null, ResultType.UNKNOWN, false);
}
}
@@ -0,0 +1,21 @@
/*
* 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.api;
import dev.brighten.antivpn.web.objects.VPNResponse;
public record CheckResult(VPNResponse response, ResultType resultType, boolean isFromCache) {}
@@ -0,0 +1,38 @@
/*
* 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.api;
import java.net.InetAddress;
import java.util.UUID;
public class OfflinePlayer extends APIPlayer {
public OfflinePlayer(UUID uuid, String name, InetAddress ip) {
super(uuid, name, ip);
}
@Override
public void sendMessage(String message) {}
@Override
public void kickPlayer(String reason) {}
@Override
public boolean hasPermission(String permission) {
return false;
}
}
@@ -0,0 +1,32 @@
/*
* 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.api;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface PlayerExecutor {
Optional<APIPlayer> getPlayer(String name);
Optional<APIPlayer> getPlayer(UUID uuid);
void unloadPlayer(UUID uuid);
List<APIPlayer> getOnlinePlayers();
}
@@ -0,0 +1,34 @@
/*
* 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.api;
import lombok.Getter;
public enum ResultType {
ALLOWED(false),
WHITELISTED(false),
DENIED_COUNTRY(true),
DENIED_PROXY(true),
API_FAILURE(false),
UNKNOWN(false);
@Getter private final boolean shouldBlock;
ResultType(boolean shouldBlock) {
this.shouldBlock = shouldBlock;
}
}
@@ -0,0 +1,220 @@
/*
* 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.api;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.utils.ConfigDefault;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import lombok.Getter;
public class VPNConfig {
private final ConfigDefault<String>
licenseDefault = new ConfigDefault<>("", "license", AntiVPN.getInstance()),
kickStringDefault =
new ConfigDefault<>(
"Proxies are not allowed on our server", "kickMessage", AntiVPN.getInstance()),
defaultDatabaseType = new ConfigDefault<>("H2", "database.type", AntiVPN.getInstance()),
defaultDatabaseName =
new ConfigDefault<>("kaurivpn", "database.database", AntiVPN.getInstance()),
defaultMongoURL = new ConfigDefault<>("", "database.mongoURL", AntiVPN.getInstance()),
defaultUsername = new ConfigDefault<>("root", "database.username", AntiVPN.getInstance()),
defaultPassword = new ConfigDefault<>("password", "database.password", AntiVPN.getInstance()),
defaultCountryKickReason =
new ConfigDefault<>(
"&cSorry, but our server does not allow connections from\n&f%country%",
"countries.vanillaKickReason", AntiVPN.getInstance()),
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());
private final ConfigDefault<Boolean>
cacheResultsDefault = new ConfigDefault<>(true, "cachedResults", AntiVPN.getInstance()),
defaultUseCredentials =
new ConfigDefault<>(true, "database.useCredentials", AntiVPN.getInstance()),
defaultDatabaseEnabled =
new ConfigDefault<>(false, "database.enabled", AntiVPN.getInstance()),
defaultCommandsEnable = new ConfigDefault<>(false, "commands.enabled", AntiVPN.getInstance()),
defaultKickPlayers = new ConfigDefault<>(true, "kickPlayers", AntiVPN.getInstance()),
defaultAlertToStaff = new ConfigDefault<>(true, "alerts.enabled", AntiVPN.getInstance()),
defaultWhitelistCountries =
new ConfigDefault<>(true, "countries.whitelist", AntiVPN.getInstance()),
defaultMetrics = new ConfigDefault<>(true, "bstats", AntiVPN.getInstance());
private final ConfigDefault<Integer> 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",
AntiVPN.getInstance()),
defCountryKickCommands =
new ConfigDefault<>(Collections.emptyList(), "countries.commands", AntiVPN.getInstance()),
defCountrylist =
new ConfigDefault<>(new ArrayList<>(), "countries.list", AntiVPN.getInstance());
@Getter private String license;
@Getter private String kickMessage;
@Getter private String databaseType;
@Getter private String databaseName;
private String mongoURL;
@Getter private String username;
@Getter private String password;
@Getter private String ip;
@Getter private String alertMsg;
@Getter private String countryVanillaKickReason;
@Getter private List<String> prefixWhitelists;
private List<String> commands;
@Getter private List<String> countryList;
private List<String> countryKickCommands;
private int port;
private boolean cacheResults;
@Getter private boolean databaseEnabled;
private boolean useCredentials;
@Getter private boolean commandsEnabled;
@Getter private boolean kickPlayers;
private boolean alertToStaff;
private boolean metrics;
private boolean whitelistCountries;
/**
* If true, results will be cached to reduce queries to <a href="https://funkemunky.cc">...</a>
*
* @return boolean
*/
public boolean cachedResults() {
return cacheResults;
}
/**
* If true, staff will be alerted on proxy detection.
*
* @return boolean
*/
public boolean isAlertToSTaff() {
return alertToStaff;
}
/**
* Commands to run on proxy detection.
*
* @return List
*/
public List<String> commands() {
return commands;
}
/**
* Whether or not the database we want to connect to requires credentials.
*
* @return boolean
*/
public boolean useDatabaseCreds() {
return useCredentials;
}
/**
* Only for Mongo only. URL used for connecting to database. Overrides other fields
*
* @return String
*/
public String mongoDatabaseURL() {
return mongoURL;
}
/**
* If true, we only allow the {@link VPNConfig#countryKickCommands()}. If false, we blacklist
* them.
*
* @return boolean
*/
public boolean getWhitelistCountries() {
return whitelistCountries;
}
/**
* Returns our configured commands to run on player country detection.
*
* @return List
*/
public List<String> countryKickCommands() {
return countryKickCommands;
}
/**
* Gets the port based on configuration. If {@link VPNConfig#port} is -1, will get default port
* based on {@link VPNConfig#getDatabaseType()} lowerCase().
*
* @return int
*/
public int getPort() {
if (port == -1) {
switch (getDatabaseType().toLowerCase()) {
case "mongodb":
case "mongo":
case "mongod":
return 27017;
case "sql":
case "mysql":
return 3306;
}
}
return port;
}
/**
* If true, <a href="https://bstats.org">...</a> metrics will be collected to improve KauriVPN.
*
* @return boolean
*/
public boolean metrics() {
return metrics;
}
/** Grabs all information from the config.yml */
public void update() {
license = licenseDefault.get();
kickMessage = kickStringDefault.get();
cacheResults = cacheResultsDefault.get();
prefixWhitelists = prefixWhitelistsDefault.get();
databaseEnabled = defaultDatabaseEnabled.get();
useCredentials = defaultUseCredentials.get();
databaseType = defaultDatabaseType.get();
databaseName = defaultDatabaseName.get();
mongoURL = defaultMongoURL.get();
username = defaultUsername.get();
password = defaultPassword.get();
ip = defaultIp.get();
port = defaultPort.get();
commandsEnabled = defaultCommandsEnable.get();
commands = defaultCommands.get();
kickPlayers = defaultKickPlayers.get();
alertToStaff = defaultAlertToStaff.get();
alertMsg = defaultAlertMsg.get();
metrics = defaultMetrics.get();
countryList = defCountrylist.get();
whitelistCountries = defaultWhitelistCountries.get();
countryKickCommands = defCountryKickCommands.get();
countryVanillaKickReason = defaultCountryKickReason.get();
}
}
@@ -0,0 +1,217 @@
/*
* 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.api;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.utils.StringUtil;
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 java.io.IOException;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.Level;
import lombok.Getter;
@Getter
public abstract class VPNExecutor {
private final ScheduledExecutorService threadExecutor = Executors.newScheduledThreadPool(2);
private final Set<UUID> whitelisted = Collections.synchronizedSet(new HashSet<>());
private final Set<CIDRUtils> whitelistedIps = Collections.synchronizedSet(new HashSet<>());
private final Queue<Tuple<CheckResult, UUID>> toKick = new LinkedBlockingQueue<>();
private final Queue<APIPlayer> playersToRecheck = new LinkedBlockingQueue<>();
private ScheduledFuture<?> kickTask = null;
public abstract void registerListeners();
public abstract void log(Level level, String log, Object... objects);
public abstract void log(String log, Object... objects);
public abstract void logException(String message, Throwable ex);
public abstract void runCommand(String command);
public void logException(Throwable ex) {
logException("An exception occurred: " + ex.getMessage(), ex);
}
public void startKickChecks() {
kickTask =
threadExecutor.scheduleAtFixedRate(
() -> {
synchronized (toKick) {
if (toKick.isEmpty()) return;
Tuple<CheckResult, UUID> toCheck;
while ((toCheck = toKick.poll()) != null) {
Optional<APIPlayer> player =
AntiVPN.getInstance().getPlayerExecutor().getPlayer(toCheck.second());
if (player.isEmpty()) {
continue;
}
handleKickingOfPlayer(toCheck.first(), player.get());
}
}
},
8,
2,
TimeUnit.SECONDS);
}
public void handleKickingOfPlayer(CheckResult result, APIPlayer player) {
// Ensuring kick task is always running
if (kickTask == null || kickTask.isDone() || kickTask.isCancelled()) {
startKickChecks();
}
if (AntiVPN.getInstance().getVpnConfig().isAlertToSTaff())
AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream()
.filter(APIPlayer::isAlertsEnabled)
.forEach(
pl ->
pl.sendMessage(
StringUtil.translateAlternateColorCodes(
'&',
StringUtil.varReplace(
dev.brighten.antivpn.AntiVPN.getInstance()
.getVpnConfig()
.getAlertMsg(),
player,
result.response()))));
if (AntiVPN.getInstance().getVpnConfig().isKickPlayers()) {
switch (result.resultType()) {
case DENIED_PROXY ->
player.kickPlayer(
StringUtil.varReplace(
AntiVPN.getInstance().getVpnConfig().getKickMessage(),
player,
result.response()));
case DENIED_COUNTRY ->
player.kickPlayer(
StringUtil.varReplace(
AntiVPN.getInstance().getVpnConfig().getCountryVanillaKickReason(),
player,
result.response()));
}
} else {
if (!AntiVPN.getInstance().getVpnConfig().isCommandsEnabled()) return;
}
Runnable runCommands =
() -> {
switch (result.resultType()) {
case DENIED_PROXY -> {
for (String command : AntiVPN.getInstance().getVpnConfig().commands()) {
runCommand(StringUtil.varReplace(command, player, result.response()));
}
}
case DENIED_COUNTRY -> {
for (String command : AntiVPN.getInstance().getVpnConfig().countryKickCommands()) {
runCommand(StringUtil.varReplace(command, player, result.response()));
}
}
}
};
// Fixes the commands running too fast and causing messaging errors by any downstream plugins
// like LiteBans
var scheduleResult = threadExecutor.schedule(runCommands, 200, TimeUnit.MILLISECONDS);
if (scheduleResult.isCancelled()) {
runCommands.run();
}
var toAdd = new Tuple<>(result, player.getUuid());
// Ensuring players are actually kicked as they are supposed to be.
threadExecutor.schedule(
() -> {
toKick.add(toAdd);
},
500,
TimeUnit.MILLISECONDS);
}
public boolean isWhitelisted(UUID uuid) {
if (AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) {
return AntiVPN.getInstance().getDatabase().isWhitelisted(uuid);
}
return whitelisted.contains(uuid);
}
public boolean isWhitelisted(String cidr) {
if (AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) {
return AntiVPN.getInstance().getDatabase().isWhitelisted(cidr);
}
try {
return whitelistedIps.contains(new CIDRUtils(cidr));
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
private final Cache<String, VPNResponse> cachedResponses =
Caffeine.newBuilder().expireAfterWrite(20, TimeUnit.MINUTES).maximumSize(4000).build();
public CompletableFuture<VPNResponse> checkIp(String ip) {
VPNResponse cached = cachedResponses.getIfPresent(ip);
if (cached != null) {
return CompletableFuture.completedFuture(cached);
}
return CompletableFuture.supplyAsync(
() -> {
Optional<VPNResponse> cachedRes =
AntiVPN.getInstance().getDatabase().getStoredResponse(ip);
if (cachedRes.isPresent()) {
return cachedRes.get();
} else {
try {
VPNResponse response =
FunkemunkyAPI.getVPNResponse(
ip, AntiVPN.getInstance().getVpnConfig().getLicense(), true);
if (response.isSuccess()) {
AntiVPN.getInstance().getDatabase().cacheResponse(response);
} else {
log("Query to VPN API failed! Reason: " + response.getFailureReason());
}
return response;
} catch (JSONException | IOException e) {
log("Query to VPN API failed! Reason: " + e.getMessage());
return VPNResponse.FAILED_RESPONSE;
}
}
},
threadExecutor);
}
public abstract void disablePlugin();
}
@@ -0,0 +1,40 @@
/*
* 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.command;
import java.util.List;
public abstract class Command {
public abstract String permission();
public abstract String name();
public abstract String[] aliases();
public abstract String description();
public abstract String usage();
public abstract String parent();
public abstract Command[] children();
public abstract String execute(CommandExecutor executor, String[] args);
public abstract List<String> tabComplete(CommandExecutor executor, String alias, String[] args);
}
@@ -0,0 +1,31 @@
/*
* 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.command;
import dev.brighten.antivpn.api.APIPlayer;
import java.util.Optional;
public interface CommandExecutor {
void sendMessage(String message, Object... objects);
boolean hasPermission(String permission);
Optional<APIPlayer> getPlayer();
boolean isPlayer();
}
@@ -0,0 +1,88 @@
/*
* 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.command.impl;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor;
import dev.brighten.antivpn.message.VpnString;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class AlertsCommand extends Command {
@Override
public String permission() {
return "antivpn.command.alerts";
}
@Override
public String name() {
return "alerts";
}
@Override
public String[] aliases() {
return new String[] {"valerts", "vpnalerts"};
}
@Override
public String description() {
return "toggle VPN use alerts";
}
@Override
public String usage() {
return "";
}
@Override
public String parent() {
return "antivpn";
}
@Override
public Command[] children() {
return new Command[0];
}
@Override
public String execute(CommandExecutor executor, String[] args) {
Optional<APIPlayer> pgetter = executor.getPlayer();
if (!pgetter.isPresent())
return AntiVPN.getInstance()
.getMessageHandler()
.getString("command-misc-playerRequired")
.getMessage();
APIPlayer player = pgetter.get();
player.setAlertsEnabled(!player.isAlertsEnabled());
player.updateAlertsState();
return AntiVPN.getInstance()
.getMessageHandler()
.getString("command-alerts-toggled")
.getFormattedMessage(new VpnString.Var<>("state", player.isAlertsEnabled()));
}
@Override
public List<String> tabComplete(CommandExecutor executor, String alias, String[] args) {
return Collections.emptyList();
}
}
@@ -0,0 +1,370 @@
/*
* 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.command.impl;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.utils.MiscUtils;
import java.net.UnknownHostException;
import java.util.*;
import java.util.stream.Collectors;
public class AllowlistCommand extends Command {
private static final String[] secondArgs = new String[] {"add", "remove", "show", "search"};
@Override
public String permission() {
return "antivpn.command.allowlist";
}
@Override
public String name() {
return "allowlist";
}
@Override
public String[] aliases() {
return new String[] {"whitelist"};
}
@Override
public String description() {
return "Add/remove players to/from exemption list.";
}
@Override
public String usage() {
return "<add <player/uuid/ip> | remove <player/uuid/ip> | show [page] | search <query> [page]>";
}
@Override
public String parent() {
return "antivpn";
}
@Override
public Command[] children() {
return new Command[0];
}
@Override
public String execute(CommandExecutor executor, String[] args) {
if (args.length == 0
|| Arrays.stream(secondArgs).noneMatch(arg -> arg.equalsIgnoreCase(args[0]))) {
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) {
return "&cUsage: /antivpn allowlist show [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) {
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.";
boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled();
if (!databaseEnabled)
executor.sendMessage(
"&cThe database is currently not setup, "
+ "so any changes here will disappear after a restart.");
CIDRUtils cidrUtils;
try {
cidrUtils = new CIDRUtils(args[1]);
} catch (IllegalArgumentException | UnknownHostException e) {
cidrUtils = null;
}
if (cidrUtils != null) {
if (!databaseEnabled) {
return switch (args[0].toLowerCase()) {
case "add", "insert" -> {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().add(cidrUtils);
yield String.format("&aAdded &6%s &ato exemption allowlist.", cidrUtils.getCidr());
}
case "remove", "delete" -> {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().remove(cidrUtils);
yield String.format(
"&cRemoved &%s &cfrom the exemption allowlist.", cidrUtils.getCidr());
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
} else
return switch (args[0].toLowerCase()) {
case "add", "insert" -> {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().add(cidrUtils);
AntiVPN.getInstance().getDatabase().addWhitelist(cidrUtils);
yield String.format("&aAdded &6%s &ato exemption allowlist.", cidrUtils.getCidr());
}
case "remove", "delete" -> {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().remove(cidrUtils);
AntiVPN.getInstance().getDatabase().removeWhitelist(cidrUtils);
yield String.format(
"&cRemoved &6%s &cfrom the exemption allowlist.", cidrUtils.getCidr());
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
}
if (MiscUtils.isIpv4(args[1])) {
if (!databaseEnabled) {
try {
return switch (args[0].toLowerCase()) {
case "add", "insert" -> {
AntiVPN.getInstance()
.getExecutor()
.getWhitelistedIps()
.add(new CIDRUtils(args[1] + "/32"));
AntiVPN.getInstance().getDatabase().addWhitelist(new CIDRUtils(args[1] + "/32"));
yield String.format("&aAdded &6%s &ato the exemption allowlist.", args[1] + "/32");
}
case "remove", "delete" -> {
AntiVPN.getInstance()
.getExecutor()
.getWhitelistedIps()
.remove(new CIDRUtils(args[1] + "/32"));
AntiVPN.getInstance().getDatabase().removeWhitelist(new CIDRUtils(args[1] + "/32"));
yield String.format(
"&cRemoved &6%s &cfrom the exemption allowlist.", args[1] + "/32");
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
} catch (UnknownHostException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Invalid IP format for allowlist command", e);
return "&cInvalid IP format for allowlist command";
}
} else {
try {
return switch (args[0].toLowerCase()) {
case "add", "insert" -> {
AntiVPN.getInstance().getDatabase().addWhitelist(new CIDRUtils(args[1] + "/32"));
yield String.format("&aAdded &6%s &a to the exemption allowlist.", args[1] + "/32");
}
case "remove", "delete" -> {
AntiVPN.getInstance().getDatabase().removeWhitelist(new CIDRUtils(args[1] + "/32"));
yield String.format(
"&cRemoved &6%s &c from the exemption allowlist.", args[1] + "/32");
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
} catch (UnknownHostException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Invalid IP format for allowlist command", e);
return "&cInvalid IP format for allowlist command";
}
}
} else {
UUID uuid;
try {
uuid = UUID.fromString(args[1]);
} catch (IllegalArgumentException e) {
Optional<APIPlayer> player = AntiVPN.getInstance().getPlayerExecutor().getPlayer(args[1]);
if (player.isPresent()) {
uuid = player.get().getUuid();
} else {
uuid = MiscUtils.lookupUUID(args[1]);
if (uuid == null) {
return "&cCould not find a UUID for \""
+ args[1]
+ "\". They might not have provided a valid username.";
}
}
}
if (!databaseEnabled) {
return switch (args[0].toLowerCase()) {
case "add" -> {
AntiVPN.getInstance().getExecutor().getWhitelisted().add(uuid);
yield String.format("&aAdded &6%s &auuid to the exemption allowlist.", uuid.toString());
}
case "remove", "delete" -> {
AntiVPN.getInstance().getExecutor().getWhitelisted().remove(uuid);
yield String.format(
"&cRemoved &6%s &cuuid from the exemption allowlist.", uuid.toString());
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
} else {
return switch (args[0].toLowerCase()) {
case "add" -> {
AntiVPN.getInstance().getDatabase().addWhitelist(uuid);
yield String.format("&aAdded &6%s &auuid to the exemption allowlist.", uuid.toString());
}
case "remove", "delete" -> {
AntiVPN.getInstance().getDatabase().removeWhitelist(uuid);
yield String.format(
"&cRemoved &6%s &cuuid from the exemption allowlist.", uuid.toString());
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
}
}
}
@Override
public List<String> tabComplete(CommandExecutor executor, String alias, String[] args) {
return switch (args.length) {
case 1 ->
Arrays.stream(secondArgs)
.filter(narg -> narg.toLowerCase().startsWith(args[0].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);
}
}
@@ -0,0 +1,121 @@
/*
* 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.command.impl;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor;
import dev.brighten.antivpn.utils.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class AntiVPNCommand extends Command {
@Override
public String permission() {
return "antivpn.command";
}
@Override
public String name() {
return "antivpn";
}
@Override
public String[] aliases() {
return new String[] {"kaurivpn", "kvpn", "vpn", "avpn"};
}
@Override
public String description() {
return "The main help command";
}
@Override
public String usage() {
return "";
}
@Override
public String parent() {
return "";
}
@Override
public Command[] children() {
return new Command[] {
new LookupCommand(),
new AllowlistCommand(),
new AlertsCommand(),
new ClearCacheCommand(),
new PlanCommand(),
new ReloadCommand()
};
}
@Override
public String execute(CommandExecutor uuid, String[] args) {
List<String> messages = new ArrayList<>();
messages.add(StringUtil.line("&8"));
messages.add("&6&lAntiVPN Help Page");
messages.add("");
for (Command cmd : AntiVPN.getInstance().getCommands()) {
messages.add(
String.format(
"&8/&f%s &8- &7&o%s",
"&7"
+ cmd.parent()
+ (cmd.parent().length() > 0 ? " " : "")
+ "&f"
+ cmd.name()
+ " &7"
+ cmd.usage(),
cmd.description()));
}
for (Command child : children()) {
messages.add(
String.format(
"&8/&f%s &8- &7&o%s",
"&7"
+ child.parent()
+ (child.parent().length() > 0 ? " " : "")
+ "&f"
+ child.name()
+ " &7"
+ child.usage(),
child.description()));
}
messages.add(StringUtil.line("&8"));
return String.join("\n", messages);
}
@Override
public List<String> tabComplete(CommandExecutor executor, String alias, String[] args) {
if (args.length == 1)
return Arrays.stream(children())
.map(Command::name)
.filter(name -> name.toLowerCase().startsWith(args[0].toLowerCase()))
.collect(Collectors.toList());
return Collections.emptyList();
}
}
@@ -0,0 +1,74 @@
/*
* 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.command.impl;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor;
import java.util.Collections;
import java.util.List;
public class ClearCacheCommand extends Command {
@Override
public String permission() {
return "antivpn.command.clearcache";
}
@Override
public String name() {
return "clearcache";
}
@Override
public String[] aliases() {
return new String[] {"clear", "cc"};
}
@Override
public String description() {
return "Clear the API response cache if you're having problems.";
}
@Override
public String usage() {
return "";
}
@Override
public String parent() {
return "antivpn";
}
@Override
public Command[] children() {
return new Command[0];
}
@Override
public String execute(CommandExecutor executor, String[] args) {
AntiVPN.getInstance()
.getExecutor()
.getThreadExecutor()
.execute(() -> AntiVPN.getInstance().getDatabase().clearResponses());
return "&aCleared all cached API response information!";
}
@Override
public List<String> tabComplete(CommandExecutor executor, String alias, String[] args) {
return Collections.emptyList();
}
}
@@ -0,0 +1,117 @@
/*
* 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.command.impl;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor;
import dev.brighten.antivpn.utils.StringUtil;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class LookupCommand extends Command {
@Override
public String permission() {
return "antivpn.command.lookup";
}
@Override
public String name() {
return "lookup";
}
@Override
public String[] aliases() {
return new String[] {"check"};
}
@Override
public String description() {
return "Lookup a player's ip info";
}
@Override
public String usage() {
return "<player>";
}
@Override
public String parent() {
return "antivpn";
}
@Override
public Command[] children() {
return new Command[0];
}
@Override
public String execute(CommandExecutor executor, String[] args) {
if (args.length == 0) {
return "&cPlease supply a player to check.";
}
Optional<APIPlayer> player = AntiVPN.getInstance().getPlayerExecutor().getPlayer(args[0]);
if (player.isEmpty()) {
return String.format("&cNo player found with the name \"%s\"", args[0]);
}
AntiVPN.getInstance()
.getExecutor()
.checkIp(player.get().getIp().getHostAddress())
.thenAccept(
result -> {
if (!result.isSuccess()) {
executor.sendMessage(
"&cThere was an error trying to find the " + "information of this player.");
return;
}
executor.sendMessage(StringUtil.line("&8"));
executor.sendMessage(
"&6&l" + player.get().getName() + "&7&l's Connection Information");
executor.sendMessage("");
executor.sendMessage(
"&e%s&8: &f%s", "Proxy", result.isProxy() ? "&a" + result.getMethod() : "&cNo");
executor.sendMessage("&e%s&8: &f%s", "ISP", result.getIsp());
executor.sendMessage("&e%s&8: &f%s", "Country", result.getCountryName());
executor.sendMessage("&e%s&8: &f%s", "City", result.getCity());
executor.sendMessage(
"&e%s&8: &f%s",
"Coordinates", result.getLatitude() + "&7/&f" + result.getLongitude());
executor.sendMessage(StringUtil.line("&8"));
});
return "&7Looking up the IP information for player " + player.get().getName() + "...";
}
@Override
public List<String> tabComplete(CommandExecutor executor, String alias, String[] args) {
if (args.length == 1)
return AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream()
.map(APIPlayer::getName)
.filter(name -> name.toLowerCase().startsWith(args[0].toLowerCase()))
.collect(Collectors.toList());
return Collections.emptyList();
}
}
@@ -0,0 +1,126 @@
/*
* 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.command.impl;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor;
import dev.brighten.antivpn.utils.StringUtil;
import dev.brighten.antivpn.utils.json.JSONException;
import dev.brighten.antivpn.web.FunkemunkyAPI;
import dev.brighten.antivpn.web.objects.QueryResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
public class PlanCommand extends Command {
@Override
public String permission() {
return "antivpn.command.plan";
}
@Override
public String name() {
return "plan";
}
@Override
public String[] aliases() {
return new String[] {"queries", "query"};
}
@Override
public String description() {
return "Info related to KauriVPN Plan";
}
@Override
public String usage() {
return "";
}
@Override
public String parent() {
return "antivpn";
}
@Override
public Command[] children() {
return new Command[0];
}
@Override
public String execute(CommandExecutor executor, String[] args) {
AntiVPN.getInstance()
.getExecutor()
.getThreadExecutor()
.execute(
() -> {
QueryResponse result;
try {
if (AntiVPN.getInstance().getVpnConfig().getLicense().isEmpty()) {
result = FunkemunkyAPI.getQueryResponse();
} else {
result =
FunkemunkyAPI.getQueryResponse(
AntiVPN.getInstance().getVpnConfig().getLicense());
if (!result.isValidPlan()) {
executor.sendMessage(
"&cThe license &f%s &cis not a valid license, "
+ "checking your Free plan information...",
AntiVPN.getInstance().getVpnConfig().getLicense());
result = FunkemunkyAPI.getQueryResponse();
}
}
String plan = result.getPlanType();
if (plan.equals("IP")) plan += " (Free)";
String queryMax =
result.getQueriesMax() == Long.MAX_VALUE
? "Unlimited"
: String.valueOf(result.getQueriesMax());
executor.sendMessage(StringUtil.line("&8"));
executor.sendMessage("&6&lKauriVPN Plan Information");
executor.sendMessage("");
executor.sendMessage("&e%s&8: &f%s", "Plan", plan);
executor.sendMessage(
"&e%s&8: &f%s&7/&f%s", "Queries Used", result.getQueries(), queryMax);
executor.sendMessage(StringUtil.line("&8"));
} catch (JSONException e) {
AntiVPN.getInstance().getExecutor().logException(e);
executor.sendMessage(
"&cThere was a JSONException thrown while looking up your query "
+ "information. Check console for more details.");
} catch (IOException e) {
AntiVPN.getInstance().getExecutor().logException(e);
executor.sendMessage(
"&cThere was a IOException thrown while looking up your query "
+ "information. Check console for more details.");
}
});
return "&7Looking up your query information...";
}
@Override
public List<String> tabComplete(CommandExecutor executor, String alias, String[] args) {
return Collections.emptyList();
}
}
@@ -0,0 +1,83 @@
/*
* 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.command.impl;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor;
import java.util.Collections;
import java.util.List;
public class ReloadCommand extends Command {
@Override
public String permission() {
return "antivpn.command.reload";
}
@Override
public String name() {
return "reload";
}
@Override
public String[] aliases() {
return new String[0];
}
@Override
public String description() {
return "Reload the plugin";
}
@Override
public String usage() {
return "";
}
@Override
public String parent() {
return "antivpn";
}
@Override
public Command[] children() {
return new Command[0];
}
@Override
public String execute(CommandExecutor executor, String[] args) {
// Loading changes from the config.yml
AntiVPN.getInstance().reloadConfig();
// Updating the cache of these values in VPNConfig
AntiVPN.getInstance().getVpnConfig().update();
AntiVPN.getInstance().getMessageHandler().reloadStrings();
AntiVPN.getInstance().reloadDatabase();
return AntiVPN.getInstance()
.getMessageHandler()
.getString("command-reload-complete")
.getMessage();
}
@Override
public List<String> tabComplete(CommandExecutor executor, String alias, String[] args) {
return Collections.emptyList();
}
}
@@ -0,0 +1,23 @@
/*
* 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;
public class DatabaseException extends RuntimeException {
public DatabaseException(String message, Throwable e) {
super(message, e);
}
}
@@ -0,0 +1,60 @@
/*
* 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;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.web.objects.VPNResponse;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
public interface VPNDatabase {
Optional<VPNResponse> getStoredResponse(String ip);
void cacheResponse(VPNResponse toCache);
void deleteResponse(String ip);
boolean isWhitelisted(UUID uuid);
boolean isWhitelisted(String cidr);
boolean isWhitelisted(CIDRUtils cidr);
void addWhitelist(UUID uuid);
void removeWhitelist(UUID uuid);
void addWhitelist(CIDRUtils cidr);
void removeWhitelist(CIDRUtils cidr);
List<UUID> getAllWhitelisted();
List<CIDRUtils> getAllWhitelistedIps();
void alertsState(UUID uuid, Consumer<Boolean> result);
void updateAlertsState(UUID uuid, boolean state);
void clearResponses();
void init();
void shutdown();
}
@@ -0,0 +1,453 @@
/*
* 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;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.sql.utils.ExecutableStatement;
import dev.brighten.antivpn.database.sql.utils.MySQL;
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.web.objects.VPNResponse;
import java.io.File;
import java.math.BigInteger;
import java.net.UnknownHostException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import lombok.SneakyThrows;
public class H2VPN implements VPNDatabase {
public H2VPN() {
AntiVPN.getInstance()
.getExecutor()
.getThreadExecutor()
.scheduleAtFixedRate(
() -> {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed())
return;
// Refreshing whitelisted players
AntiVPN.getInstance().getExecutor().getWhitelisted().clear();
AntiVPN.getInstance()
.getExecutor()
.getWhitelisted()
.addAll(AntiVPN.getInstance().getDatabase().getAllWhitelisted());
// Refreshing whitlisted IPs
AntiVPN.getInstance().getExecutor().getWhitelistedIps().clear();
AntiVPN.getInstance()
.getExecutor()
.getWhitelistedIps()
.addAll(AntiVPN.getInstance().getDatabase().getAllWhitelistedIps());
},
2,
30,
TimeUnit.SECONDS);
}
@Override
public Optional<VPNResponse> getStoredResponse(String ip) {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed())
return Optional.empty();
try (ExecutableStatement statement =
Query.prepare("select * from `responses` where `ip` = ? limit 1").append(ip)) {
try (ResultSet rs = statement.executeQuery()) {
if (rs != null && rs.next()) {
return Optional.of(
new VPNResponse(
rs.getString("asn"),
rs.getString("ip"),
rs.getString("countryName"),
rs.getString("countryCode"),
rs.getString("city"),
rs.getString("timeZone"),
rs.getString("method"),
rs.getString("isp"),
"N/A",
rs.getBoolean("proxy"),
rs.getBoolean("cached"),
true,
rs.getDouble("latitude"),
rs.getDouble("longitude"),
rs.getTimestamp("inserted").getTime(),
-1));
}
}
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("There was a problem getting a response for " + ip, e);
} catch (Exception e) {
throw new RuntimeException(e);
}
return Optional.empty();
}
/*
* Query.
* prepare("create table if not exists `responses` (`ip` varchar(45) not null, "
* +
* "`countryName` varchar(64), `countryCode` varchar(10), `city` varchar(64), `timeZone` varchar(64), "
* +
* "`method` varchar(32), `isp` varchar(32), `proxy` boolean, `cached` boolean "
* + "`latitude` double, `longitude` double)");
*/
@Override
public void cacheResponse(VPNResponse toCache) {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return;
try (var statement =
Query.prepare(
"insert into `responses` (`ip`,`asn`,`countryName`,`countryCode`,`city`,`timeZone`,"
+ "`method`,`isp`,`proxy`,`cached`,`inserted`,`latitude`,`longitude`) values (?,?,?,?,?,?,?,?,?,?,?,?,?)")
.append(toCache.getIp())
.append(toCache.getAsn())
.append(toCache.getCountryName())
.append(toCache.getCountryCode())
.append(toCache.getCity())
.append(toCache.getTimeZone())
.append(toCache.getMethod())
.append(toCache.getIsp())
.append(toCache.isProxy())
.append(toCache.isCached())
.append(new Timestamp(System.currentTimeMillis()))
.append(toCache.getLatitude())
.append(toCache.getLongitude())) {
statement.execute();
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not cache response for IP: " + toCache.getIp(), e);
}
}
@Override
public void deleteResponse(String ip) {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return;
try (var statement = Query.prepare("delete from `responses` where `ip` = ?").append(ip)) {
statement.execute();
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not delete response from IP: " + ip, e);
}
}
@Override
public boolean isWhitelisted(UUID uuid) {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return false;
try (var statement =
Query.prepare("select uuid from `whitelisted` where `uuid` = ? limit 1")
.append(uuid.toString())) {
try (var set = statement.executeQuery()) {
return set != null && set.next() && set.getString("uuid") != null;
}
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not check whitelist for uuid '" + uuid + "' due to SQL error.", e);
return false;
}
}
@SneakyThrows
@Override
public boolean isWhitelisted(String cidr) {
return isWhitelisted(new CIDRUtils(cidr));
}
@Override
public boolean isWhitelisted(CIDRUtils cidr) {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return false;
BigInteger start = cidr.getStartIpInt();
BigInteger end = cidr.getEndIpInt();
try (var statement =
Query.prepare("SELECT * FROM `whitelisted-ranges` WHERE ip_start <= ? AND ip_end >= ?")
.append(start)
.append(end)) {
try (var result = statement.executeQuery()) {
return result.next();
}
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not check whitelist for cidr '" + cidr + "' due to SQL error.", e);
}
return false;
}
@Override
public void addWhitelist(UUID uuid) {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return;
try (var statement =
Query.prepare("insert into `whitelisted` (`uuid`) values (?)").append(uuid.toString())) {
statement.execute();
AntiVPN.getInstance().getExecutor().getWhitelisted().add(uuid);
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not add uuid '" + uuid + "' to whitelist due to SQL error.", e);
}
}
@Override
public void removeWhitelist(UUID uuid) {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return;
try (var statement =
Query.prepare("delete from `whitelisted` where `uuid` = ?").append(uuid.toString())) {
statement.execute();
AntiVPN.getInstance().getExecutor().getWhitelisted().remove(uuid);
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not remove uuid '" + uuid + "' from whitelist due to SQL error.", e);
}
}
@Override
public void addWhitelist(CIDRUtils cidr) {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return;
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);
}
}
@Override
public void removeWhitelist(CIDRUtils cidr) {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return;
try (var statement =
Query.prepare("delete from `whitelisted-ranges` where `cidr_string` = ?")
.append(cidr.getCidr())) {
statement.execute();
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not remove cidr '" + cidr + "' from whitelist due to SQL error.", e);
}
}
@Override
public List<UUID> getAllWhitelisted() {
List<UUID> uuids = new ArrayList<>();
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return uuids;
try (var statement = Query.prepare("select uuid from `whitelisted`")) {
statement.execute(set -> uuids.add(UUID.fromString(set.getString("uuid"))));
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not get all whitelisted players due to SQL error.", e);
}
return uuids;
}
@Override
public List<CIDRUtils> getAllWhitelistedIps() {
List<CIDRUtils> ips = new ArrayList<>();
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return ips;
try (var statement =
Query.prepare("select `cidr_string`, `ip_start`, `ip_end` from `whitelisted-ranges`")) {
statement.execute(
set -> {
try {
String cidrString = set.getString("cidr_string");
ips.add(new CIDRUtils(cidrString));
} catch (UnknownHostException e) {
AntiVPN.getInstance()
.getExecutor()
.logException(
"Could not format ip " + set.getString("cidr_string") + " into a CIDR!", e);
}
});
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not get all whitelisted ips due to SQL error.", e);
}
return ips;
}
@Override
public void alertsState(UUID uuid, Consumer<Boolean> result) {
if (MySQL.isClosed()) return;
AntiVPN.getInstance()
.getExecutor()
.getThreadExecutor()
.execute(
() -> {
try (var statement =
Query.prepare("select * from `alerts` where `uuid` = ? limit 1")
.append(uuid.toString())) {
try (var set = statement.executeQuery()) {
result.accept(set != null && set.next() && set.getString("uuid") != null);
}
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("There was a problem getting alerts state for " + uuid, e);
result.accept(false);
}
});
}
@Override
public void updateAlertsState(UUID uuid, boolean enabled) {
if (MySQL.isClosed()) return;
if (enabled) {
// We want to make sure there isn't already a uuid inserted to prevent double insertions
alertsState(
uuid,
alreadyEnabled -> { // No need to make another thread execute, already async
if (!alreadyEnabled) {
try (var statement =
Query.prepare("insert into `alerts` (`uuid`) values (?)")
.append(uuid.toString())) {
statement.execute();
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("There was a problem updating alerts state for " + uuid, e);
}
} // No need to insert again of already enabled
});
// Removing any uuid from the alerts table will disable alerts globally.
} else {
try (var statement =
Query.prepare("delete from `alerts` where `uuid` = ?").append(uuid.toString())) {
statement.execute();
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("There was a problem updating alerts state for " + uuid, e);
}
}
}
@Override
public void clearResponses() {
if (MySQL.isClosed()) return;
try (var statement = Query.prepare("delete from `responses`")) {
statement.execute();
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("There was a problem clearing responses.", e);
}
}
@Override
public void init() {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) return;
AntiVPN.getInstance().getExecutor().log("Initializing H2...");
MySQL.initH2();
try {
for (Version<H2VPN> version : Version.h2Versions) {
if (version.needsUpdate(this)) {
version.update(this);
}
}
} catch (Exception e) {
throw new RuntimeException("Could not complete version setup due to SQL error", e);
}
AntiVPN.getInstance().getExecutor().log("Creating tables...");
// Running check for old table types to update
}
@Override
public void shutdown() {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) return;
MySQL.shutdown();
}
public void backupDatabase() {
File dataFolder = new File(AntiVPN.getInstance().getPluginFolder(), "databases");
if (!dataFolder.exists() || MySQL.isClosed()) {
return;
}
try {
var connection = Query.getConn();
if (connection == null
|| connection.getMetaData() == null
|| !connection.getMetaData().getDatabaseProductName().equalsIgnoreCase("H2")) {
return;
}
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not verify database type before H2 backup.", e);
return;
}
File backupDir = new File(dataFolder, "backups");
if (!backupDir.exists() && !backupDir.mkdirs()) {
AntiVPN.getInstance().getExecutor().log("Could not create backup directory");
return;
}
File backupFile =
new File(backupDir, "database.h2_backup_" + System.currentTimeMillis() + ".zip");
String backupPath = backupFile.getAbsolutePath().replace("\\", "/").replace("'", "''");
try (var statement = Query.prepare("BACKUP TO '" + backupPath + "'")) {
statement.execute();
AntiVPN.getInstance().getExecutor().log("Created H2 backup at " + backupFile.getName());
} catch (SQLException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Could not create H2 backup before migration.", e);
}
}
}
@@ -0,0 +1,134 @@
/*
* 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.ExecutableStatement;
import dev.brighten.antivpn.database.sql.utils.Query;
import dev.brighten.antivpn.database.version.Version;
import dev.brighten.antivpn.utils.MiscUtils;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class First implements Version<VPNDatabase> {
private final List<AutoCloseable> toClose = new ArrayList<>();
@Override
public void update(VPNDatabase database) throws DatabaseException {
try {
closeOnEnd(
Query.prepare(
"create table if not exists `whitelisted` (`uuid` varchar(36) not null)"))
.execute();
closeOnEnd(
Query.prepare(
"create table if not exists `whitelisted-ips` (`ip` varchar(45) not null)"))
.execute();
closeOnEnd(
Query.prepare(
"create table if not exists `responses` (`ip` varchar(45) not null, `asn` varchar(12),"
+ "`countryName` text, `countryCode` varchar(10), `city` text, `timeZone` varchar(64), "
+ "`method` varchar(32), `isp` text, `proxy` boolean, `cached` boolean, `inserted` timestamp,"
+ "`latitude` double, `longitude` double)"))
.execute();
closeOnEnd(Query.prepare("create table if not exists `alerts` (`uuid` varchar(36) not null)"))
.execute();
closeOnEnd(Query.prepare("create table if not exists `database_version` (`version` int)"))
.execute();
closeOnEnd(
Query.prepare("insert into `database_version` (`version`) values (?)")
.append(versionNumber()))
.execute();
AntiVPN.getInstance().getExecutor().log("Creating indexes...");
createIndexIfAbsent("whitelisted", "whitelisted_uuid_1", "`uuid`");
createIndexIfAbsent("responses", "responses_ip_1", "`ip`");
createIndexIfAbsent("responses", "responses_proxy_1", "`proxy`");
createIndexIfAbsent("responses", "responses_inserted_1", "`inserted`");
createIndexIfAbsent("whitelisted-ips", "whitelisted_ips_ip_1", "`ip`");
} catch (SQLException e) {
throw new DatabaseException("Failed to update database", e);
} finally {
MiscUtils.close(toClose.toArray(AutoCloseable[]::new));
toClose.clear();
}
}
private ExecutableStatement closeOnEnd(ExecutableStatement statement) {
toClose.add(statement);
return statement;
}
protected void createIndexIfAbsent(String tableName, String indexName, String columnList)
throws SQLException {
if (hasIndex(tableName, indexName)) {
return;
}
closeOnEnd(
Query.prepare(
String.format("create index `%s` on `%s` (%s)", indexName, tableName, columnList)))
.execute();
}
protected void dropIndexIfPresent(String tableName, String indexName) throws SQLException {
if (!hasIndex(tableName, indexName)) {
return;
}
closeOnEnd(Query.prepare(String.format("drop index `%s` on `%s`", indexName, tableName)))
.execute();
}
protected boolean hasIndex(String tableName, String indexName) throws SQLException {
DatabaseMetaData metaData = Query.getConn().getMetaData();
try (ResultSet indexes = metaData.getIndexInfo(null, null, tableName, false, false)) {
while (indexes.next()) {
String existingIndexName = indexes.getString("INDEX_NAME");
if (existingIndexName != null && existingIndexName.equalsIgnoreCase(indexName)) {
return true;
}
}
}
return false;
}
@Override
public int versionNumber() {
return 0;
}
@Override
public boolean needsUpdate(VPNDatabase database) {
try (var statement = Query.prepare("select * from `database_version` where version = 0")) {
try (ResultSet set = statement.executeQuery()) {
return !set.next();
}
} catch (SQLException e) {
return true;
}
}
}
@@ -0,0 +1,173 @@
/*
* 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.local.H2VPN;
import dev.brighten.antivpn.database.sql.MySqlVPN;
import dev.brighten.antivpn.database.sql.utils.ExecutableStatement;
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.net.UnknownHostException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class Second extends First implements Version<VPNDatabase> {
private final List<AutoCloseable> toClose = new ArrayList<>();
@Override
public void update(VPNDatabase database) throws DatabaseException {
if (database instanceof H2VPN h2VPN && !(database instanceof MySqlVPN)) {
h2VPN.backupDatabase();
}
List<String> whitelistedIps = new ArrayList<>();
try (var statement = Query.prepare("SELECT * FROM `whitelisted-ips`")) {
try (var set = statement.executeQuery()) {
while (set.next()) {
whitelistedIps.add(set.getString("ip"));
}
}
} catch (SQLException e) {
throw new DatabaseException("Could not get whitelisted ips from database!", e);
}
try {
closeOnEnd(
Query.prepare(
"CREATE TABLE IF NOT EXISTS `whitelisted-ranges` "
+ "(id INT AUTO_INCREMENT PRIMARY KEY, "
+ "cidr_string VARCHAR(45), "
+ "ip_start BIGINT NOT NULL, "
+ "ip_end BIGINT NOT NULL)"))
.execute();
createIndexIfAbsent("whitelisted-ranges", "idx_ip_range", "ip_start, ip_end");
var cidrs =
whitelistedIps.stream()
.map(
ip -> {
try {
return new CIDRUtils(ip + "/32");
} catch (UnknownHostException e) {
throw new RuntimeException("Could not format ip " + ip + " into a CIDR!", e);
}
})
.toList();
var insertStatement =
Query.prepare(
"INSERT INTO `whitelisted-ranges` (`cidr_string`, `ip_start`, `ip_end`) VALUES (?, ?, ?)");
for (CIDRUtils cidr : cidrs) {
insertStatement =
insertStatement
.append(cidr.toString())
.append(cidr.getStartIpInt())
.append(cidr.getEndIpInt())
.addBatch();
}
int[] updateCounts = insertStatement.executeBatch();
for (int updateCount : updateCounts) {
if (updateCount == 0) {
throw new RuntimeException(
"Could not insert a CIDR from previous whitelisted lists, attempted to restore previous database!");
}
}
dropIndexIfPresent("whitelisted-ips", "ip_1");
dropIndexIfPresent("whitelisted-ips", "whitelisted_ips_ip_1");
closeOnEnd(Query.prepare("DROP TABLE `whitelisted-ips`")).execute();
closeOnEnd(
Query.prepare("INSERT INTO `database_version` (`version`) VALUES (?)")
.append(versionNumber()))
.execute();
} catch (Throwable e) {
AntiVPN.getInstance()
.getExecutor()
.log("Failed to update database to version 1: " + e.getMessage());
try {
rollback(whitelistedIps);
} catch (SQLException ex) {
throw new DatabaseException("Failed to rollback database!", e);
}
throw new DatabaseException("Failed to update to version one, rolling back database!", e);
} finally {
MiscUtils.close(toClose.toArray(AutoCloseable[]::new));
toClose.clear();
}
}
private ExecutableStatement closeOnEnd(ExecutableStatement statement) {
toClose.add(statement);
return statement;
}
private void rollback(List<String> ipAddresses) throws SQLException {
AntiVPN.getInstance().getExecutor().log("Rolling back to version 0...");
dropIndexIfPresent("whitelisted-ranges", "idx_ip_range");
try (var statement = Query.prepare("DROP TABLE `whitelisted-ranges`")) {
statement.execute();
}
try (var statement =
Query.prepare("DELETE FROM `database_version` WHERE version = ?").append(versionNumber())) {
statement.execute();
}
try (var statement =
Query.prepare("CREATE TABLE IF NOT EXISTS `whitelisted-ips` (`ip` VARCHAR(45) NOT NULL)")) {
statement.execute();
}
createIndexIfAbsent("whitelisted-ips", "whitelisted_ips_ip_1", "`ip`");
try (var statement = Query.prepare("DELETE FROM `whitelisted-ips`")) {
statement.execute();
}
try (var statement = Query.prepare("INSERT INTO `whitelisted-ips` (`ip`) VALUES (?)")) {
for (String ip : ipAddresses) {
statement.append(ip);
statement.addBatch();
}
statement.executeBatch();
}
}
@Override
public int versionNumber() {
return 1;
}
@Override
public boolean needsUpdate(VPNDatabase database) {
try (var statement = Query.prepare("select * from `database_version` where version = 1")) {
try (var set = statement.executeQuery()) {
return !set.next();
}
} catch (SQLException e) {
return true;
}
}
}
@@ -0,0 +1,162 @@
/*
* 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 `whitelisted-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.next();
}
} catch (SQLException e) {
return true;
}
}
}
@@ -0,0 +1,332 @@
/*
* 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;
import com.mongodb.*;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.UpdateOptions;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.version.Version;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.web.objects.VPNResponse;
import java.math.BigDecimal;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.Decimal128;
public class MongoVPN implements VPNDatabase {
public MongoCollection<Document> settingsDocument;
MongoCollection<Document> cacheDocument;
private MongoClient client;
public MongoDatabase antivpnDatabase;
public MongoVPN() {
AntiVPN.getInstance()
.getExecutor()
.getThreadExecutor()
.scheduleAtFixedRate(
() -> {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) return;
// Refreshing whitelisted players
AntiVPN.getInstance().getExecutor().getWhitelisted().clear();
AntiVPN.getInstance()
.getExecutor()
.getWhitelisted()
.addAll(AntiVPN.getInstance().getDatabase().getAllWhitelisted());
// Refreshing whitlisted IPs
AntiVPN.getInstance().getExecutor().getWhitelistedIps().clear();
AntiVPN.getInstance()
.getExecutor()
.getWhitelistedIps()
.addAll(AntiVPN.getInstance().getDatabase().getAllWhitelistedIps());
},
2,
30,
TimeUnit.SECONDS);
}
@Override
public Optional<VPNResponse> getStoredResponse(String ip) {
Document rdoc = cacheDocument.find(Filters.eq("ip", ip)).first();
if (rdoc != null) {
long lastUpdate = rdoc.get("lastAccess", 0L);
if (System.currentTimeMillis() - lastUpdate > TimeUnit.HOURS.toMillis(1)) {
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> deleteResponse(ip));
return null;
}
return Optional.of(
VPNResponse.builder()
.asn(rdoc.getString("asn"))
.ip(ip)
.countryName(rdoc.getString("countryName"))
.countryCode(rdoc.getString("countryCode"))
.city(rdoc.getString("city"))
.isp(rdoc.getString("isp"))
.method(rdoc.getString("method"))
.timeZone(rdoc.getString("timeZone"))
.proxy(rdoc.getBoolean("proxy"))
.cached(rdoc.getBoolean("cached"))
.success(true)
.latitude(rdoc.getDouble("latitude"))
.longitude(rdoc.getDouble("longitude"))
.lastAccess(rdoc.get("lastAccess", 0L))
.build());
}
return Optional.empty();
}
@Override
public void cacheResponse(VPNResponse toCache) {
if (AntiVPN.getInstance().getVpnConfig().cachedResults()) {
Document rdoc = new Document("ip", toCache.getIp());
rdoc.put("asn", toCache.getAsn());
rdoc.put("countryName", toCache.getCountryName());
rdoc.put("countryCode", toCache.getCountryCode());
rdoc.put("city", toCache.getCity());
rdoc.put("isp", toCache.getIsp());
rdoc.put("method", toCache.getMethod());
rdoc.put("timeZone", toCache.getTimeZone());
rdoc.put("proxy", toCache.isProxy());
rdoc.put("cached", toCache.isCached());
rdoc.put("success", toCache.isSuccess());
rdoc.put("latitude", toCache.getLatitude());
rdoc.put("longitude", toCache.getLongitude());
rdoc.put("lastAccess", System.currentTimeMillis());
AntiVPN.getInstance()
.getExecutor()
.getThreadExecutor()
.execute(
() -> {
Bson update = new Document("$set", rdoc);
cacheDocument.updateOne(
Filters.eq("ip", toCache.getIp()), update, new UpdateOptions().upsert(true));
});
}
}
@Override
public void deleteResponse(String ip) {
cacheDocument.deleteMany(Filters.eq("ip", ip));
}
@Override
public boolean isWhitelisted(UUID uuid) {
return settingsDocument
.find(
Filters.and(
Filters.eq("setting", "whitelist"), Filters.eq("uuid", uuid.toString())))
.first()
!= null;
}
@Override
public boolean isWhitelisted(String cidr) {
try {
return isWhitelisted(new CIDRUtils(cidr));
} catch (UnknownHostException e) {
AntiVPN.getInstance().getExecutor().log("Failed to check whitelist for IP: " + cidr, e);
return false;
}
}
@Override
public boolean isWhitelisted(CIDRUtils cidr) {
var start = new Decimal128(new BigDecimal(cidr.getStartIpInt()));
var end = new Decimal128(new BigDecimal(cidr.getEndIpInt()));
return settingsDocument
.find(
Filters.and(
Filters.eq("setting", "whitelist"),
Filters.lte("ip_start", start),
Filters.gte("ip_end", end)))
.first()
!= null;
}
@Override
public void addWhitelist(UUID uuid) {
Document wdoc = new Document("setting", "whitelist");
wdoc.put("uuid", uuid.toString());
AntiVPN.getInstance().getExecutor().getWhitelisted().add(uuid);
settingsDocument.insertOne(wdoc);
}
@Override
public void removeWhitelist(UUID uuid) {
AntiVPN.getInstance().getExecutor().getWhitelisted().remove(uuid);
settingsDocument.deleteMany(
Filters.and(Filters.eq("setting", "whitelist"), Filters.eq("uuid", uuid.toString())));
}
@Override
public void addWhitelist(CIDRUtils 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());
settingsDocument.insertOne(doc);
}
@Override
public void removeWhitelist(CIDRUtils cidr) {
settingsDocument.deleteMany(
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())))));
}
@Override
public List<UUID> getAllWhitelisted() {
List<UUID> uuids = new ArrayList<>();
settingsDocument
.find(Filters.and(Filters.eq("setting", "whitelist"), Filters.exists("uuid")))
.forEach(
(Consumer<? super Document>) doc -> uuids.add(UUID.fromString(doc.getString("uuid"))));
return uuids;
}
@Override
public List<CIDRUtils> getAllWhitelistedIps() {
List<CIDRUtils> ips = new ArrayList<>();
settingsDocument
.find(Filters.and(Filters.eq("setting", "whitelist"), Filters.exists("cidr_string")))
.forEach(
(Consumer<? super Document>)
doc -> {
try {
var cidr = new CIDRUtils(doc.getString("cidr_string"));
ips.add(cidr);
} catch (UnknownHostException e) {
AntiVPN.getInstance()
.getExecutor()
.logException(
"Could not format ip " + doc.getString("cidr_string") + " into a CIDR!",
e);
}
});
return ips;
}
@Override
public void alertsState(UUID uuid, Consumer<Boolean> result) {
AntiVPN.getInstance()
.getExecutor()
.getThreadExecutor()
.execute(
() ->
result.accept(
settingsDocument
.find(
Filters.and(
Filters.eq("setting", "alerts"),
Filters.eq("uuid", uuid.toString())))
.first()
!= null));
}
@Override
public void updateAlertsState(UUID uuid, boolean state) {
AntiVPN.getInstance()
.getExecutor()
.getThreadExecutor()
.execute(
() -> {
settingsDocument.deleteMany(
Filters.and(
Filters.eq("setting", "alerts"), Filters.eq("uuid", uuid.toString())));
if (state) {
Document adoc = new Document("setting", "alerts");
adoc.put("uuid", uuid.toString());
settingsDocument.insertOne(adoc);
}
});
}
@Override
public void clearResponses() {
cacheDocument.deleteMany(Filters.exists("ip"));
}
@Override
public void init() {
if (!AntiVPN.getInstance().getVpnConfig().mongoDatabaseURL().isEmpty()) { // URL
ConnectionString cs =
new ConnectionString(AntiVPN.getInstance().getVpnConfig().mongoDatabaseURL());
MongoClientSettings settings =
MongoClientSettings.builder().applyConnectionString(cs).build();
client = MongoClients.create(settings);
} else {
MongoClientSettings.Builder settingsBld =
MongoClientSettings.builder()
.readPreference(ReadPreference.nearest())
.applyToClusterSettings(
builder ->
builder.hosts(
Collections.singletonList(
new ServerAddress(
AntiVPN.getInstance().getVpnConfig().getIp(),
AntiVPN.getInstance().getVpnConfig().getPort()))));
if (AntiVPN.getInstance().getVpnConfig().useDatabaseCreds()) {
settingsBld.credential(
MongoCredential.createCredential(
AntiVPN.getInstance().getVpnConfig().getUsername(),
AntiVPN.getInstance().getVpnConfig().getDatabaseName(),
AntiVPN.getInstance().getVpnConfig().getPassword().toCharArray()));
}
client = MongoClients.create(settingsBld.build());
}
antivpnDatabase = client.getDatabase(AntiVPN.getInstance().getVpnConfig().getDatabaseName());
settingsDocument = antivpnDatabase.getCollection("settings");
cacheDocument = antivpnDatabase.getCollection("cache");
for (Version<MongoVPN> mongoDbVersion : Version.mongoDbVersions) {
if (mongoDbVersion.needsUpdate(this)) {
mongoDbVersion.update(this);
}
}
}
@Override
public void shutdown() {
settingsDocument = null;
cacheDocument = null;
client.close();
}
}
@@ -0,0 +1,52 @@
/*
* 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 com.mongodb.client.model.Indexes;
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 org.bson.Document;
public class MongoFirst implements Version<MongoVPN> {
@Override
public void update(MongoVPN database) throws DatabaseException {
if (database.settingsDocument.listIndexes().first() == null) {
AntiVPN.getInstance().getExecutor().log("Created index for settings collection!");
database.settingsDocument.createIndex(Indexes.ascending("ip"));
database.settingsDocument.createIndex(Indexes.ascending("setting"));
}
var versionCollect = database.antivpnDatabase.getCollection("version");
versionCollect.insertOne(new Document("version", versionNumber()));
}
@Override
public int versionNumber() {
return 0;
}
@Override
public boolean needsUpdate(MongoVPN database) {
var versionCollect = database.antivpnDatabase.getCollection("version");
return versionCollect.find(Filters.eq("version", versionNumber())).first() == null;
}
}
@@ -0,0 +1,90 @@
/*
* 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 com.mongodb.client.model.Indexes;
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 java.math.BigDecimal;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.bson.Document;
import org.bson.types.Decimal128;
public class MongoSecond implements Version<MongoVPN> {
@Override
public void update(MongoVPN database) throws DatabaseException {
List<Document> backup = new ArrayList<>();
database
.settingsDocument
.find(Filters.and(Filters.eq("setting", "whitelist"), Filters.exists("ip")))
.forEach(
(Consumer<? super Document>)
doc -> {
backup.add(new Document(doc));
String ip = doc.getString("ip");
try {
var cidr = new CIDRUtils(ip + "/32");
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.remove("ip");
database.settingsDocument.replaceOne(
Filters.eq("_id", doc.getObjectId("_id")), doc);
} catch (UnknownHostException e) {
rollback(backup, database);
throw new RuntimeException(e);
}
});
database.settingsDocument.createIndex(
Indexes.compoundIndex(Indexes.ascending("ip_start"), Indexes.ascending("ip_end")));
database.settingsDocument.createIndex(Indexes.ascending("cidr_string"));
var versionCollect = database.antivpnDatabase.getCollection("version");
versionCollect.insertOne(new Document("version", versionNumber()));
}
private void rollback(List<Document> toRollback, MongoVPN database) {
AntiVPN.getInstance().getExecutor().log("Rolling back to version 0...");
toRollback.forEach(
doc ->
database.settingsDocument.replaceOne(Filters.eq("_id", doc.getObjectId("_id")), doc));
toRollback.clear();
}
@Override
public int versionNumber() {
return 1;
}
@Override
public boolean needsUpdate(MongoVPN database) {
var versionCollect = database.antivpnDatabase.getCollection("version");
return versionCollect.find(Filters.eq("version", versionNumber())).first() == null;
}
}
@@ -0,0 +1,137 @@
/*
* 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 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;
import org.bson.Document;
import org.bson.types.Decimal128;
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;
}
}
@@ -0,0 +1,74 @@
/*
* 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.sql;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.local.H2VPN;
import dev.brighten.antivpn.database.sql.utils.MySQL;
import dev.brighten.antivpn.database.version.Version;
import java.util.concurrent.TimeUnit;
public class MySqlVPN extends H2VPN {
public MySqlVPN() {
AntiVPN.getInstance()
.getExecutor()
.getThreadExecutor()
.scheduleAtFixedRate(
() -> {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed())
return;
// Refreshing whitelisted players
AntiVPN.getInstance().getExecutor().getWhitelisted().clear();
AntiVPN.getInstance()
.getExecutor()
.getWhitelisted()
.addAll(AntiVPN.getInstance().getDatabase().getAllWhitelisted());
// Refreshing whitlisted IPs
AntiVPN.getInstance().getExecutor().getWhitelistedIps().clear();
AntiVPN.getInstance()
.getExecutor()
.getWhitelistedIps()
.addAll(AntiVPN.getInstance().getDatabase().getAllWhitelistedIps());
},
2,
30,
TimeUnit.SECONDS);
}
@Override
public void init() {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) return;
AntiVPN.getInstance().getExecutor().log("Initializing MySQL...");
MySQL.init();
AntiVPN.getInstance().getExecutor().log("Checking for updates...");
// Running check for old table types to update
try {
for (Version<MySqlVPN> version : Version.mysqlVersions) {
if (version.needsUpdate(this)) {
version.update(this);
}
}
} catch (Exception e) {
throw new RuntimeException("Could not complete version setup due to SQL error", e);
}
}
}
@@ -0,0 +1,145 @@
/*
* 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.sql.utils;
import java.sql.*;
import java.util.UUID;
import lombok.Getter;
import lombok.SneakyThrows;
public class ExecutableStatement implements AutoCloseable {
@Getter private final PreparedStatement statement;
private int pos = 1;
public ExecutableStatement(PreparedStatement statement) {
this.statement = statement;
}
public int execute() throws SQLException {
return statement.executeUpdate();
}
public void execute(ResultSetIterator iterator) throws SQLException {
try (var rs = statement.executeQuery()) {
while (rs.next()) iterator.next(rs);
}
}
public int[] executeBatch() throws SQLException {
return statement.executeBatch();
}
public ResultSet executeQuery() throws SQLException {
return statement.executeQuery();
}
@SneakyThrows
public ExecutableStatement append(Object obj) {
statement.setObject(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(String obj) {
statement.setString(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(UUID uuid) {
if (uuid != null) statement.setString(pos++, uuid.toString().replace("-", ""));
else statement.setString(pos++, null);
return this;
}
@SneakyThrows
public ExecutableStatement append(Array obj) {
statement.setArray(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(Integer obj) {
statement.setInt(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(Short obj) {
statement.setShort(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(Long obj) {
statement.setLong(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(Float obj) {
statement.setFloat(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(Double obj) {
statement.setDouble(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(Date obj) {
statement.setDate(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(Timestamp obj) {
statement.setTimestamp(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(Time obj) {
statement.setTime(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(Blob obj) {
statement.setBlob(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement append(byte[] obj) {
statement.setBytes(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement addBatch() {
statement.addBatch();
return this;
}
@Override
public void close() throws SQLException {
statement.close();
}
}
@@ -0,0 +1,219 @@
/*
* 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.sql.utils;
import com.mysql.cj.jdbc.Driver;
import dev.brighten.antivpn.AntiVPN;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
import java.util.Properties;
import java.util.logging.Level;
import org.h2.jdbc.JdbcSQLFeatureNotSupportedException;
import org.h2.jdbc.JdbcSQLNonTransientConnectionException;
public class MySQL {
private static Connection conn;
public static void init() {
try {
if (conn == null || conn.isClosed()) {
String url =
"jdbc:mysql://"
+ AntiVPN.getInstance().getVpnConfig().getIp()
+ ":"
+ AntiVPN.getInstance().getVpnConfig().getPort()
+ "/?useSSL=true&autoReconnect=true";
Properties properties = new Properties();
properties.setProperty("user", AntiVPN.getInstance().getVpnConfig().getUsername());
properties.setProperty("password", AntiVPN.getInstance().getVpnConfig().getPassword());
conn = new Driver().connect(url, properties);
if (conn == null) {
throw new SQLException("MySQL driver did not accept URL: " + url);
}
conn.setAutoCommit(true);
Query.use(conn);
String databaseName = AntiVPN.getInstance().getVpnConfig().getDatabaseName();
try {
Query.prepare("CREATE DATABASE IF NOT EXISTS `" + databaseName + "`").execute();
} catch (SQLException ex) {
if (!isDatabaseCreationPermissionIssue(ex)) {
throw ex;
}
AntiVPN.getInstance()
.getExecutor()
.log(
"No permission to create MySQL database `"
+ databaseName
+ "`. Attempting to use the existing database instead.");
}
Query.prepare("USE `" + databaseName + "`").execute();
AntiVPN.getInstance().getExecutor().log("Connection to MySQL has been established.");
}
} catch (Exception e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Failed to load mysql: " + e.getMessage(), e);
throw new RuntimeException("Could not initialize MySQL connection", e);
}
}
private static boolean isDatabaseCreationPermissionIssue(SQLException ex) {
return ex instanceof SQLSyntaxErrorException
&& ex.getMessage() != null
&& ex.getMessage().contains("Access denied");
}
public static void initH2() {
initH2(true);
}
private static void initH2(boolean allowRetry) {
File dataFolder = new File(AntiVPN.getInstance().getPluginFolder(), "databases");
if (!dataFolder.exists() && dataFolder.mkdirs()) {
AntiVPN.getInstance().getExecutor().log("Created database directory");
}
File dbFile = new File(dataFolder, "database.mv.db");
File databaseFile = new File(dataFolder, "database");
try {
conn =
new NonClosableConnection(
new org.h2.jdbc.JdbcConnection(
"jdbc:h2:file:" + databaseFile.getAbsolutePath(),
new Properties(),
AntiVPN.getInstance().getVpnConfig().getUsername(),
AntiVPN.getInstance().getVpnConfig().getPassword(),
false));
conn.setAutoCommit(true);
Query.use(conn);
AntiVPN.getInstance().getExecutor().log("Connection to H2 has been established.");
} catch (SQLException ex) {
AntiVPN.getInstance().getExecutor().logException("H2 exception on initialize", ex);
if (ex instanceof JdbcSQLFeatureNotSupportedException
|| ex instanceof JdbcSQLNonTransientConnectionException) {
AntiVPN.getInstance()
.getExecutor()
.log(
"H2 database file is incompatible with this version of AntiVPN. "
+ "Backing up old database file...");
shutdown();
if (allowRetry && backupOldDB(dbFile, dataFolder)) {
initH2(false);
} else {
AntiVPN.getInstance()
.getExecutor()
.log("Could not back up and remove the incompatible H2 database file automatically.");
}
} else {
AntiVPN.getInstance()
.getExecutor()
.logException("Failed to load H2 database: " + ex.getCause().toString(), ex);
}
} catch (Exception e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Failed to load H2 database: " + e.getMessage(), e);
AntiVPN.getInstance()
.getExecutor()
.log(Level.INFO, "TIP: Try deleting the plugin folder and restarting your server!");
}
}
public static boolean backupOldDB(File dbFile, File dataFolder) {
if (!dbFile.exists()) {
return true;
}
if (!dbFile.isFile()) {
AntiVPN.getInstance()
.getExecutor()
.log("Skipping backup for non-file path: " + dbFile.getAbsolutePath());
return false;
}
try {
File backupDir = new File(dataFolder, "backups");
if (backupDir.mkdirs()) {
AntiVPN.getInstance().getExecutor().log("Created backup directory");
} else if (backupDir.exists()) {
AntiVPN.getInstance().getExecutor().log("Backup directory already exists");
} else {
AntiVPN.getInstance().getExecutor().log("Could not create backup directory");
return false;
}
File backupFile =
new File(backupDir, dbFile.getName() + ".backup_" + System.currentTimeMillis());
Files.copy(dbFile.toPath(), backupFile.toPath());
if (!dbFile.delete()) {
dbFile.deleteOnExit();
AntiVPN.getInstance()
.getExecutor()
.log("Could not delete database file - will try again on shutdown");
return false;
}
AntiVPN.getInstance().getExecutor().log("Successfully deleted incompatible database file");
return true;
} catch (IOException ex) {
AntiVPN.getInstance().getExecutor().logException("Failed to handle database file", ex);
}
return false;
}
public static void use() {
try {
init();
} catch (Exception e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
public static void shutdown() {
try {
if (conn != null && !conn.isClosed()) {
if (conn instanceof NonClosableConnection) {
((NonClosableConnection) conn).shutdown();
} else conn.close();
conn = null;
}
} catch (Exception e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
public static boolean isClosed() {
if (conn == null) return true;
try {
return conn.isClosed();
} catch (SQLException e) {
AntiVPN.getInstance().getExecutor().logException(e);
return true;
}
}
}
@@ -0,0 +1,323 @@
/*
* 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.sql.utils;
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
/**
* A wrapper around a {@link Connection} which blocks usage of the default {@link #close()} method.
*/
public class NonClosableConnection implements Connection {
private final Connection delegate;
public NonClosableConnection(Connection delegate) {
this.delegate = delegate;
}
/** Actually {@link #close() closes} the underlying connection. */
public final void shutdown() throws SQLException {
this.delegate.close();
}
@Override
public final void close() throws SQLException {
// do nothing
}
@Override
public final boolean isWrapperFor(Class<?> iface) throws SQLException {
return iface.isInstance(this.delegate) || this.delegate.isWrapperFor(iface);
}
@SuppressWarnings("unchecked")
@Override
public final <T> T unwrap(Class<T> iface) throws SQLException {
if (iface.isInstance(this.delegate)) {
return (T) this.delegate;
}
return this.delegate.unwrap(iface);
}
// Forward to the delegate connection
@Override
public Statement createStatement() throws SQLException {
return this.delegate.createStatement();
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return this.delegate.prepareStatement(sql);
}
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return this.delegate.prepareCall(sql);
}
@Override
public String nativeSQL(String sql) throws SQLException {
return this.delegate.nativeSQL(sql);
}
@Override
public void setAutoCommit(boolean autoCommit) throws SQLException {
this.delegate.setAutoCommit(autoCommit);
}
@Override
public boolean getAutoCommit() throws SQLException {
return this.delegate.getAutoCommit();
}
@Override
public void commit() throws SQLException {
this.delegate.commit();
}
@Override
public void rollback() throws SQLException {
this.delegate.rollback();
}
@Override
public boolean isClosed() throws SQLException {
return this.delegate.isClosed();
}
@Override
public DatabaseMetaData getMetaData() throws SQLException {
return this.delegate.getMetaData();
}
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
this.delegate.setReadOnly(readOnly);
}
@Override
public boolean isReadOnly() throws SQLException {
return this.delegate.isReadOnly();
}
@Override
public void setCatalog(String catalog) throws SQLException {
this.delegate.setCatalog(catalog);
}
@Override
public String getCatalog() throws SQLException {
return this.delegate.getCatalog();
}
@Override
public void setTransactionIsolation(int level) throws SQLException {
this.delegate.setTransactionIsolation(level);
}
@Override
public int getTransactionIsolation() throws SQLException {
return this.delegate.getTransactionIsolation();
}
@Override
public SQLWarning getWarnings() throws SQLException {
return this.delegate.getWarnings();
}
@Override
public void clearWarnings() throws SQLException {
this.delegate.clearWarnings();
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency)
throws SQLException {
return this.delegate.createStatement(resultSetType, resultSetConcurrency);
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
throws SQLException {
return this.delegate.prepareStatement(sql, resultSetType, resultSetConcurrency);
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency)
throws SQLException {
return this.delegate.prepareCall(sql, resultSetType, resultSetConcurrency);
}
@Override
public Map<String, Class<?>> getTypeMap() throws SQLException {
return this.delegate.getTypeMap();
}
@Override
public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
this.delegate.setTypeMap(map);
}
@Override
public void setHoldability(int holdability) throws SQLException {
this.delegate.setHoldability(holdability);
}
@Override
public int getHoldability() throws SQLException {
return this.delegate.getHoldability();
}
@Override
public Savepoint setSavepoint() throws SQLException {
return this.delegate.setSavepoint();
}
@Override
public Savepoint setSavepoint(String name) throws SQLException {
return this.delegate.setSavepoint(name);
}
@Override
public void rollback(Savepoint savepoint) throws SQLException {
this.delegate.rollback(savepoint);
}
@Override
public void releaseSavepoint(Savepoint savepoint) throws SQLException {
this.delegate.releaseSavepoint(savepoint);
}
@Override
public Statement createStatement(
int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return this.delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
}
@Override
public PreparedStatement prepareStatement(
String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability)
throws SQLException {
return this.delegate.prepareStatement(
sql, resultSetType, resultSetConcurrency, resultSetHoldability);
}
@Override
public CallableStatement prepareCall(
String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability)
throws SQLException {
return this.delegate.prepareCall(
sql, resultSetType, resultSetConcurrency, resultSetHoldability);
}
@Override
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
return this.delegate.prepareStatement(sql, autoGeneratedKeys);
}
@Override
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
return this.delegate.prepareStatement(sql, columnIndexes);
}
@Override
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
return this.delegate.prepareStatement(sql, columnNames);
}
@Override
public Clob createClob() throws SQLException {
return this.delegate.createClob();
}
@Override
public Blob createBlob() throws SQLException {
return this.delegate.createBlob();
}
@Override
public NClob createNClob() throws SQLException {
return this.delegate.createNClob();
}
@Override
public SQLXML createSQLXML() throws SQLException {
return this.delegate.createSQLXML();
}
@Override
public boolean isValid(int timeout) throws SQLException {
return this.delegate.isValid(timeout);
}
@Override
public void setClientInfo(String name, String value) throws SQLClientInfoException {
this.delegate.setClientInfo(name, value);
}
@Override
public void setClientInfo(Properties properties) throws SQLClientInfoException {
this.delegate.setClientInfo(properties);
}
@Override
public String getClientInfo(String name) throws SQLException {
return this.delegate.getClientInfo(name);
}
@Override
public Properties getClientInfo() throws SQLException {
return this.delegate.getClientInfo();
}
@Override
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
return this.delegate.createArrayOf(typeName, elements);
}
@Override
public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
return this.delegate.createStruct(typeName, attributes);
}
@Override
public void setSchema(String schema) throws SQLException {
this.delegate.setSchema(schema);
}
@Override
public String getSchema() throws SQLException {
return this.delegate.getSchema();
}
@Override
public void abort(Executor executor) throws SQLException {
this.delegate.abort(executor);
}
@Override
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
this.delegate.setNetworkTimeout(executor, milliseconds);
}
@Override
public int getNetworkTimeout() throws SQLException {
return this.delegate.getNetworkTimeout();
}
}
@@ -0,0 +1,35 @@
/*
* 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.sql.utils;
import java.sql.Connection;
import java.sql.SQLException;
import lombok.Getter;
import org.intellij.lang.annotations.Language;
public class Query {
@Getter private static Connection conn;
public static void use(Connection conn) {
Query.conn = conn;
}
@SuppressWarnings("SqlSourceToSinkFlow")
public static ExecutableStatement prepare(@Language("SQL") String sql) throws SQLException {
return new ExecutableStatement(conn.prepareStatement(sql));
}
}
@@ -0,0 +1,24 @@
/*
* 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.sql.utils;
import java.sql.ResultSet;
import java.sql.SQLException;
public interface ResultSetIterator {
void next(ResultSet rs) throws SQLException;
}
@@ -0,0 +1,54 @@
/*
* 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.sql.version;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.DatabaseException;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.local.version.First;
import dev.brighten.antivpn.database.sql.utils.Query;
import java.sql.SQLException;
public class MySQLFirst extends First {
@Override
public void update(VPNDatabase database) throws DatabaseException {
try (var statement =
Query.prepare(
"select `DATA_TYPE` from INFORMATION_SCHEMA.COLUMNS "
+ "WHERE table_name = 'responses' AND COLUMN_NAME = 'isp';")) {
statement.execute(
set -> {
if (set.getObject("DATA_TYPE").toString().contains("varchar")) {
AntiVPN.getInstance()
.getExecutor()
.log(
"Using old database format for storing responses! "
+ "Dropping table and creating a new one...");
try (var state = Query.prepare("drop table `responses`")) {
if (state.execute() > 0) {
AntiVPN.getInstance().getExecutor().log("Successfully dropped table!");
}
}
}
});
} catch (SQLException e) {
throw new DatabaseException("Could not update MySQL database", e);
}
super.update(database);
}
}
@@ -0,0 +1,42 @@
/*
* 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.version;
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;
public interface Version<DB> {
void update(DB database) throws DatabaseException;
int versionNumber();
boolean needsUpdate(DB database);
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()};
}
@@ -0,0 +1,952 @@
/*
* 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.depends;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.utils.NonnullByDefault;
import dev.brighten.antivpn.utils.Supplier;
import dev.brighten.antivpn.utils.Suppliers;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import lombok.Getter;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.RecordComponentVisitor;
import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.commons.Remapper;
/**
* Resolves {@link MavenLibrary} annotations for a class, and loads the dependency into the
* classloader.
*/
@SuppressWarnings("CallToPrintStackTrace")
@NonnullByDefault
public final class LibraryLoader {
private static final int RELOCATION_FORMAT_VERSION = 5;
private static final String RELOCATION_METADATA_PATH = "META-INF/antivpn-relocation.properties";
@SuppressWarnings("Guava")
private static final Supplier<URLClassLoaderAccess> URL_INJECTOR =
AntiVPN.getInstance().getClass().getClassLoader() instanceof URLClassLoader
? Suppliers.memoize(
() ->
URLClassLoaderAccess.create(
(URLClassLoader) AntiVPN.getInstance().getClass().getClassLoader()))
: null;
public static void loadAll(Object object) {
if (URL_INJECTOR == null) return;
loadAll(object.getClass());
}
public static void loadAll(Class<?> clazz) {
if (URL_INJECTOR == null) return;
MavenLibrary[] libs = clazz.getDeclaredAnnotationsByType(MavenLibrary.class);
for (MavenLibrary lib : libs) {
// Create relocations map if any are defined
Map<String, String> relocations = new HashMap<>();
for (Relocate relocate : lib.relocations()) {
relocations.put(relocate.from().replace("\\", ""), relocate.to());
}
load(
lib.groupId().replace("\\", ""),
lib.artifactId(),
lib.version(),
lib.repo().url(),
relocations);
}
}
public static void load(
String groupId,
String artifactId,
String version,
String repoUrl,
Map<String, String> relocations) {
load(new Dependency(groupId, artifactId, version, repoUrl), relocations);
}
public static void load(Dependency d, Map<String, String> relocations) {
System.out.printf(
"Loading dependency %s:%s:%s from %s%n",
d.getGroupId(), d.getArtifactId(), d.getVersion(), d.getRepoUrl());
String name = d.getArtifactId() + "-" + d.getVersion();
// If we have relocations, add a suffix to identify the relocated version
String fileName = name + ".jar";
if (!relocations.isEmpty()) {
fileName = name + "-relocated.jar";
}
File saveLocation = new File(getLibFolder(), fileName);
File originalJar = new File(getLibFolder(), name + ".jar");
// Download the original jar if it doesn't exist
if (!originalJar.exists()) {
try {
System.out.println(
"Dependency '"
+ name
+ "' is not already in the libraries folder. Attempting to download...");
URL url = d.getUrl();
try (InputStream is = url.openStream()) {
Files.copy(is, originalJar.toPath());
}
System.out.println("Dependency '" + name + "' successfully downloaded.");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Unable to download dependency: " + d, e);
}
}
// Rebuild relocated jars when the relocation format changes or the cached jar is stale.
if (!relocations.isEmpty() && shouldRebuildRelocatedJar(saveLocation, relocations)) {
try {
System.out.println("Relocating packages for " + name + "...");
relocateJar(originalJar, saveLocation, relocations);
System.out.println("Successfully relocated packages for " + name);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Failed to relocate packages for dependency: " + d, e);
}
}
// Load the appropriate jar (original or relocated)
File jarToLoad = relocations.isEmpty() ? originalJar : saveLocation;
if (!jarToLoad.exists()) {
throw new RuntimeException("Unable to find dependency jar: " + jarToLoad.getAbsolutePath());
}
try {
URL_INJECTOR.get().addURL(jarToLoad.toURI().toURL());
} catch (Exception e) {
throw new RuntimeException("Unable to load dependency: " + jarToLoad, e);
}
System.out.println("Loaded dependency '" + name + "' successfully.");
}
private static void relocateJar(File sourceJar, File targetJar, Map<String, String> relocations)
throws IOException {
// Track service files to avoid duplicates
Map<String, StringBuilder> serviceFiles = new HashMap<>();
Files.deleteIfExists(targetJar.toPath());
try (JarFile jar = new JarFile(sourceJar);
JarOutputStream jos = new JarOutputStream(Files.newOutputStream(targetJar.toPath()))) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
// Skip directories
if (entry.isDirectory()) {
continue;
}
try (InputStream is = jar.getInputStream(entry)) {
if (name.startsWith("META-INF/services/")) {
// Process service files but don't write yet
processServiceFile(name, is, serviceFiles, relocations);
} else if (name.endsWith(".class")) {
// Relocate class file path as well as content
String relocatedPath = relocateClassPath(name, relocations);
JarEntry newEntry = new JarEntry(relocatedPath);
jos.putNextEntry(newEntry);
byte[] classBytes = readAllBytes(is);
byte[] relocatedBytes = relocateClass(name, classBytes, relocations);
jos.write(relocatedBytes);
jos.closeEntry();
} else {
// Relocate package-scoped resources so ResourceBundle lookups follow relocated
// packages.
String relocatedPath = relocateResourcePath(name, relocations);
JarEntry newEntry = new JarEntry(relocatedPath);
jos.putNextEntry(newEntry);
copyStream(is, jos);
jos.closeEntry();
}
}
}
// Now write all service files after processing
for (Map.Entry<String, StringBuilder> entry : serviceFiles.entrySet()) {
try {
JarEntry serviceEntry = new JarEntry(entry.getKey());
jos.putNextEntry(serviceEntry);
jos.write(entry.getValue().toString().getBytes(StandardCharsets.UTF_8));
jos.closeEntry();
} catch (Exception e) {
// Log but continue with other service files
System.out.println(
"Warning: Could not write service file " + entry.getKey() + ": " + e.getMessage());
}
}
writeRelocationMetadata(jos, relocations);
}
validateRelocatedJar(targetJar, relocations);
}
private static boolean shouldRebuildRelocatedJar(
File relocatedJar, Map<String, String> relocations) {
if (!relocatedJar.exists()) {
return true;
}
try (JarFile jar = new JarFile(relocatedJar)) {
JarEntry metadataEntry = jar.getJarEntry(RELOCATION_METADATA_PATH);
if (metadataEntry == null) {
return true;
}
Properties metadata = new Properties();
try (InputStream is = jar.getInputStream(metadataEntry)) {
metadata.load(is);
}
if (!String.valueOf(RELOCATION_FORMAT_VERSION)
.equals(metadata.getProperty("formatVersion"))) {
return true;
}
for (Map.Entry<String, String> relocation : relocations.entrySet()) {
String key = "relocation." + relocation.getKey();
if (!relocation.getValue().equals(metadata.getProperty(key))) {
return true;
}
}
return Integer.toString(relocations.size()).equals(metadata.getProperty("relocationCount"));
} catch (IOException e) {
return true;
}
}
private static void writeRelocationMetadata(JarOutputStream jos, Map<String, String> relocations)
throws IOException {
Properties metadata = new Properties();
metadata.setProperty("formatVersion", Integer.toString(RELOCATION_FORMAT_VERSION));
metadata.setProperty("relocationCount", Integer.toString(relocations.size()));
Map<String, String> sortedRelocations = new TreeMap<>(relocations);
for (Map.Entry<String, String> relocation : sortedRelocations.entrySet()) {
metadata.setProperty("relocation." + relocation.getKey(), relocation.getValue());
}
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
metadata.store(buffer, "AntiVPN relocation metadata");
JarEntry metadataEntry = new JarEntry(RELOCATION_METADATA_PATH);
jos.putNextEntry(metadataEntry);
jos.write(buffer.toByteArray());
jos.closeEntry();
}
private static void processServiceFile(
String name,
InputStream is,
Map<String, StringBuilder> serviceFiles,
Map<String, String> relocations)
throws IOException {
// Read service file content
String content = new String(readAllBytes(is));
StringBuilder contentBuilder = serviceFiles.computeIfAbsent(name, k -> new StringBuilder());
// Process and relocate service implementations
for (String line : content.split("\n")) {
String trimmed = line.trim();
if (!trimmed.isEmpty() && !trimmed.startsWith("#")) {
for (Map.Entry<String, String> relocation : relocations.entrySet()) {
if (trimmed.startsWith(relocation.getKey())) {
trimmed = relocation.getValue() + trimmed.substring(relocation.getKey().length());
break;
}
}
}
contentBuilder.append(trimmed).append("\n");
}
}
private static byte[] relocateClass(
String entryName, byte[] classBytes, Map<String, String> relocations) {
try {
// Convert to slash notation for ASM
Remapper prefixRemapper = getPrefixRemapper(relocations);
// Create custom ClassWriter to handle missing classes
ClassReader reader = new ClassReader(classBytes);
ClassWriter writer =
new ClassWriter(ClassWriter.COMPUTE_MAXS) {
@Override
protected String getCommonSuperClass(String type1, String type2) {
try {
return super.getCommonSuperClass(type1, type2);
} catch (RuntimeException e) {
// Fall back to Object when classes can't be loaded
return "java/lang/Object";
}
}
};
ClassVisitor visitor =
createStringRelocationVisitor(new ClassRemapper(writer, prefixRemapper), relocations);
visitor = createMySqlUtilFallbackVisitor(entryName, visitor);
// Process class with remapper
reader.accept(visitor, 0);
return relocateUtf8Constants(writer.toByteArray(), relocations);
} catch (Exception e) {
throw new IllegalStateException("Failed to relocate class entry " + entryName, e);
}
}
public static String relocateReflectiveClassName(String className) {
if (className == null || className.startsWith("dev.brighten.antivpn.shaded.")) {
return className;
}
if (className.startsWith("com.mysql.cj") || className.startsWith("com.mysql.jdbc")) {
return "dev.brighten.antivpn.shaded." + className;
}
return className;
}
private static byte[] relocateUtf8Constants(byte[] classBytes, Map<String, String> relocations)
throws IOException {
Map<String, String> dotMappings = new HashMap<>();
Map<String, String> slashMappings = new HashMap<>();
for (Map.Entry<String, String> entry : relocations.entrySet()) {
dotMappings.put(entry.getKey(), entry.getValue());
slashMappings.put(entry.getKey().replace('.', '/'), entry.getValue().replace('.', '/'));
}
DataInputStream in = new DataInputStream(new ByteArrayInputStream(classBytes));
ByteArrayOutputStream baos = new ByteArrayOutputStream(classBytes.length + 256);
DataOutputStream out = new DataOutputStream(baos);
out.writeInt(in.readInt());
out.writeShort(in.readUnsignedShort());
out.writeShort(in.readUnsignedShort());
int constantPoolCount = in.readUnsignedShort();
out.writeShort(constantPoolCount);
for (int i = 1; i < constantPoolCount; i++) {
int tag = in.readUnsignedByte();
out.writeByte(tag);
switch (tag) {
case 1 -> {
String value = in.readUTF();
String relocated = relocateStringValue(value, dotMappings, slashMappings);
out.writeUTF(relocated);
}
case 3, 4 -> out.writeInt(in.readInt());
case 5, 6 -> {
out.writeLong(in.readLong());
i++;
}
case 7, 8, 16, 19, 20 -> out.writeShort(in.readUnsignedShort());
case 9, 10, 11, 12, 17, 18 -> {
out.writeShort(in.readUnsignedShort());
out.writeShort(in.readUnsignedShort());
}
case 15 -> {
out.writeByte(in.readUnsignedByte());
out.writeShort(in.readUnsignedShort());
}
default -> throw new IOException("Unknown constant pool tag " + tag);
}
}
copyStream(in, out);
out.flush();
return baos.toByteArray();
}
private static Remapper getPrefixRemapper(Map<String, String> relocations) {
Map<String, String> slashMappings = new HashMap<>();
Map<String, String> dotMappings = new HashMap<>();
for (Map.Entry<String, String> entry : relocations.entrySet()) {
dotMappings.put(entry.getKey(), entry.getValue());
String fromSlash = entry.getKey().replace('.', '/');
String toSlash = entry.getValue().replace('.', '/');
slashMappings.put(fromSlash, toSlash);
}
// Create customized remapper for package prefixes
return new Remapper() {
@Override
public String map(String typeName) {
if (typeName == null) return null;
for (Map.Entry<String, String> entry : slashMappings.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
if (typeName.startsWith(from)) {
return to + typeName.substring(from.length());
}
}
return typeName;
}
@Override
public Object mapValue(Object value) {
if (value instanceof String stringValue) {
return relocateStringValue(stringValue, dotMappings, slashMappings);
}
return super.mapValue(value);
}
};
}
private static ClassVisitor createMySqlUtilFallbackVisitor(
String entryName, ClassVisitor delegate) {
if (!"com/mysql/cj/util/Util.class".equals(entryName)) {
return delegate;
}
return new ClassVisitor(Opcodes.ASM9, delegate) {
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (visitor == null) {
return null;
}
if (!"getInstance".equals(name)
|| !"(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/Object;Lcom/mysql/cj/exceptions/ExceptionInterceptor;)Ljava/lang/Object;"
.equals(descriptor)) {
return visitor;
}
return new MethodVisitor(Opcodes.ASM9, visitor) {
@Override
public void visitCode() {
super.visitCode();
super.visitVarInsn(Opcodes.ALOAD, 1);
super.visitMethodInsn(
Opcodes.INVOKESTATIC,
"dev/brighten/antivpn/depends/LibraryLoader",
"relocateReflectiveClassName",
"(Ljava/lang/String;)Ljava/lang/String;",
false);
super.visitVarInsn(Opcodes.ASTORE, 1);
}
};
}
};
}
private static ClassVisitor createStringRelocationVisitor(
ClassVisitor delegate, Map<String, String> relocations) {
Map<String, String> dotMappings = new HashMap<>();
Map<String, String> slashMappings = new HashMap<>();
for (Map.Entry<String, String> entry : relocations.entrySet()) {
dotMappings.put(entry.getKey(), entry.getValue());
slashMappings.put(entry.getKey().replace('.', '/'), entry.getValue().replace('.', '/'));
}
return new ClassVisitor(Opcodes.ASM9, delegate) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return wrapAnnotationVisitor(
super.visitAnnotation(descriptor, visible), dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitTypeAnnotation(
int typeRef, org.objectweb.asm.TypePath typePath, String descriptor, boolean visible) {
return wrapAnnotationVisitor(
super.visitTypeAnnotation(typeRef, typePath, descriptor, visible),
dotMappings,
slashMappings);
}
@Override
public RecordComponentVisitor visitRecordComponent(
String name, String descriptor, String signature) {
RecordComponentVisitor visitor = super.visitRecordComponent(name, descriptor, signature);
if (visitor == null) {
return null;
}
return new RecordComponentVisitor(Opcodes.ASM9, visitor) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return wrapAnnotationVisitor(
super.visitAnnotation(descriptor, visible), dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitTypeAnnotation(
int typeRef,
org.objectweb.asm.TypePath typePath,
String descriptor,
boolean visible) {
return wrapAnnotationVisitor(
super.visitTypeAnnotation(typeRef, typePath, descriptor, visible),
dotMappings,
slashMappings);
}
};
}
@Override
public FieldVisitor visitField(
int access, String name, String descriptor, String signature, Object value) {
FieldVisitor visitor =
super.visitField(
access,
name,
descriptor,
signature,
relocateAsmValue(value, dotMappings, slashMappings));
if (visitor == null) {
return null;
}
return new FieldVisitor(Opcodes.ASM9, visitor) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return wrapAnnotationVisitor(
super.visitAnnotation(descriptor, visible), dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitTypeAnnotation(
int typeRef,
org.objectweb.asm.TypePath typePath,
String descriptor,
boolean visible) {
return wrapAnnotationVisitor(
super.visitTypeAnnotation(typeRef, typePath, descriptor, visible),
dotMappings,
slashMappings);
}
};
}
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (visitor == null) {
return null;
}
return new MethodVisitor(Opcodes.ASM9, visitor) {
@Override
public AnnotationVisitor visitAnnotationDefault() {
return wrapAnnotationVisitor(
super.visitAnnotationDefault(), dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return wrapAnnotationVisitor(
super.visitAnnotation(descriptor, visible), dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitTypeAnnotation(
int typeRef,
org.objectweb.asm.TypePath typePath,
String descriptor,
boolean visible) {
return wrapAnnotationVisitor(
super.visitTypeAnnotation(typeRef, typePath, descriptor, visible),
dotMappings,
slashMappings);
}
@Override
public AnnotationVisitor visitParameterAnnotation(
int parameter, String descriptor, boolean visible) {
return wrapAnnotationVisitor(
super.visitParameterAnnotation(parameter, descriptor, visible),
dotMappings,
slashMappings);
}
@Override
public AnnotationVisitor visitInsnAnnotation(
int typeRef,
org.objectweb.asm.TypePath typePath,
String descriptor,
boolean visible) {
return wrapAnnotationVisitor(
super.visitInsnAnnotation(typeRef, typePath, descriptor, visible),
dotMappings,
slashMappings);
}
@Override
public AnnotationVisitor visitTryCatchAnnotation(
int typeRef,
org.objectweb.asm.TypePath typePath,
String descriptor,
boolean visible) {
return wrapAnnotationVisitor(
super.visitTryCatchAnnotation(typeRef, typePath, descriptor, visible),
dotMappings,
slashMappings);
}
@Override
public AnnotationVisitor visitLocalVariableAnnotation(
int typeRef,
org.objectweb.asm.TypePath typePath,
org.objectweb.asm.Label[] start,
org.objectweb.asm.Label[] end,
int[] index,
String descriptor,
boolean visible) {
return wrapAnnotationVisitor(
super.visitLocalVariableAnnotation(
typeRef, typePath, start, end, index, descriptor, visible),
dotMappings,
slashMappings);
}
@Override
public void visitLdcInsn(Object value) {
super.visitLdcInsn(relocateAsmValue(value, dotMappings, slashMappings));
}
@Override
public void visitInvokeDynamicInsn(
String name,
String descriptor,
org.objectweb.asm.Handle bootstrapMethodHandle,
Object... bootstrapMethodArguments) {
Object[] relocatedArgs = new Object[bootstrapMethodArguments.length];
for (int i = 0; i < bootstrapMethodArguments.length; i++) {
relocatedArgs[i] =
relocateAsmValue(bootstrapMethodArguments[i], dotMappings, slashMappings);
}
super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, relocatedArgs);
}
};
}
};
}
private static AnnotationVisitor wrapAnnotationVisitor(
AnnotationVisitor delegate,
Map<String, String> dotMappings,
Map<String, String> slashMappings) {
if (delegate == null) {
return null;
}
return new AnnotationVisitor(Opcodes.ASM9, delegate) {
@Override
public void visit(String name, Object value) {
super.visit(name, relocateAsmValue(value, dotMappings, slashMappings));
}
@Override
public AnnotationVisitor visitAnnotation(String name, String descriptor) {
return wrapAnnotationVisitor(
super.visitAnnotation(name, descriptor), dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitArray(String name) {
return wrapAnnotationVisitor(super.visitArray(name), dotMappings, slashMappings);
}
};
}
private static Object relocateAsmValue(
Object value, Map<String, String> dotMappings, Map<String, String> slashMappings) {
if (value instanceof String stringValue) {
return relocateStringValue(stringValue, dotMappings, slashMappings);
}
return value;
}
private static String relocateStringValue(
String value, Map<String, String> dotMappings, Map<String, String> slashMappings) {
for (Map.Entry<String, String> entry : dotMappings.entrySet()) {
String from = entry.getKey();
String relocated = relocateByPrefixes(value, from, entry.getValue(), '.', '$');
if (!relocated.equals(value)) {
return relocated;
}
}
for (Map.Entry<String, String> entry : slashMappings.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
String relocated = relocateByPrefixes(value, from, to, '/', '$');
if (!relocated.equals(value)) {
return relocated;
}
relocated = relocateByPrefixes(value, "/" + from, "/" + to, '/', '$');
if (!relocated.equals(value)) {
return relocated;
}
relocated = relocateByPrefixes(value, "L" + from, "L" + to, '/', '$', ';');
if (!relocated.equals(value)) {
return relocated;
}
relocated = relocateByPrefixes(value, "[L" + from, "[L" + to, '/', '$', ';');
if (!relocated.equals(value)) {
return relocated;
}
}
return value;
}
private static String relocateByPrefixes(
String value, String from, String to, char... delimiters) {
if (value.equals(from)) {
return to;
}
for (char delimiter : delimiters) {
if (value.startsWith(from + delimiter)) {
return to + value.substring(from.length());
}
}
return value;
}
private static void validateRelocatedJar(File targetJar, Map<String, String> relocations)
throws IOException {
Set<String> relocatedPrefixes = new HashSet<>();
Map<String, String> dotMappings = new HashMap<>();
Map<String, String> slashMappings = new HashMap<>();
for (Map.Entry<String, String> relocation : relocations.entrySet()) {
relocatedPrefixes.add(relocation.getValue().replace('.', '/') + "/");
dotMappings.put(relocation.getKey(), relocation.getValue());
slashMappings.put(
relocation.getKey().replace('.', '/'), relocation.getValue().replace('.', '/'));
}
try (JarFile jar = new JarFile(targetJar)) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
continue;
}
boolean shouldValidate = false;
for (String relocatedPrefix : relocatedPrefixes) {
if (entry.getName().startsWith(relocatedPrefix)) {
shouldValidate = true;
break;
}
}
if (!shouldValidate) {
continue;
}
try (InputStream is = jar.getInputStream(entry)) {
findUnrelocatedConstant(entry.getName(), readAllBytes(is), dotMappings, slashMappings);
}
}
}
}
private static void findUnrelocatedConstant(
String entryName,
byte[] classBytes,
Map<String, String> dotMappings,
Map<String, String> slashMappings)
throws IOException {
DataInputStream in = new DataInputStream(new ByteArrayInputStream(classBytes));
in.readInt();
in.readUnsignedShort();
in.readUnsignedShort();
int constantPoolCount = in.readUnsignedShort();
for (int i = 1; i < constantPoolCount; i++) {
int tag = in.readUnsignedByte();
switch (tag) {
case 1 -> {
String value = in.readUTF();
String relocated = relocateStringValue(value, dotMappings, slashMappings);
if (!value.equals(relocated)) {
throw new IOException(
"Relocated jar still contains original reference '"
+ value
+ "' in class entry "
+ entryName);
}
}
case 3, 4 -> in.readInt();
case 5, 6 -> {
in.readLong();
i++;
}
case 7, 8, 16, 19, 20 -> in.readUnsignedShort();
case 9, 10, 11, 12, 17, 18 -> {
in.readUnsignedShort();
in.readUnsignedShort();
}
case 15 -> {
in.readUnsignedByte();
in.readUnsignedShort();
}
default ->
throw new IOException(
"Unknown constant pool tag " + tag + " while validating " + entryName);
}
}
}
private static String relocateClassPath(String path, Map<String, String> relocations) {
// Convert path to package format (replacing / with .)
String packagePath = path.substring(0, path.length() - 6).replace('/', '.');
// Apply relocations
for (Map.Entry<String, String> relocation : relocations.entrySet()) {
if (packagePath.startsWith(relocation.getKey())) {
packagePath = relocation.getValue() + packagePath.substring(relocation.getKey().length());
break;
}
}
// Convert back to path format
return packagePath.replace('.', '/') + ".class";
}
private static String relocateResourcePath(String path, Map<String, String> relocations) {
if (path.startsWith("META-INF/")) {
return path;
}
for (Map.Entry<String, String> relocation : relocations.entrySet()) {
String fromPath = relocation.getKey().replace('.', '/');
String toPath = relocation.getValue().replace('.', '/');
if (path.startsWith(fromPath + "/")) {
return toPath + path.substring(fromPath.length());
}
}
return path;
}
private static byte[] readAllBytes(InputStream is) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int bytesRead;
byte[] data = new byte[1024];
while ((bytesRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, bytesRead);
}
return buffer.toByteArray();
}
private static void copyStream(InputStream is, OutputStream os) throws IOException {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
private static File getLibFolder() {
File pluginDataFolder = AntiVPN.getInstance().getPluginFolder();
File libs = new File(pluginDataFolder, "libraries");
if (libs.mkdirs()) {
System.out.println("Created libraries folder!");
}
return libs;
}
@Getter
@NonnullByDefault
// Fix the Dependency class to preserve original groupId for downloading
public static final class Dependency {
private final String groupId;
private final String artifactId;
private final String version;
private final String repoUrl;
// Keep the original groupId/artifactId for Maven downloads
private final String originalGroupId;
private final String originalArtifactId;
public Dependency(String groupId, String artifactId, String version, String repoUrl) {
this.originalGroupId = Objects.requireNonNull(groupId, "groupId");
this.originalArtifactId = Objects.requireNonNull(artifactId, "artifactId");
this.groupId = this.originalGroupId;
this.artifactId = this.originalArtifactId;
this.version = Objects.requireNonNull(version, "version");
this.repoUrl = Objects.requireNonNull(repoUrl, "repoUrl");
}
public URL getUrl() throws MalformedURLException {
String repo = this.repoUrl;
if (!repo.endsWith("/")) {
repo += "/";
}
repo += "%s/%s/%s/%s-%s.jar";
// Always use original groupId for Maven repository URL
String url =
String.format(
repo,
this.originalGroupId.replace(".", "/"),
this.originalArtifactId,
this.version,
this.originalArtifactId,
this.version);
return new URL(url);
}
// Rest of the class unchanged
}
}
@@ -0,0 +1,28 @@
/*
* 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.depends;
import java.lang.annotation.*;
/** Annotation to indicate the required libraries for a class. */
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MavenLibraries {
MavenLibrary[] value() default {};
}
@@ -0,0 +1,57 @@
/*
* 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.depends;
import java.lang.annotation.*;
/** Annotation to indicate a required library for a class. */
@Documented
@Repeatable(MavenLibraries.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MavenLibrary {
/**
* The group id of the library
*
* @return the group id of the library
*/
String groupId();
/**
* The artifact id of the library
*
* @return the artifact id of the library
*/
String artifactId();
/**
* The version of the library
*
* @return the version of the library
*/
String version();
/**
* The repo where the library can be obtained from
*
* @return the repo where the library can be obtained from
*/
Repository repo() default @Repository(url = "https://repo1.maven.org/maven2");
Relocate[] relocations() default {}; // Add this line
}
@@ -0,0 +1,31 @@
/*
* 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.depends;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Relocate {
String from();
String to();
}
@@ -0,0 +1,33 @@
/*
* 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.depends;
import java.lang.annotation.*;
/** Represents a maven repository. */
@Documented
@Target(ElementType.LOCAL_VARIABLE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Repository {
/**
* Gets the base url of the repository.
*
* @return the base url of the repository
*/
String url();
}
@@ -0,0 +1,161 @@
/*
* 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.depends;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
/** Provides access to {@link URLClassLoader}#addURL. */
public abstract class URLClassLoaderAccess {
/**
* Creates a {@link URLClassLoaderAccess} for the given class loader.
*
* @param classLoader the class loader
* @return the access object
*/
static URLClassLoaderAccess create(URLClassLoader classLoader) {
if (Reflection.isSupported()) {
return new Reflection(classLoader);
} else if (Unsafe.isSupported()) {
return new Unsafe(classLoader);
} else {
return Noop.INSTANCE;
}
}
private final URLClassLoader classLoader;
protected URLClassLoaderAccess(URLClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* Adds the given URL to the class loader.
*
* @param url the URL to add
*/
public abstract void addURL(URL url);
/** Accesses using reflection, not supported on Java 9+. */
private static class Reflection extends URLClassLoaderAccess {
private static final Method ADD_URL_METHOD;
static {
Method addUrlMethod;
try {
addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
addUrlMethod.setAccessible(true);
} catch (Exception e) {
addUrlMethod = null;
}
ADD_URL_METHOD = addUrlMethod;
}
private static boolean isSupported() {
return ADD_URL_METHOD != null;
}
Reflection(URLClassLoader classLoader) {
super(classLoader);
}
@Override
public void addURL(URL url) {
try {
ADD_URL_METHOD.invoke(super.classLoader, url);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}
/**
* Accesses using sun.misc.Unsafe, supported on Java 9+.
*
* @author Vaishnav Anil (<a href="https://github.com/slimjar/slimjar">...</a>)
*/
private static class Unsafe extends URLClassLoaderAccess {
private static final sun.misc.Unsafe UNSAFE;
static {
sun.misc.Unsafe unsafe;
try {
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
unsafe = (sun.misc.Unsafe) unsafeField.get(null);
} catch (Throwable t) {
unsafe = null;
}
UNSAFE = unsafe;
}
private static boolean isSupported() {
return UNSAFE != null;
}
private final Collection<URL> unopenedURLs;
private final Collection<URL> pathURLs;
@SuppressWarnings("unchecked")
Unsafe(URLClassLoader classLoader) {
super(classLoader);
Collection<URL> unopenedURLs;
Collection<URL> pathURLs;
try {
Object ucp = fetchField(URLClassLoader.class, classLoader, "ucp");
unopenedURLs = (Collection<URL>) fetchField(ucp.getClass(), ucp, "unopenedUrls");
pathURLs = (Collection<URL>) fetchField(ucp.getClass(), ucp, "path");
} catch (Throwable e) {
unopenedURLs = null;
pathURLs = null;
}
this.unopenedURLs = unopenedURLs;
this.pathURLs = pathURLs;
}
private static Object fetchField(final Class<?> clazz, final Object object, final String name)
throws NoSuchFieldException {
Field field = clazz.getDeclaredField(name);
long offset = UNSAFE.objectFieldOffset(field);
return UNSAFE.getObject(object, offset);
}
@Override
public void addURL(URL url) {
this.unopenedURLs.add(url);
this.pathURLs.add(url);
}
}
private static class Noop extends URLClassLoaderAccess {
private static final Noop INSTANCE = new Noop();
private Noop() {
super(null);
}
@Override
public void addURL(URL url) {
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,67 @@
/*
* 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.message;
import dev.brighten.antivpn.AntiVPN;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class MessageHandler {
private final Map<String, VpnString> messages = new HashMap<>();
public VpnString getString(String key) {
if (!messages.containsKey(key)) {
throw new NullPointerException("There is no VpnString with the key \"" + key + "\"");
}
return messages.get(key);
}
public void reloadStrings() {
for (VpnString value : messages.values()) {
value.updateString();
}
}
public void clearStrings() {
messages.clear();
}
public void addString(VpnString string, Function<VpnString, String> getter) {
string.setConfigStringGetter(getter);
getter.apply(string);
AntiVPN.getInstance().getExecutor().log("Added string " + string.getKey());
messages.put(string.getKey(), string);
}
public void initStrings(Function<VpnString, String> getter) {
addString(
new VpnString(
"command-misc-playerRequired", "&cYou must be a player to execute this command!"),
getter);
addString(
new VpnString(
"command-alerts-toggled",
"&7Your player proxy notifications have been set to: &e%state%"),
getter);
addString(
new VpnString("command-reload-complete", "&aSuccessfully reloaded KauriVPN plugin!"),
getter);
addString(new VpnString("no-permission", "&cNo permission."), getter);
}
}
@@ -0,0 +1,75 @@
/*
* 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.message;
import dev.brighten.antivpn.api.APIPlayer;
import java.util.function.Function;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.SneakyThrows;
@Getter
public class VpnString {
private final String key;
private final String defaultMessage;
private String message;
@Setter private Function<VpnString, String> configStringGetter;
public VpnString(String key, String defaultMessage) {
this.key = key;
this.defaultMessage = defaultMessage;
}
@SneakyThrows
public void updateString() {
if (configStringGetter == null)
throw new Exception("The configStringGetter for string " + key + " is null!");
message = configStringGetter.apply(this);
}
public String getFormattedMessage(Var<String, Object>... replacements) {
String formatted = configStringGetter.apply(this);
for (Var<String, Object> replacement : replacements) {
formatted =
formatted.replace(
"%" + replacement.getKey() + "%", replacement.getReplacement().toString());
}
return formatted;
}
public void sendMessage(APIPlayer player, Var<String, Object>... replacements) {
String formatted = message;
for (Var<String, Object> replacement : replacements) {
formatted =
formatted.replace(
"%" + replacement.getKey() + "%", replacement.getReplacement().toString());
}
player.sendMessage(formatted);
}
@Getter
@RequiredArgsConstructor
public static class Var<S, O> {
private final String key;
private final Object replacement;
}
}
@@ -0,0 +1,121 @@
/*
* 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.utils;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
/**
* A class that enables to get an IP range from CIDR specification. It supports both IPv4 and IPv6.
*/
@Getter
public class CIDRUtils {
private final String cidr;
private final InetAddress inetAddress;
private InetAddress startAddress;
private BigInteger startIpInt, endIpInt;
private InetAddress endAddress;
private final int prefixLength;
public CIDRUtils(String cidr) throws UnknownHostException {
this.cidr = cidr;
/* split CIDR to address and prefix part */
if (this.cidr.contains("/")) {
int index = this.cidr.indexOf('/');
String addressPart = this.cidr.substring(0, index);
String networkPart = this.cidr.substring(index + 1);
inetAddress = InetAddress.getByName(addressPart);
prefixLength = Integer.parseInt(networkPart);
calculate();
} else {
throw new IllegalArgumentException("not an valid CIDR format!");
}
}
private void calculate() throws UnknownHostException {
ByteBuffer maskBuffer;
int targetSize;
if (inetAddress.getAddress().length == 4) {
maskBuffer = ByteBuffer.allocate(4).putInt(-1);
targetSize = 4;
} else {
maskBuffer = ByteBuffer.allocate(16).putLong(-1L).putLong(-1L);
targetSize = 16;
}
BigInteger mask = (new BigInteger(1, maskBuffer.array())).not().shiftRight(prefixLength);
ByteBuffer buffer = ByteBuffer.wrap(inetAddress.getAddress());
BigInteger ipVal = new BigInteger(1, buffer.array());
BigInteger startIp = ipVal.and(mask);
this.startIpInt = startIp;
BigInteger endIp = startIp.add(mask.not());
this.endIpInt = endIp;
byte[] startIpArr = toBytes(startIp.toByteArray(), targetSize);
byte[] endIpArr = toBytes(endIp.toByteArray(), targetSize);
this.startAddress = InetAddress.getByAddress(startIpArr);
this.endAddress = InetAddress.getByAddress(endIpArr);
}
private byte[] toBytes(byte[] array, int targetSize) {
int counter = 0;
List<Byte> newArr = new ArrayList<Byte>();
while (counter < targetSize && (array.length - 1 - counter >= 0)) {
newArr.add(0, array[array.length - 1 - counter]);
counter++;
}
int size = newArr.size();
for (int i = 0; i < (targetSize - size); i++) {
newArr.add(0, (byte) 0);
}
byte[] ret = new byte[newArr.size()];
for (int i = 0; i < newArr.size(); i++) {
ret[i] = newArr.get(i);
}
return ret;
}
public boolean isInRange(String ipAddress) throws UnknownHostException {
InetAddress address = InetAddress.getByName(ipAddress);
BigInteger start = new BigInteger(1, this.startAddress.getAddress());
BigInteger end = new BigInteger(1, this.endAddress.getAddress());
BigInteger target = new BigInteger(1, address.getAddress());
int st = start.compareTo(target);
int te = target.compareTo(end);
return (st < 0 || st == 0) && (te < 0 || te == 0);
}
}
@@ -0,0 +1,43 @@
/*
* 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.utils;
import dev.brighten.antivpn.AntiVPN;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class ConfigDefault<A> {
private final A defaultValue;
private final String path;
private final AntiVPN plugin;
public A get() {
if (plugin.getConfig().get(path) != null) return (A) plugin.getConfig().get(path);
else {
plugin.getConfig().set(path, defaultValue);
plugin.saveConfig();
return defaultValue;
}
}
public A set(A value) {
plugin.getConfig().set(path, value);
return value;
}
}
@@ -0,0 +1,33 @@
/*
* 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.utils;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class EvictingMap<K, V> extends LinkedHashMap<K, V> {
@Getter private final int size;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() >= size;
}
}
@@ -0,0 +1,23 @@
/*
* 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.utils;
/**
* Holder for extra methods of {@code Objects} only in web. Intended to be empty for regular
* version.
*/
abstract class ExtraObjectsMethodsForWeb {}
@@ -0,0 +1,111 @@
/*
* 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.utils;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;
public class IpUtils {
public static Optional<BigDecimal> getIpDecimal(String address) {
try {
InetAddress inet = InetAddress.getByName(address);
if (inet instanceof Inet4Address) {
return Optional.of(BigDecimal.valueOf(ipv4ToLong(address)));
}
return Optional.of(new BigDecimal(ipv6ToDecimalFormat(address)));
} catch (Exception e) {
return Optional.empty();
}
}
public static long ipv4ToLong(String address) {
String[] addrArray = address.split("\\.");
long ipDecimal = 0;
for (int i = 0; i < addrArray.length; i++) {
int power = 3 - i;
ipDecimal += ((Integer.parseInt(addrArray[i]) % 256 * Math.pow(256, power)));
}
return ipDecimal;
}
public static String getIpv4(long ip) {
StringBuilder sb = new StringBuilder(15);
for (int i = 0; i < 4; i++) {
sb.insert(0, ip & 0xff);
if (i < 3) {
sb.insert(0, '.');
}
ip >>= 8;
}
return sb.toString();
}
public static boolean isIpv4(BigDecimal ip) {
return ip.compareTo(BigDecimal.valueOf(4294967295L)) <= 0;
}
public static boolean isIpv6(BigDecimal ip) {
return ip.compareTo(BigDecimal.valueOf(4294967295L)) > 0;
}
public static boolean isIpv4(String ip) {
return ip.matches("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$");
}
public static boolean isNotIp(String ip) {
return !isIpv4(ip) && !isIpv6(ip);
}
public static boolean isIpv6(String ip) {
return ip.matches(
"^([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:)$|^(([0-9a-fA-F]{1,4}:){0,6}([0-9a-fA-F]{1,4}|:))?(::([0-9a-fA-F]{1,4}:){0,5}([0-9a-fA-F]{1,4}|:))?$");
}
public static String getIpv4(BigDecimal ip) {
try {
return Inet4Address.getByAddress(ip.toBigInteger().toByteArray()).getHostAddress();
} catch (UnknownHostException e) {
return "Error";
}
}
public static String getIpv6(BigDecimal ip) {
try {
return Inet6Address.getByAddress(ip.toBigInteger().toByteArray()).getHostAddress();
} catch (UnknownHostException e) {
return "Error";
}
}
public static BigInteger ipv6ToDecimalFormat(String ipAddress) throws UnknownHostException {
return new BigInteger(1, Inet6Address.getByName(ipAddress).getAddress());
}
}
@@ -0,0 +1,207 @@
/*
* 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.utils;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.utils.json.JSONException;
import dev.brighten.antivpn.utils.json.JSONObject;
import dev.brighten.antivpn.utils.json.JsonReader;
import java.io.*;
import java.math.BigInteger;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ThreadFactory;
import java.util.regex.Pattern;
public class MiscUtils {
private static final Pattern ipv4 =
Pattern.compile("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
private static final String DEFAULT_FUNKEMUNKY_UUID_ENDPOINT =
"https://funkemunky.cc/mojang/uuid?name=";
private static final String DEFAULT_MOJANG_UUID_ENDPOINT =
"https://api.mojang.com/users/profiles/minecraft/";
private static volatile String funkemunkyUuidEndpoint = DEFAULT_FUNKEMUNKY_UUID_ENDPOINT;
private static volatile String mojangUuidEndpoint = DEFAULT_MOJANG_UUID_ENDPOINT;
public static void close(Closeable... closeables) {
try {
for (Closeable closeable : closeables) if (closeable != null) closeable.close();
} catch (Exception e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
public static void close(AutoCloseable... closeables) {
try {
for (AutoCloseable closeable : closeables) if (closeable != null) closeable.close();
} catch (Exception e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
public static void copy(InputStream in, File file) {
try {
OutputStream out = new FileOutputStream(file);
int lenght;
byte[] buf = new byte[1024];
while ((lenght = in.read(buf)) > 0) {
out.write(buf, 0, lenght);
}
out.close();
in.close();
} catch (Exception e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
public static ThreadFactory createThreadFactory(String threadName) {
return r -> {
Thread thread = new Thread(r);
thread.setName(threadName);
return thread;
};
}
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); // 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) {
byte[] raw = value.toByteArray();
byte[] result = new byte[4];
int srcPos = Math.max(0, raw.length - 4);
int destPos = Math.max(0, 4 - raw.length);
System.arraycopy(raw, srcPos, result, destPos, Math.min(raw.length, 4));
return result;
}
public static UUID lookupUUID(String playername) {
try {
UUID uuid = lookupUuidFromUrl(funkemunkyUuidEndpoint + playername);
if (uuid != null) {
return uuid;
}
} catch (IOException | JSONException | URISyntaxException e) {
AntiVPN.getInstance()
.getExecutor()
.logException(
"Error while looking up UUID for " + playername + "! Falling back to Mojang API", e);
return lookupMojangUuid(playername);
}
return null;
}
private static UUID lookupUuidFromUrl(String url)
throws IOException, JSONException, URISyntaxException {
HttpURLConnection connection = (HttpURLConnection) new URI(url).toURL().openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setInstanceFollowRedirects(true);
int responseCode = connection.getResponseCode();
if (responseCode >= 500) {
throw new IOException("Server returned HTTP " + responseCode + " for " + url);
}
if (responseCode != HttpURLConnection.HTTP_OK) {
return null;
}
try (InputStream inputStream = connection.getInputStream()) {
JSONObject object =
new JSONObject(
JsonReader.readAll(
new InputStreamReader(inputStream, java.nio.charset.StandardCharsets.UTF_8)));
if (object.has("uuid")) {
return parseUuid(object.getString("uuid"));
}
if (object.has("id")) {
return parseUuid(object.getString("id"));
}
}
return null;
}
private static UUID parseUuid(String value) {
if (value.length() == 32) {
value =
value.replaceFirst(
"([0-9a-fA-F]{8})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{12})",
"$1-$2-$3-$4-$5");
}
return UUID.fromString(value);
}
private static UUID lookupMojangUuid(String playerName) {
try {
return lookupUuidFromUrl(mojangUuidEndpoint + playerName);
} catch (IOException | JSONException | URISyntaxException e) {
AntiVPN.getInstance()
.getExecutor()
.logException("Error while looking up UUID for " + playerName + " from Mojang!:", e);
}
return null;
}
static void setLookupEndpointsForTesting(String funkemunkyEndpoint, String mojangEndpoint) {
funkemunkyUuidEndpoint = funkemunkyEndpoint;
mojangUuidEndpoint = mojangEndpoint;
}
static void resetLookupEndpointsForTesting() {
funkemunkyUuidEndpoint = DEFAULT_FUNKEMUNKY_UUID_ENDPOINT;
mojangUuidEndpoint = DEFAULT_MOJANG_UUID_ENDPOINT;
}
public static boolean isIpv4(String ip) {
return ipv4.matcher(ip).matches();
}
}
@@ -0,0 +1,25 @@
/*
* 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.utils;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface NonnullByDefault {}
@@ -0,0 +1,55 @@
/*
* 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.utils;
/** A utility method to perform unchecked casts to suppress errors produced by nullness analyses. */
final class NullnessCasts {
/**
* Accepts a {@code @Nullable T} and returns a plain {@code T}, without performing any check that
* that conversion is safe.
*
* <p>This method is intended to help with usages of type parameters that have {
* ParametricNullness parametric nullness}. If a type parameter instead ranges over only non-null
* types (or if the type is a non-variable type, like {@code String}), then code should almost
* never use this method, preferring instead to call {@code requireNonNull} so as to benefit from
* its runtime check.
*
* <p>An example use case for this method is in implementing an {@code Iterator<T>} whose {@code
* next} field is lazily initialized. The type of that field would be {@code @Nullable T}, and the
* code would be responsible for populating a "real" {@code T} (which might still be the value
* {@code null}!) before returning it to callers. Depending on how the code is structured, a
* nullness analysis might not understand that the field has been populated. To avoid that problem
* without having to add {@code @SuppressWarnings}, the code can call this method.
*
* <p>Why <i>not</i> just add {@code SuppressWarnings}? The problem is that this method is
* typically useful for {@code return} statements. That leaves the code with two options: Either
* add the suppression to the whole method (which turns off checking for a large section of code),
* or extract a variable, and put the suppression on that. However, a local variable typically
* doesn't work: Because nullness analyses typically infer the nullness of local variables,
* there's no way to assign a {@code @Nullable T} to a field {@code T foo;} and instruct the
* analysis that that means "plain {@code T}" rather than the inferred type {@code @Nullable T}.
* (Even if supported added {@code @NonNull}, that would not help, since the problem case
* addressed by this method is the case in which {@code T} has parametric nullness -- and thus its
* value may be legitimately {@code null}.)
*/
@SuppressWarnings("nullness")
static <T> T uncheckedCastNullableTToT(T t) {
return t;
}
private NullnessCasts() {}
}

Some files were not shown because too many files have changed in this diff Show More