Compare commits

..

177 Commits

Author SHA1 Message Date
funkemunky c2e6168037 Updating workflows to handle new build structure, removing maven workflows 2026-04-08 21:24:20 -04:00
funkemunky 74ae0a0d4d Attempting to fix workflows 2026-04-08 21:24:20 -04:00
funkemunky c9b373a273 Updating workflows for github 2026-04-08 21:24:20 -04:00
funkemunky a943700c9c Gradle project refactor 2026-04-08 21:24:20 -04:00
Dawson e4ab168315 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 21:24:17 -04:00
funkemunky 7f6f475b21 Updating test workflow to build without tests and one with tests 2026-04-08 21:20:14 -04:00
funkemunky 1c3ab6005c Adding central test suite 2026-04-08 21:18:13 -04:00
funkemunky 7bfe6b19bb Fixing MySQL issues 2026-04-08 21:08:18 -04:00
funkemunky ae14755205 Added testing for databases, and fixed some potential database bugs 2026-04-08 21:01:29 -04:00
funkemunky 3583b15815 Using built in pipeline 2026-04-08 16:56:45 -04:00
funkemunky a825752b4b Using built in pipeline 2026-04-08 15:24:41 -04:00
funkemunky a49291192c Adding back ssl args 2026-04-08 15:20:01 -04:00
funkemunky 1e042a486f Attempting to fix gradle issues 2026-04-08 15:07:57 -04:00
funkemunky 37d8511edd Attempting to fix gradle issues 2026-04-08 14:46:13 -04:00
funkemunky eb57929bbd Adding gradlew wrapper 2026-04-08 13:47:39 -04:00
funkemunky 4b8d6caa08 replacing instances of gradlew 2026-04-08 13:47:06 -04:00
funkemunky 3839c4b916 Revert "Fixing permissions issues caused by gradle files being handled on Windows"
This reverts commit 3e4d8be955.
2026-04-08 13:46:11 -04:00
funkemunky 3e4d8be955 Fixing permissions issues caused by gradle files being handled on Windows 2026-04-08 13:40:55 -04:00
funkemunky 7088dad4eb Updating workflows to handle new build structure, removing maven workflows 2026-04-08 13:38:46 -04:00
funkemunky 2bff5c06c9 Getting rid of universal submodule 2026-04-08 13:16:43 -04:00
funkemunky 4c2ad35c96 Fixing build 2026-04-08 13:13:05 -04:00
funkemunky db3e137522 Fixing missing dependency in velocity 2026-04-08 12:59:51 -04:00
funkemunky 546c610a53 Fixing maven central proxy 2026-04-08 11:57:00 -04:00
funkemunky 64adf72e23 Attempting to fix workflows 2026-04-08 10:14:47 -04:00
funkemunky fcffb669d4 Updating workflows for github 2026-04-08 09:53:37 -04:00
funkemunky 5878fc6e43 Gradle project refactor 2026-04-08 09:51:40 -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
Dawson 353b7dad78 bugfix/velocity-kick (#56)
* Updating velocity API

* Added exception logging and fixed NullPointerException with antivpn plan command in velocity from tab-complete being null. Replaced MySQL with MariaDB driver

* Added exception logging and fixed NullPointerException with antivpn plan command in velocity from tab-complete being null. Replaced MySQL with MariaDB driver.

* Added exception logging and fixed NullPointerException with antivpn plan command in velocity from tab-complete being null. Replaced MySQL with MariaDB driver.

* Fixing kick reason

* Corrected MySQL ssl errors and fixed visual bug with velocity users kicking

* Reverted to mysql driver

---------

Co-authored-by: Dawson <dawson@funkemunky.cc>
2025-01-26 14:56:12 -05:00
dependabot[bot] 0291aca052 Merge pull request #52
* Bump com.h2database:h2 from 2.1.210 to 2.2.220 in /Common

* Merge branch 'master' into dependabot/maven/Common/com.h2database-h2-…

* Merge branch 'master' into dependabot/maven/Common/com.h2database-h2-…

* Updating mysql depends to non-vulnerable
2024-11-24 14:38:27 -05:00
funkemunky ae5893be89 Worflow now uses setup-java for caching as well. 2024-11-18 09:23:52 -05:00
funkemunky f9ed53bfec Updating action to latest versions 2024-11-18 09:19:23 -05:00
Dawson cb32dfc370 Allowing manual dispatch 2024-11-18 09:14:37 -05:00
Dawson 4c7ff3d061 Merge pull request #48 from funkemunky/bugfix-bukkit-login
Fixing bukkit and antivirus problems - Release v1.9.3
2024-03-17 14:23:42 -04:00
funkemunky aec0bb2738 Corrected error on bukkit servers when vanilla kicking players 2024-03-15 11:17:25 -04:00
funkemunky 3f5ab39877 - Correct command concurrency issues on Bukkit servers
- Removed dynamic class loading of libraries to fix antivirus flags of the plugin.
2024-03-15 11:06:49 -04:00
Dawson f2e59c0075 Merge pull request #47 from Michielo1/patch-1
Correct ISO codes link in config
2024-03-08 22:37:00 -05:00
Michielo a01b595953 Correct ISO codes link 2024-02-18 20:51:25 +01:00
funkemunky b2fcc4ff26 Changing event priority on bungee so KauriVPN checks run before anything else. 2024-01-25 13:24:51 -05:00
funkemunky 5363b7c469 In process of correcting kick issues 2024-01-06 19:56:14 -05:00
Dawson df48e3dfd4 Update issue templates 2023-12-31 13:39:31 -05:00
Dawson 0686c5fd3e Update issue templates 2023-12-31 13:38:17 -05:00
funkemunky 5b6d214e6f Patches bug on Bukkit/Spigot servers may result in some players not being kicked on VPN detection.
I believe this occurs when the API response is below 50ms, and we attempt to Player#kickPlayer() or run commands on console that attempt to kick the player. The problem is that if this is running before the tick PlayerLoginEvent runs on ends, the player wouldn't be considered "online". Therefore, the player would never be removed from the server even if desired.

I assumed wrongly that the async processing of the query would always end up on the next tick. So now I update the PlayerLoginEvent result to KICK_BANNED no matter if the processing is async or in the same thread stack as the event.
2023-12-30 14:06:44 -05:00
funkemunky 2bdd7d2c34 Cleaned up files 2023-12-04 08:55:01 -05:00
funkemunky 31a9412c0a Updated libraries, and cleaned up code for performance purposes. 2023-12-04 08:54:48 -05:00
Dawson Hessler 7f96c49ce8 Delete nightly.yml 2023-10-30 10:41:45 -04:00
Dawson Hessler 7b3f9fc6ae Update nightly.yml 2023-10-30 10:38:01 -04:00
Dawson Hessler edd08b27ce Update maven.yml 2023-10-30 10:37:36 -04:00
Dawson Hessler 158045217e Create nightly.yml 2023-10-30 10:37:01 -04:00
Dawson 63bdb0a4da Merge pull request #45 from funkemunky/dependabot/maven/Common/com.google.guava-guava-32.0.0-jre
Bump com.google.guava:guava from 31.1-jre to 32.0.0-jre in /Common
2023-10-30 10:27:05 -04:00
Dawson b9e23ba34e Updating to only run on commits and pull requests to main 2023-10-30 10:26:26 -04:00
dependabot[bot] 9f6b0f8b27 Bump com.google.guava:guava from 31.1-jre to 32.0.0-jre in /Common
Bumps [com.google.guava:guava](https://github.com/google/guava) from 31.1-jre to 32.0.0-jre.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

---
updated-dependencies:
- dependency-name: com.google.guava:guava
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-30 14:24:01 +00:00
Dawson fec1dcdef1 Merge pull request #29 from funkemunky/dependabot/maven/Common/org.yaml-snakeyaml-2.0
Bump snakeyaml from 1.30 to 2.0 in /Common
2023-10-30 10:23:21 -04:00
Dawson Hessler c062e3d910 Tested and correcting dependency issues 2023-10-30 10:20:49 -04:00
Dawson Hessler a79fb1fe9a Updating more vulnerable depends 2023-10-30 10:14:15 -04:00
Dawson Hessler d224efce3c Updating depends to make it work 2023-10-30 10:12:14 -04:00
Dawson Hessler a2554c2bba Merge branch 'master' into dependabot/maven/Common/org.yaml-snakeyaml-2.0 2023-10-30 10:06:28 -04:00
Dawson 70bfb4e83d Merge pull request #42 from C0D3-M4513R/master
Fix PR #35
2023-10-30 10:06:07 -04:00
Dawson bc666447c5 Merge pull request #43 from C0D3-M4513R/patch-bukkit-banned-whitelist
Add null checks to ip check on login
2023-10-30 10:05:51 -04:00
Dawson Hessler 26cfc3e3f8 Update maven.yml 2023-10-30 10:03:04 -04:00
Dawson Hessler 7974b24271 Revert "Revert "Revert "Update maven.yml"""
This reverts commit 87cdd57383.
2023-10-30 10:02:30 -04:00
Dawson Hessler 87cdd57383 Revert "Revert "Update maven.yml""
This reverts commit 6fe928ca14.
2023-10-30 10:02:17 -04:00
Dawson Hessler 6fe928ca14 Revert "Update maven.yml"
This reverts commit d461b5945b.
2023-10-30 10:02:02 -04:00
Dawson Hessler dae9111a34 Revert "Merge branch 'master' of https://github.com/funkemunky/AntiVPN"
This reverts commit f4d6fc2b4b, reversing
changes made to d461b5945b.
2023-10-30 10:01:50 -04:00
Dawson Hessler f4d6fc2b4b Merge branch 'master' of https://github.com/funkemunky/AntiVPN 2023-10-30 10:00:12 -04:00
Dawson Hessler d461b5945b Update maven.yml 2023-10-30 09:59:50 -04:00
Dawson c6303ec1b2 Merge pull request #44 from funkemunky/funkemunky-patch-1
Updating build action to run on pull request
2023-10-30 09:57:55 -04:00
Dawson Hessler ca8fb24134 Caching maven repos properly 2023-10-30 09:54:58 -04:00
Dawson 5e37d2c371 Updating to run on pull request 2023-10-30 09:50:47 -04:00
Dawson Hessler 48c6dd63ee Revert "Merge pull request #35 from C0D3-M4513R/patch-1"
This reverts commit db1cdad4e1, reversing
changes made to 9f66570088.
2023-10-30 09:48:04 -04:00
C0D3 M4513R 50e7059597 Add null checks to ip check on login
Supersedes: #41
Fixes: #39
2023-10-18 05:57:22 +02:00
C0D3 M4513R 464b02f416 Fix PR #35 2023-10-18 05:45:27 +02:00
Dawson db1cdad4e1 Merge pull request #35 from C0D3-M4513R/patch-1 2023-10-17 18:58:31 -04:00
Dawson 9f66570088 Merge pull request #37 from C0D3-M4513R/command-suggestion 2023-10-17 18:57:47 -04:00
Dawson 5f0b2796b3 Merge pull request #40 from alexkarezin/master 2023-10-17 18:56:42 -04:00
Alex Karezin be5eb4e953 Update README.md by adding a link to repository map
Adding a link to the high-level diagrams including module, library dependency and others (https://sourcespy.com/github/funkemunkyantivpn/). Built directly from source and updated on schedule. Intended to simplify developer's introduction to the project.

In the spirit of transparency - I am the author of the diagrams. Hope contributors find it useful.
2023-07-31 14:43:20 -04:00
C0D3 M4513R dde81b0495 Bump version 2023-07-14 13:35:50 +02:00
C0D3 M4513R cbc00b79e2 Add command suggestion for empty args 2023-07-14 13:31:36 +02:00
C0D3 M4513R 3b2a463e58 Update PlanCommand.java 2023-07-14 13:17:47 +02:00
Dawson Hessler 96e48594d8 Fixing velocity loading issues 2023-07-12 08:45:39 -04:00
dependabot[bot] 903dd8e73e Bump snakeyaml from 1.30 to 2.0 in /Common
Bumps [snakeyaml](https://bitbucket.org/snakeyaml/snakeyaml) from 1.30 to 2.0.
- [Commits](https://bitbucket.org/snakeyaml/snakeyaml/branches/compare/snakeyaml-2.0..snakeyaml-1.30)

---
updated-dependencies:
- dependency-name: org.yaml:snakeyaml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-28 19:31:59 +00:00
196 changed files with 8977 additions and 5251 deletions
+34
View File
@@ -0,0 +1,34 @@
---
name: Bug report
about: Create a bug report that will allow us to fix any unexpected behavior
title: "[BUG]"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**What instance are you running KauriVPN on?**
*Put an 'x' in the brackets to check it*
- [ ] Velocity
- [ ] Bukkit/Spigot
- [ ] Bungeecord
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea you would like added
title: "[FEATURE] "
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+54
View File
@@ -0,0 +1,54 @@
name: create-release.yml
on:
workflow_dispatch:
jobs:
build-and-release:
name: Build and Release
runs-on: ubuntu-latest
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: 'wrapper'
- name: Build
run: ./gradlew build --no-daemon
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version Number from Gradle
id: get_version
run: |
VERSION=$(./gradlew 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)
CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
echo "Extracted latest release notes from CHANGELOG.md:"
echo -e "$CHANGELOG_CONTENT"
echo "content=$CHANGELOG_ESCAPED" >> "$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
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: 'wrapper'
- name: Build
run: ./gradlew build --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: Upload Sponge plugin
uses: actions/upload-artifact@v4
with:
name: AntiVPN-Sponge
path: Sponge/SpongeLoader/build/libs/*.jar
-29
View File
@@ -1,29 +0,0 @@
on:
push:
branches: ["**"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 17.0.2
uses: actions/setup-java@v1
with:
java-version: 17.0
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- name: Compile
run: mvn -B -Pclean install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload AntiVPN
uses: actions/upload-artifact@v2
with:
name: AntiVPN
path: Assembly/target/Assembly-*.jar
+22
View File
@@ -0,0 +1,22 @@
on:
push:
jobs:
build:
runs-on: ubuntu-latest
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: 'wrapper'
- name: Build
run: ./gradlew build --no-daemon
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2
View File
@@ -188,6 +188,7 @@ fabric.properties
# Package Files #
*.jar
!gradle/wrapper/gradle-wrapper.jar
*.war
*.nar
*.ear
@@ -293,3 +294,4 @@ $RECYCLE.BIN/
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,maven,java,intellij,eclipse,netbeans
/.gradle/
-50
View File
@@ -1,50 +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/maven-v4_0_0.xsd">
<parent>
<artifactId>AntiVPN</artifactId>
<groupId>dev.brighten.antivpn</groupId>
<version>1.9.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Assembly</artifactId>
<build>
<plugins>
<plugin>
<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</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.google</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>cc.funkemunky.utils</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.target>8</maven.compiler.target>
<maven.compiler.source>8</maven.compiler.source>
</properties>
</project>
-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.0</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</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.google</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>
+27
View File
@@ -0,0 +1,27 @@
plugins {
id 'com.github.johnrengelman.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')
}
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.build.dependsOn shadowJar
@@ -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.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
+18
View File
@@ -0,0 +1,18 @@
plugins {
id 'com.github.johnrengelman.shadow'
}
dependencies {
compileOnly 'org.spigotmc:spigot-api:1.20.2-R0.1-SNAPSHOT'
compileOnly project(':Common:Source')
compileOnly project(':Common:loader-utils')
implementation 'org.bstats:bstats-bukkit:2.2.1'
}
shadowJar {
archiveClassifier.set('')
relocate 'org.bstats', 'dev.brighten.antivpn.bukkit.org.bstats'
relocate 'org.yaml.snakeyaml', 'dev.brighten.antivpn.shaded.org.yaml.snakeyaml'
}
tasks.build.dependsOn shadowJar
@@ -1,3 +1,19 @@
/*
* 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;
@@ -0,0 +1,115 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.OfflinePlayer;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.utils.StringUtil;
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 java.util.logging.Level;
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) {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(),
ChatColor.translateAlternateColorCodes('&', command));
}
@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()
));
player.checkPlayer(result -> {
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());
}
}
@@ -1,3 +1,19 @@
/*
* 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;
@@ -27,7 +43,7 @@ public class BukkitPlayer extends APIPlayer {
public void run() {
player.kickPlayer(ChatColor.translateAlternateColorCodes('&', reason));
}
}.runTask(BukkitPlugin.pluginInstance);
}.runTask(BukkitPlugin.pluginInstance.getPlugin());
} else player.kickPlayer(ChatColor.translateAlternateColorCodes('&', reason));
}
@@ -1,3 +1,19 @@
/*
* 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;
@@ -1,10 +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.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 lombok.Getter;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SingleLineChart;
import org.bstats.charts.SimplePie;
import org.bukkit.Bukkit;
import org.bukkit.command.SimpleCommandMap;
import org.bukkit.event.HandlerList;
@@ -12,17 +34,33 @@ import org.bukkit.plugin.SimplePluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class BukkitPlugin extends JavaPlugin {
public class BukkitPlugin implements LoaderBootstrap {
public static BukkitPlugin pluginInstance;
private SimpleCommandMap commandMap;
private List<org.bukkit.command.Command> registeredCommands = new ArrayList<>();
private SingleLineChart vpnDetections, ipsChecked;
@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;
@@ -30,31 +68,30 @@ public class BukkitPlugin extends JavaPlugin {
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));
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(this, 12000, 12000);
}.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 (pluginInstance.getServer().getPluginManager() instanceof SimplePluginManager) {
SimplePluginManager manager = (SimplePluginManager) pluginInstance.getServer().getPluginManager();
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) {
e.printStackTrace();
AntiVPN.getInstance().getExecutor().logException(e);
}
}
@@ -67,7 +104,7 @@ public class BukkitPlugin extends JavaPlugin {
registeredCommands.add(newCommand);
// This tells Bukkit to register our command for use.
commandMap.register(pluginInstance.getName(), newCommand);
commandMap.register(plugin.getName(), newCommand);
}
//TODO Finish system before implementing on startup
@@ -77,31 +114,49 @@ public class BukkitPlugin extends JavaPlugin {
.get());
AntiVPN.getInstance().getMessageHandler().reloadStrings();*/
reloadConfig();
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);
Map<String, org.bukkit.command.Command> knownCommands =
(Map<String, org.bukkit.command.Command>) field.get(commandMap);
knownCommands.values().removeAll(registeredCommands);
registeredCommands.clear();
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) {
e.printStackTrace();
AntiVPN.getInstance().getExecutor().logException(e);
}
Bukkit.getLogger().info("Unregistering listeners...");
HandlerList.unregisterAll(this);
HandlerList.unregisterAll(plugin);
Bukkit.getLogger().info("Cancelling any running tasks...");
Bukkit.getScheduler().cancelTasks(this);
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,77 @@
/*
* 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 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.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;
}
}
@@ -1,3 +1,19 @@
/*
* 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;
@@ -0,0 +1,111 @@
package dev.brighten.antivpn.bukkit;
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.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 org.bukkit.event.player.PlayerLoginEvent;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
public class BukkitListenerTest {
private ServerMock server;
private BukkitListener listener;
private VPNExecutor vpnExecutor;
@BeforeEach
public void setUp() throws Exception {
server = MockBukkit.mock();
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);
MockBukkit.unmock();
}
@Test
public void testLoginEventAllowed() throws Exception {
PlayerMock player = server.addPlayer("TestPlayer");
InetAddress address = InetAddress.getByName("127.0.0.1");
PlayerLoginEvent event = new PlayerLoginEvent(player, "localhost", address);
listener.onLogin(event);
assertEquals(PlayerLoginEvent.Result.ALLOWED, event.getResult());
}
@Test
public void testLoginEventBlocked() throws Exception {
PlayerMock player = server.addPlayer("ProxyPlayer");
InetAddress address = InetAddress.getByName("1.1.1.1");
// Mock proxy response
when(vpnExecutor.checkIp("1.1.1.1")).thenReturn(CompletableFuture.completedFuture(
VPNResponse.builder().success(true).proxy(true).ip("1.1.1.1")
.method("N/A").countryName("N/A").countryCode("N/A").city("N/A").build()
));
PlayerLoginEvent event = new PlayerLoginEvent(player, "localhost", address);
listener.onLogin(event);
assertEquals(PlayerLoginEvent.Result.KICK_BANNED, event.getResult());
assertEquals("Blocked!", net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(event.kickMessage()));
}
}
-82
View File
@@ -1,82 +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/maven-v4_0_0.xsd">
<parent>
<artifactId>AntiVPN</artifactId>
<groupId>dev.brighten.antivpn</groupId>
<version>1.9.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Bukkit</artifactId>
<build>
<resources>
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<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>
<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>
<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</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.google</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.github.spigot</groupId>
<artifactId>1.13.2</artifactId>
<version>1.13.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Common</artifactId>
<version>1.9.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cc.funkemunky.utils</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.target>8</maven.compiler.target>
<maven.compiler.source>8</maven.compiler.source>
</properties>
</project>
-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.0</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</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.google</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.github.spigot</groupId>
<artifactId>1.13.2</artifactId>
<version>1.13.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Common</artifactId>
<version>1.9.0</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,202 +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.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;
import org.bukkit.scheduler.BukkitTask;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class BukkitListener extends VPNExecutor implements Listener {
private BukkitTask cacheResetTask;
private final Cache<UUID, VPNResponse> responseCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
@Override
public void registerListeners() {
BukkitPlugin.pluginInstance.getServer().getPluginManager()
.registerEvents(this, BukkitPlugin.pluginInstance);
}
@Override
public void runCacheReset() {
// Reset cache every 20 minutes
cacheResetTask = new BukkitRunnable() {
public void run() {
resetCache();
}
}.runTaskTimerAsynchronously(BukkitPlugin.pluginInstance, 24000, 24000);
HandlerList.unregisterAll(this);
threadExecutor.shutdown();
}
@Override
public void shutdown() {
if(cacheResetTask != null && !cacheResetTask.isCancelled()) cacheResetTask.cancel();
}
@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));
}
@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) {
AntiVPN.getInstance().getExecutor().log("Enabled");
player.setAlertsEnabled(true);
player.sendMessage(AntiVPN.getInstance().getMessageHandler()
.getString("command-alerts-toggled")
.getFormattedMessage(new VpnString.Var<>("state", true)));
} else AntiVPN.getInstance().getExecutor().log("Not enabled");
});
});
}
@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
new BukkitRunnable() {
public void run() {
//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
if(AntiVPN.getInstance().getExecutor().isWhitelisted(event.getPlayer().getAddress().getAddress()
.getHostAddress())) {
log("IP is whitelisted: %s",
event.getPlayer().getAddress().getAddress().getHostAddress());
return;
}
// If the countryList() size is zero, no need to check.
// Running country check first
if(AntiVPN.getInstance().getVpnConfig().countryList().size() > 0
// 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().size() == 0) {
final String kickReason = AntiVPN.getInstance().getVpnConfig()
.countryVanillaKickReason();
// Kicking our player
event.getPlayer().kickPlayer(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
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), formattedCommand);
}
}
} else if(result.isProxy()) {
if(AntiVPN.getInstance().getVpnConfig().kickPlayersOnDetect())
player.kickPlayer(org.bukkit.ChatColor.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString()));
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()) {
for (String command : AntiVPN.getInstance().getVpnConfig().commands()) {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
ChatColor.translateAlternateColorCodes('&',
command.replace("%player%",
event.getPlayer().getName())));
}
}
AntiVPN.getInstance().detections++;
}
}
}.runTask(BukkitPlugin.pluginInstance);
} 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());
}
}
-5
View File
@@ -1,5 +0,0 @@
name: KauriVPN
main: dev.brighten.antivpn.bukkit.BukkitPlugin
version: ${project.version}
author: funkemunky
api-version: 1.13
+26
View File
@@ -0,0 +1,26 @@
plugins {
id 'com.github.johnrengelman.shadow'
}
evaluationDependsOn(':Bungee:BungeePlugin')
dependencies {
compileOnly 'net.md-5:bungeecord-api:1.21-R0.2'
compileOnly project(':Bungee:BungeePlugin')
implementation project(':Common:loader-utils')
}
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.build.dependsOn shadowJar
@@ -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.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
+18
View File
@@ -0,0 +1,18 @@
plugins {
id 'com.github.johnrengelman.shadow'
}
dependencies {
compileOnly 'net.md-5:bungeecord-api:1.21-R0.2'
compileOnly project(':Common:Source')
compileOnly project(':Common:loader-utils')
implementation 'org.bstats:bstats-bungeecord:2.2.1'
}
shadowJar {
archiveClassifier.set('')
relocate 'org.bstats', 'dev.brighten.antivpn.bungee.org.bstats'
relocate 'org.yaml.snakeyaml', 'dev.brighten.antivpn.shaded.org.yaml.snakeyaml'
}
tasks.build.dependsOn shadowJar
@@ -0,0 +1,122 @@
/*
* 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 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;
import java.net.InetSocketAddress;
import java.util.UUID;
import java.util.logging.Level;
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());
});
player.checkPlayer(result -> {
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());
}
}
@@ -1,3 +1,19 @@
/*
* 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;
@@ -1,8 +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.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.*;
@@ -14,7 +29,7 @@ public class BungeePlayerExecutor implements PlayerExecutor {
@Override
public Optional<APIPlayer> getPlayer(String name) {
ProxiedPlayer player = BungeeCord.getInstance().getPlayer(name);
ProxiedPlayer player = BungeePlugin.pluginInstance.getProxy().getPlayer(name);
if(player == null) return Optional.empty();
@@ -23,7 +38,7 @@ public class BungeePlayerExecutor implements PlayerExecutor {
@Override
public Optional<APIPlayer> getPlayer(UUID uuid) {
ProxiedPlayer player = BungeeCord.getInstance().getPlayer(uuid);
ProxiedPlayer player = BungeePlugin.pluginInstance.getProxy().getPlayer(uuid);
if(player == null) return Optional.empty();
@@ -37,7 +52,7 @@ public class BungeePlayerExecutor implements PlayerExecutor {
@Override
public List<APIPlayer> getOnlinePlayers() {
return BungeeCord.getInstance().getPlayers().stream()
return BungeePlugin.pluginInstance.getProxy().getPlayers().stream()
.map(pl -> cachedPlayers.computeIfAbsent(pl.getUniqueId(), key -> new BungeePlayer(pl)))
.collect(Collectors.toList());
}
@@ -0,0 +1,102 @@
/*
* 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 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;
import java.io.File;
import java.util.concurrent.TimeUnit;
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();
}
}
@@ -1,11 +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.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.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
@@ -1,3 +1,19 @@
/*
* 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;
@@ -0,0 +1,110 @@
package dev.brighten.antivpn.bungee;
import dev.brighten.antivpn.AntiVPN;
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 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;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import static org.mockito.Mockito.*;
public class BungeeListenerTest {
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() {
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
when(vpnExecutor.checkIp("1.1.1.1")).thenReturn(CompletableFuture.completedFuture(
VPNResponse.builder().success(true).proxy(true).ip("1.1.1.1")
.method("N/A").countryName("N/A").countryCode("N/A").city("N/A").build()
));
listener.onListener(event);
verify(event).setCancelled(true);
verify(event).setReason(any());
}
}
-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/maven-v4_0_0.xsd">
<parent>
<artifactId>AntiVPN</artifactId>
<groupId>dev.brighten.antivpn</groupId>
<version>1.9.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Bungee</artifactId>
<build>
<resources>
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<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>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<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>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Common</artifactId>
<version>1.9.0</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<artifactId>snakeyaml</artifactId>
<groupId>org.yaml</groupId>
</exclusion>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.github.bungee</groupId>
<artifactId>BungeeCord-1.8</artifactId>
<version>1.8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cc.funkemunky.utils</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.target>8</maven.compiler.target>
<maven.compiler.source>8</maven.compiler.source>
</properties>
</project>
-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.0</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.0</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 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(10000)
.build();
@Override
public void registerListeners() {
BungeePlugin.pluginInstance.getProxy().getPluginManager()
.registerListener(BungeePlugin.pluginInstance, this);
}
@Override
public void runCacheReset() {
cacheResetTask = BungeePlugin.pluginInstance.getProxy().getScheduler().schedule(BungeePlugin.pluginInstance,
this::resetCache, 20, 20, TimeUnit.MINUTES);
}
@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));
}
@EventHandler
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
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().size() > 0
// 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().size() == 0) {
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,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();
}
}
+49
View File
@@ -0,0 +1,49 @@
# 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.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
+33
View File
@@ -0,0 +1,33 @@
plugins {
id 'com.github.johnrengelman.shadow'
}
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.2.220'
compileOnly 'com.github.ben-manes.caffeine:caffeine:3.1.8'
compileOnly 'org.mongodb:mongo-java-driver:3.12.14'
}
shadowJar {
archiveClassifier.set('')
relocate 'org.yaml.snakeyaml', 'dev.brighten.antivpn.shaded.org.yaml.snakeyaml'
relocate 'com.github.benmanes.caffeine', 'dev.brighten.antivpn.shaded.com.github.benmanes.caffeine'
relocate 'org.h2', 'dev.brighten.antivpn.shaded.org.h2'
relocate 'org.bson', 'dev.brighten.antivpn.shaded.org.bson'
relocate 'com.mongodb', 'dev.brighten.antivpn.shaded.com.mongodb'
relocate 'com.mysql.cj', 'dev.brighten.antivpn.shaded.com.mysql.cj'
relocate 'com.mysql.jdbc', 'dev.brighten.antivpn.shaded.com.mysql.jdbc'
dependencies {
exclude 'dev/brighten/antivpn/depends/Relocate*'
exclude 'dev/brighten/antivpn/depends/MavenLibraries*'
}
}
tasks.build.dependsOn shadowJar
@@ -1,3 +1,19 @@
/*
* 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;
@@ -11,6 +27,7 @@ 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.depends.Relocate;
import dev.brighten.antivpn.message.MessageHandler;
import dev.brighten.antivpn.utils.ConfigDefault;
import dev.brighten.antivpn.utils.MiscUtils;
@@ -31,9 +48,25 @@ import java.util.List;
@Getter
@Setter(AccessLevel.PRIVATE)
@MavenLibrary(groupId = "com.h2database", artifactId ="h2", version = "2.1.214")
@MavenLibrary(groupId = "org.mongodb", artifactId = "mongo-java-driver", version = "3.12.11")
@MavenLibrary(groupId = "mysql", artifactId = "mysql-connector-java", version = "8.0.30")
@MavenLibrary(groupId = "com.h2database", artifactId ="h2", version = "2.2.220", relocations = {
@Relocate(from ="org" + ".\\h2", to ="dev.brighten.antivpn.shaded.org.h2")})
@MavenLibrary(groupId = "org.mongodb", artifactId = "mongo-java-driver", version = "3.12.14", relocations = {
@Relocate(from = "com." + "\\mongodb", to = "dev.brighten.antivpn.shaded.com.mongodb"),
@Relocate(from = "org" + "\\.bson", to = "dev.brighten.antivpn.shaded.org.bson")
})
@MavenLibrary(
groupId = "com.mysql",
artifactId = "mysql-connector-j",
version = "9.3.0",
relocations = {
@Relocate(from = "com.my\\" + "sql.cj", to = "dev.brighten.antivpn.shaded.com.mysql.cj"),
@Relocate(from = "com.my\\" + "sql.jdbc", to = "dev.brighten.antivpn.shaded.com.mysql.jdbc"),
}
)
@MavenLibrary(groupId = "com.\\github\\.ben-manes\\.caffeine", artifactId = "caffeine", version = "3.1.8",
relocations = {
@Relocate(from = "com\\.github\\.benmanes\\.caffeine", to = "dev.brighten.antivpn.shaded.com.github.benmanes.caffeine"),
})
public class AntiVPN {
private static AntiVPN INSTANCE;
@@ -61,13 +94,17 @@ public class AntiVPN {
try {
File configFile = new File(pluginFolder, "config.yml");
if(!configFile.exists()){
configFile.getParentFile().mkdirs();
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) {
e.printStackTrace();
AntiVPN.getInstance().getExecutor().logException("Could not load config.yml, plugin disabling...", e);
executor.disablePlugin();
return;
}
INSTANCE.vpnConfig = new VPNConfig();
@@ -77,34 +114,40 @@ public class AntiVPN {
INSTANCE.messageHandler = new MessageHandler();
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;
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
@@ -124,6 +167,9 @@ public class AntiVPN {
(vpnString.getDefaultMessage(), "messages." + vpnString.getKey(), AntiVPN.getInstance())
.get());
AntiVPN.getInstance().getMessageHandler().reloadStrings();
// Starting kick checks
AntiVPN.getInstance().getExecutor().startKickChecks();
}
public InputStream getResource(String filename) {
@@ -146,8 +192,26 @@ public class AntiVPN {
}
public void stop() {
executor.shutdown();
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() {
@@ -195,7 +259,7 @@ public class AntiVPN {
ConfigurationProvider.getProvider(YamlConfiguration.class)
.save(getConfig(), new File(pluginFolder.getPath() + File.separator + "config.yml"));
} catch (IOException e) {
e.printStackTrace();
AntiVPN.getInstance().getExecutor().logException(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.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 lombok.Getter;
import lombok.Setter;
import java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Level;
@Getter
public abstract class APIPlayer {
private final UUID uuid;
private final String name;
private final InetAddress ip;
@Setter
private boolean alertsEnabled;
private 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();
}
})
);
}
public void checkPlayer(Consumer<CheckResult> onResult) {
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)) {
onResult.accept(new CheckResult(null, ResultType.WHITELISTED, false));
return;
}
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);
}
onResult.accept(cachedResult);
return;
}
}
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");
onResult.accept(new CheckResult(null, ResultType.API_FAILURE, false));
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);
}
onResult.accept(checkResult);
AntiVPN.getInstance().checked++;
});
onResult.accept(new CheckResult(null, ResultType.UNKNOWN, false));
}
}
@@ -0,0 +1,22 @@
/*
* 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,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.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,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.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;
}
}
@@ -1,7 +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.api;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.utils.ConfigDefault;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Collections;
@@ -52,61 +69,60 @@ public class VPNConfig {
defCountrylist = new ConfigDefault<>(new ArrayList<>(), "countries.list",
AntiVPN.getInstance());
private String license, kickMessage, databaseType, databaseName, mongoURL, username, password, ip, alertMsg,
countryVanillaKickReason;
private List<String> prefixWhitelists, commands, countryList, countryKickCommands;
@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, databaseEnabled, useCredentials, commandsEnabled, kickPlayers, alertToStaff,
metrics, whitelistCountries;
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;
/**
* License from https://funkemunky.cc/shop to be used for more queries.
* @return String
*/
public String getLicense() {
return license;
}
/**
* If true, results will be cached to reduce queries to https://funkemunky.cc
* If true, results will be cached to reduce queries to <a href="https://funkemunky.cc">...</a>
* @return boolean
*/
public boolean cachedResults() {
return cacheResults;
}
/**
* Will be used for vanilla kick message when {@link VPNConfig#runCommands()} is true.
* @return String
*/
public String getKickString() {
return kickMessage;
}
/**
* Message to send staff on proxy detection.
* @return String
*/
public String alertMessage() {
return alertMsg;
}
/**
* If true, staff will be alerted on proxy detection.
* @return boolean
*/
public boolean alertToStaff() {
public boolean isAlertToSTaff() {
return alertToStaff;
}
/**
* If true, will run {@link VPNConfig#commands()} on detect. If not, it will use vanilla kicking methods.
* @return boolean
*/
public boolean runCommands() {
return commandsEnabled;
}
/**
* Commands to run on proxy detection.
* @return List
@@ -115,31 +131,6 @@ public class VPNConfig {
return commands;
}
/**
* If false, no commands nor kick will be run on proxy detection.
* @return boolean
*/
public boolean kickPlayersOnDetect() {
return kickPlayers;
}
/**
* Returns Strings of which are checked against the beginning of player names. Used to
* allow Geyser-connected players to join.
* @return List
*/
public List<String> getPrefixWhitelists() {
return prefixWhitelists;
}
/**
* Returns true if we want to use a database
* @return boolean
*/
public boolean isDatabaseEnabled() {
return databaseEnabled;
}
/**
* Whether or not the database we want to connect to requires credentials.
* @return boolean
@@ -156,59 +147,11 @@ public class VPNConfig {
return mongoURL;
}
/**
* Database type. Either MySQL and Mongo.
* @return String
*/
public String getDatabaseType() {
return databaseType;
}
/**
* Database name
* @return String
*/
public String getDatabaseName() {
return databaseName;
}
/**
* Database username
* @return String
*/
public String getUsername() {
return username;
}
/**
* Database Password
* @return String
*/
public String getPassword() {
return password;
}
/**
* Database IP
* @return String
*/
public String getIp() {
return ip;
}
/**
* Returns the list of ISO country codes we need to check.
* @return List
*/
public List<String> countryList() {
return countryList;
}
/**
* If true, we only allow the {@link VPNConfig#countryKickCommands()}. If false, we blacklist them.
* @return boolean
*/
public boolean whitelistCountries() {
public boolean getWhitelistCountries() {
return whitelistCountries;
}
@@ -220,14 +163,6 @@ public class VPNConfig {
return countryKickCommands;
}
/**
* Returns the vanilla kick reason for bad country locations
* @return String
*/
public String countryVanillaKickReason() {
return countryVanillaKickReason;
}
/**
* Gets the port based on configuration. If {@link VPNConfig#port} is -1, will get default port
* based on {@link VPNConfig#getDatabaseType()} lowerCase().
@@ -251,7 +186,7 @@ public class VPNConfig {
/**
* If true, https://bstats.org metrics will be collected to improve KauriVPN.
* If true, <a href="https://bstats.org">...</a> metrics will be collected to improve KauriVPN.
* @return boolean
*/
public boolean metrics() {
@@ -287,4 +222,4 @@ public class VPNConfig {
countryVanillaKickReason = defaultCountryKickReason.get();
}
}
}
@@ -0,0 +1,190 @@
/*
* 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 lombok.Getter;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.Level;
@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, 1, TimeUnit.SECONDS);
if(scheduleResult.isCancelled()) {
runCommands.run();
}
//Ensuring players are actually kicked as they are supposed to be.
toKick.add(new Tuple<>(result, player.getUuid()));
}
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,30 @@
/*
* 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();
}
@@ -1,3 +1,19 @@
/*
* 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;
@@ -0,0 +1,334 @@
/*
* 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) {
page = 1;
}
}
boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled();
List<UUID> uuids = databaseEnabled
? AntiVPN.getInstance().getDatabase().getAllWhitelisted()
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelisted());
List<CIDRUtils> ips = databaseEnabled
? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps()
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps());
List<String> entries = new ArrayList<>();
for (UUID uuid : uuids) {
entries.add("&7- &fUUID: &e" + uuid);
}
for (CIDRUtils cidr : ips) {
entries.add("&7- &fIP: &e" + cidr.getCidr());
}
return buildPage(entries, page, null, "show");
}
if(args[0].equalsIgnoreCase("search")) {
// args[1..n-1] = query terms; args[n] = optional page number if last arg is an integer
if (args.length < 2) {
return "&cUsage: /antivpn allowlist search <query> [page]";
}
// Detect optional trailing page number
int page = 1;
int queryEnd = args.length;
try {
int candidate = Integer.parseInt(args[args.length - 1]);
if (candidate >= 1 && args.length > 2) {
page = candidate;
queryEnd = args.length - 1;
}
} catch (NumberFormatException ignored) {}
String search = String.join(" ", Arrays.copyOfRange(args, 1, queryEnd)).toLowerCase();
// Strip color code characters to prevent formatting injection in output
String safeSearch = search.replace("&", "");
if (safeSearch.isEmpty()) {
return "&cUsage: /antivpn allowlist search <query> [page]";
}
boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled();
List<UUID> uuids = databaseEnabled
? AntiVPN.getInstance().getDatabase().getAllWhitelisted()
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelisted());
List<CIDRUtils> ips = databaseEnabled
? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps()
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps());
List<String> entries = new ArrayList<>();
for (UUID uuid : uuids) {
String entry = uuid.toString();
if (entry.toLowerCase().contains(search)) {
entries.add("&7- &fUUID: &e" + entry);
}
}
for (CIDRUtils cidr : ips) {
String entry = cidr.getCidr();
if (entry.toLowerCase().contains(search)) {
entries.add("&7- &fIP: &e" + entry);
}
}
return buildPage(entries, page, safeSearch, "search " + safeSearch);
}
if(args.length == 1)
return "&cYou have to provide a player to allow or deny exemption.";
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.isEmpty()) {
return "&cThe player \"" + args[1] + "\" is not online, so please provide a UUID.";
}
uuid = player.get().getUuid();
}
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);
}
}
@@ -1,3 +1,19 @@
/*
* 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;
@@ -1,3 +1,19 @@
/*
* 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;
@@ -46,8 +62,7 @@ public class ClearCacheCommand extends Command {
@Override
public String execute(CommandExecutor executor, String[] args) {
AntiVPN.getInstance().getDatabase().clearResponses();
VPNExecutor.responseCache.clear();
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> AntiVPN.getInstance().getDatabase().clearResponses());
return "&aCleared all cached API response information!";
}
@@ -1,3 +1,19 @@
/*
* 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;
@@ -55,28 +71,31 @@ public class LookupCommand extends Command {
Optional<APIPlayer> player = AntiVPN.getInstance().getPlayerExecutor().getPlayer(args[0]);
if(!player.isPresent()) {
if(player.isEmpty()) {
return String.format("&cNo player found with the name \"%s\"", args[0]);
}
AntiVPN.getInstance().getExecutor().checkIp(player.get().getIp().getHostAddress(),
false, result -> {
if(!result.isSuccess()) {
executor.sendMessage("&cThere was an error trying to find the information of this player.");
} else {
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"));
}
});
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() + "...";
@@ -1,3 +1,19 @@
/*
* 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;
@@ -10,6 +26,7 @@ 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 {
@@ -50,10 +67,10 @@ public class PlanCommand extends Command {
@Override
public String execute(CommandExecutor executor, String[] args) {
VPNExecutor.threadExecutor.execute(() -> {
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> {
QueryResponse result;
try {
if(AntiVPN.getInstance().getVpnConfig().getLicense().equals("")) {
if(AntiVPN.getInstance().getVpnConfig().getLicense().isEmpty()) {
result = FunkemunkyAPI.getQueryResponse();
} else {
result = FunkemunkyAPI.getQueryResponse(AntiVPN.getInstance().getVpnConfig().getLicense());
@@ -81,11 +98,11 @@ public class PlanCommand extends Command {
result.getQueries(), queryMax);
executor.sendMessage(StringUtil.line("&8"));
} catch(JSONException e) {
e.printStackTrace();
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) {
e.printStackTrace();
AntiVPN.getInstance().getExecutor().logException(e);
executor.sendMessage("&cThere was a IOException thrown while looking up your query " +
"information. Check console for more details.");
}
@@ -95,6 +112,6 @@ public class PlanCommand extends Command {
@Override
public List<String> tabComplete(CommandExecutor executor, String alias, String[] args) {
return null;
return Collections.emptyList();
}
}
@@ -1,13 +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.command.impl;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor;
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.message.VpnString;
import java.util.Collections;
import java.util.List;
@@ -58,9 +69,6 @@ public class ReloadCommand extends Command {
AntiVPN.getInstance().getMessageHandler().reloadStrings();
// Clearing the local response cache
VPNExecutor.responseCache.clear();
AntiVPN.getInstance().reloadDatabase();
return AntiVPN.getInstance().getMessageHandler().getString("command-reload-complete").getMessage();
@@ -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,61 @@
/*
* 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,380 @@
/*
* 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 lombok.SneakyThrows;
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;
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,130 @@
/*
* 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,158 @@
/*
* 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,115 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.local.version;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.DatabaseException;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.sql.utils.Query;
import dev.brighten.antivpn.database.version.Version;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.utils.MiscUtils;
import java.math.BigInteger;
import java.net.UnknownHostException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
public class Third implements Version<VPNDatabase> {
@Override
public void update(VPNDatabase database) throws DatabaseException {
List<CIDRUtils> ipRanges = new ArrayList<>();
List<CIDRUtils> rangesToInsert = new ArrayList<>();
List<BigInteger[]> rangesToRemove = new ArrayList<>();
try (var preparedQuery = Query.prepare("select ip_start, ip_end from `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,280 @@
/*
* 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.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
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 org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.Decimal128;
import java.math.BigDecimal;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
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,84 @@
/*
* 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 org.bson.Document;
import org.bson.types.Decimal128;
import java.math.BigDecimal;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
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,108 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.mongo.version;
import com.mongodb.client.model.Filters;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.DatabaseException;
import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.version.Version;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.utils.MiscUtils;
import org.bson.Document;
import org.bson.types.Decimal128;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.logging.Level;
public class MongoThird implements Version<MongoVPN> {
@Override
public void update(MongoVPN database) throws DatabaseException {
List<CIDRUtils> ipRanges = new ArrayList<>();
List<CIDRUtils> rangesToInsert = new ArrayList<>();
List<BigInteger[]> rangesToRemove = new ArrayList<>();
database.settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"), Filters.exists("cidr_string")))
.forEach((Consumer<? super Document>) doc -> {
BigInteger start = doc.get("ip_start", Decimal128.class).bigDecimalValue().toBigInteger();
BigInteger end = doc.get("ip_end", Decimal128.class).bigDecimalValue().toBigInteger();
try {
var range = MiscUtils.rangeToCidrs(start, end);
if(range.size() > 1) {
rangesToRemove.add(new BigInteger[]{start, end});
rangesToInsert.addAll(range);
AntiVPN.getInstance().getExecutor().log(Level.WARNING, "Found multiple CIDR ranges for whitelist range for %s, %s!", start, end);
} else ipRanges.addAll(range);
} catch (UnknownHostException e) {
AntiVPN.getInstance().getExecutor().logException(
String.format("Could not convert ip range to CIDR! %s, %s", start, end), e);
}
});
if(!rangesToInsert.isEmpty()) {
AntiVPN.getInstance().getExecutor().log("Inserting %s new ranges into database...", rangesToInsert.size());
var documentsToInsert = rangesToInsert.stream().map(cidr -> {
Document doc = new Document("setting", "whitelist");
doc.append("ip_start", new Decimal128(new BigDecimal(cidr.getStartIpInt())));
doc.append("ip_end", new Decimal128(new BigDecimal(cidr.getEndIpInt())));
doc.append("cidr_string", cidr.getCidr());
return doc;
}).toList();
database.settingsDocument.insertMany(documentsToInsert);
}
if(!rangesToRemove.isEmpty()) {
AntiVPN.getInstance().getExecutor().log("Removing %s old ranges from database...", rangesToRemove.size());
rangesToRemove.forEach(range -> database.settingsDocument
.deleteMany(Filters.and(
Filters.gte("ip_start", new Decimal128(new BigDecimal(range[0]))),
Filters.lte("ip_end", new Decimal128(new BigDecimal(range[1]))))));
}
if(!ipRanges.isEmpty()) {
AntiVPN.getInstance().getExecutor().log("Updating %s CIDRs in database with proper notation...", ipRanges.size());
ipRanges.forEach(cidr -> database.settingsDocument
.updateMany(Filters.and(Filters.eq("setting", "whitelist"),
Filters.eq("ip_start", new Decimal128(new BigDecimal(cidr.getStartIpInt()))),
Filters.eq("ip_end", new Decimal128(new BigDecimal(cidr.getEndIpInt())))),
new Document("$set", new Document("cidr_string", cidr.getCidr()))));
}
var versionCollect = database.antivpnDatabase.getCollection("version");
versionCollect.insertOne(new Document("version", versionNumber()));
}
@Override
public int versionNumber() {
return 2;
}
@Override
public boolean needsUpdate(MongoVPN database) {
var versionCollect = database.antivpnDatabase.getCollection("version");
return versionCollect.find(Filters.eq("version", versionNumber())).first() == null;
}
}
@@ -0,0 +1,64 @@
/*
* 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);
}
}
}
@@ -1,53 +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.database.sql.utils;
import dev.brighten.antivpn.utils.MiscUtils;
import lombok.Getter;
import lombok.SneakyThrows;
import java.sql.*;
import java.util.UUID;
public class ExecutableStatement {
private PreparedStatement statement;
public class ExecutableStatement implements AutoCloseable {
@Getter
private final PreparedStatement statement;
private int pos = 1;
public ExecutableStatement(PreparedStatement statement) {
this.statement = statement;
}
@SneakyThrows
public Integer execute() {
try {
return statement.executeUpdate();
} finally {
MiscUtils.close(statement);
}
public int execute() throws SQLException {
return statement.executeUpdate();
}
@SneakyThrows
public void execute(ResultSetIterator iterator) {
ResultSet rs = null;
try {
rs = statement.executeQuery();
public void execute(ResultSetIterator iterator) throws SQLException {
try(var rs = statement.executeQuery()) {
while (rs.next()) iterator.next(rs);
} finally {
MiscUtils.close(statement, rs);
}
}
@SneakyThrows
public void executeSingle(ResultSetIterator iterator) {
ResultSet rs = null;
try {
rs = statement.executeQuery();
if (rs.next()) iterator.next(rs);
else iterator.next(null);
} finally {
MiscUtils.close(statement, rs);
}
public int[] executeBatch() throws SQLException {
return statement.executeBatch();
}
@SneakyThrows
public ResultSet executeQuery() {
public ResultSet executeQuery() throws SQLException {
return statement.executeQuery();
}
@@ -135,4 +133,15 @@ public class ExecutableStatement {
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,195 @@
/*
* 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 org.h2.jdbc.JdbcSQLFeatureNotSupportedException;
import org.h2.jdbc.JdbcSQLNonTransientConnectionException;
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;
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;
}
}
}
@@ -1,26 +1,17 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
* Copyright 2026 Dawson Hessler
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
* 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
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* http://www.apache.org/licenses/LICENSE-2.0
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* 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;
@@ -0,0 +1,39 @@
/*
* 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 lombok.Getter;
import org.intellij.lang.annotations.Language;
import java.sql.Connection;
import java.sql.SQLException;
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,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.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,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.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,864 @@
/*
* 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 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;
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;
/**
* 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,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.*;
/**
* 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,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.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,30 @@
/*
* 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,36 @@
/*
* 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();
}
@@ -1,26 +1,17 @@
/*
* This file is part of helper, licensed under the MIT License.
* Copyright 2026 Dawson Hessler
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
* 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
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* http://www.apache.org/licenses/LICENSE-2.0
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* 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;
@@ -104,7 +95,7 @@ public abstract class URLClassLoaderAccess {
/**
* Accesses using sun.misc.Unsafe, supported on Java 9+.
*
* @author Vaishnav Anil (https://github.com/slimjar/slimjar)
* @author Vaishnav Anil (<a href="https://github.com/slimjar/slimjar">...</a>)
*/
private static class Unsafe extends URLClassLoaderAccess {
private static final sun.misc.Unsafe UNSAFE;
@@ -1,3 +1,19 @@
/*
* 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;
@@ -1,3 +1,19 @@
/*
* 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;
@@ -0,0 +1,131 @@
/*
* 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 lombok.Getter;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* 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);
}
}
@@ -1,3 +1,19 @@
/*
* 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;
@@ -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.utils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.LinkedHashMap;
import java.util.Map;
@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,109 @@
/*
* 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,150 @@
/*
* 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.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ThreadFactory;
import java.util.regex.Pattern;
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}");
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, 4); // use 16 for IPv6
String cidr = InetAddress.getByAddress(addrBytes).getHostAddress() + "/" + prefixLen;
cidrs.add(new CIDRUtils(cidr));
// Advance past this block
start = start.add(BigInteger.ONE.shiftLeft(blockBits));
}
return cidrs;
}
private static byte[] toFixedLengthBytes(BigInteger value, int length) {
byte[] raw = value.toByteArray();
byte[] result = new byte[length];
int srcPos = Math.max(0, raw.length - length);
int destPos = Math.max(0, length - raw.length);
System.arraycopy(raw, srcPos, result, destPos, Math.min(raw.length, length));
return result;
}
public static UUID lookupUUID(String playername) {
try {
JSONObject object = JsonReader
.readJsonFromUrl("https://funkemunky.cc/mojang/uuid?name=" + playername);
if(object.has("uuid")) {
return UUID.fromString(object.getString("uuid"));
}
} catch (IOException | JSONException 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 lookupMojangUuid(String playerName) {
try {
JSONObject object = JsonReader.readJsonFromUrl("https://api.mojang.com/users/profiles/minecraft/" + playerName);
if(object.has("id")) {
return UUID.fromString(object.getString("id"));
}
} catch (IOException | JSONException e) {
AntiVPN.getInstance().getExecutor().logException("Error while looking up UUID for " + playerName + " from Mojang!:", e);
}
return null;
}
public static boolean isIpv4(String ip)
{
return ipv4.matcher(ip).matches();
}
}
@@ -0,0 +1,27 @@
/*
* 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 {
}
@@ -1,15 +1,17 @@
/*
* Copyright (C) 2021 The Guava Authors
* 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
* 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
* 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.
* 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;
@@ -45,7 +47,7 @@ final class NullnessCasts {
* value may be legitimately {@code null}.)
*/
@SuppressWarnings("nullness")
static <T extends Object> T uncheckedCastNullableTToT(T t) {
static <T> T uncheckedCastNullableTToT(T t) {
return t;
}
@@ -0,0 +1,260 @@
/*
* 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.
*/
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package dev.brighten.antivpn.utils;
public final class Preconditions {
private Preconditions() {
}
public static <T> T checkNotNull(T reference) {
if (reference == null) {
throw new NullPointerException();
} else {
return reference;
}
}
public static <T> T checkNotNull(T reference, Object errorMessage) {
if (reference == null) {
throw new NullPointerException(String.valueOf(errorMessage));
} else {
return reference;
}
}
public static <T> T checkNotNull(T reference, String errorMessageTemplate, Object... errorMessageArgs) {
if (reference == null) {
throw new NullPointerException(format(errorMessageTemplate, errorMessageArgs));
} else {
return reference;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, char p1) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, int p1) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, long p1) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, Object p1) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, char p1, char p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, char p1, int p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, char p1, long p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, char p1, Object p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, int p1, char p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, int p1, int p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, int p1, long p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, int p1, Object p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, long p1, char p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, long p1, int p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, long p1, long p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, long p1, Object p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, Object p1, char p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, Object p1, int p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, Object p1, long p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, Object p1, Object p2) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, Object p1, Object p2, Object p3) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2, p3));
} else {
return obj;
}
}
public static <T> T checkNotNull(T obj, String errorMessageTemplate, Object p1, Object p2, Object p3, Object p4) {
if (obj == null) {
throw new NullPointerException(format(errorMessageTemplate, p1, p2, p3, p4));
} else {
return obj;
}
}
static String format(String template, Object... args) {
template = String.valueOf(template);
StringBuilder builder = new StringBuilder(template.length() + 16 * args.length);
int templateStart = 0;
int i;
int placeholderStart;
for(i = 0; i < args.length; templateStart = placeholderStart + 2) {
placeholderStart = template.indexOf("%s", templateStart);
if (placeholderStart == -1) {
break;
}
builder.append(template, templateStart, placeholderStart);
builder.append(args[i++]);
}
builder.append(template, templateStart, template.length());
if (i < args.length) {
builder.append(" [");
builder.append(args[i++]);
while(i < args.length) {
builder.append(", ");
builder.append(args[i++]);
}
builder.append(']');
}
return builder.toString();
}
}
@@ -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.utils;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.web.objects.VPNResponse;
public class StringUtil {
public static String line(String color) {
return color + "&m-----------------------------------------------------";
}
public static String line() {
return "&m-----------------------------------------------------";
}
public static String varReplace(String input, APIPlayer player, VPNResponse result) {
return translateAlternateColorCodes('&', input.replace("%player%", player.getName())
.replace("%reason%", result.getMethod())
.replace("%country%", result.getCountryName())
.replace("%city%", result.getCity()));
}
public static String translateAlternateColorCodes(char altColorChar, String textToTranslate) {
char[] b = textToTranslate.toCharArray();
for(int i = 0; i < b.length - 1; ++i) {
if (b[i] == altColorChar && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(b[i + 1]) > -1) {
b[i] = 167;
b[i + 1] = Character.toLowerCase(b[i + 1]);
}
}
return new String(b);
}
}
@@ -0,0 +1,27 @@
/*
* 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.
*/
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package dev.brighten.antivpn.utils;
@FunctionalInterface
public interface Supplier<T> extends java.util.function.Supplier<T> {
T get();
}
@@ -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.utils;
import java.io.Serializable;
import static dev.brighten.antivpn.utils.NullnessCasts.uncheckedCastNullableTToT;
import static dev.brighten.antivpn.utils.Preconditions.checkNotNull;
import static java.util.Objects.requireNonNull;
/**
* Useful suppliers.
*
* <p>All methods return serializable suppliers as long as they're given serializable parameters.
*
* @author Laurence Gonsalves
* @author Harry Heymann
* @since 2.0
*/
public final class Suppliers {
private Suppliers() {}
/**
* Returns a supplier which caches the instance retrieved during the first call to {@code get()}
* and returns that value on subsequent calls to {@code get()}. See: <a
* href="http://en.wikipedia.org/wiki/Memoization">memoization</a>
*
* <p>The returned supplier is thread-safe. The delegate's {@code get()} method will be invoked at
* most once unless the underlying {@code get()} throws an exception. The supplier's serialized
* form does not contain the cached value, which will be recalculated when {@code get()} is called
* on the reserialized instance.
*
* <p>When the underlying delegate throws an exception then this memoizing supplier will keep
* delegating calls until it returns valid data.
*
* <p>If {@code delegate} is an instance created by an earlier call to {@code memoize}, it is
* returned directly.
*/
public static <T> Supplier<T> memoize(Supplier<T> delegate) {
if (delegate instanceof NonSerializableMemoizingSupplier
|| delegate instanceof MemoizingSupplier) {
return delegate;
}
return delegate instanceof Serializable
? new MemoizingSupplier<>(delegate)
: new NonSerializableMemoizingSupplier<>(delegate);
}
static class MemoizingSupplier<T> implements Supplier<T>, Serializable {
final Supplier<T> delegate;
transient volatile boolean initialized;
// "value" does not need to be volatile; visibility piggy-backs
// on volatile read of "initialized".
transient T value;
MemoizingSupplier(Supplier<T> delegate) {
this.delegate = checkNotNull(delegate);
}
@Override
public T get() {
// A 2-field variant of Double Checked Locking.
if (!initialized) {
synchronized (this) {
if (!initialized) {
T t = delegate.get();
value = t;
initialized = true;
return t;
}
}
}
// This is safe because we checked `initialized.`
return uncheckedCastNullableTToT(value);
}
@Override
public String toString() {
return "Suppliers.memoize("
+ (initialized ? "<supplier that returned " + value + ">" : delegate)
+ ")";
}
private static final long serialVersionUID = 0;
}
static class NonSerializableMemoizingSupplier<T> implements Supplier<T> {
volatile Supplier<T> delegate;
volatile boolean initialized;
// "value" does not need to be volatile; visibility piggy-backs
// on volatile read of "initialized".
T value;
NonSerializableMemoizingSupplier(Supplier<T> delegate) {
this.delegate = checkNotNull(delegate);
}
@Override
public T get() {
// A 2-field variant of Double Checked Locking.
if (!initialized) {
synchronized (this) {
if (!initialized) {
/*
* requireNonNull is safe because we read and write `delegate` under synchronization.
*
* TODO(cpovirk): To avoid having to check for null, replace `delegate` with a singleton
* `Supplier` that always throws an exception.
*/
T t = requireNonNull(delegate).get();
value = t;
initialized = true;
// Release the delegate to GC.
delegate = null;
return t;
}
}
}
// This is safe because we checked `initialized.`
return uncheckedCastNullableTToT(value);
}
@Override
public String toString() {
Supplier<T> delegate = this.delegate;
return "Suppliers.memoize("
+ (delegate == null ? "<supplier that returned " + value + ">" : delegate)
+ ")";
}
}
}
@@ -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.utils;
public record Tuple<F, S>(F first, S second) {
}
@@ -1,3 +1,19 @@
/*
* 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.config;
import java.util.*;
@@ -1,3 +1,19 @@
/*
* 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.config;
import java.io.*;

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