Compare commits

...

106 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] dd7dfd8502 fix: use multiline heredoc output to avoid URL-encoded newlines in release body
Agent-Logs-Url: https://github.com/funkemunky/AntiVPN/sessions/23c299d0-3c90-4eaa-9716-59c61aeea471

Co-authored-by: funkemunky <30784509+funkemunky@users.noreply.github.com>
2026-04-29 17:28:07 +00:00
funkemunky 00449f0006 Updating changelog 2026-04-29 13:21:28 -04:00
funkemunky 4ac0e57f5d Fixing build issues causing startup problems on velocity 2026-04-29 12:53:56 -04:00
Copilot 980d0c4af8 Add 1.10.1 entry to CHANGELOG.md (#86)
* Add 1.10.1 entry to CHANGELOG.md for 04/28/2026 release

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

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

* Update CHANGELOG.md

---------

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

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

* Updating gradle piplines and files

* correcting startup bug as a result of a packaging issue

* Fixing asynchronous run command

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

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

* Fixing MySQL issues

* Adding central test suite

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

* Updating workflows for github

* Attempting to fix workflows

* Fixing maven central proxy

* Fixing missing dependency in velocity

* Fixing build

* Getting rid of universal submodule

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

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

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

This reverts commit 3e4d8be955.

* replacing instances of gradlew

* Adding gradlew wrapper

* Attempting to fix gradle issues

* Attempting to fix gradle issues

* Adding back ssl args

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

* Removing guava, using caffiene instead

* Merge cleanup

* Maybe this will get caches working properly now?

* Refactored to be more clean and reliable

* Fixing bungee compile

---------

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

* Removing from PlayerLoginEvent and doing PlayerJoinEvent only instead.

---------

Co-authored-by: Dawson <dawson@funkemunky.cc>
2025-04-22 09:58:46 -04:00
funkemunky ac628811cc Merge branch 'master' into bugfix/bukkit-kick 2025-04-22 09:55:08 -04:00
Dawson 3aae8d8f49 Backing up and resetting database on versioning compatibility (#61)
Co-authored-by: Dawson <dawson@funkemunky.cc>
2025-04-22 09:54:43 -04:00
funkemunky f0c37c6ff0 Merge branch 'master' into bugfix/bukkit-kick
# Conflicts:
#	Assembly/pom.xml
#	Bukkit/pom.xml
#	Bungee/pom.xml
#	Common/pom.xml
#	Sponge/pom.xml
#	Velocity/pom.xml
#	pom.xml
2025-04-22 09:53:47 -04:00
funkemunky 311f1e198b Backing up and resetting database on versioning compatibility 2025-04-21 13:20:13 -04:00
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
182 changed files with 9400 additions and 3352 deletions
+59
View File
@@ -0,0 +1,59 @@
name: create-release.yml
on:
workflow_dispatch:
jobs:
build-and-release:
name: Build and Release
runs-on: ubuntu-latest
env:
JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStoreType=JKS -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit
steps:
- uses: actions/checkout@v5
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'zulu'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '9.4.1'
- name: Build
run: gradle build --no-daemon
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version Number from Gradle
id: get_version
run: |
VERSION=$(gradle properties -q | awk -F': ' '/^version:/ {print $2; exit}')
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Extract latest CHANGELOG entry
id: changelog
run: |
CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
echo "Extracted latest release notes from CHANGELOG.md:"
echo -e "$CHANGELOG_CONTENT"
{
echo "content<<EOF"
echo "$CHANGELOG_CONTENT"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Create Release
uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ env.VERSION }}
release_name: Release v${{ env.VERSION }}
draft: false
prerelease: false
body: ${{ steps.changelog.outputs.content }}
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./build/libs/AntiVPN-${{ env.VERSION }}-universal.jar
asset_name: AntiVPN-v${{ env.VERSION }}.jar
asset_content_type: application/java-archive
+34
View File
@@ -0,0 +1,34 @@
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
env:
JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStoreType=JKS -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit
steps:
- uses: actions/checkout@v5
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'zulu'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '9.4.1'
- name: Build
run: gradle build -x test --no-daemon
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload AntiVPN
uses: actions/upload-artifact@v4
with:
name: AntiVPN-Universal
path: build/libs/AntiVPN-*-universal.jar
-28
View File
@@ -1,28 +0,0 @@
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17.0.2
uses: actions/setup-java@v4
with:
java-version: 17.0
distribution: 'zulu'
cache: 'maven'
- name: Compile
run: mvn -B package --file pom.xml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload AntiVPN
uses: actions/upload-artifact@v4
with:
name: AntiVPN
path: Assembly/target/Assembly-*.jar
+50
View File
@@ -0,0 +1,50 @@
on:
push:
jobs:
build:
runs-on: ubuntu-latest
env:
JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStoreType=JKS -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit
steps:
- uses: actions/checkout@v5
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'zulu'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '9.4.1'
- name: Build
run: gradle build -x test --no-daemon
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload AntiVPN
uses: actions/upload-artifact@v4
with:
name: AntiVPN-Universal
path: build/libs/AntiVPN-*-universal.jar
build-and-test:
runs-on: ubuntu-latest
env:
JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStoreType=JKS -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit
steps:
- uses: actions/checkout@v5
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'zulu'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '9.4.1'
- name: Build and Test
run: gradle build --no-daemon
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3
View File
@@ -188,6 +188,7 @@ fabric.properties
# Package Files #
*.jar
!gradle/wrapper/gradle-wrapper.jar
*.war
*.nar
*.ear
@@ -293,3 +294,5 @@ $RECYCLE.BIN/
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,maven,java,intellij,eclipse,netbeans
/.gradle/
.grade/**
-76
View File
@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>AntiVPN</artifactId>
<groupId>dev.brighten.antivpn</groupId>
<version>1.9.3.1</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.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.yaml.snakeyaml</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.org.yaml.snakeyaml</shadedPattern>
</relocation>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.google.common</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Bungee</artifactId>
<version>${version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Velocity</artifactId>
<version>${version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Common</artifactId>
<version>${version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Bukkit</artifactId>
<version>${version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
+27
View File
@@ -0,0 +1,27 @@
plugins {
id 'com.gradleup.shadow'
}
evaluationDependsOn(':Bukkit:Plugin')
dependencies {
compileOnly 'org.spigotmc:spigot-api:1.20.2-R0.1-SNAPSHOT'
compileOnly project(':Bukkit:Plugin')
implementation project(':Common:loader-utils')
}
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
+27
View File
@@ -0,0 +1,27 @@
plugins {
id 'com.gradleup.shadow'
}
dependencies {
compileOnly 'org.spigotmc:spigot-api:1.20.2-R0.1-SNAPSHOT'
implementation project(':Common:Source')
implementation project(':Common:loader-utils')
implementation 'org.bstats:bstats-bukkit:2.2.1'
testImplementation 'com.github.seeseemelk:MockBukkit-v1.20:3.84.0'
testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation 'org.mockito:mockito-subclass:5.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'
}
shadowJar {
archiveClassifier.set('')
relocate 'org.bstats', 'dev.brighten.antivpn.bukkit.org.bstats'
}
test {
useJUnitPlatform()
systemProperty 'mockito.mockmaker', 'subclass'
}
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,120 @@
/*
* 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 org.bukkit.scheduler.BukkitRunnable;
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) {
new BukkitRunnable() {
public void run() {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(),
ChatColor.translateAlternateColorCodes('&', command));
}
}.runTask(BukkitPlugin.pluginInstance.getPlugin());
}
@Override
public void disablePlugin() {
HandlerList.unregisterAll(this);
Bukkit.getPluginManager().disablePlugin(BukkitPlugin.pluginInstance.getPlugin());
}
@EventHandler(priority = EventPriority.HIGH)
public void onLogin(final PlayerLoginEvent event) {
APIPlayer player = AntiVPN.getInstance().getPlayerExecutor().getPlayer(event.getPlayer().getUniqueId())
.orElse(new OfflinePlayer(
event.getPlayer().getUniqueId(),
event.getPlayer().getName(),
event.getAddress()
));
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());
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.api.APIPlayer;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
public class BukkitPlayer extends APIPlayer {
private final Player player;
public BukkitPlayer(Player player) {
super(player.getUniqueId(), player.getName(), player.getAddress() != null ? player.getAddress().getAddress() : null);
this.player = player;
}
@Override
public void sendMessage(String message) {
player.sendMessage(ChatColor.translateAlternateColorCodes('&', message));
}
@Override
public void kickPlayer(String reason) {
new BukkitRunnable() {
public void run() {
player.kickPlayer(ChatColor.translateAlternateColorCodes('&', reason));
}
}.runTask(BukkitPlugin.pluginInstance.getPlugin());
}
@Override
public boolean hasPermission(String permission) {
return player.hasPermission(permission);
}
}
@@ -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,11 +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;
@@ -13,22 +34,34 @@ 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;
@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 SingleLineChart vpnDetections, ipsChecked;
@Getter
private PlayerCommandRunner playerCommandRunner;
@Override
public void onLoad(File dataFolder) {
this.dataFolder = dataFolder;
}
public void onEnable() {
pluginInstance = this;
@@ -41,28 +74,24 @@ public class BukkitPlugin extends JavaPlugin {
// 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);
}
}
@@ -75,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
@@ -85,10 +114,11 @@ 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();
@@ -98,19 +128,35 @@ public class BukkitPlugin extends JavaPlugin {
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";
}
}
}
@@ -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.utils.MiscUtils;
@@ -35,7 +51,7 @@ public class PlayerCommandRunner {
public void run() {
action.getAction().run();
}
}.runTask(BukkitPlugin.pluginInstance);
}.runTask(BukkitPlugin.pluginInstance.getPlugin());
playerActions.poll();
}
@@ -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,200 @@
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.bukkit.command.CommandSender;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;
public class BukkitListenerTest {
private ServerMock server;
private BukkitListener listener;
private VPNExecutor vpnExecutor;
@BeforeEach
public void setUp() throws Exception {
server = MockBukkit.mock(new RecordingServerMock());
JavaPlugin plugin = MockBukkit.loadWith(
TestPlugin.class,
new PluginDescriptionFile("AntiVPNTest", "1.0.0", TestPlugin.class.getName())
);
BukkitPlugin.pluginInstance = new BukkitPlugin(plugin);
AntiVPN antiVPN = mock(AntiVPN.class);
VPNConfig config = mock(VPNConfig.class);
PlayerExecutor playerExecutor = mock(PlayerExecutor.class);
vpnExecutor = mock(VPNExecutor.class);
MessageHandler messageHandler = mock(MessageHandler.class);
when(antiVPN.getVpnConfig()).thenReturn(config);
when(antiVPN.getPlayerExecutor()).thenReturn(playerExecutor);
when(antiVPN.getExecutor()).thenReturn(vpnExecutor);
when(antiVPN.getMessageHandler()).thenReturn(messageHandler);
when(playerExecutor.getPlayer(any(UUID.class))).thenReturn(Optional.empty());
when(config.getPrefixWhitelists()).thenReturn(java.util.Collections.emptyList());
when(config.getCountryList()).thenReturn(java.util.Collections.emptyList());
when(config.isKickPlayers()).thenReturn(true);
when(config.getKickMessage()).thenReturn("Blocked!");
VpnString mockVpnString = mock(VpnString.class);
when(mockVpnString.getFormattedMessage(any())).thenReturn("Blocked!");
when(messageHandler.getString(anyString())).thenReturn(mockVpnString);
when(vpnExecutor.checkIp(anyString())).thenReturn(CompletableFuture.completedFuture(
VPNResponse.builder().success(true).proxy(false).ip("127.0.0.1")
.method("N/A").countryName("N/A").city("N/A").build()
));
// Use reflection to set the private static INSTANCE field
Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE");
instanceField.setAccessible(true);
instanceField.set(null, antiVPN);
listener = new BukkitListener();
}
@AfterEach
public void tearDown() throws Exception {
// Reset the singleton
Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE");
instanceField.setAccessible(true);
instanceField.set(null, null);
BukkitPlugin.pluginInstance = null;
MockBukkit.unmock();
}
@Test
public void testLoginEventAllowed() throws Exception {
PlayerMock player = server.addPlayer("TestPlayer");
InetAddress address = InetAddress.getByName("127.0.0.1");
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()));
}
@Test
public void testLoginPipelineProxyPlayerIsKickedWithoutErrors() throws Exception {
PlayerMock player = server.addPlayer("PipelineProxyPlayer");
InetAddress address = InetAddress.getByName("1.1.1.1");
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);
assertDoesNotThrow(() -> listener.onLogin(event));
assertEquals(PlayerLoginEvent.Result.KICK_BANNED, event.getResult());
assertEquals("Blocked!", net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(event.kickMessage()));
}
@Test
public void testRunCommandDispatchesOnPrimaryThreadWhenCalledAsynchronously() {
RecordingServerMock recordingServer = (RecordingServerMock) server;
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
CompletableFuture<Void> asyncCall = CompletableFuture.runAsync(
() -> listener.runCommand("antivpn-test &aok"),
executor
);
assertDoesNotThrow(() -> asyncCall.get(5, TimeUnit.SECONDS));
assertFalse(recordingServer.commandDispatched(), "Command should be scheduled, not dispatched asynchronously");
server.getScheduler().performOneTick();
assertTrue(recordingServer.commandDispatched(), "Scheduled command should be dispatched on the next server tick");
assertTrue(recordingServer.dispatchedOnPrimaryThread(), "Command dispatch must happen on Bukkit's primary thread");
assertEquals("antivpn-test §aok", recordingServer.dispatchedCommand());
} finally {
executor.shutdownNow();
}
}
public static class TestPlugin extends JavaPlugin {
}
private static class RecordingServerMock extends ServerMock {
private final AtomicBoolean commandDispatched = new AtomicBoolean();
private final AtomicBoolean dispatchedOnPrimaryThread = new AtomicBoolean();
private final AtomicReference<String> dispatchedCommand = new AtomicReference<>();
@Override
public boolean dispatchCommand(@NotNull CommandSender sender, @NotNull String commandLine) {
commandDispatched.set(true);
dispatchedOnPrimaryThread.set(isPrimaryThread());
dispatchedCommand.set(commandLine);
return super.dispatchCommand(sender, commandLine);
}
private boolean commandDispatched() {
return commandDispatched.get();
}
private boolean dispatchedOnPrimaryThread() {
return dispatchedOnPrimaryThread.get();
}
private String dispatchedCommand() {
return dispatchedCommand.get();
}
}
}
@@ -0,0 +1,56 @@
package dev.brighten.antivpn.bukkit;
import be.seeseemelk.mockbukkit.MockBukkit;
import be.seeseemelk.mockbukkit.ServerMock;
import be.seeseemelk.mockbukkit.entity.PlayerMock;
import org.bukkit.plugin.java.JavaPlugin;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class BukkitPlayerTest {
private ServerMock server;
@BeforeEach
void setUp() {
server = MockBukkit.mock();
BukkitPlugin pluginBootstrap = mock(BukkitPlugin.class);
JavaPlugin plugin = MockBukkit.createMockPlugin();
when(pluginBootstrap.getPlugin()).thenReturn(plugin);
BukkitPlugin.pluginInstance = pluginBootstrap;
}
@AfterEach
void tearDown() {
BukkitPlugin.pluginInstance = null;
MockBukkit.unmock();
}
@Test
void kickPlayerCalledFromAsyncContext_isScheduledAndExecutedOnMainThread() {
PlayerMock player = server.addPlayer("AsyncKickPlayer");
BukkitPlayer bukkitPlayer = new BukkitPlayer(player);
assertTrue(player.isOnline());
CompletableFuture<Void> asyncKick = CompletableFuture.runAsync(() -> bukkitPlayer.kickPlayer("&cBlocked!"));
assertDoesNotThrow(() -> asyncKick.get(1, TimeUnit.SECONDS));
assertTrue(player.isOnline(), "Kick should be deferred to the server scheduler");
server.getScheduler().performTicks(1);
assertFalse(player.isOnline(), "Player should be kicked when scheduled task runs");
}
}
-91
View File
@@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>AntiVPN</artifactId>
<groupId>dev.brighten.antivpn</groupId>
<version>1.9.3.1</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.13.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.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<!-- Replace this with your package! -->
<shadedPattern>dev.brighten.antivpn.bukkit.org.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>org.yaml.snakeyaml</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.org.yaml.snakeyaml</shadedPattern>
</relocation>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.google.common</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Common</artifactId>
<version>1.9.3.1</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,231 +0,0 @@
package dev.brighten.antivpn.bukkit;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.message.VpnString;
import dev.brighten.antivpn.web.objects.VPNResponse;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitRunnable;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
@SuppressWarnings("unchecked")
public class BukkitListener extends VPNExecutor implements Listener {
private final Cache<UUID, VPNResponse> responseCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(2000)
.build();
@Override
public void registerListeners() {
BukkitPlugin.pluginInstance.getServer().getPluginManager()
.registerEvents(this, BukkitPlugin.pluginInstance);
}
@Override
public void shutdown() {
}
@Override
public void log(Level level, String log, Object... objects) {
Bukkit.getLogger().log(level, String.format(log, objects));
}
@Override
public void log(String log, Object... objects) {
log(Level.INFO, String.format(log, objects));
}
@Override
public void logException(String message, Exception ex) {
Bukkit.getLogger().log(Level.SEVERE, message, ex);
}
@EventHandler
public void onJoin(final PlayerJoinEvent event) {
AntiVPN.getInstance().getPlayerExecutor().getPlayer(event.getPlayer().getUniqueId())
.ifPresent(player -> AntiVPN.getInstance().getDatabase().alertsState(player.getUuid(), enabled -> {
if(enabled) {
player.setAlertsEnabled(true);
player.sendMessage(AntiVPN.getInstance().getMessageHandler()
.getString("command-alerts-toggled")
.getFormattedMessage(new VpnString.Var<>("state", true)));
}
}));
}
@EventHandler
public void onListener(final PlayerLoginEvent event) {
//If they're exempt, don't check.
if(event.getPlayer().hasPermission("antivpn.bypass") //Has bypass permission
|| AntiVPN.getInstance().getExecutor().isWhitelisted(event.getPlayer().getUniqueId()) //Is exempt
//Or has a name that starts with a certain prefix. This is for Bedrock exempting.
|| AntiVPN.getInstance().getExecutor().isWhitelisted(event.getAddress().getHostAddress())
|| AntiVPN.getInstance().getVpnConfig().getPrefixWhitelists().stream()
.anyMatch(prefix -> event.getPlayer().getName().startsWith(prefix))) return;
if(responseCache.asMap().containsKey(event.getPlayer().getUniqueId())) {
VPNResponse cached = responseCache.getIfPresent(event.getPlayer().getUniqueId());
if (cached != null && cached.isProxy()) {
event.setResult(PlayerLoginEvent.Result.KICK_BANNED);
event.setKickMessage(org.bukkit.ChatColor.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString()));
return;
}
}
final Player player = event.getPlayer();
checkIp(event.getAddress().getHostAddress(),
AntiVPN.getInstance().getVpnConfig().cachedResults(), result -> {
if(result.isSuccess()) {
//We need to run on main thread or kicking and running commands will cause errors
//If the player is whitelisted, we don't want to kick them
if(AntiVPN.getInstance().getExecutor().isWhitelisted(event.getPlayer().getUniqueId())) {
log("UUID is whitelisted: %s",
event.getPlayer().getUniqueId().toString());
return;
}
//If the IP is whitelisted, we don't want to kick them
InetSocketAddress address = event.getPlayer().getAddress();
if (address != null){
InetAddress address1 = address.getAddress();
if (address1 != null && AntiVPN.getInstance().getExecutor()
.isWhitelisted(address1.getHostAddress())) {
log("IP is whitelisted: %s",
address1.getHostAddress());
return;
}
}
// If the countryList() size is zero, no need to check.
// Running country check first
if(!AntiVPN.getInstance().getVpnConfig().countryList().isEmpty()
// This bit of code will decide whether or not to kick the player
// If it contains the code and it is set to whitelist, it will not kick as they are equal
// and vise versa. However, if the contains does not match the state, it will kick.
&& AntiVPN.getInstance().getVpnConfig().countryList()
.contains(result.getCountryCode())
!= AntiVPN.getInstance().getVpnConfig().whitelistCountries()) {
final String kickReason = AntiVPN.getInstance().getVpnConfig()
.countryVanillaKickReason();
// Start "online" fix
// In case the response was so fast from API the player wouldn't be "online".
event.setResult(PlayerLoginEvent.Result.KICK_BANNED);
event.setKickMessage(ChatColor
.translateAlternateColorCodes('&',
kickReason
.replace("%player%", event.getPlayer().getName())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode())));
// End "online" fix
//Using our built in kicking system if no commands are configured
if(AntiVPN.getInstance().getVpnConfig().countryKickCommands().isEmpty()) {
// Kicking our player
new BukkitRunnable() {
public void run() {
event.getPlayer().kickPlayer(ChatColor
.translateAlternateColorCodes('&',
kickReason
.replace("%player%", event.getPlayer().getName())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode())));
}
}.runTask(BukkitPlugin.pluginInstance);
} else {
final String playerName = event.getPlayer().getName();
BukkitPlugin.pluginInstance.getPlayerCommandRunner()
.addAction(event.getPlayer().getUniqueId(), () -> {
for (String cmd : AntiVPN.getInstance().getVpnConfig().countryKickCommands()) {
final String formattedCommand = ChatColor.translateAlternateColorCodes('&',
cmd.replace("%player%", playerName)
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode()));
// Runs our command from console
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), formattedCommand);
}
});
}
} else if(result.isProxy()) {
// Start "online" fix
// In case the response was so fast from API the player wouldn't be "online".
event.setResult(PlayerLoginEvent.Result.KICK_BANNED);
event.setKickMessage(ChatColor
.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString()
.replace("%player%", event.getPlayer().getName())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode())));
// End "online" fix
if(AntiVPN.getInstance().getVpnConfig().kickPlayersOnDetect()) {
new BukkitRunnable() {
public void run() {
player.kickPlayer(org.bukkit.ChatColor.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString()));
}
}.runTask(BukkitPlugin.pluginInstance);
}
log(Level.INFO, event.getPlayer().getName()
+ " joined on a VPN/Proxy (" + result.getMethod() + ")");
//Ensuring the user wishes to alert to staff
if(AntiVPN.getInstance().getVpnConfig().alertToStaff())
AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream()
.filter(APIPlayer::isAlertsEnabled)
.forEach(pl -> pl.sendMessage(AntiVPN.getInstance().getVpnConfig().alertMessage()
.replace("%player%", event.getPlayer().getName())
.replace("%reason%", result.getMethod())
.replace("%country%", result.getCountryName())
.replace("%city%", result.getCity())));
//In case the user wants to run their own commands instead of using the built in kicking
if(AntiVPN.getInstance().getVpnConfig().runCommands()) {
String playerName = event.getPlayer().getName();
BukkitPlugin.pluginInstance.getPlayerCommandRunner()
.addAction(event.getPlayer().getUniqueId(), () -> {
for (String command : AntiVPN.getInstance().getVpnConfig().commands()) {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
ChatColor.translateAlternateColorCodes('&',
command.replace("%player%",
playerName)));
}
});
}
AntiVPN.getInstance().detections++;
}
} else {
log(Level.WARNING,
"The API query was not a success! " +
"You may need to upgrade your license on https://funkemunky.cc/shop");
}
AntiVPN.getInstance().checked++;
});
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
AntiVPN.getInstance().getPlayerExecutor().unloadPlayer(event.getPlayer().getUniqueId());
}
}
@@ -1,38 +0,0 @@
package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.api.APIPlayer;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
public class BukkitPlayer extends APIPlayer {
private final Player player;
public BukkitPlayer(Player player) {
super(player.getUniqueId(), player.getName(), player.getAddress().getAddress());
this.player = player;
}
@Override
public void sendMessage(String message) {
player.sendMessage(ChatColor.translateAlternateColorCodes('&', message));
}
@Override
public void kickPlayer(String reason) {
if(!Bukkit.isPrimaryThread()) {
new BukkitRunnable() {
public void run() {
player.kickPlayer(ChatColor.translateAlternateColorCodes('&', reason));
}
}.runTask(BukkitPlugin.pluginInstance);
} else player.kickPlayer(ChatColor.translateAlternateColorCodes('&', reason));
}
@Override
public boolean hasPermission(String permission) {
return player.hasPermission(permission);
}
}
-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.gradleup.shadow'
}
evaluationDependsOn(':Bungee:BungeePlugin')
dependencies {
compileOnly 'net.md-5:bungeecord-api:1.21-R0.2'
compileOnly project(':Bungee:BungeePlugin')
implementation project(':Common:loader-utils')
}
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
+29
View File
@@ -0,0 +1,29 @@
plugins {
id 'com.gradleup.shadow'
}
dependencies {
compileOnly 'net.md-5:bungeecord-api:1.21-R0.2'
testImplementation 'net.md-5:bungeecord-api:1.21-R0.2'
implementation project(':Common:Source')
implementation project(':Common:loader-utils')
implementation 'org.bstats:bstats-bungeecord:2.2.1'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation 'org.mockito:mockito-subclass:5.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'
testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
}
tasks.compileJava.dependsOn(':Common:Source:jar')
test {
useJUnitPlatform()
systemProperty 'mockito.mockmaker', 'subclass'
}
shadowJar {
archiveClassifier.set('')
relocate 'org.bstats', 'dev.brighten.antivpn.bungee.org.bstats'
}
tasks.build.dependsOn shadowJar
@@ -0,0 +1,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,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;
@@ -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());
}
}
-91
View File
@@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>AntiVPN</artifactId>
<groupId>dev.brighten.antivpn</groupId>
<version>1.9.3.1</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.13.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.5.2</version>
<configuration>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<!-- Replace this with your package! -->
<shadedPattern>dev.brighten.antivpn.bungee.org.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>org.yaml.snakeyaml</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.org.yaml.snakeyaml</shadedPattern>
</relocation>
<relocation>
<pattern>com.google</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.google</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Common</artifactId>
<version>1.9.3.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.github.bungee</groupId>
<artifactId>BungeeCord-1.8</artifactId>
<version>1.8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bungeecord</artifactId>
<version>2.2.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -1,179 +0,0 @@
package dev.brighten.antivpn.bungee;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.web.objects.VPNResponse;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.scheduler.ScheduledTask;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class BungeeListener extends VPNExecutor implements Listener {
private ScheduledTask cacheResetTask;
private final Cache<UUID, VPNResponse> responseCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(2000)
.build();
@Override
public void registerListeners() {
BungeePlugin.pluginInstance.getProxy().getPluginManager()
.registerListener(BungeePlugin.pluginInstance, this);
}
@Override
public void shutdown() {
if(cacheResetTask != null) {
cacheResetTask.cancel();
cacheResetTask = null;
}
threadExecutor.shutdown();
BungeePlugin.pluginInstance.getProxy().getPluginManager().unregisterListener(this);
}
@Override
public void log(Level level, String log, Object... objects) {
BungeeCord.getInstance().getLogger().log(Level.INFO, String.format(log, objects));
}
@Override
public void log(String log, Object... objects) {
log(Level.INFO, String.format(log, objects));
}
@Override
public void logException(String message, Exception ex) {
BungeeCord.getInstance().getLogger().log(Level.SEVERE, message, ex);
}
@EventHandler(priority = EventPriority.LOWEST)
public void onListener(final PreLoginEvent event) {
if(!responseCache.asMap().containsKey(event.getConnection().getUniqueId())) return;
VPNResponse cached = responseCache.getIfPresent(event.getConnection().getUniqueId());
if(cached != null && cached.isProxy()) {
event.setCancelled(true);
event.setCancelReason(TextComponent.fromLegacyText(ChatColor
.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString())));
AntiVPN.getInstance().getExecutor().log(Level.INFO,
"%s was kicked from pre-login proxy cache.",
event.getConnection().getName());
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onListener(final PostLoginEvent event) {
if(event.getPlayer().hasPermission("antivpn.bypass") //Has bypass permission
|| AntiVPN.getInstance().getVpnConfig().getPrefixWhitelists().stream()
.anyMatch(prefix -> event.getPlayer().getName().startsWith(prefix))) return;
checkIp(event.getPlayer().getAddress().getAddress().getHostAddress(),
AntiVPN.getInstance().getVpnConfig().cachedResults(), result -> {
if(result.isSuccess()) {
//If the player is whitelisted, we don't want to kick them
if(AntiVPN.getInstance().getExecutor().isWhitelisted(event.getPlayer().getUniqueId())) {
AntiVPN.getInstance().getExecutor().log("UUID is whitelisted: %s",
event.getPlayer().getUniqueId().toString());
return;
}
//If the IP is whitelisted, we don't want to kick them
if(AntiVPN.getInstance().getExecutor().isWhitelisted(event.getPlayer().getAddress().getAddress()
.getHostAddress())) {
AntiVPN.getInstance().getExecutor().log("IP is whitelisted: %s",
event.getPlayer().getAddress().getAddress().getHostAddress());
return;
}
responseCache.put(event.getPlayer().getUniqueId(), result);
if(!AntiVPN.getInstance().getVpnConfig().countryList().isEmpty()
// This bit of code will decide whether or not to kick the player
// If it contains the code and it is set to whitelist, it will not kick as they are equal
// and vise versa. However, if the contains does not match the state, it will kick.
&& AntiVPN.getInstance().getVpnConfig().countryList()
.contains(result.getCountryCode()) != AntiVPN.getInstance().getVpnConfig().whitelistCountries()) {
//Using our built in kicking system if no commands are configured
if(AntiVPN.getInstance().getVpnConfig().countryKickCommands().isEmpty()) {
final String kickReason = AntiVPN.getInstance().getVpnConfig()
.countryVanillaKickReason();
// Kicking our player
event.getPlayer().disconnect(TextComponent.fromLegacyText(ChatColor
.translateAlternateColorCodes('&',
kickReason
.replace("%player%", event.getPlayer().getName())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode()))));
} else {
for (String cmd : AntiVPN.getInstance().getVpnConfig().countryKickCommands()) {
final String formattedCommand = ChatColor.translateAlternateColorCodes('&',
cmd.replace("%player%", event.getPlayer().getName())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode()));
// Runs our command from console
BungeeCord.getInstance().getPluginManager().dispatchCommand(
BungeeCord.getInstance().getConsole(), formattedCommand);
}
}
} else if(result.isProxy()) {
if(AntiVPN.getInstance().getVpnConfig().kickPlayersOnDetect())
event.getPlayer().disconnect(TextComponent.fromLegacyText(ChatColor
.translateAlternateColorCodes('&',
AntiVPN.getInstance().getVpnConfig().getKickString())));
BungeeCord.getInstance().getLogger().info(event.getPlayer().getName()
+ " joined on a VPN/Proxy (" + result.getMethod() + ")");
if(AntiVPN.getInstance().getVpnConfig().alertToStaff()) //Ensuring the user wishes to alert to staff
AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream()
.filter(APIPlayer::isAlertsEnabled)
.forEach(pl -> pl.sendMessage(AntiVPN.getInstance().getVpnConfig().alertMessage()
.replace("%player%", event.getPlayer().getName())
.replace("%reason%", result.getMethod())
.replace("%country%", result.getCountryName())
.replace("%city%", result.getCity())));
//In case the user wants to run their own commands instead of using the built in kicking
if(AntiVPN.getInstance().getVpnConfig().runCommands()) {
for (String command : AntiVPN.getInstance().getVpnConfig().commands()) {
BungeeCord.getInstance().getPluginManager()
.dispatchCommand(BungeeCord.getInstance().getConsole(),
ChatColor.translateAlternateColorCodes('&',
command.replace("%player%", event.getPlayer().getName())));
}
}
AntiVPN.getInstance().detections++;
}
} else {
BungeeCord.getInstance().getLogger()
.log(Level.WARNING,
"The API query was not a success! " +
"You may need to upgrade your license on https://funkemunky.cc/shop");
}
AntiVPN.getInstance().checked++;
});
}
@EventHandler
public void onLeave(PlayerDisconnectEvent event) {
AntiVPN.getInstance().getPlayerExecutor().unloadPlayer(event.getPlayer().getUniqueId());
}
}
@@ -1,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();
}
}
+54
View File
@@ -0,0 +1,54 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.10.1] - 2026-04-28
### Fixed
- Startup error on velocity instances is now corrected.
## [1.10.0] - 2026-04-07
### Added
- CIDR allowlisting, including commands to add, remove, view, and search entries
- MongoDB support for CIDR allowlist storage
- VPN detection webhooks with Discord and Slack formatting options
- Mojang API fallback support for player lookups
- Folia support
### Changed
- Improved player blocking so flagged users are removed more reliably across platforms
- Updated allowlist handling to validate CIDR entries more consistently
- Improved database cleanup for outdated cached responses
### Fixed
- SQL startup and loading issues, including MySQL library injection problems
- CIDR parsing issues and MongoDB CIDR lookup failures
- Allowlist-related SQL errors
- Repeated webhook spam from duplicate VPN detection events
### Documentation
- Expanded webhook setup documentation for Discord and Slack
## [1.9.4] - 2025-09-30
### Added
- Sponge platform support
- UUID lookup support for player validation
- Better scheduled kick checking
- Java 17 and Java 21 support
- Database metrics tracking for bStats
### Changed
- **BREAKING**: Minimum Java version upgraded from 8 to 17
- Replaced the old cache implementation with Caffeine for better performance
- Improved asynchronous player checking and VPN detection handling
- Improved database connection management and error handling
### Fixed
- H2 database compatibility issues with automatic backup and recovery
- Memory leaks and resource cleanup problems in database handling
- Thread safety issues in player cache management
- Command registration issues during plugin startup and shutdown
+45
View File
@@ -0,0 +1,45 @@
plugins {
id 'com.gradleup.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'
implementation'com.github.ben-manes.caffeine:caffeine:3.1.8'
compileOnly 'org.mongodb:mongo-java-driver:3.12.14'
testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation "org.junit.jupiter:junit-jupiter:5.8.1"
testImplementation "org.testcontainers:testcontainers:2.0.4"
testImplementation "org.testcontainers:testcontainers-junit-jupiter:2.0.4"
testImplementation 'org.testcontainers:mysql:1.20.4'
testImplementation 'org.testcontainers:mongodb:1.20.4'
testRuntimeOnly 'org.slf4j:slf4j-simple:2.0.16'
testImplementation 'com.mysql:mysql-connector-j:9.3.0'
testImplementation 'com.h2database:h2:2.2.220'
testImplementation 'org.mongodb:mongo-java-driver:3.12.14'
testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
}
shadowJar {
archiveClassifier.set('')
}
tasks.build.dependsOn shadowJar
components.java.withVariantsFromConfiguration(configurations.shadowRuntimeElements) {
skip()
}
test {
useJUnitPlatform()
}
jar {
archiveClassifier.set('raw')
}
@@ -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;
@@ -9,6 +25,8 @@ import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.local.H2VPN;
import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.sql.MySqlVPN;
import dev.brighten.antivpn.depends.LibraryLoader;
import dev.brighten.antivpn.depends.MavenLibrary;
import dev.brighten.antivpn.message.MessageHandler;
import dev.brighten.antivpn.utils.ConfigDefault;
import dev.brighten.antivpn.utils.MiscUtils;
@@ -29,6 +47,13 @@ import java.util.List;
@Getter
@Setter(AccessLevel.PRIVATE)
@MavenLibrary(groupId = "com.h2database", artifactId ="h2", version = "2.2.220")
@MavenLibrary(groupId = "org.mongodb", artifactId = "mongo-java-driver", version = "3.12.14")
@MavenLibrary(
groupId = "com.mysql",
artifactId = "mysql-connector-j",
version = "9.3.0"
)
public class AntiVPN {
private static AntiVPN INSTANCE;
@@ -51,16 +76,22 @@ public class AntiVPN {
INSTANCE.executor = executor;
INSTANCE.playerExecutor = playerExecutor;
LibraryLoader.loadAll(INSTANCE);
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();
@@ -70,34 +101,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
@@ -117,6 +154,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) {
@@ -139,8 +179,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() {
@@ -188,7 +246,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,336 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.command.impl;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.utils.MiscUtils;
import java.net.UnknownHostException;
import java.util.*;
import java.util.stream.Collectors;
public class AllowlistCommand extends Command {
private static final String[] secondArgs = new String[] {"add", "remove", "show", "search"};
@Override
public String permission() {
return "antivpn.command.allowlist";
}
@Override
public String name() {
return "allowlist";
}
@Override
public String[] aliases() {
return new String[] {"whitelist"};
}
@Override
public String description() {
return "Add/remove players to/from exemption list.";
}
@Override
public String usage() {
return "<add <player/uuid/ip> | remove <player/uuid/ip> | show [page] | search <query> [page]>";
}
@Override
public String parent() {
return "antivpn";
}
@Override
public Command[] children() {
return new Command[0];
}
@Override
public String execute(CommandExecutor executor, String[] args) {
if(args.length == 0 || Arrays.stream(secondArgs).noneMatch(arg -> arg.equalsIgnoreCase(args[0]))) {
return "&cUsage: /antivpn allowlist " + usage();
}
if(args[0].equalsIgnoreCase("show")) {
// args[1] = optional page number (defaults to 1)
int page = 1;
if (args.length > 1) {
try {
page = Integer.parseInt(args[1]);
if (page < 1) page = 1;
} catch (NumberFormatException e) {
return "&cUsage: /antivpn allowlist show [page]";
}
}
boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled();
List<UUID> uuids = databaseEnabled
? AntiVPN.getInstance().getDatabase().getAllWhitelisted()
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelisted());
List<CIDRUtils> ips = databaseEnabled
? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps()
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps());
List<String> entries = new ArrayList<>();
for (UUID uuid : uuids) {
entries.add("&7- &fUUID: &e" + uuid);
}
for (CIDRUtils cidr : ips) {
entries.add("&7- &fIP: &e" + cidr.getCidr());
}
return buildPage(entries, page, null, "show");
}
if(args[0].equalsIgnoreCase("search")) {
// args[1..n-1] = query terms; args[n] = optional page number if last arg is an integer
if (args.length < 2) {
return "&cUsage: /antivpn allowlist search <query> [page]";
}
// Detect optional trailing page number
int page = 1;
int queryEnd = args.length;
try {
int candidate = Integer.parseInt(args[args.length - 1]);
if (candidate >= 1 && args.length > 2) {
page = candidate;
queryEnd = args.length - 1;
}
} catch (NumberFormatException ignored) {}
String search = String.join(" ", Arrays.copyOfRange(args, 1, queryEnd)).toLowerCase();
// Strip color code characters to prevent formatting injection in output
String safeSearch = search.replace("&", "");
if (safeSearch.isEmpty()) {
return "&cUsage: /antivpn allowlist search <query> [page]";
}
boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled();
List<UUID> uuids = databaseEnabled
? AntiVPN.getInstance().getDatabase().getAllWhitelisted()
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelisted());
List<CIDRUtils> ips = databaseEnabled
? AntiVPN.getInstance().getDatabase().getAllWhitelistedIps()
: new ArrayList<>(AntiVPN.getInstance().getExecutor().getWhitelistedIps());
List<String> entries = new ArrayList<>();
for (UUID uuid : uuids) {
String entry = uuid.toString();
if (entry.toLowerCase().contains(search)) {
entries.add("&7- &fUUID: &e" + entry);
}
}
for (CIDRUtils cidr : ips) {
String entry = cidr.getCidr();
if (entry.toLowerCase().contains(search)) {
entries.add("&7- &fIP: &e" + entry);
}
}
return buildPage(entries, page, safeSearch, "search " + safeSearch);
}
if(args.length == 1)
return "&cYou have to provide a player to allow or deny exemption.";
boolean databaseEnabled = AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled();
if(!databaseEnabled) executor.sendMessage("&cThe database is currently not setup, " +
"so any changes here will disappear after a restart.");
CIDRUtils cidrUtils;
try {
cidrUtils = new CIDRUtils(args[1]);
} catch(IllegalArgumentException | UnknownHostException e) {
cidrUtils = null;
}
if(cidrUtils != null) {
if(!databaseEnabled) {
return switch (args[0].toLowerCase()) {
case "add", "insert" -> {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().add(cidrUtils);
yield String.format("&aAdded &6%s &ato exemption allowlist.", cidrUtils.getCidr());
}
case "remove", "delete" -> {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().remove(cidrUtils);
yield String.format("&cRemoved &%s &cfrom the exemption allowlist.", cidrUtils.getCidr());
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
} else return switch (args[0].toLowerCase()) {
case "add", "insert" -> {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().add(cidrUtils);
AntiVPN.getInstance().getDatabase().addWhitelist(cidrUtils);
yield String.format("&aAdded &6%s &ato exemption allowlist.", cidrUtils.getCidr());
}
case "remove", "delete" -> {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().remove(cidrUtils);
AntiVPN.getInstance().getDatabase().removeWhitelist(cidrUtils);
yield String.format("&cRemoved &6%s &cfrom the exemption allowlist.", cidrUtils.getCidr());
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
}
if(MiscUtils.isIpv4(args[1])) {
if(!databaseEnabled) {
try {
return switch(args[0].toLowerCase()) {
case "add", "insert" -> {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().add(new CIDRUtils(args[1] + "/32"));
AntiVPN.getInstance().getDatabase().addWhitelist(new CIDRUtils(args[1] + "/32"));
yield String.format("&aAdded &6%s &ato the exemption allowlist.", args[1] + "/32");
}
case "remove", "delete" -> {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().remove(new CIDRUtils(args[1] + "/32"));
AntiVPN.getInstance().getDatabase().removeWhitelist(new CIDRUtils(args[1] + "/32"));
yield String.format("&cRemoved &6%s &cfrom the exemption allowlist.", args[1] + "/32");
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
} catch (UnknownHostException e) {
AntiVPN.getInstance().getExecutor().logException("Invalid IP format for allowlist command", e);
return "&cInvalid IP format for allowlist command";
}
} else {
try {
return switch (args[0].toLowerCase()) {
case "add", "insert" -> {
AntiVPN.getInstance().getDatabase().addWhitelist(new CIDRUtils(args[1] + "/32"));
yield String.format("&aAdded &6%s &a to the exemption allowlist.", args[1] + "/32");
}
case "remove", "delete" -> {
AntiVPN.getInstance().getDatabase().removeWhitelist(new CIDRUtils(args[1] + "/32"));
yield String.format("&cRemoved &6%s &c from the exemption allowlist.", args[1] + "/32");
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
} catch (UnknownHostException e) {
AntiVPN.getInstance().getExecutor().logException("Invalid IP format for allowlist command", e);
return "&cInvalid IP format for allowlist command";
}
}
} else {
UUID uuid;
try {
uuid = UUID.fromString(args[1]);
} catch(IllegalArgumentException e) {
Optional<APIPlayer> player = AntiVPN.getInstance().getPlayerExecutor().getPlayer(args[1]);
if (player.isPresent()) {
uuid = player.get().getUuid();
} else {
uuid = MiscUtils.lookupUUID(args[1]);
if (uuid == null) {
return "&cCould not find a UUID for \"" + args[1] + "\". They might not have provided a valid username.";
}
}
}
if(!databaseEnabled) {
return switch (args[0].toLowerCase()) {
case "add" -> {
AntiVPN.getInstance().getExecutor().getWhitelisted().add(uuid);
yield String.format("&aAdded &6%s &auuid to the exemption allowlist.", uuid.toString());
}
case "remove", "delete" -> {
AntiVPN.getInstance().getExecutor().getWhitelisted().remove(uuid);
yield String.format("&cRemoved &6%s &cuuid from the exemption allowlist.", uuid.toString());
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
} else {
return switch (args[0].toLowerCase()) {
case "add" -> {
AntiVPN.getInstance().getDatabase().addWhitelist(uuid);
yield String.format("&aAdded &6%s &auuid to the exemption allowlist.", uuid.toString());
}
case "remove", "delete" -> {
AntiVPN.getInstance().getDatabase().removeWhitelist(uuid);
yield String.format("&cRemoved &6%s &cuuid from the exemption allowlist.", uuid.toString());
}
default -> "&c\"" + args[0] + "\" is not a valid argument";
};
}
}
}
@Override
public List<String> tabComplete(CommandExecutor executor, String alias, String[] args) {
return switch (args.length) {
case 1 -> Arrays.stream(secondArgs)
.filter(narg -> narg.toLowerCase().startsWith(args[0].toLowerCase()))
.collect(Collectors.toList());
case 2 -> {
if (args[0].equalsIgnoreCase("show") || args[0].equalsIgnoreCase("search")) {
yield Collections.emptyList();
}
yield AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream()
.map(APIPlayer::getName)
.filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase()))
.collect(Collectors.toList());
}
default -> Collections.emptyList();
};
}
private String buildPage(List<String> entries, int page, String safeSearch, String subcommandPrefix) {
int pageSize = 10;
int totalPages = Math.max(1, (entries.size() + pageSize - 1) / pageSize);
if (page > totalPages) page = totalPages;
List<String> messages = new ArrayList<>();
messages.add("&8&m-----------------------------------------------------");
messages.add("&6&lAllowlist Entries &8(&7Page &f" + page + "&7/&f" + totalPages + "&8)"
+ (safeSearch != null ? " &7(search: &f" + safeSearch + "&7)" : ""));
messages.add("");
if (entries.isEmpty()) {
messages.add(safeSearch != null
? "&cNo allowlist entries matching &f\"" + safeSearch + "&c\" were found."
: "&cThe allowlist is empty.");
} else {
int start = (page - 1) * pageSize;
int end = Math.min(start + pageSize, entries.size());
for (int i = start; i < end; i++) {
messages.add(entries.get(i));
}
if (totalPages > 1) {
messages.add("");
if (page > 1) {
messages.add("&7Previous page: &f/antivpn allowlist " + subcommandPrefix + " " + (page - 1));
}
if (page < totalPages) {
messages.add("&7Next page: &f/antivpn allowlist " + subcommandPrefix + " " + (page + 1));
}
}
}
messages.add("&8&m-----------------------------------------------------");
return String.join("\n", messages);
}
}
@@ -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,6 +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.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;
@@ -45,7 +62,7 @@ public class ClearCacheCommand extends Command {
@Override
public String execute(CommandExecutor executor, String[] args) {
AntiVPN.getInstance().getDatabase().clearResponses();
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;
@@ -51,7 +67,7 @@ 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().isEmpty()) {
@@ -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,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;
}
}
}
@@ -1,31 +1,54 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.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.Indexes;
import com.mongodb.client.model.UpdateOptions;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.VPNExecutor;
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 {
private MongoCollection<Document> settingsDocument, cacheDocument;
public MongoCollection<Document> settingsDocument;
MongoCollection<Document> cacheDocument;
private MongoClient client;
public MongoDatabase antivpnDatabase;
public MongoVPN() {
VPNExecutor.threadExecutor.scheduleAtFixedRate(() -> {
AntiVPN.getInstance().getExecutor().getThreadExecutor().scheduleAtFixedRate(() -> {
if(!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) return;
//Refreshing whitelisted players
@@ -47,8 +70,8 @@ public class MongoVPN implements VPNDatabase {
long lastUpdate = rdoc.get("lastAccess", 0L);
if(System.currentTimeMillis() - lastUpdate > TimeUnit.HOURS.toMillis(1)) {
VPNExecutor.threadExecutor.execute(() -> deleteResponse(ip));
return Optional.empty();
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> deleteResponse(ip));
return null;
}
return Optional.of(VPNResponse.builder().asn(rdoc.getString("asn")).ip(ip)
@@ -71,27 +94,29 @@ public class MongoVPN implements VPNDatabase {
@Override
public void cacheResponse(VPNResponse toCache) {
Document rdoc = new Document("ip", toCache.getIp());
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());
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());
VPNExecutor.threadExecutor.execute(() -> {
Bson update = new Document("$set", rdoc);
cacheDocument.updateOne(Filters.eq("ip", toCache.getIp()), update,
new UpdateOptions().upsert(true));
});
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> {
Bson update = new Document("$set", rdoc);
cacheDocument.updateOne(Filters.eq("ip", toCache.getIp()), update,
new UpdateOptions().upsert(true));
});
}
}
@Override
@@ -107,42 +132,57 @@ public class MongoVPN implements VPNDatabase {
}
@Override
public boolean isWhitelisted(String ip) {
return settingsDocument
.find(Filters.and(Filters.eq("setting", "whitelist"),
Filters.eq("ip", ip))).first() != null;
}
@Override
public void setWhitelisted(UUID uuid, boolean whitelisted) {
if(whitelisted) {
Document wdoc = new Document("setting", "whitelist");
wdoc.put("uuid", uuid.toString());
AntiVPN.getInstance().getExecutor().getWhitelisted().add(uuid);
VPNExecutor.threadExecutor.execute(() -> settingsDocument.insertOne(wdoc));
} else {
AntiVPN.getInstance().getExecutor().getWhitelisted().remove(uuid);
VPNExecutor.threadExecutor.execute(() -> settingsDocument.deleteMany(Filters
.and(
Filters.eq("setting", "whitelist"),
Filters.eq("uuid", uuid.toString()))));
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 void setWhitelisted(String ip, boolean whitelisted) {
if(whitelisted) {
Document wdoc = new Document("setting", "whitelist").append("ip", ip);
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;
}
AntiVPN.getInstance().getExecutor().getWhitelistedIps().add(ip);
VPNExecutor.threadExecutor.execute(() -> settingsDocument.insertOne(wdoc));
} else {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().remove(ip);
VPNExecutor.threadExecutor.execute(() -> settingsDocument.deleteMany(Filters
.and(
Filters.eq("setting", "whitelist"),
Filters.eq("ip", ip))));
}
@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
@@ -155,39 +195,30 @@ public class MongoVPN implements VPNDatabase {
}
@Override
public List<String> getAllWhitelistedIps() {
List<String> ips = new ArrayList<>();
public List<CIDRUtils> getAllWhitelistedIps() {
List<CIDRUtils> ips = new ArrayList<>();
settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"),
Filters.exists("ip")))
.forEach((Consumer<? super Document>) doc -> ips.add(doc.getString("ip")));
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 getStoredResponseAsync(String ip, Consumer<Optional<VPNResponse>> result) {
VPNExecutor.threadExecutor.execute(() -> result.accept(getStoredResponse(ip)));
}
@Override
public void isWhitelistedAsync(UUID uuid, Consumer<Boolean> result) {
VPNExecutor.threadExecutor.execute(() -> result.accept(isWhitelisted(uuid)));
}
@Override
public void isWhitelistedAsync(String ip, Consumer<Boolean> result) {
VPNExecutor.threadExecutor.execute(() -> result.accept(isWhitelisted(ip)));
}
@Override
public void alertsState(UUID uuid, Consumer<Boolean> result) {
VPNExecutor.threadExecutor.execute(() -> result.accept(settingsDocument
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) {
VPNExecutor.threadExecutor.execute(() -> {
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> {
settingsDocument.deleteMany(Filters.and(Filters.eq("setting", "alerts"),
Filters.eq("uuid", uuid.toString())));
if(state) {
@@ -227,14 +258,17 @@ public class MongoVPN implements VPNDatabase {
client = MongoClients.create(settingsBld.build());
}
MongoDatabase antivpnDatabase = client.getDatabase(AntiVPN.getInstance().getVpnConfig().getDatabaseName());
antivpnDatabase = client.getDatabase(AntiVPN.getInstance().getVpnConfig().getDatabaseName());
settingsDocument = antivpnDatabase.getCollection("settings");
if(settingsDocument.listIndexes().first() == null) {
AntiVPN.getInstance().getExecutor().log("Created index for settings collection!");
settingsDocument.createIndex(Indexes.ascending("ip"));
}
cacheDocument = antivpnDatabase.getCollection("cache");
for (Version<MongoVPN> mongoDbVersion : Version.mongoDbVersions) {
if(mongoDbVersion.needsUpdate(this)) {
mongoDbVersion.update(this);
}
}
}
@Override
@@ -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();
}
@@ -0,0 +1,166 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.depends;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
/**
* Provides access to {@link URLClassLoader}#addURL.
*/
public abstract class URLClassLoaderAccess {
/**
* Creates a {@link URLClassLoaderAccess} for the given class loader.
*
* @param classLoader the class loader
* @return the access object
*/
static URLClassLoaderAccess create(URLClassLoader classLoader) {
if (Reflection.isSupported()) {
return new Reflection(classLoader);
} else if (Unsafe.isSupported()) {
return new Unsafe(classLoader);
} else {
return Noop.INSTANCE;
}
}
private final URLClassLoader classLoader;
protected URLClassLoaderAccess(URLClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* Adds the given URL to the class loader.
*
* @param url the URL to add
*/
public abstract void addURL(URL url);
/**
* Accesses using reflection, not supported on Java 9+.
*/
private static class Reflection extends URLClassLoaderAccess {
private static final Method ADD_URL_METHOD;
static {
Method addUrlMethod;
try {
addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
addUrlMethod.setAccessible(true);
} catch (Exception e) {
addUrlMethod = null;
}
ADD_URL_METHOD = addUrlMethod;
}
private static boolean isSupported() {
return ADD_URL_METHOD != null;
}
Reflection(URLClassLoader classLoader) {
super(classLoader);
}
@Override
public void addURL(URL url) {
try {
ADD_URL_METHOD.invoke(super.classLoader, url);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}
/**
* Accesses using sun.misc.Unsafe, supported on Java 9+.
*
* @author Vaishnav Anil (<a href="https://github.com/slimjar/slimjar">...</a>)
*/
private static class Unsafe extends URLClassLoaderAccess {
private static final sun.misc.Unsafe UNSAFE;
static {
sun.misc.Unsafe unsafe;
try {
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
unsafe = (sun.misc.Unsafe) unsafeField.get(null);
} catch (Throwable t) {
unsafe = null;
}
UNSAFE = unsafe;
}
private static boolean isSupported() {
return UNSAFE != null;
}
private final Collection<URL> unopenedURLs;
private final Collection<URL> pathURLs;
@SuppressWarnings("unchecked")
Unsafe(URLClassLoader classLoader) {
super(classLoader);
Collection<URL> unopenedURLs;
Collection<URL> pathURLs;
try {
Object ucp = fetchField(URLClassLoader.class, classLoader, "ucp");
unopenedURLs = (Collection<URL>) fetchField(ucp.getClass(), ucp, "unopenedUrls");
pathURLs = (Collection<URL>) fetchField(ucp.getClass(), ucp, "path");
} catch (Throwable e) {
unopenedURLs = null;
pathURLs = null;
}
this.unopenedURLs = unopenedURLs;
this.pathURLs = pathURLs;
}
private static Object fetchField(final Class<?> clazz, final Object object, final String name) throws NoSuchFieldException {
Field field = clazz.getDeclaredField(name);
long offset = UNSAFE.objectFieldOffset(field);
return UNSAFE.getObject(object, offset);
}
@Override
public void addURL(URL url) {
this.unopenedURLs.add(url);
this.pathURLs.add(url);
}
}
private static class Noop extends URLClassLoaderAccess {
private static final Noop INSTANCE = new Noop();
private Noop() {
super(null);
}
@Override
public void addURL(URL url) {
throw new UnsupportedOperationException();
}
}
}
@@ -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,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.utils;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.utils.json.JSONException;
import dev.brighten.antivpn.utils.json.JSONObject;
import dev.brighten.antivpn.utils.json.JsonReader;
import java.io.*;
import java.math.BigInteger;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ThreadFactory;
import java.util.regex.Pattern;
public class MiscUtils {
private static final Pattern ipv4 = Pattern.compile("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
private static final String DEFAULT_FUNKEMUNKY_UUID_ENDPOINT = "https://funkemunky.cc/mojang/uuid?name=";
private static final String DEFAULT_MOJANG_UUID_ENDPOINT = "https://api.mojang.com/users/profiles/minecraft/";
private static volatile String funkemunkyUuidEndpoint = DEFAULT_FUNKEMUNKY_UUID_ENDPOINT;
private static volatile String mojangUuidEndpoint = DEFAULT_MOJANG_UUID_ENDPOINT;
public static void close(Closeable... closeables) {
try {
for (Closeable closeable : closeables) if (closeable != null) closeable.close();
} catch (Exception e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
public static void close(AutoCloseable... closeables) {
try {
for (AutoCloseable closeable : closeables) if (closeable != null) closeable.close();
} catch (Exception e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
public static void copy(InputStream in, File file) {
try {
OutputStream out = new FileOutputStream(file);
int lenght;
byte[] buf = new byte[1024];
while ((lenght = in.read(buf)) > 0)
{
out.write(buf, 0, lenght);
}
out.close();
in.close();
} catch (Exception e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
public static ThreadFactory createThreadFactory(String threadName) {
return r -> {
Thread thread = new Thread(r);
thread.setName(threadName);
return thread;
};
}
public static List<CIDRUtils> rangeToCidrs(BigInteger start, BigInteger end) throws UnknownHostException {
List<CIDRUtils> cidrs = new ArrayList<>();
while (start.compareTo(end) <= 0) {
// Find the number of trailing zero bits — this determines max block size alignment
int trailingZeros = start.equals(BigInteger.ZERO)
? 128 // handle the edge case
: start.getLowestSetBit();
// Find the largest block that fits
BigInteger remaining = end.subtract(start).add(BigInteger.ONE);
int maxBits = remaining.bitLength() - 1;
int blockBits = Math.min(trailingZeros, maxBits);
int prefixLen = 32 - blockBits; // use 128 for IPv6
// Build the CIDR string
byte[] addrBytes = toFixedLengthBytes(start); // use 16 for IPv6
String cidr = InetAddress.getByAddress(addrBytes).getHostAddress() + "/" + prefixLen;
cidrs.add(new CIDRUtils(cidr));
// Advance past this block
start = start.add(BigInteger.ONE.shiftLeft(blockBits));
}
return cidrs;
}
private static byte[] toFixedLengthBytes(BigInteger value) {
byte[] raw = value.toByteArray();
byte[] result = new byte[4];
int srcPos = Math.max(0, raw.length - 4);
int destPos = Math.max(0, 4 - raw.length);
System.arraycopy(raw, srcPos, result, destPos, Math.min(raw.length, 4));
return result;
}
public static UUID lookupUUID(String playername) {
try {
UUID uuid = lookupUuidFromUrl(funkemunkyUuidEndpoint + playername);
if (uuid != null) {
return uuid;
}
} catch (IOException | JSONException | URISyntaxException e) {
AntiVPN.getInstance().getExecutor().logException("Error while looking up UUID for " + playername + "! Falling back to Mojang API", e);
return lookupMojangUuid(playername);
}
return null;
}
private static UUID lookupUuidFromUrl(String url) throws IOException, JSONException, URISyntaxException {
HttpURLConnection connection = (HttpURLConnection) new URI(url).toURL().openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setInstanceFollowRedirects(true);
int responseCode = connection.getResponseCode();
if (responseCode >= 500) {
throw new IOException("Server returned HTTP " + responseCode + " for " + url);
}
if (responseCode != HttpURLConnection.HTTP_OK) {
return null;
}
try (InputStream inputStream = connection.getInputStream()) {
JSONObject object = new JSONObject(JsonReader.readAll(new InputStreamReader(inputStream, java.nio.charset.StandardCharsets.UTF_8)));
if (object.has("uuid")) {
return parseUuid(object.getString("uuid"));
}
if (object.has("id")) {
return parseUuid(object.getString("id"));
}
}
return null;
}
private static UUID parseUuid(String value) {
if (value.length() == 32) {
value = value.replaceFirst(
"([0-9a-fA-F]{8})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{12})",
"$1-$2-$3-$4-$5"
);
}
return UUID.fromString(value);
}
private static UUID lookupMojangUuid(String playerName) {
try {
return lookupUuidFromUrl(mojangUuidEndpoint + playerName);
} catch (IOException | JSONException | URISyntaxException e) {
AntiVPN.getInstance().getExecutor().logException("Error while looking up UUID for " + playerName + " from Mojang!:", e);
}
return null;
}
static void setLookupEndpointsForTesting(String funkemunkyEndpoint, String mojangEndpoint) {
funkemunkyUuidEndpoint = funkemunkyEndpoint;
mojangUuidEndpoint = mojangEndpoint;
}
static void resetLookupEndpointsForTesting() {
funkemunkyUuidEndpoint = DEFAULT_FUNKEMUNKY_UUID_ENDPOINT;
mojangUuidEndpoint = DEFAULT_MOJANG_UUID_ENDPOINT;
}
public static boolean isIpv4(String ip)
{
return ipv4.matcher(ip).matches();
}
}
@@ -0,0 +1,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 {
}
@@ -0,0 +1,55 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.utils;
/** A utility method to perform unchecked casts to suppress errors produced by nullness analyses. */
final class NullnessCasts {
/**
* Accepts a {@code @Nullable T} and returns a plain {@code T}, without performing any check that
* that conversion is safe.
*
* <p>This method is intended to help with usages of type parameters that have {
* ParametricNullness parametric nullness}. If a type parameter instead ranges over only non-null
* types (or if the type is a non-variable type, like {@code String}), then code should almost
* never use this method, preferring instead to call {@code requireNonNull} so as to benefit from
* its runtime check.
*
* <p>An example use case for this method is in implementing an {@code Iterator<T>} whose {@code
* next} field is lazily initialized. The type of that field would be {@code @Nullable T}, and the
* code would be responsible for populating a "real" {@code T} (which might still be the value
* {@code null}!) before returning it to callers. Depending on how the code is structured, a
* nullness analysis might not understand that the field has been populated. To avoid that problem
* without having to add {@code @SuppressWarnings}, the code can call this method.
*
* <p>Why <i>not</i> just add {@code SuppressWarnings}? The problem is that this method is
* typically useful for {@code return} statements. That leaves the code with two options: Either
* add the suppression to the whole method (which turns off checking for a large section of code),
* or extract a variable, and put the suppression on that. However, a local variable typically
* doesn't work: Because nullness analyses typically infer the nullness of local variables,
* there's no way to assign a {@code @Nullable T} to a field {@code T foo;} and instruct the
* analysis that that means "plain {@code T}" rather than the inferred type {@code @Nullable T}.
* (Even if supported added {@code @NonNull}, that would not help, since the problem case
* addressed by this method is the case in which {@code T} has parametric nullness -- and thus its
* value may be legitimately {@code null}.)
*/
@SuppressWarnings("nullness")
static <T> T uncheckedCastNullableTToT(T t) {
return t;
}
private NullnessCasts() {}
}
@@ -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.*;
@@ -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 lombok.AccessLevel;
@@ -1,28 +1,20 @@
package dev.brighten.antivpn.utils.json;
/*
Copyright (c) 2002 JSON.org
* 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.
*/
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The Software shall be used for Good, not Evil.
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.
*/
package dev.brighten.antivpn.utils.json;
/**
* This provides static methods to convert comma delimited text into a
@@ -1,28 +1,20 @@
package dev.brighten.antivpn.utils.json;
/*
Copyright (c) 2002 JSON.org
* 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.
*/
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The Software shall be used for Good, not Evil.
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.
*/
package dev.brighten.antivpn.utils.json;
/**
* Convert a web browser cookie specification to a JSONObject and back.

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