Compare commits

...

79 Commits

Author SHA1 Message Date
funkemunky 8d46d25db1 Adds a regression test to ensure this doesnt happen again 2026-04-28 21:28:16 -04:00
funkemunky 89b20df703 Fixing asynchronous run command 2026-04-28 21:23:47 -04:00
funkemunky aa0dc4001a correcting startup bug as a result of a packaging issue 2026-04-28 21:02:43 -04:00
funkemunky 2e079079d7 Updating gradle piplines and files 2026-04-24 20:53:23 -04:00
funkemunky 8185c9aad0 Fixing async kick error, adding condition that allows players to be whitelisted even while they are offline (assuming this is not a cracked server). 2026-04-17 10:51:09 -04:00
funkemunky 5f07b2393a Forcing BukkitPlayer#kickPlayer to always run within a main thread context using BukkitRunnable. Bumping version to 1.10.1 2026-04-17 09:47:46 -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 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 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
188 changed files with 8129 additions and 4286 deletions
+21 -58
View File
@@ -2,59 +2,31 @@ name: create-release.yml
on: on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: build-and-release:
name: Build and Test name: Build and Release
runs-on: ubuntu-latest 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: steps:
- name: Cache local Maven repository
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v5 uses: actions/setup-java@v5
with: with:
java-version: '21' java-version: '21'
distribution: 'zulu' distribution: 'zulu'
- name: Set up Maven - name: Set up Gradle
uses: stCarolas/setup-maven@v5 uses: gradle/actions/setup-gradle@v4
with: with:
maven-version: 3.9.6 gradle-version: '9.4.1'
- name: Compile - name: Build
run: mvn -B package --file pom.xml run: gradle build --no-daemon
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload AntiVPN - name: Get Version Number from Gradle
uses: actions/upload-artifact@v4
with:
name: AntiVPN-Universal
path: Universal/target/AntiVPN-*.jar
- name: Upload Sponge plugin
uses: actions/upload-artifact@v4
with:
name: AntiVPN-Sponge
path: Sponge/target/Sponge-*.jar
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Download AntiVPN
uses: actions/download-artifact@v5
with:
name: AntiVPN-Universal
- name: Download Sponge plugin
uses: actions/download-artifact@v5
with:
name: AntiVPN-Sponge
- name: Get Version Number from Pom
id: get_version id: get_version
run: echo "VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV run: |
VERSION=$(gradle properties -q | awk -F': ' '/^version:/ {print $2; exit}')
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Extract latest CHANGELOG entry - name: Extract latest CHANGELOG entry
id: changelog id: changelog
run: | run: |
@@ -62,32 +34,23 @@ jobs:
CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g') CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
echo "Extracted latest release notes from CHANGELOG.md:" echo "Extracted latest release notes from CHANGELOG.md:"
echo -e "$CHANGELOG_CONTENT" echo -e "$CHANGELOG_CONTENT"
echo "::set-output name=content::$CHANGELOG_ESCAPED" echo "content=$CHANGELOG_ESCAPED" >> "$GITHUB_OUTPUT"
- name: Create Release - name: Create Release
uses: actions/create-release@v1 uses: actions/create-release@v1
id: create_release id: create_release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: v${{ github.run_number }} tag_name: v${{ env.VERSION }}
release_name: Release v${{ github.run_number }} release_name: Release v${{ env.VERSION }}
draft: false draft: false
prerelease: false prerelease: false
body: ${{ steps.changelog.outputs.content }} 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: ./AntiVPN-*.jar
asset_name: AntiVPN-Universal-v${{ env.VERSION }}.jar
asset_content_type: application/java-archive
- uses: actions/upload-release-asset@v1 - uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./Sponge-*.jar asset_path: ./build/libs/AntiVPN-${{ env.VERSION }}-universal.jar
asset_name: AntiVPN-Sponge-v${{ env.VERSION }}.jar asset_name: AntiVPN-v${{ env.VERSION }}.jar
asset_content_type: application/java-archive 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
-44
View File
@@ -1,44 +0,0 @@
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Cache local Maven repository
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- uses: actions/checkout@v5
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'zulu'
- name: Set up Maven
uses: stCarolas/setup-maven@v5
with:
maven-version: 3.9.6
- 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-Universal
path: Universal/target/AntiVPN-*.jar
- name: Upload Sponge plugin
uses: actions/upload-artifact@v4
with:
name: AntiVPN-Sponge
path: Sponge/target/Sponge-*.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 # # Package Files #
*.jar *.jar
!gradle/wrapper/gradle-wrapper.jar
*.war *.war
*.nar *.nar
*.ear *.ear
@@ -293,3 +294,5 @@ $RECYCLE.BIN/
*.lnk *.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,maven,java,intellij,eclipse,netbeans # End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,maven,java,intellij,eclipse,netbeans
/.gradle/
.grade/**
+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; package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.AntiVPN; 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; package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.api.APIPlayer; import dev.brighten.antivpn.api.APIPlayer;
@@ -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; package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.AntiVPN; import dev.brighten.antivpn.AntiVPN;
@@ -7,6 +23,7 @@ import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.local.H2VPN; import dev.brighten.antivpn.database.local.H2VPN;
import dev.brighten.antivpn.database.mongo.MongoVPN; import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.sql.MySqlVPN; import dev.brighten.antivpn.database.sql.MySqlVPN;
import dev.brighten.antivpn.loader.LoaderBootstrap;
import lombok.Getter; import lombok.Getter;
import org.bstats.bukkit.Metrics; import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie; import org.bstats.charts.SimplePie;
@@ -17,20 +34,34 @@ import org.bukkit.plugin.SimplePluginManager;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitRunnable;
import java.io.File;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public class BukkitPlugin extends JavaPlugin { public class BukkitPlugin implements LoaderBootstrap {
public static BukkitPlugin pluginInstance; public static BukkitPlugin pluginInstance;
private SimpleCommandMap commandMap; private SimpleCommandMap commandMap;
@Getter
private File dataFolder;
private final List<org.bukkit.command.Command> registeredCommands = new ArrayList<>(); private final List<org.bukkit.command.Command> registeredCommands = new ArrayList<>();
@Getter
private final JavaPlugin plugin;
public BukkitPlugin(JavaPlugin plugin) {
this.plugin = plugin;
}
@Getter @Getter
private PlayerCommandRunner playerCommandRunner; private PlayerCommandRunner playerCommandRunner;
@Override
public void onLoad(File dataFolder) {
this.dataFolder = dataFolder;
}
public void onEnable() { public void onEnable() {
pluginInstance = this; pluginInstance = this;
@@ -43,18 +74,18 @@ public class BukkitPlugin extends JavaPlugin {
// Loading our bStats metrics to be pushed to https://bstats.org // Loading our bStats metrics to be pushed to https://bstats.org
if(AntiVPN.getInstance().getVpnConfig().metrics()) { if(AntiVPN.getInstance().getVpnConfig().metrics()) {
Bukkit.getLogger().info("Starting bStats metrics..."); Bukkit.getLogger().info("Starting bStats metrics...");
Metrics metrics = new Metrics(this, 12615); Metrics metrics = new Metrics(plugin, 12615);
metrics.addCustomChart(new SimplePie("database_used", this::getDatabaseType)); metrics.addCustomChart(new SimplePie("database_used", this::getDatabaseType));
new BukkitRunnable() { new BukkitRunnable() {
public void run() { public void run() {
AntiVPN.getInstance().checked = AntiVPN.getInstance().detections = 0; AntiVPN.getInstance().checked = AntiVPN.getInstance().detections = 0;
} }
}.runTaskTimerAsynchronously(this, 12000, 12000); }.runTaskTimerAsynchronously(plugin, 12000, 12000);
} }
Bukkit.getLogger().info("Setting up and registering commands..."); Bukkit.getLogger().info("Setting up and registering commands...");
// We need access to the commandMap to register our commands without using the "proper" method // We need access to the commandMap to register our commands without using the "proper" method
if (pluginInstance.getServer().getPluginManager() instanceof SimplePluginManager manager) { if (Bukkit.getServer().getPluginManager() instanceof SimplePluginManager manager) {
try { try {
Field field = SimplePluginManager.class.getDeclaredField("commandMap"); Field field = SimplePluginManager.class.getDeclaredField("commandMap");
field.setAccessible(true); field.setAccessible(true);
@@ -73,7 +104,7 @@ public class BukkitPlugin extends JavaPlugin {
registeredCommands.add(newCommand); registeredCommands.add(newCommand);
// This tells Bukkit to register our command for use. // 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 //TODO Finish system before implementing on startup
@@ -83,7 +114,7 @@ public class BukkitPlugin extends JavaPlugin {
.get()); .get());
AntiVPN.getInstance().getMessageHandler().reloadStrings();*/ AntiVPN.getInstance().getMessageHandler().reloadStrings();*/
reloadConfig(); plugin.reloadConfig();
} }
@Override @Override
@@ -109,19 +140,19 @@ public class BukkitPlugin extends JavaPlugin {
} }
Bukkit.getLogger().info("Unregistering listeners..."); Bukkit.getLogger().info("Unregistering listeners...");
HandlerList.unregisterAll(this); HandlerList.unregisterAll(plugin);
Bukkit.getLogger().info("Cancelling any running tasks..."); Bukkit.getLogger().info("Cancelling any running tasks...");
Bukkit.getScheduler().cancelTasks(this); Bukkit.getScheduler().cancelTasks(plugin);
} }
private String getDatabaseType() { private String getDatabaseType() {
VPNDatabase database = AntiVPN.getInstance().getDatabase(); VPNDatabase database = AntiVPN.getInstance().getDatabase();
if(database instanceof H2VPN) { if(database instanceof MySqlVPN) {
return "H2";
} else if(database instanceof MySqlVPN) {
return "MySQL"; return "MySQL";
} else if(database instanceof H2VPN) {
return "H2";
} else if(database instanceof MongoVPN) { } else if(database instanceof MongoVPN) {
return "MongoDB"; return "MongoDB";
} else { } else {
@@ -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; package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.utils.MiscUtils; import dev.brighten.antivpn.utils.MiscUtils;
@@ -35,7 +51,7 @@ public class PlayerCommandRunner {
public void run() { public void run() {
action.getAction().run(); action.getAction().run();
} }
}.runTask(BukkitPlugin.pluginInstance); }.runTask(BukkitPlugin.pluginInstance.getPlugin());
playerActions.poll(); 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; package dev.brighten.antivpn.bukkit.command;
import dev.brighten.antivpn.AntiVPN; 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");
}
}
-95
View File
@@ -1,95 +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.4</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>17</source>
<target>17</target>
<compilerArgument>-XDignore.symbol.file</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<!-- Replace this with your package! -->
<shadedPattern>dev.brighten.antivpn.bukkit.org.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>org.yaml.snakeyaml</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.org.yaml.snakeyaml</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
</repositories>
<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.4</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,133 +0,0 @@
package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.APIPlayer;
import dev.brighten.antivpn.api.CheckResult;
import dev.brighten.antivpn.api.OfflinePlayer;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.message.VpnString;
import dev.brighten.antivpn.utils.StringUtil;
import dev.brighten.antivpn.utils.Tuple;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.logging.Level;
@SuppressWarnings("unchecked")
public class BukkitListener extends VPNExecutor implements Listener {
@Override
public void registerListeners() {
BukkitPlugin.pluginInstance.getServer().getPluginManager()
.registerEvents(this, BukkitPlugin.pluginInstance);
}
@Override
public void log(Level level, String log, Object... objects) {
Bukkit.getLogger().log(level, String.format(log, objects));
}
@Override
public void log(String log, Object... objects) {
log(Level.INFO, String.format(log, objects));
}
@Override
public void logException(String message, Throwable ex) {
Bukkit.getLogger().log(Level.SEVERE, message, ex);
}
@Override
public void runCommand(String command) {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(),
ChatColor.translateAlternateColorCodes('&', command));
}
@Override
public void disablePlugin() {
HandlerList.unregisterAll(this);
BukkitPlugin.pluginInstance.getServer().getPluginManager().disablePlugin(BukkitPlugin.pluginInstance);
}
@EventHandler(priority = EventPriority.HIGH)
public void onLogin(final PlayerLoginEvent event) {
APIPlayer player = AntiVPN.getInstance().getPlayerExecutor().getPlayer(event.getPlayer().getUniqueId())
.orElse(new OfflinePlayer(
event.getPlayer().getUniqueId(),
event.getPlayer().getName(),
event.getAddress()
));
CheckResult instantResult = player.checkPlayer(result -> {
if(!result.resultType().isShouldBlock()) return;
AntiVPN.getInstance().getExecutor().log(Level.INFO, "Adding %s to kick", event.getPlayer().getName());
AntiVPN.getInstance().getExecutor().getToKick().add(new Tuple<>(result, event.getPlayer().getUniqueId()));
});
if(!instantResult.resultType().isShouldBlock()) return;
AntiVPN.getInstance().getExecutor().getToKick()
.add(new Tuple<>(instantResult, event.getPlayer().getUniqueId()));
if(!AntiVPN.getInstance().getVpnConfig().kickPlayersOnDetect()) {
return;
}
AntiVPN.getInstance().getExecutor().log(Level.INFO, "%s was kicked from pre-login cache with IP %s", event.getPlayer().getName(), instantResult.response().getIp());
event.setResult(PlayerLoginEvent.Result.KICK_BANNED);
switch (instantResult.resultType()) {
case DENIED_COUNTRY -> event.setKickMessage(StringUtil.translateAlternateColorCodes('&',
StringUtil.varReplace(
AntiVPN.getInstance().getVpnConfig().countryVanillaKickReason(),
player,
instantResult.response()
)));
case DENIED_PROXY -> {
if(AntiVPN.getInstance().getVpnConfig().alertToStaff()) {
AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream()
.filter(APIPlayer::isAlertsEnabled)
.forEach(pl ->
pl.sendMessage(StringUtil.varReplace(
ChatColor.translateAlternateColorCodes(
'&',
AntiVPN.getInstance().getVpnConfig().alertMessage()),
player,
instantResult.response())));
}
event.setKickMessage(StringUtil.translateAlternateColorCodes('&',
StringUtil.varReplace(
AntiVPN.getInstance().getVpnConfig().getKickString(),
player,
instantResult.response()
)));
}
}
}
@EventHandler(priority = EventPriority.MONITOR)
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 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 name: KauriVPN
main: dev.brighten.antivpn.bungee.BungeePlugin main: dev.brighten.antivpn.bungee.BungeeLoaderPlugin
description: A simple and fast antivpn plugin. description: A simple and fast antivpn plugin.
version: ${project.version} version: ${project.version}
author: funkemunky 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
@@ -1,12 +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.bungee; package dev.brighten.antivpn.bungee;
import dev.brighten.antivpn.AntiVPN; import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.*; import dev.brighten.antivpn.api.*;
import dev.brighten.antivpn.utils.MiscUtils; import dev.brighten.antivpn.utils.MiscUtils;
import dev.brighten.antivpn.utils.StringUtil; import dev.brighten.antivpn.utils.StringUtil;
import dev.brighten.antivpn.utils.Tuple;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TextComponent; 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.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PreLoginEvent; import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Listener;
@@ -25,7 +40,7 @@ public class BungeeListener extends VPNExecutor implements Listener {
@Override @Override
public void registerListeners() { public void registerListeners() {
BungeePlugin.pluginInstance.getProxy().getPluginManager() BungeePlugin.pluginInstance.getProxy().getPluginManager()
.registerListener(BungeePlugin.pluginInstance, this); .registerListener(BungeePlugin.pluginInstance.getPlugin(), this);
} }
@Override @Override
@@ -35,7 +50,7 @@ public class BungeeListener extends VPNExecutor implements Listener {
@Override @Override
public void log(String log, Object... objects) { public void log(String log, Object... objects) {
log(Level.INFO, String.format(log, objects)); log(Level.INFO, log, objects);
} }
@Override @Override
@@ -51,12 +66,12 @@ public class BungeeListener extends VPNExecutor implements Listener {
@Override @Override
public void disablePlugin() { public void disablePlugin() {
BungeePlugin.pluginInstance.getProxy().getPluginManager().unregisterListeners(BungeePlugin.pluginInstance); BungeePlugin.pluginInstance.getProxy().getPluginManager().unregisterListeners(BungeePlugin.pluginInstance.getPlugin());
if (cacheResetTask != null) { if (cacheResetTask != null) {
cacheResetTask.cancel(); cacheResetTask.cancel();
cacheResetTask = null; cacheResetTask = null;
} }
BungeePlugin.pluginInstance.getProxy().getPluginManager().unregisterCommands(BungeePlugin.pluginInstance); BungeePlugin.pluginInstance.getProxy().getPluginManager().unregisterCommands(BungeePlugin.pluginInstance.getPlugin());
BungeePlugin.pluginInstance.onDisable(); BungeePlugin.pluginInstance.onDisable();
} }
@@ -73,43 +88,31 @@ public class BungeeListener extends VPNExecutor implements Listener {
((InetSocketAddress) event.getConnection().getSocketAddress()).getAddress()); ((InetSocketAddress) event.getConnection().getSocketAddress()).getAddress());
}); });
CheckResult instantResult = player.checkPlayer(result -> { player.checkPlayer(result -> {
if (!result.resultType().isShouldBlock()) return; if (!result.resultType().isShouldBlock()) return;
AntiVPN.getInstance().getExecutor().getToKick()
.add(new Tuple<>(result, player.getUuid())); 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())));
}); });
}
if (!instantResult.resultType().isShouldBlock()) { @EventHandler(priority = EventPriority.HIGH)
return; public void onJoin(LoginEvent event) {
} if(event.isCancelled()) return;
AntiVPN.getInstance().getExecutor().getToKick() // Handling player alerts on join
.add(new Tuple<>(instantResult, player.getUuid())); AntiVPN.getInstance().getPlayerExecutor().getPlayer(event.getConnection().getUniqueId())
.ifPresent(APIPlayer::checkAlertsState);
if (!AntiVPN.getInstance().getVpnConfig().kickPlayersOnDetect()) {
return;
}
event.setCancelled(true);
AntiVPN.getInstance().getExecutor().log(Level.INFO,
"%s was kicked from pre-login proxy cache.",
event.getConnection().getName());
switch (instantResult.resultType()) {
case DENIED_PROXY -> event.setReason(TextComponent.fromLegacy(ChatColor
.translateAlternateColorCodes('&',
StringUtil.varReplace(
AntiVPN.getInstance().getVpnConfig().getKickString(),
player,
instantResult.response()))));
case DENIED_COUNTRY -> event.setReason(TextComponent.fromLegacy(ChatColor
.translateAlternateColorCodes('&',
StringUtil.varReplace(
AntiVPN.getInstance().getVpnConfig().countryVanillaKickReason(),
player,
instantResult.response()))));
}
} }
@EventHandler @EventHandler
@@ -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; package dev.brighten.antivpn.bungee;
import dev.brighten.antivpn.api.APIPlayer; import dev.brighten.antivpn.api.APIPlayer;
@@ -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; package dev.brighten.antivpn.bungee;
import dev.brighten.antivpn.api.APIPlayer; import dev.brighten.antivpn.api.APIPlayer;
@@ -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; package dev.brighten.antivpn.bungee.command;
import dev.brighten.antivpn.AntiVPN; 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; package dev.brighten.antivpn.bungee.command;
import dev.brighten.antivpn.AntiVPN; 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());
}
}
-88
View File
@@ -1,88 +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.4</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>17</source>
<target>17</target>
<compilerArgument>-XDignore.symbol.file</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<!-- Replace this with your package! -->
<shadedPattern>dev.brighten.antivpn.bungee.org.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>org.yaml.snakeyaml</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.org.yaml.snakeyaml</shadedPattern>
</relocation>
</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>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>dev.brighten.antivpn</groupId>
<artifactId>Common</artifactId>
<version>1.9.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.21-R0.2</version>
<type>jar</type>
<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,64 +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 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 net.md_5.bungee.api.plugin.Plugin;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
import java.util.concurrent.TimeUnit;
public class BungeePlugin extends Plugin {
public static BungeePlugin pluginInstance;
@Override
public void onEnable() {
pluginInstance = this;
//Setting up config
getProxy().getLogger().info("Loading config...");
//Loading plugin
getProxy().getLogger().info("Starting AntiVPN services...");
AntiVPN.start(new BungeeListener(), new BungeePlayerExecutor(), getDataFolder());
if(AntiVPN.getInstance().getVpnConfig().metrics()) {
getProxy().getLogger().info("Starting bStats metrics...");
Metrics metrics = new Metrics(this, 12616);
metrics.addCustomChart(new SimplePie("database_used", this::getDatabaseType));
getProxy().getScheduler().schedule(this,
() -> AntiVPN.getInstance().checked = AntiVPN.getInstance().detections = 0,
10, 10, TimeUnit.MINUTES);
}
for (Command command : AntiVPN.getInstance().getCommands()) {
getProxy().getPluginManager().registerCommand(pluginInstance, new BungeeCommand(command));
}
}
@Override
public void onDisable() {
AntiVPN.getInstance().stop();
}
private String getDatabaseType() {
VPNDatabase database = AntiVPN.getInstance().getDatabase();
if(database instanceof H2VPN) {
return "H2";
} else if(database instanceof MySqlVPN) {
return "MySQL";
} else if(database instanceof MongoVPN) {
return "MongoDB";
} else {
return "No-Database";
}
}
}
+33 -23
View File
@@ -4,36 +4,46 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.10.0] - 2026-04-07
### Added
- CIDR allowlisting, including commands to add, remove, view, and search entries
- MongoDB support for CIDR allowlist storage
- VPN detection webhooks with Discord and Slack formatting options
- Mojang API fallback support for player lookups
- Folia support
### Changed
- Improved player blocking so flagged users are removed more reliably across platforms
- Updated allowlist handling to validate CIDR entries more consistently
- Improved database cleanup for outdated cached responses
### Fixed
- SQL startup and loading issues, including MySQL library injection problems
- CIDR parsing issues and MongoDB CIDR lookup failures
- Allowlist-related SQL errors
- Repeated webhook spam from duplicate VPN detection events
### Documentation
- Expanded webhook setup documentation for Discord and Slack
## [1.9.4] - 2025-09-30 ## [1.9.4] - 2025-09-30
### Added ### Added
- New dependency management system with automatic library loading and relocation - Sponge platform support
- Caffeine cache implementation to replace Guava - UUID lookup support for player validation
- Sponge platform support with full event handling and command system - Better scheduled kick checking
- UUID lookup functionality for player validation - Java 17 and Java 21 support
- Enhanced kick checking system with scheduled task execution - Database metrics tracking for bStats
- Support for Java 17 and Java 21 runtime environments
- New database metrics tracking for bStats
### Changed ### Changed
- **BREAKING**: Minimum Java version upgraded from 8 to 17 - **BREAKING**: Minimum Java version upgraded from 8 to 17
- Replaced Guava cache with Caffeine cache for better performance - Replaced the old cache implementation with Caffeine for better performance
- Modernized player checking system with asynchronous processing - Improved asynchronous player checking and VPN detection handling
- Improved database connection handling with proper resource management - Improved database connection management and error handling
- Enhanced VPN/Proxy detection with new `CheckResult` and `ResultType` system
- Updated Maven dependencies and build process
- Reorganized project structure (Assembly → Universal module)
- Improved error handling and exception logging throughout codebase
### Fixed ### Fixed
- H2 database compatibility issues with automatic backup and recovery - H2 database compatibility issues with automatic backup and recovery
- Memory leaks in database result set handling with try-with-resources - Memory leaks and resource cleanup problems in database handling
- Thread safety issues in player cache management - Thread safety issues in player cache management
- Command registration and unregistration during plugin lifecycle - Command registration issues during plugin startup and shutdown
- Proper cleanup of database drivers on shutdown
- Resource management in SQL connections and prepared statements
### Removed
- Guava dependency (replaced with Caffeine and built-in utilities)
- Legacy cached response handling system
- Old table format compatibility code
+50
View File
@@ -0,0 +1,50 @@
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('')
dependencies {
exclude 'dev/brighten/antivpn/depends/Relocate*'
exclude 'dev/brighten/antivpn/depends/MavenLibraries*'
}
}
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; package dev.brighten.antivpn;
import dev.brighten.antivpn.api.PlayerExecutor; import dev.brighten.antivpn.api.PlayerExecutor;
@@ -11,7 +27,6 @@ import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.sql.MySqlVPN; import dev.brighten.antivpn.database.sql.MySqlVPN;
import dev.brighten.antivpn.depends.LibraryLoader; import dev.brighten.antivpn.depends.LibraryLoader;
import dev.brighten.antivpn.depends.MavenLibrary; import dev.brighten.antivpn.depends.MavenLibrary;
import dev.brighten.antivpn.depends.Relocate;
import dev.brighten.antivpn.message.MessageHandler; import dev.brighten.antivpn.message.MessageHandler;
import dev.brighten.antivpn.utils.ConfigDefault; import dev.brighten.antivpn.utils.ConfigDefault;
import dev.brighten.antivpn.utils.MiscUtils; import dev.brighten.antivpn.utils.MiscUtils;
@@ -32,25 +47,13 @@ import java.util.List;
@Getter @Getter
@Setter(AccessLevel.PRIVATE) @Setter(AccessLevel.PRIVATE)
@MavenLibrary(groupId = "com.h2database", artifactId ="h2", version = "2.2.220", relocations = { @MavenLibrary(groupId = "com.h2database", artifactId ="h2", version = "2.2.220")
@Relocate(from ="org" + ".\\h2", to ="dev.brighten.antivpn.shaded.org.h2")}) @MavenLibrary(groupId = "org.mongodb", artifactId = "mongo-java-driver", version = "3.12.14")
@MavenLibrary(groupId = "org.mongodb", artifactId = "mongo-java-driver", version = "3.12.14", relocations = {
@Relocate(from = "com." + "\\mongodb", to = "dev.brighten.antivpn.shaded.com.mongodb"),
@Relocate(from = "org" + "\\.bson", to = "dev.brighten.antivpn.shaded.org.bson")
})
@MavenLibrary( @MavenLibrary(
groupId = "com.mysql", groupId = "com.mysql",
artifactId = "mysql-connector-j", artifactId = "mysql-connector-j",
version = "9.1.0", version = "9.3.0"
relocations = {
@Relocate(from = "com.my\\" + "sql.cj", to = "dev.brighten.antivpn.shaded.com.mysql.cj"),
@Relocate(from = "com.my\\" + "sql.jdbc", to = "dev.brighten.antivpn.shaded.com.mysql.jdbc")
}
) )
@MavenLibrary(groupId = "com.\\github\\.ben-manes\\.caffeine", artifactId = "caffeine", version = "3.1.8",
relocations = {
@Relocate(from = "com\\.github\\.benmanes\\.caffeine", to = "dev.brighten.antivpn.shaded.com.github.benmanes.caffeine"),
})
public class AntiVPN { public class AntiVPN {
private static AntiVPN INSTANCE; private static AntiVPN INSTANCE;
@@ -109,7 +112,7 @@ public class AntiVPN {
break; break;
} }
case "mysql": case "mysql":
case "sql":{ case "sql": {
AntiVPN.getInstance().getExecutor().log("Using databaseType MySQL..."); AntiVPN.getInstance().getExecutor().log("Using databaseType MySQL...");
INSTANCE.database = new MySqlVPN(); INSTANCE.database = new MySqlVPN();
INSTANCE.database.init(); INSTANCE.database.init();
@@ -190,8 +193,12 @@ public class AntiVPN {
executor.log("Failed to deregister H2 driver: " + e.getMessage()); executor.log("Failed to deregister H2 driver: " + e.getMessage());
} }
} }
VPNExecutor.threadExecutor.shutdown(); if (executor != null && executor.getThreadExecutor() != null) {
executor.getThreadExecutor().shutdown();
}
if(database != null) database.shutdown(); if(database != null) database.shutdown();
INSTANCE = null;
} }
public void reloadDatabase() { public void reloadDatabase() {
@@ -1,8 +1,25 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.api; package dev.brighten.antivpn.api;
import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import dev.brighten.antivpn.AntiVPN; import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.message.VpnString;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -40,23 +57,45 @@ public abstract class APIPlayer {
public void updateAlertsState() { public void updateAlertsState() {
//Updating into database so its synced across servers and saved on logout. //Updating into database so its synced across servers and saved on logout.
AntiVPN.getInstance().getDatabase().updateAlertsState(uuid, alertsEnabled); AntiVPN.getInstance().getDatabase().updateAlertsState(uuid, alertsEnabled);
sendMessage(AntiVPN.getInstance().getMessageHandler()
.getString("command-alerts-toggled")
.getFormattedMessage(new VpnString.Var<>("state", alertsEnabled)));
} }
public CheckResult checkPlayer(Consumer<CheckResult> onKick) { 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 if (hasPermission("antivpn.bypass") //Has bypass permission
//Is exempt //Is exempt
|| (uuid != null && AntiVPN.getInstance().getExecutor().isWhitelisted(uuid)) || (uuid != null && AntiVPN.getInstance().getExecutor().isWhitelisted(uuid))
//Or has a name that starts with a certain prefix. This is for Bedrock exempting. //Or has a name that starts with a certain prefix. This is for Bedrock exempting.
|| AntiVPN.getInstance().getExecutor().isWhitelisted(ip.getHostAddress()) || AntiVPN.getInstance().getExecutor().isWhitelisted(ip.getHostAddress() + "/32")
|| AntiVPN.getInstance().getVpnConfig().getPrefixWhitelists().stream() || AntiVPN.getInstance().getVpnConfig().getPrefixWhitelists().stream()
.anyMatch(name::startsWith)) return new CheckResult(null, ResultType.WHITELISTED); .anyMatch(name::startsWith)) {
onResult.accept(new CheckResult(null, ResultType.WHITELISTED, false));
return;
}
CheckResult cachedResult = checkResultCache.getIfPresent(ip.getHostAddress()); CheckResult cachedResult = checkResultCache.getIfPresent(ip.getHostAddress());
if(cachedResult != null) { if(cachedResult != null) {
if(cachedResult.response().getIp().equals(ip.getHostAddress())) { if(cachedResult.response().getIp().equals(ip.getHostAddress())) {
AntiVPN.getInstance().getExecutor().log(Level.FINE, "Cached result for " + ip.getHostAddress() + " is " + cachedResult.resultType()); AntiVPN.getInstance().getExecutor().log(Level.FINE, "Cached result for " + ip.getHostAddress() + " is " + cachedResult.resultType());
return cachedResult; if(cachedResult.resultType().isShouldBlock()) {
AntiVPN.getInstance().getExecutor().handleKickingOfPlayer(cachedResult, this);
}
onResult.accept(cachedResult);
return;
} }
} }
@@ -66,36 +105,41 @@ public abstract class APIPlayer {
AntiVPN.getInstance().getExecutor().log(Level.WARNING, "The API query was not a success! " + AntiVPN.getInstance().getExecutor().log(Level.WARNING, "The API query was not a success! " +
"You may need to upgrade your license on " + "You may need to upgrade your license on " +
"https://funkemunky.cc/shop"); "https://funkemunky.cc/shop");
onResult.accept(new CheckResult(null, ResultType.API_FAILURE, false));
return;
} }
// If the countryList() size is zero, no need to check. // If the countryList() size is zero, no need to check.
// Running country check first // Running country check first
CheckResult checkResult; CheckResult checkResult;
if (!AntiVPN.getInstance().getVpnConfig().countryList().isEmpty() if (!AntiVPN.getInstance().getVpnConfig().getCountryList().isEmpty()
&& !((uuid != null && AntiVPN.getInstance().getExecutor() && !((uuid != null && AntiVPN.getInstance().getExecutor()
.isWhitelisted(uuid)) .isWhitelisted(uuid))
//Or has a name that starts with a certain prefix. This is for Bedrock exempting. //Or has a name that starts with a certain prefix. This is for Bedrock exempting.
|| AntiVPN.getInstance().getExecutor().isWhitelisted(ip.getHostAddress())) || AntiVPN.getInstance().getExecutor().isWhitelisted(ip.getHostAddress() + "/32"))
// This bit of code will decide whether or not to kick the player // 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 // 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 // as they are equal and vise versa. However, if the contains does not match
// the state, it will kick. // the state, it will kick.
&& AntiVPN.getInstance().getVpnConfig().countryList() && AntiVPN.getInstance().getVpnConfig().getCountryList()
.contains(result.getCountryCode()) .contains(result.getCountryCode())
!= AntiVPN.getInstance().getVpnConfig().whitelistCountries()) { != AntiVPN.getInstance().getVpnConfig().getWhitelistCountries()) {
//Using our built in kicking system if no commands are configured //Using our built in kicking system if no commands are configured
checkResult = new CheckResult(result, ResultType.DENIED_COUNTRY); checkResult = new CheckResult(result, ResultType.DENIED_COUNTRY, false);
} else if (result.isProxy()) { } else if (result.isProxy()) {
checkResult = new CheckResult(result, ResultType.DENIED_PROXY); checkResult = new CheckResult(result, ResultType.DENIED_PROXY, false);
} else { } else {
checkResult = new CheckResult(result, ResultType.ALLOWED); checkResult = new CheckResult(result, ResultType.ALLOWED, false);
} }
AntiVPN.getInstance().getExecutor().log(Level.FINE, "Result for " + ip.getHostAddress() + " is " + checkResult.resultType()); AntiVPN.getInstance().getExecutor().log(Level.FINE, "Result for " + ip.getHostAddress() + " is " + checkResult.resultType());
checkResultCache.put(ip.getHostAddress(), checkResult); checkResultCache.put(ip.getHostAddress(), new CheckResult(checkResult.response(), checkResult.resultType(), true));
onKick.accept(checkResult); if(checkResult.resultType().isShouldBlock()) {
AntiVPN.getInstance().getExecutor().handleKickingOfPlayer(checkResult, this);
}
onResult.accept(checkResult);
AntiVPN.getInstance().checked++; AntiVPN.getInstance().checked++;
}); });
return new CheckResult(null, ResultType.UNKNOWN); 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; package dev.brighten.antivpn.api;
import dev.brighten.antivpn.AntiVPN; import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.utils.ConfigDefault; import dev.brighten.antivpn.utils.ConfigDefault;
import lombok.Getter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -52,61 +69,60 @@ public class VPNConfig {
defCountrylist = new ConfigDefault<>(new ArrayList<>(), "countries.list", defCountrylist = new ConfigDefault<>(new ArrayList<>(), "countries.list",
AntiVPN.getInstance()); AntiVPN.getInstance());
private String license, kickMessage, databaseType, databaseName, mongoURL, username, password, ip, alertMsg, @Getter
countryVanillaKickReason; private String license;
private List<String> prefixWhitelists, commands, countryList, countryKickCommands; @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 int port;
private boolean cacheResults, databaseEnabled, useCredentials, commandsEnabled, kickPlayers, alertToStaff, private boolean cacheResults;
metrics, whitelistCountries; @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. * If true, results will be cached to reduce queries to <a href="https://funkemunky.cc">...</a>
* @return String
*/
public String getLicense() {
return license;
}
/**
* If true, results will be cached to reduce queries to https://funkemunky.cc
* @return boolean * @return boolean
*/ */
public boolean cachedResults() { public boolean cachedResults() {
return cacheResults; 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. * If true, staff will be alerted on proxy detection.
* @return boolean * @return boolean
*/ */
public boolean alertToStaff() { public boolean isAlertToSTaff() {
return alertToStaff; 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. * Commands to run on proxy detection.
* @return List * @return List
@@ -115,31 +131,6 @@ public class VPNConfig {
return commands; 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. * Whether or not the database we want to connect to requires credentials.
* @return boolean * @return boolean
@@ -156,59 +147,11 @@ public class VPNConfig {
return mongoURL; 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. * If true, we only allow the {@link VPNConfig#countryKickCommands()}. If false, we blacklist them.
* @return boolean * @return boolean
*/ */
public boolean whitelistCountries() { public boolean getWhitelistCountries() {
return whitelistCountries; return whitelistCountries;
} }
@@ -220,14 +163,6 @@ public class VPNConfig {
return countryKickCommands; 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 * Gets the port based on configuration. If {@link VPNConfig#port} is -1, will get default port
* based on {@link VPNConfig#getDatabaseType()} lowerCase(). * 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 * @return boolean
*/ */
public boolean metrics() { public boolean metrics() {
@@ -287,4 +222,4 @@ public class VPNConfig {
countryVanillaKickReason = defaultCountryKickReason.get(); countryVanillaKickReason = defaultCountryKickReason.get();
} }
} }
@@ -1,6 +1,25 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.api; 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.AntiVPN;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.utils.StringUtil; import dev.brighten.antivpn.utils.StringUtil;
import dev.brighten.antivpn.utils.Tuple; import dev.brighten.antivpn.utils.Tuple;
import dev.brighten.antivpn.utils.json.JSONException; import dev.brighten.antivpn.utils.json.JSONException;
@@ -9,23 +28,20 @@ import dev.brighten.antivpn.web.objects.VPNResponse;
import lombok.Getter; import lombok.Getter;
import java.io.IOException; import java.io.IOException;
import java.net.UnknownHostException;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level; import java.util.logging.Level;
@Getter
public abstract class VPNExecutor { public abstract class VPNExecutor {
public static ScheduledExecutorService threadExecutor = Executors.newScheduledThreadPool(2); private final ScheduledExecutorService threadExecutor = Executors.newScheduledThreadPool(2);
@Getter
private final Set<UUID> whitelisted = Collections.synchronizedSet(new HashSet<>()); private final Set<UUID> whitelisted = Collections.synchronizedSet(new HashSet<>());
@Getter private final Set<CIDRUtils> whitelistedIps = Collections.synchronizedSet(new HashSet<>());
private final Set<String> 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;
@Getter
private final List<Tuple<CheckResult, UUID>> toKick = Collections.synchronizedList(new LinkedList<>());
public abstract void registerListeners(); public abstract void registerListeners();
@@ -42,15 +58,13 @@ public abstract class VPNExecutor {
} }
public void startKickChecks() { public void startKickChecks() {
threadExecutor.scheduleAtFixedRate(() -> { kickTask = threadExecutor.scheduleAtFixedRate(() -> {
synchronized (toKick) { synchronized (toKick) {
if(toKick.isEmpty()) return; if(toKick.isEmpty()) return;
Iterator<Tuple<CheckResult, UUID>> i = toKick.iterator(); Tuple<CheckResult, UUID> toCheck;
while(i.hasNext()) {
var toCheck = i.next();
while((toCheck = toKick.poll()) != null) {
Optional<APIPlayer> player = AntiVPN.getInstance().getPlayerExecutor().getPlayer(toCheck.second()); Optional<APIPlayer> player = AntiVPN.getInstance().getPlayerExecutor().getPlayer(toCheck.second());
if(player.isEmpty()) { if(player.isEmpty()) {
@@ -58,48 +72,62 @@ public abstract class VPNExecutor {
} }
handleKickingOfPlayer(toCheck.first(), player.get()); handleKickingOfPlayer(toCheck.first(), player.get());
i.remove();
} }
} }
}, 8, 2, TimeUnit.SECONDS); }, 8, 2, TimeUnit.SECONDS);
} }
public void handleKickingOfPlayer(CheckResult result, APIPlayer player) { public void handleKickingOfPlayer(CheckResult result, APIPlayer player) {
if (AntiVPN.getInstance().getVpnConfig().alertToStaff()) AntiVPN.getInstance().getPlayerExecutor()
//Ensuring kick task is always running
if(kickTask == null || kickTask.isDone() || kickTask.isCancelled()) {
startKickChecks();
}
if (AntiVPN.getInstance().getVpnConfig().isAlertToSTaff()) AntiVPN.getInstance().getPlayerExecutor()
.getOnlinePlayers() .getOnlinePlayers()
.stream() .stream()
.filter(APIPlayer::isAlertsEnabled) .filter(APIPlayer::isAlertsEnabled)
.forEach(pl -> .forEach(pl ->
pl.sendMessage(StringUtil.translateAlternateColorCodes('&', pl.sendMessage(StringUtil.translateAlternateColorCodes('&',
StringUtil.varReplace(dev.brighten.antivpn.AntiVPN.getInstance().getVpnConfig() StringUtil.varReplace(dev.brighten.antivpn.AntiVPN.getInstance().getVpnConfig()
.alertMessage(), player, result.response())))); .getAlertMsg(), player, result.response()))));
if(AntiVPN.getInstance().getVpnConfig().kickPlayersOnDetect()) { if(AntiVPN.getInstance().getVpnConfig().isKickPlayers()) {
switch (result.resultType()) { switch (result.resultType()) {
case DENIED_PROXY -> player.kickPlayer(StringUtil.varReplace(AntiVPN.getInstance().getVpnConfig() case DENIED_PROXY -> player.kickPlayer(StringUtil.varReplace(AntiVPN.getInstance().getVpnConfig()
.getKickString(), player, result.response())); .getKickMessage(), player, result.response()));
case DENIED_COUNTRY -> player.kickPlayer(StringUtil.varReplace(AntiVPN.getInstance().getVpnConfig() case DENIED_COUNTRY -> player.kickPlayer(StringUtil.varReplace(AntiVPN.getInstance().getVpnConfig()
.countryVanillaKickReason(), player, result.response())); .getCountryVanillaKickReason(), player, result.response()));
} }
} else {
if(!AntiVPN.getInstance().getVpnConfig().isCommandsEnabled()) return;
} }
if(!AntiVPN.getInstance().getVpnConfig().runCommands()) 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()));
}
}
}
};
switch (result.resultType()) { // Fixes the commands running too fast and causing messaging errors by any downstream plugins like LiteBans
case DENIED_PROXY -> { var scheduleResult = threadExecutor.schedule(runCommands, 1, TimeUnit.SECONDS);
for (String command : AntiVPN.getInstance().getVpnConfig().commands()) {
runCommand(StringUtil.translateAlternateColorCodes('&', if(scheduleResult.isCancelled()) {
StringUtil.varReplace(command, player, result.response()))); runCommands.run();
}
}
case DENIED_COUNTRY -> {
for (String command : AntiVPN.getInstance().getVpnConfig().countryKickCommands()) {
runCommand(StringUtil.translateAlternateColorCodes('&',
StringUtil.varReplace(command, player, result.response())));
}
}
} }
//Ensuring players are actually kicked as they are supposed to be.
toKick.add(new Tuple<>(result, player.getUuid()));
} }
public boolean isWhitelisted(UUID uuid) { public boolean isWhitelisted(UUID uuid) {
@@ -109,14 +137,29 @@ public abstract class VPNExecutor {
return whitelisted.contains(uuid); return whitelisted.contains(uuid);
} }
public boolean isWhitelisted(String ip) { public boolean isWhitelisted(String cidr) {
if(AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) { if(AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) {
return AntiVPN.getInstance().getDatabase().isWhitelisted(ip); return AntiVPN.getInstance().getDatabase().isWhitelisted(cidr);
}
try {
return whitelistedIps.contains(new CIDRUtils(cidr));
} catch (UnknownHostException e) {
throw new RuntimeException(e);
} }
return whitelistedIps.contains(ip);
} }
private final Cache<String, VPNResponse> cachedResponses = Caffeine.newBuilder()
.expireAfterWrite(20, TimeUnit.MINUTES)
.maximumSize(4000)
.build();
public CompletableFuture<VPNResponse> checkIp(String ip) { public CompletableFuture<VPNResponse> checkIp(String ip) {
VPNResponse cached = cachedResponses.getIfPresent(ip);
if(cached != null) {
return CompletableFuture.completedFuture(cached);
}
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
Optional<VPNResponse> cachedRes = AntiVPN.getInstance().getDatabase().getStoredResponse(ip); Optional<VPNResponse> cachedRes = AntiVPN.getInstance().getDatabase().getStoredResponse(ip);
@@ -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; package dev.brighten.antivpn.command.impl;
import dev.brighten.antivpn.AntiVPN; 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; package dev.brighten.antivpn.command.impl;
import dev.brighten.antivpn.AntiVPN; 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; package dev.brighten.antivpn.command.impl;
import dev.brighten.antivpn.AntiVPN; import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.command.Command; import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor; import dev.brighten.antivpn.command.CommandExecutor;
@@ -45,7 +62,7 @@ public class ClearCacheCommand extends Command {
@Override @Override
public String execute(CommandExecutor executor, String[] args) { 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!"; 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; package dev.brighten.antivpn.command.impl;
import dev.brighten.antivpn.AntiVPN; import dev.brighten.antivpn.AntiVPN;
@@ -1,3 +1,19 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.command.impl; package dev.brighten.antivpn.command.impl;
import dev.brighten.antivpn.AntiVPN; import dev.brighten.antivpn.AntiVPN;
@@ -51,7 +67,7 @@ public class PlanCommand extends Command {
@Override @Override
public String execute(CommandExecutor executor, String[] args) { public String execute(CommandExecutor executor, String[] args) {
VPNExecutor.threadExecutor.execute(() -> { AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> {
QueryResponse result; QueryResponse result;
try { try {
if(AntiVPN.getInstance().getVpnConfig().getLicense().isEmpty()) { 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; package dev.brighten.antivpn.command.impl;
import dev.brighten.antivpn.AntiVPN; 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;
}
}
}
@@ -0,0 +1,280 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.mongo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.mongodb.*;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.UpdateOptions;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.version.Version;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.web.objects.VPNResponse;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.Decimal128;
import java.math.BigDecimal;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
public class MongoVPN implements VPNDatabase {
public MongoCollection<Document> settingsDocument;
MongoCollection<Document> cacheDocument;
private MongoClient client;
public MongoDatabase antivpnDatabase;
public MongoVPN() {
AntiVPN.getInstance().getExecutor().getThreadExecutor().scheduleAtFixedRate(() -> {
if(!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) return;
//Refreshing whitelisted players
AntiVPN.getInstance().getExecutor().getWhitelisted().clear();
AntiVPN.getInstance().getExecutor().getWhitelisted()
.addAll(AntiVPN.getInstance().getDatabase().getAllWhitelisted());
//Refreshing whitlisted IPs
AntiVPN.getInstance().getExecutor().getWhitelistedIps().clear();
AntiVPN.getInstance().getExecutor().getWhitelistedIps()
.addAll(AntiVPN.getInstance().getDatabase().getAllWhitelistedIps());
}, 2, 30, TimeUnit.SECONDS);
}
@Override
public Optional<VPNResponse> getStoredResponse(String ip) {
Document rdoc = cacheDocument.find(Filters.eq("ip", ip)).first();
if(rdoc != null) {
long lastUpdate = rdoc.get("lastAccess", 0L);
if(System.currentTimeMillis() - lastUpdate > TimeUnit.HOURS.toMillis(1)) {
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> deleteResponse(ip));
return null;
}
return Optional.of(VPNResponse.builder().asn(rdoc.getString("asn")).ip(ip)
.countryName(rdoc.getString("countryName"))
.countryCode(rdoc.getString("countryCode"))
.city(rdoc.getString("city"))
.isp(rdoc.getString("isp"))
.method(rdoc.getString("method"))
.timeZone(rdoc.getString("timeZone"))
.proxy(rdoc.getBoolean("proxy"))
.cached(rdoc.getBoolean("cached"))
.success(true)
.latitude(rdoc.getDouble("latitude"))
.longitude(rdoc.getDouble("longitude"))
.lastAccess(rdoc.get("lastAccess", 0L))
.build());
}
return Optional.empty();
}
@Override
public void cacheResponse(VPNResponse toCache) {
if(AntiVPN.getInstance().getVpnConfig().cachedResults()) {
Document rdoc = new Document("ip", toCache.getIp());
rdoc.put("asn", toCache.getAsn());
rdoc.put("countryName", toCache.getCountryName());
rdoc.put("countryCode", toCache.getCountryCode());
rdoc.put("city", toCache.getCity());
rdoc.put("isp", toCache.getIsp());
rdoc.put("method", toCache.getMethod());
rdoc.put("timeZone", toCache.getTimeZone());
rdoc.put("proxy", toCache.isProxy());
rdoc.put("cached", toCache.isCached());
rdoc.put("success", toCache.isSuccess());
rdoc.put("latitude", toCache.getLatitude());
rdoc.put("longitude", toCache.getLongitude());
rdoc.put("lastAccess", System.currentTimeMillis());
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> {
Bson update = new Document("$set", rdoc);
cacheDocument.updateOne(Filters.eq("ip", toCache.getIp()), update,
new UpdateOptions().upsert(true));
});
}
}
@Override
public void deleteResponse(String ip) {
cacheDocument.deleteMany(Filters.eq("ip", ip));
}
@Override
public boolean isWhitelisted(UUID uuid) {
return settingsDocument
.find(Filters.and(Filters.eq("setting", "whitelist"),
Filters.eq("uuid", uuid.toString()))).first() != null;
}
@Override
public boolean isWhitelisted(String cidr) {
try {
return isWhitelisted(new CIDRUtils(cidr));
} catch (UnknownHostException e) {
AntiVPN.getInstance().getExecutor().log("Failed to check whitelist for IP: " + cidr, e);
return false;
}
}
@Override
public boolean isWhitelisted(CIDRUtils cidr) {
var start = new Decimal128(new BigDecimal(cidr.getStartIpInt()));
var end = new Decimal128(new BigDecimal(cidr.getEndIpInt()));
return settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"),
Filters.lte("ip_start", start), Filters.gte("ip_end", end))).first() != null;
}
@Override
public void addWhitelist(UUID uuid) {
Document wdoc = new Document("setting", "whitelist");
wdoc.put("uuid", uuid.toString());
AntiVPN.getInstance().getExecutor().getWhitelisted().add(uuid);
settingsDocument.insertOne(wdoc);
}
@Override
public void removeWhitelist(UUID uuid) {
AntiVPN.getInstance().getExecutor().getWhitelisted().remove(uuid);
settingsDocument.deleteMany(Filters
.and(
Filters.eq("setting", "whitelist"),
Filters.eq("uuid", uuid.toString())));
}
@Override
public void addWhitelist(CIDRUtils cidr) {
Document doc = new Document("setting", "whitelist");
doc.append("ip_start", new Decimal128(new BigDecimal(cidr.getStartIpInt())));
doc.append("ip_end", new Decimal128(new BigDecimal(cidr.getEndIpInt())));
doc.append("cidr_string", cidr.getCidr());
settingsDocument.insertOne(doc);
}
@Override
public void removeWhitelist(CIDRUtils cidr) {
settingsDocument.deleteMany(Filters
.and(
Filters.eq("setting", "whitelist"),
Filters.eq("ip_start", new Decimal128(new BigDecimal(cidr.getStartIpInt()))),
Filters.eq("ip_end", new Decimal128(new BigDecimal(cidr.getEndIpInt())))));
}
@Override
public List<UUID> getAllWhitelisted() {
List<UUID> uuids = new ArrayList<>();
settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"),
Filters.exists("uuid")))
.forEach((Consumer<? super Document>) doc -> uuids.add(UUID.fromString(doc.getString("uuid"))));
return uuids;
}
@Override
public List<CIDRUtils> getAllWhitelistedIps() {
List<CIDRUtils> ips = new ArrayList<>();
settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"),
Filters.exists("cidr_string"))).forEach((Consumer<? super Document>) doc -> {
try {
var cidr = new CIDRUtils(doc.getString("cidr_string"));
ips.add(cidr);
} catch (UnknownHostException e) {
AntiVPN.getInstance().getExecutor().logException("Could not format ip " + doc.getString("cidr_string") + " into a CIDR!", e);
}
});
return ips;
}
@Override
public void alertsState(UUID uuid, Consumer<Boolean> result) {
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(settingsDocument
.find(Filters.and(Filters.eq("setting", "alerts"),
Filters.eq("uuid", uuid.toString()))).first() != null));
}
@Override
public void updateAlertsState(UUID uuid, boolean state) {
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> {
settingsDocument.deleteMany(Filters.and(Filters.eq("setting", "alerts"),
Filters.eq("uuid", uuid.toString())));
if(state) {
Document adoc = new Document("setting", "alerts");
adoc.put("uuid", uuid.toString());
settingsDocument.insertOne(adoc);
}
});
}
@Override
public void clearResponses() {
cacheDocument.deleteMany(Filters.exists("ip"));
}
@Override
public void init() {
if(!AntiVPN.getInstance().getVpnConfig().mongoDatabaseURL().isEmpty()) { //URL
ConnectionString cs = new ConnectionString(AntiVPN.getInstance().getVpnConfig().mongoDatabaseURL());
MongoClientSettings settings = MongoClientSettings.builder().applyConnectionString(cs).build();
client = MongoClients.create(settings);
} else {
MongoClientSettings.Builder settingsBld = MongoClientSettings.builder().readPreference(ReadPreference.nearest())
.applyToClusterSettings(builder -> builder.
hosts(Collections.singletonList(
new ServerAddress(
AntiVPN.getInstance().getVpnConfig().getIp(),
AntiVPN.getInstance().getVpnConfig().getPort())
)));
if(AntiVPN.getInstance().getVpnConfig().useDatabaseCreds()) {
settingsBld.credential(MongoCredential
.createCredential(AntiVPN.getInstance().getVpnConfig().getUsername(),
AntiVPN.getInstance().getVpnConfig().getDatabaseName(),
AntiVPN.getInstance().getVpnConfig().getPassword().toCharArray()));
}
client = MongoClients.create(settingsBld.build());
}
antivpnDatabase = client.getDatabase(AntiVPN.getInstance().getVpnConfig().getDatabaseName());
settingsDocument = antivpnDatabase.getCollection("settings");
cacheDocument = antivpnDatabase.getCollection("cache");
for (Version<MongoVPN> mongoDbVersion : Version.mongoDbVersions) {
if(mongoDbVersion.needsUpdate(this)) {
mongoDbVersion.update(this);
}
}
}
@Override
public void shutdown() {
settingsDocument = null;
cacheDocument = null;
client.close();
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.mongo.version;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Indexes;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.DatabaseException;
import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.version.Version;
import org.bson.Document;
public class MongoFirst implements Version<MongoVPN> {
@Override
public void update(MongoVPN database) throws DatabaseException {
if(database.settingsDocument.listIndexes().first() == null) {
AntiVPN.getInstance().getExecutor().log("Created index for settings collection!");
database.settingsDocument.createIndex(Indexes.ascending("ip"));
database.settingsDocument.createIndex(Indexes.ascending("setting"));
}
var versionCollect = database.antivpnDatabase.getCollection("version");
versionCollect.insertOne(new Document("version", versionNumber()));
}
@Override
public int versionNumber() {
return 0;
}
@Override
public boolean needsUpdate(MongoVPN database) {
var versionCollect = database.antivpnDatabase.getCollection("version");
return versionCollect.find(Filters.eq("version", versionNumber())).first() == null;
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.mongo.version;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Indexes;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.DatabaseException;
import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.version.Version;
import dev.brighten.antivpn.utils.CIDRUtils;
import org.bson.Document;
import org.bson.types.Decimal128;
import java.math.BigDecimal;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class MongoSecond implements Version<MongoVPN> {
@Override
public void update(MongoVPN database) throws DatabaseException {
List<Document> backup = new ArrayList<>();
database.settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"),
Filters.exists("ip")))
.forEach((Consumer<? super Document>) doc -> {
backup.add(new Document(doc));
String ip = doc.getString("ip");
try {
var cidr = new CIDRUtils(ip + "/32");
doc.append("ip_start", new Decimal128(new BigDecimal(cidr.getStartIpInt())));
doc.append("ip_end", new Decimal128(new BigDecimal(cidr.getEndIpInt())));
doc.append("cidr_string", cidr.toString());
doc.remove("ip");
database.settingsDocument.replaceOne(Filters.eq("_id", doc.getObjectId("_id")), doc);
} catch (UnknownHostException e) {
rollback(backup, database);
throw new RuntimeException(e);
}
});
database.settingsDocument.createIndex(Indexes.compoundIndex(Indexes.ascending("ip_start"), Indexes.ascending("ip_end")));
database.settingsDocument.createIndex(Indexes.ascending("cidr_string"));
var versionCollect = database.antivpnDatabase.getCollection("version");
versionCollect.insertOne(new Document("version", versionNumber()));
}
private void rollback(List<Document> toRollback, MongoVPN database) {
AntiVPN.getInstance().getExecutor().log("Rolling back to version 0...");
toRollback.forEach(doc -> database.settingsDocument.replaceOne(Filters.eq("_id", doc.getObjectId("_id")), doc));
toRollback.clear();
}
@Override
public int versionNumber() {
return 1;
}
@Override
public boolean needsUpdate(MongoVPN database) {
var versionCollect = database.antivpnDatabase.getCollection("version");
return versionCollect.find(Filters.eq("version", versionNumber())).first() == null;
}
}
@@ -0,0 +1,108 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.mongo.version;
import com.mongodb.client.model.Filters;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.DatabaseException;
import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.version.Version;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.utils.MiscUtils;
import org.bson.Document;
import org.bson.types.Decimal128;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.logging.Level;
public class MongoThird implements Version<MongoVPN> {
@Override
public void update(MongoVPN database) throws DatabaseException {
List<CIDRUtils> ipRanges = new ArrayList<>();
List<CIDRUtils> rangesToInsert = new ArrayList<>();
List<BigInteger[]> rangesToRemove = new ArrayList<>();
database.settingsDocument.find(Filters.and(Filters.eq("setting", "whitelist"), Filters.exists("cidr_string")))
.forEach((Consumer<? super Document>) doc -> {
BigInteger start = doc.get("ip_start", Decimal128.class).bigDecimalValue().toBigInteger();
BigInteger end = doc.get("ip_end", Decimal128.class).bigDecimalValue().toBigInteger();
try {
var range = MiscUtils.rangeToCidrs(start, end);
if(range.size() > 1) {
rangesToRemove.add(new BigInteger[]{start, end});
rangesToInsert.addAll(range);
AntiVPN.getInstance().getExecutor().log(Level.WARNING, "Found multiple CIDR ranges for whitelist range for %s, %s!", start, end);
} else ipRanges.addAll(range);
} catch (UnknownHostException e) {
AntiVPN.getInstance().getExecutor().logException(
String.format("Could not convert ip range to CIDR! %s, %s", start, end), e);
}
});
if(!rangesToInsert.isEmpty()) {
AntiVPN.getInstance().getExecutor().log("Inserting %s new ranges into database...", rangesToInsert.size());
var documentsToInsert = rangesToInsert.stream().map(cidr -> {
Document doc = new Document("setting", "whitelist");
doc.append("ip_start", new Decimal128(new BigDecimal(cidr.getStartIpInt())));
doc.append("ip_end", new Decimal128(new BigDecimal(cidr.getEndIpInt())));
doc.append("cidr_string", cidr.getCidr());
return doc;
}).toList();
database.settingsDocument.insertMany(documentsToInsert);
}
if(!rangesToRemove.isEmpty()) {
AntiVPN.getInstance().getExecutor().log("Removing %s old ranges from database...", rangesToRemove.size());
rangesToRemove.forEach(range -> database.settingsDocument
.deleteMany(Filters.and(
Filters.gte("ip_start", new Decimal128(new BigDecimal(range[0]))),
Filters.lte("ip_end", new Decimal128(new BigDecimal(range[1]))))));
}
if(!ipRanges.isEmpty()) {
AntiVPN.getInstance().getExecutor().log("Updating %s CIDRs in database with proper notation...", ipRanges.size());
ipRanges.forEach(cidr -> database.settingsDocument
.updateMany(Filters.and(Filters.eq("setting", "whitelist"),
Filters.eq("ip_start", new Decimal128(new BigDecimal(cidr.getStartIpInt()))),
Filters.eq("ip_end", new Decimal128(new BigDecimal(cidr.getEndIpInt())))),
new Document("$set", new Document("cidr_string", cidr.getCidr()))));
}
var versionCollect = database.antivpnDatabase.getCollection("version");
versionCollect.insertOne(new Document("version", versionNumber()));
}
@Override
public int versionNumber() {
return 2;
}
@Override
public boolean needsUpdate(MongoVPN database) {
var versionCollect = database.antivpnDatabase.getCollection("version");
return versionCollect.find(Filters.eq("version", versionNumber())).first() == null;
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.sql;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.local.H2VPN;
import dev.brighten.antivpn.database.sql.utils.MySQL;
import dev.brighten.antivpn.database.version.Version;
import java.util.concurrent.TimeUnit;
public class MySqlVPN extends H2VPN {
public MySqlVPN() {
AntiVPN.getInstance().getExecutor().getThreadExecutor().scheduleAtFixedRate(() -> {
if(!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return;
//Refreshing whitelisted players
AntiVPN.getInstance().getExecutor().getWhitelisted().clear();
AntiVPN.getInstance().getExecutor().getWhitelisted()
.addAll(AntiVPN.getInstance().getDatabase().getAllWhitelisted());
//Refreshing whitlisted IPs
AntiVPN.getInstance().getExecutor().getWhitelistedIps().clear();
AntiVPN.getInstance().getExecutor().getWhitelistedIps()
.addAll(AntiVPN.getInstance().getDatabase().getAllWhitelistedIps());
}, 2, 30, TimeUnit.SECONDS);
}
@Override
public void init() {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled())
return;
AntiVPN.getInstance().getExecutor().log("Initializing MySQL...");
MySQL.init();
AntiVPN.getInstance().getExecutor().log("Checking for updates...");
//Running check for old table types to update
try {
for (Version<MySqlVPN> version : Version.mysqlVersions) {
if(version.needsUpdate(this)) {
version.update(this);
}
}
} catch (Exception e) {
throw new RuntimeException("Could not complete version setup due to SQL error", e);
}
}
}
@@ -1,53 +1,51 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.sql.utils; package dev.brighten.antivpn.database.sql.utils;
import dev.brighten.antivpn.utils.MiscUtils; import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import java.sql.*; import java.sql.*;
import java.util.UUID; import java.util.UUID;
public class ExecutableStatement { public class ExecutableStatement implements AutoCloseable {
private PreparedStatement statement; @Getter
private final PreparedStatement statement;
private int pos = 1; private int pos = 1;
public ExecutableStatement(PreparedStatement statement) { public ExecutableStatement(PreparedStatement statement) {
this.statement = statement; this.statement = statement;
} }
@SneakyThrows public int execute() throws SQLException {
public Integer execute() { return statement.executeUpdate();
try {
return statement.executeUpdate();
} finally {
MiscUtils.close(statement);
}
} }
@SneakyThrows public void execute(ResultSetIterator iterator) throws SQLException {
public void execute(ResultSetIterator iterator) { try(var rs = statement.executeQuery()) {
ResultSet rs = null;
try {
rs = statement.executeQuery();
while (rs.next()) iterator.next(rs); while (rs.next()) iterator.next(rs);
} finally {
MiscUtils.close(statement, rs);
} }
} }
@SneakyThrows public int[] executeBatch() throws SQLException {
public void executeSingle(ResultSetIterator iterator) { return statement.executeBatch();
ResultSet rs = null;
try {
rs = statement.executeQuery();
if (rs.next()) iterator.next(rs);
else iterator.next(null);
} finally {
MiscUtils.close(statement, rs);
}
} }
@SneakyThrows public ResultSet executeQuery() throws SQLException {
public ResultSet executeQuery() {
return statement.executeQuery(); return statement.executeQuery();
} }
@@ -135,4 +133,15 @@ public class ExecutableStatement {
statement.setBytes(pos++, obj); statement.setBytes(pos++, obj);
return this; 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> * Licensed under the Apache License, Version 2.0 (the "License");
* Copyright (c) contributors * 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 * http://www.apache.org/licenses/LICENSE-2.0
* 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 * Unless required by applicable law or agreed to in writing, software
* copies or substantial portions of the Software. * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * See the License for the specific language governing permissions and
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * limitations under the License.
* 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.database.sql.utils; package dev.brighten.antivpn.database.sql.utils;
@@ -0,0 +1,39 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.sql.utils;
import lombok.Getter;
import org.intellij.lang.annotations.Language;
import java.sql.Connection;
import java.sql.SQLException;
public class Query {
@Getter
private static Connection conn;
public static void use(Connection conn) {
Query.conn = conn;
}
@SuppressWarnings("SqlSourceToSinkFlow")
public static ExecutableStatement prepare(@Language("SQL") String sql) throws SQLException {
return new ExecutableStatement(conn.prepareStatement(sql));
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.sql.utils;
import java.sql.ResultSet;
import java.sql.SQLException;
public interface ResultSetIterator {
void next(ResultSet rs) throws SQLException;
}
@@ -0,0 +1,51 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.sql.version;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.DatabaseException;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.local.version.First;
import dev.brighten.antivpn.database.sql.utils.Query;
import java.sql.SQLException;
public class MySQLFirst extends First {
@Override
public void update(VPNDatabase database) throws DatabaseException {
try(var statement = Query.prepare("select `DATA_TYPE` from INFORMATION_SCHEMA.COLUMNS " +
"WHERE table_name = 'responses' AND COLUMN_NAME = 'isp';")) {
statement.execute(set -> {
if(set.getObject("DATA_TYPE").toString().contains("varchar")) {
AntiVPN.getInstance().getExecutor().log("Using old database format for storing responses! " +
"Dropping table and creating a new one...");
try(var state = Query.prepare("drop table `responses`")) {
if(state.execute() > 0) {
AntiVPN.getInstance().getExecutor().log("Successfully dropped table!");
}
}
}
});
} catch (SQLException e) {
throw new DatabaseException("Could not update MySQL database", e);
}
super.update(database);
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.version;
import dev.brighten.antivpn.database.DatabaseException;
import dev.brighten.antivpn.database.local.H2VPN;
import dev.brighten.antivpn.database.local.version.First;
import dev.brighten.antivpn.database.local.version.Second;
import dev.brighten.antivpn.database.local.version.Third;
import dev.brighten.antivpn.database.mongo.MongoVPN;
import dev.brighten.antivpn.database.mongo.version.MongoFirst;
import dev.brighten.antivpn.database.mongo.version.MongoSecond;
import dev.brighten.antivpn.database.mongo.version.MongoThird;
import dev.brighten.antivpn.database.sql.MySqlVPN;
import dev.brighten.antivpn.database.sql.version.MySQLFirst;
public interface Version<DB> {
void update(DB database) throws DatabaseException;
int versionNumber();
boolean needsUpdate(DB database);
Version<MongoVPN>[] mongoDbVersions = new Version[] {new MongoFirst(), new MongoSecond(), new MongoThird()};
Version<MySqlVPN>[] mysqlVersions = new Version[] {new MySQLFirst(), new Second(), new Third()};
Version<H2VPN>[] h2Versions = new Version[] {new First(), new Second(), new Third()};
}
@@ -0,0 +1,864 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.depends;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.utils.NonnullByDefault;
import dev.brighten.antivpn.utils.Supplier;
import dev.brighten.antivpn.utils.Suppliers;
import lombok.Getter;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.RecordComponentVisitor;
import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.commons.Remapper;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
/**
* Resolves {@link MavenLibrary} annotations for a class, and loads the dependency
* into the classloader.
*/
@SuppressWarnings("CallToPrintStackTrace")
@NonnullByDefault
public final class LibraryLoader {
private static final int RELOCATION_FORMAT_VERSION = 5;
private static final String RELOCATION_METADATA_PATH = "META-INF/antivpn-relocation.properties";
@SuppressWarnings("Guava")
private static final Supplier<URLClassLoaderAccess> URL_INJECTOR = AntiVPN.getInstance().getClass().getClassLoader() instanceof URLClassLoader ?
Suppliers.memoize(() ->
URLClassLoaderAccess.create((URLClassLoader) AntiVPN.getInstance().getClass().getClassLoader()))
: null;
public static void loadAll(Object object) {
if(URL_INJECTOR == null)
return;
loadAll(object.getClass());
}
public static void loadAll(Class<?> clazz) {
if(URL_INJECTOR == null)
return;
MavenLibrary[] libs = clazz.getDeclaredAnnotationsByType(MavenLibrary.class);
for (MavenLibrary lib : libs) {
// Create relocations map if any are defined
Map<String, String> relocations = new HashMap<>();
for (Relocate relocate : lib.relocations()) {
relocations.put(relocate.from().replace("\\", ""), relocate.to());
}
load(lib.groupId().replace("\\", ""), lib.artifactId(), lib.version(), lib.repo().url(), relocations);
}
}
public static void load(String groupId, String artifactId, String version, String repoUrl,
Map<String, String> relocations) {
load(new Dependency(groupId, artifactId, version, repoUrl), relocations);
}
public static void load(Dependency d, Map<String, String> relocations) {
System.out.printf("Loading dependency %s:%s:%s from %s%n",
d.getGroupId(), d.getArtifactId(), d.getVersion(), d.getRepoUrl());
String name = d.getArtifactId() + "-" + d.getVersion();
// If we have relocations, add a suffix to identify the relocated version
String fileName = name + ".jar";
if (!relocations.isEmpty()) {
fileName = name + "-relocated.jar";
}
File saveLocation = new File(getLibFolder(), fileName);
File originalJar = new File(getLibFolder(), name + ".jar");
// Download the original jar if it doesn't exist
if (!originalJar.exists()) {
try {
System.out.println("Dependency '" + name +
"' is not already in the libraries folder. Attempting to download...");
URL url = d.getUrl();
try (InputStream is = url.openStream()) {
Files.copy(is, originalJar.toPath());
}
System.out.println("Dependency '" + name + "' successfully downloaded.");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Unable to download dependency: " + d, e);
}
}
// Rebuild relocated jars when the relocation format changes or the cached jar is stale.
if (!relocations.isEmpty() && shouldRebuildRelocatedJar(saveLocation, relocations)) {
try {
System.out.println("Relocating packages for " + name + "...");
relocateJar(originalJar, saveLocation, relocations);
System.out.println("Successfully relocated packages for " + name);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Failed to relocate packages for dependency: " + d, e);
}
}
// Load the appropriate jar (original or relocated)
File jarToLoad = relocations.isEmpty() ? originalJar : saveLocation;
if (!jarToLoad.exists()) {
throw new RuntimeException("Unable to find dependency jar: " + jarToLoad.getAbsolutePath());
}
try {
URL_INJECTOR.get().addURL(jarToLoad.toURI().toURL());
} catch (Exception e) {
throw new RuntimeException("Unable to load dependency: " + jarToLoad, e);
}
System.out.println("Loaded dependency '" + name + "' successfully.");
}
private static void relocateJar(File sourceJar, File targetJar, Map<String, String> relocations)
throws IOException {
// Track service files to avoid duplicates
Map<String, StringBuilder> serviceFiles = new HashMap<>();
Files.deleteIfExists(targetJar.toPath());
try (JarFile jar = new JarFile(sourceJar);
JarOutputStream jos = new JarOutputStream(Files.newOutputStream(targetJar.toPath()))) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
// Skip directories
if (entry.isDirectory()) {
continue;
}
try (InputStream is = jar.getInputStream(entry)) {
if (name.startsWith("META-INF/services/")) {
// Process service files but don't write yet
processServiceFile(name, is, serviceFiles, relocations);
} else if (name.endsWith(".class")) {
// Relocate class file path as well as content
String relocatedPath = relocateClassPath(name, relocations);
JarEntry newEntry = new JarEntry(relocatedPath);
jos.putNextEntry(newEntry);
byte[] classBytes = readAllBytes(is);
byte[] relocatedBytes = relocateClass(name, classBytes, relocations);
jos.write(relocatedBytes);
jos.closeEntry();
} else {
// Relocate package-scoped resources so ResourceBundle lookups follow relocated packages.
String relocatedPath = relocateResourcePath(name, relocations);
JarEntry newEntry = new JarEntry(relocatedPath);
jos.putNextEntry(newEntry);
copyStream(is, jos);
jos.closeEntry();
}
}
}
// Now write all service files after processing
for (Map.Entry<String, StringBuilder> entry : serviceFiles.entrySet()) {
try {
JarEntry serviceEntry = new JarEntry(entry.getKey());
jos.putNextEntry(serviceEntry);
jos.write(entry.getValue().toString().getBytes(StandardCharsets.UTF_8));
jos.closeEntry();
} catch (Exception e) {
// Log but continue with other service files
System.out.println("Warning: Could not write service file " +
entry.getKey() + ": " + e.getMessage());
}
}
writeRelocationMetadata(jos, relocations);
}
validateRelocatedJar(targetJar, relocations);
}
private static boolean shouldRebuildRelocatedJar(File relocatedJar, Map<String, String> relocations) {
if (!relocatedJar.exists()) {
return true;
}
try (JarFile jar = new JarFile(relocatedJar)) {
JarEntry metadataEntry = jar.getJarEntry(RELOCATION_METADATA_PATH);
if (metadataEntry == null) {
return true;
}
Properties metadata = new Properties();
try (InputStream is = jar.getInputStream(metadataEntry)) {
metadata.load(is);
}
if (!String.valueOf(RELOCATION_FORMAT_VERSION).equals(metadata.getProperty("formatVersion"))) {
return true;
}
for (Map.Entry<String, String> relocation : relocations.entrySet()) {
String key = "relocation." + relocation.getKey();
if (!relocation.getValue().equals(metadata.getProperty(key))) {
return true;
}
}
return Integer.toString(relocations.size()).equals(metadata.getProperty("relocationCount"));
} catch (IOException e) {
return true;
}
}
private static void writeRelocationMetadata(JarOutputStream jos, Map<String, String> relocations)
throws IOException {
Properties metadata = new Properties();
metadata.setProperty("formatVersion", Integer.toString(RELOCATION_FORMAT_VERSION));
metadata.setProperty("relocationCount", Integer.toString(relocations.size()));
Map<String, String> sortedRelocations = new TreeMap<>(relocations);
for (Map.Entry<String, String> relocation : sortedRelocations.entrySet()) {
metadata.setProperty("relocation." + relocation.getKey(), relocation.getValue());
}
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
metadata.store(buffer, "AntiVPN relocation metadata");
JarEntry metadataEntry = new JarEntry(RELOCATION_METADATA_PATH);
jos.putNextEntry(metadataEntry);
jos.write(buffer.toByteArray());
jos.closeEntry();
}
private static void processServiceFile(String name, InputStream is,
Map<String, StringBuilder> serviceFiles,
Map<String, String> relocations) throws IOException {
// Read service file content
String content = new String(readAllBytes(is));
StringBuilder contentBuilder = serviceFiles.computeIfAbsent(name, k -> new StringBuilder());
// Process and relocate service implementations
for (String line : content.split("\n")) {
String trimmed = line.trim();
if (!trimmed.isEmpty() && !trimmed.startsWith("#")) {
for (Map.Entry<String, String> relocation : relocations.entrySet()) {
if (trimmed.startsWith(relocation.getKey())) {
trimmed = relocation.getValue() +
trimmed.substring(relocation.getKey().length());
break;
}
}
}
contentBuilder.append(trimmed).append("\n");
}
}
private static byte[] relocateClass(String entryName, byte[] classBytes, Map<String, String> relocations) {
try {
// Convert to slash notation for ASM
Remapper prefixRemapper = getPrefixRemapper(relocations);
// Create custom ClassWriter to handle missing classes
ClassReader reader = new ClassReader(classBytes);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS) {
@Override
protected String getCommonSuperClass(String type1, String type2) {
try {
return super.getCommonSuperClass(type1, type2);
} catch (RuntimeException e) {
// Fall back to Object when classes can't be loaded
return "java/lang/Object";
}
}
};
ClassVisitor visitor = createStringRelocationVisitor(new ClassRemapper(writer, prefixRemapper), relocations);
visitor = createMySqlUtilFallbackVisitor(entryName, visitor);
// Process class with remapper
reader.accept(visitor, 0);
return relocateUtf8Constants(writer.toByteArray(), relocations);
} catch (Exception e) {
throw new IllegalStateException("Failed to relocate class entry " + entryName, e);
}
}
public static String relocateReflectiveClassName(String className) {
if (className == null || className.startsWith("dev.brighten.antivpn.shaded.")) {
return className;
}
if (className.startsWith("com.mysql.cj") || className.startsWith("com.mysql.jdbc")) {
return "dev.brighten.antivpn.shaded." + className;
}
return className;
}
private static byte[] relocateUtf8Constants(byte[] classBytes, Map<String, String> relocations) throws IOException {
Map<String, String> dotMappings = new HashMap<>();
Map<String, String> slashMappings = new HashMap<>();
for (Map.Entry<String, String> entry : relocations.entrySet()) {
dotMappings.put(entry.getKey(), entry.getValue());
slashMappings.put(entry.getKey().replace('.', '/'), entry.getValue().replace('.', '/'));
}
DataInputStream in = new DataInputStream(new ByteArrayInputStream(classBytes));
ByteArrayOutputStream baos = new ByteArrayOutputStream(classBytes.length + 256);
DataOutputStream out = new DataOutputStream(baos);
out.writeInt(in.readInt());
out.writeShort(in.readUnsignedShort());
out.writeShort(in.readUnsignedShort());
int constantPoolCount = in.readUnsignedShort();
out.writeShort(constantPoolCount);
for (int i = 1; i < constantPoolCount; i++) {
int tag = in.readUnsignedByte();
out.writeByte(tag);
switch (tag) {
case 1 -> {
String value = in.readUTF();
String relocated = relocateStringValue(value, dotMappings, slashMappings);
out.writeUTF(relocated);
}
case 3, 4 -> out.writeInt(in.readInt());
case 5, 6 -> {
out.writeLong(in.readLong());
i++;
}
case 7, 8, 16, 19, 20 -> out.writeShort(in.readUnsignedShort());
case 9, 10, 11, 12, 17, 18 -> {
out.writeShort(in.readUnsignedShort());
out.writeShort(in.readUnsignedShort());
}
case 15 -> {
out.writeByte(in.readUnsignedByte());
out.writeShort(in.readUnsignedShort());
}
default -> throw new IOException("Unknown constant pool tag " + tag);
}
}
copyStream(in, out);
out.flush();
return baos.toByteArray();
}
private static Remapper getPrefixRemapper(Map<String, String> relocations) {
Map<String, String> slashMappings = new HashMap<>();
Map<String, String> dotMappings = new HashMap<>();
for (Map.Entry<String, String> entry : relocations.entrySet()) {
dotMappings.put(entry.getKey(), entry.getValue());
String fromSlash = entry.getKey().replace('.', '/');
String toSlash = entry.getValue().replace('.', '/');
slashMappings.put(fromSlash, toSlash);
}
// Create customized remapper for package prefixes
return new Remapper() {
@Override
public String map(String typeName) {
if (typeName == null) return null;
for (Map.Entry<String, String> entry : slashMappings.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
if (typeName.startsWith(from)) {
return to + typeName.substring(from.length());
}
}
return typeName;
}
@Override
public Object mapValue(Object value) {
if (value instanceof String stringValue) {
return relocateStringValue(stringValue, dotMappings, slashMappings);
}
return super.mapValue(value);
}
};
}
private static ClassVisitor createMySqlUtilFallbackVisitor(String entryName, ClassVisitor delegate) {
if (!"com/mysql/cj/util/Util.class".equals(entryName)) {
return delegate;
}
return new ClassVisitor(Opcodes.ASM9, delegate) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (visitor == null) {
return null;
}
if (!"getInstance".equals(name)
|| !"(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/Object;Lcom/mysql/cj/exceptions/ExceptionInterceptor;)Ljava/lang/Object;".equals(descriptor)) {
return visitor;
}
return new MethodVisitor(Opcodes.ASM9, visitor) {
@Override
public void visitCode() {
super.visitCode();
super.visitVarInsn(Opcodes.ALOAD, 1);
super.visitMethodInsn(Opcodes.INVOKESTATIC,
"dev/brighten/antivpn/depends/LibraryLoader",
"relocateReflectiveClassName",
"(Ljava/lang/String;)Ljava/lang/String;",
false);
super.visitVarInsn(Opcodes.ASTORE, 1);
}
};
}
};
}
private static ClassVisitor createStringRelocationVisitor(ClassVisitor delegate,
Map<String, String> relocations) {
Map<String, String> dotMappings = new HashMap<>();
Map<String, String> slashMappings = new HashMap<>();
for (Map.Entry<String, String> entry : relocations.entrySet()) {
dotMappings.put(entry.getKey(), entry.getValue());
slashMappings.put(entry.getKey().replace('.', '/'), entry.getValue().replace('.', '/'));
}
return new ClassVisitor(Opcodes.ASM9, delegate) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return wrapAnnotationVisitor(super.visitAnnotation(descriptor, visible), dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, org.objectweb.asm.TypePath typePath,
String descriptor, boolean visible) {
return wrapAnnotationVisitor(super.visitTypeAnnotation(typeRef, typePath, descriptor, visible),
dotMappings, slashMappings);
}
@Override
public RecordComponentVisitor visitRecordComponent(String name, String descriptor, String signature) {
RecordComponentVisitor visitor = super.visitRecordComponent(name, descriptor, signature);
if (visitor == null) {
return null;
}
return new RecordComponentVisitor(Opcodes.ASM9, visitor) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return wrapAnnotationVisitor(super.visitAnnotation(descriptor, visible),
dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, org.objectweb.asm.TypePath typePath,
String descriptor, boolean visible) {
return wrapAnnotationVisitor(super.visitTypeAnnotation(typeRef, typePath, descriptor, visible),
dotMappings, slashMappings);
}
};
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
FieldVisitor visitor = super.visitField(access, name, descriptor, signature,
relocateAsmValue(value, dotMappings, slashMappings));
if (visitor == null) {
return null;
}
return new FieldVisitor(Opcodes.ASM9, visitor) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return wrapAnnotationVisitor(super.visitAnnotation(descriptor, visible),
dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, org.objectweb.asm.TypePath typePath,
String descriptor, boolean visible) {
return wrapAnnotationVisitor(super.visitTypeAnnotation(typeRef, typePath, descriptor, visible),
dotMappings, slashMappings);
}
};
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (visitor == null) {
return null;
}
return new MethodVisitor(Opcodes.ASM9, visitor) {
@Override
public AnnotationVisitor visitAnnotationDefault() {
return wrapAnnotationVisitor(super.visitAnnotationDefault(), dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return wrapAnnotationVisitor(super.visitAnnotation(descriptor, visible),
dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, org.objectweb.asm.TypePath typePath,
String descriptor, boolean visible) {
return wrapAnnotationVisitor(super.visitTypeAnnotation(typeRef, typePath, descriptor, visible),
dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor,
boolean visible) {
return wrapAnnotationVisitor(super.visitParameterAnnotation(parameter, descriptor, visible),
dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitInsnAnnotation(int typeRef, org.objectweb.asm.TypePath typePath,
String descriptor, boolean visible) {
return wrapAnnotationVisitor(super.visitInsnAnnotation(typeRef, typePath, descriptor, visible),
dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitTryCatchAnnotation(int typeRef, org.objectweb.asm.TypePath typePath,
String descriptor, boolean visible) {
return wrapAnnotationVisitor(super.visitTryCatchAnnotation(typeRef, typePath, descriptor, visible),
dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitLocalVariableAnnotation(int typeRef,
org.objectweb.asm.TypePath typePath,
org.objectweb.asm.Label[] start,
org.objectweb.asm.Label[] end,
int[] index, String descriptor,
boolean visible) {
return wrapAnnotationVisitor(
super.visitLocalVariableAnnotation(typeRef, typePath, start, end, index, descriptor, visible),
dotMappings, slashMappings);
}
@Override
public void visitLdcInsn(Object value) {
super.visitLdcInsn(relocateAsmValue(value, dotMappings, slashMappings));
}
@Override
public void visitInvokeDynamicInsn(String name, String descriptor, org.objectweb.asm.Handle bootstrapMethodHandle,
Object... bootstrapMethodArguments) {
Object[] relocatedArgs = new Object[bootstrapMethodArguments.length];
for (int i = 0; i < bootstrapMethodArguments.length; i++) {
relocatedArgs[i] = relocateAsmValue(bootstrapMethodArguments[i], dotMappings, slashMappings);
}
super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, relocatedArgs);
}
};
}
};
}
private static AnnotationVisitor wrapAnnotationVisitor(AnnotationVisitor delegate,
Map<String, String> dotMappings,
Map<String, String> slashMappings) {
if (delegate == null) {
return null;
}
return new AnnotationVisitor(Opcodes.ASM9, delegate) {
@Override
public void visit(String name, Object value) {
super.visit(name, relocateAsmValue(value, dotMappings, slashMappings));
}
@Override
public AnnotationVisitor visitAnnotation(String name, String descriptor) {
return wrapAnnotationVisitor(super.visitAnnotation(name, descriptor), dotMappings, slashMappings);
}
@Override
public AnnotationVisitor visitArray(String name) {
return wrapAnnotationVisitor(super.visitArray(name), dotMappings, slashMappings);
}
};
}
private static Object relocateAsmValue(Object value, Map<String, String> dotMappings,
Map<String, String> slashMappings) {
if (value instanceof String stringValue) {
return relocateStringValue(stringValue, dotMappings, slashMappings);
}
return value;
}
private static String relocateStringValue(String value, Map<String, String> dotMappings,
Map<String, String> slashMappings) {
for (Map.Entry<String, String> entry : dotMappings.entrySet()) {
String from = entry.getKey();
String relocated = relocateByPrefixes(value, from, entry.getValue(), '.', '$');
if (!relocated.equals(value)) {
return relocated;
}
}
for (Map.Entry<String, String> entry : slashMappings.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
String relocated = relocateByPrefixes(value, from, to, '/', '$');
if (!relocated.equals(value)) {
return relocated;
}
relocated = relocateByPrefixes(value, "/" + from, "/" + to, '/', '$');
if (!relocated.equals(value)) {
return relocated;
}
relocated = relocateByPrefixes(value, "L" + from, "L" + to, '/', '$', ';');
if (!relocated.equals(value)) {
return relocated;
}
relocated = relocateByPrefixes(value, "[L" + from, "[L" + to, '/', '$', ';');
if (!relocated.equals(value)) {
return relocated;
}
}
return value;
}
private static String relocateByPrefixes(String value, String from, String to, char... delimiters) {
if (value.equals(from)) {
return to;
}
for (char delimiter : delimiters) {
if (value.startsWith(from + delimiter)) {
return to + value.substring(from.length());
}
}
return value;
}
private static void validateRelocatedJar(File targetJar, Map<String, String> relocations) throws IOException {
Set<String> relocatedPrefixes = new HashSet<>();
Map<String, String> dotMappings = new HashMap<>();
Map<String, String> slashMappings = new HashMap<>();
for (Map.Entry<String, String> relocation : relocations.entrySet()) {
relocatedPrefixes.add(relocation.getValue().replace('.', '/') + "/");
dotMappings.put(relocation.getKey(), relocation.getValue());
slashMappings.put(relocation.getKey().replace('.', '/'), relocation.getValue().replace('.', '/'));
}
try (JarFile jar = new JarFile(targetJar)) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
continue;
}
boolean shouldValidate = false;
for (String relocatedPrefix : relocatedPrefixes) {
if (entry.getName().startsWith(relocatedPrefix)) {
shouldValidate = true;
break;
}
}
if (!shouldValidate) {
continue;
}
try (InputStream is = jar.getInputStream(entry)) {
findUnrelocatedConstant(entry.getName(), readAllBytes(is), dotMappings, slashMappings);
}
}
}
}
private static void findUnrelocatedConstant(String entryName, byte[] classBytes, Map<String, String> dotMappings,
Map<String, String> slashMappings) throws IOException {
DataInputStream in = new DataInputStream(new ByteArrayInputStream(classBytes));
in.readInt();
in.readUnsignedShort();
in.readUnsignedShort();
int constantPoolCount = in.readUnsignedShort();
for (int i = 1; i < constantPoolCount; i++) {
int tag = in.readUnsignedByte();
switch (tag) {
case 1 -> {
String value = in.readUTF();
String relocated = relocateStringValue(value, dotMappings, slashMappings);
if (!value.equals(relocated)) {
throw new IOException("Relocated jar still contains original reference '" + value
+ "' in class entry " + entryName);
}
}
case 3, 4 -> in.readInt();
case 5, 6 -> {
in.readLong();
i++;
}
case 7, 8, 16, 19, 20 -> in.readUnsignedShort();
case 9, 10, 11, 12, 17, 18 -> {
in.readUnsignedShort();
in.readUnsignedShort();
}
case 15 -> {
in.readUnsignedByte();
in.readUnsignedShort();
}
default -> throw new IOException("Unknown constant pool tag " + tag + " while validating " + entryName);
}
}
}
private static String relocateClassPath(String path, Map<String, String> relocations) {
// Convert path to package format (replacing / with .)
String packagePath = path.substring(0, path.length() - 6).replace('/', '.');
// Apply relocations
for (Map.Entry<String, String> relocation : relocations.entrySet()) {
if (packagePath.startsWith(relocation.getKey())) {
packagePath = relocation.getValue() + packagePath.substring(relocation.getKey().length());
break;
}
}
// Convert back to path format
return packagePath.replace('.', '/') + ".class";
}
private static String relocateResourcePath(String path, Map<String, String> relocations) {
if (path.startsWith("META-INF/")) {
return path;
}
for (Map.Entry<String, String> relocation : relocations.entrySet()) {
String fromPath = relocation.getKey().replace('.', '/');
String toPath = relocation.getValue().replace('.', '/');
if (path.startsWith(fromPath + "/")) {
return toPath + path.substring(fromPath.length());
}
}
return path;
}
private static byte[] readAllBytes(InputStream is) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int bytesRead;
byte[] data = new byte[1024];
while ((bytesRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, bytesRead);
}
return buffer.toByteArray();
}
private static void copyStream(InputStream is, OutputStream os) throws IOException {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
private static File getLibFolder() {
File pluginDataFolder = AntiVPN.getInstance().getPluginFolder();
File libs = new File(pluginDataFolder, "libraries");
if(libs.mkdirs()) {
System.out.println("Created libraries folder!");
}
return libs;
}
@Getter
@NonnullByDefault
// Fix the Dependency class to preserve original groupId for downloading
public static final class Dependency {
private final String groupId;
private final String artifactId;
private final String version;
private final String repoUrl;
// Keep the original groupId/artifactId for Maven downloads
private final String originalGroupId;
private final String originalArtifactId;
public Dependency(String groupId, String artifactId, String version, String repoUrl) {
this.originalGroupId = Objects.requireNonNull(groupId, "groupId");
this.originalArtifactId = Objects.requireNonNull(artifactId, "artifactId");
this.groupId = this.originalGroupId;
this.artifactId = this.originalArtifactId;
this.version = Objects.requireNonNull(version, "version");
this.repoUrl = Objects.requireNonNull(repoUrl, "repoUrl");
}
public URL getUrl() throws MalformedURLException {
String repo = this.repoUrl;
if (!repo.endsWith("/")) {
repo += "/";
}
repo += "%s/%s/%s/%s-%s.jar";
// Always use original groupId for Maven repository URL
String url = String.format(repo, this.originalGroupId.replace(".", "/"),
this.originalArtifactId, this.version, this.originalArtifactId, this.version);
return new URL(url);
}
// Rest of the class unchanged
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.depends;
import java.lang.annotation.*;
/**
* Annotation to indicate the required libraries for a class.
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MavenLibraries {
MavenLibrary[] value() default {};
}
@@ -0,0 +1,60 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.depends;
import java.lang.annotation.*;
/**
* Annotation to indicate a required library for a class.
*/
@Documented
@Repeatable(MavenLibraries.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MavenLibrary {
/**
* The group id of the library
*
* @return the group id of the library
*/
String groupId();
/**
* The artifact id of the library
*
* @return the artifact id of the library
*/
String artifactId();
/**
* The version of the library
*
* @return the version of the library
*/
String version();
/**
* The repo where the library can be obtained from
*
* @return the repo where the library can be obtained from
*/
Repository repo() default @Repository(url = "https://repo1.maven.org/maven2");
Relocate[] relocations() default {}; // Add this line
}
@@ -0,0 +1,30 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.depends;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Relocate {
String from();
String to();
}
@@ -0,0 +1,36 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.depends;
import java.lang.annotation.*;
/**
* Represents a maven repository.
*/
@Documented
@Target(ElementType.LOCAL_VARIABLE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Repository {
/**
* Gets the base url of the repository.
*
* @return the base url of the repository
*/
String url();
}
@@ -1,26 +1,17 @@
/* /*
* This file is part of helper, licensed under the MIT License. * Copyright 2026 Dawson Hessler
* *
* Copyright (c) lucko (Luck) <luck@lucko.me> * Licensed under the Apache License, Version 2.0 (the "License");
* Copyright (c) contributors * 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 * http://www.apache.org/licenses/LICENSE-2.0
* 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 * Unless required by applicable law or agreed to in writing, software
* copies or substantial portions of the Software. * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * See the License for the specific language governing permissions and
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * limitations under the License.
* 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.depends; package dev.brighten.antivpn.depends;
@@ -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; package dev.brighten.antivpn.message;
import dev.brighten.antivpn.AntiVPN; 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; package dev.brighten.antivpn.message;
import dev.brighten.antivpn.api.APIPlayer; 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; package dev.brighten.antivpn.utils;
import dev.brighten.antivpn.AntiVPN; 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 {
}
@@ -1,15 +1,17 @@
/* /*
* Copyright (C) 2021 The Guava Authors * Copyright 2026 Dawson Hessler
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * Licensed under the Apache License, Version 2.0 (the "License");
* in compliance with the License. You may obtain a copy of the License at * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software distributed under the License * Unless required by applicable law or agreed to in writing, software
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * distributed under the License is distributed on an "AS IS" BASIS,
* or implied. See the License for the specific language governing permissions and limitations under * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* the License. * See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.brighten.antivpn.utils; package dev.brighten.antivpn.utils;
@@ -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.
*/
// //
// Source code recreated from a .class file by IntelliJ IDEA // Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler) // (powered by FernFlower decompiler)
@@ -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; package dev.brighten.antivpn.utils;
import dev.brighten.antivpn.api.APIPlayer; import dev.brighten.antivpn.api.APIPlayer;
@@ -12,15 +28,11 @@ public class StringUtil {
return "&m-----------------------------------------------------"; return "&m-----------------------------------------------------";
} }
public static String lineNoStrike(String color) {
return color + "-----------------------------------------------------";
}
public static String varReplace(String input, APIPlayer player, VPNResponse result) { public static String varReplace(String input, APIPlayer player, VPNResponse result) {
return input.replace("%player%", player.getName()) return translateAlternateColorCodes('&', input.replace("%player%", player.getName())
.replace("%reason%", result.getMethod()) .replace("%reason%", result.getMethod())
.replace("%country%", result.getCountryName()) .replace("%country%", result.getCountryName())
.replace("%city%", result.getCity()); .replace("%city%", result.getCity()));
} }
public static String translateAlternateColorCodes(char altColorChar, String textToTranslate) { public static String translateAlternateColorCodes(char altColorChar, String textToTranslate) {
@@ -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();
}
@@ -1,15 +1,17 @@
/* /*
* Copyright (C) 2007 The Guava Authors * Copyright 2026 Dawson Hessler
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * Licensed under the Apache License, Version 2.0 (the "License");
* in compliance with the License. You may obtain a copy of the License at * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software distributed under the License * Unless required by applicable law or agreed to in writing, software
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * distributed under the License is distributed on an "AS IS" BASIS,
* or implied. See the License for the specific language governing permissions and limitations under * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* the License. * See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.brighten.antivpn.utils; package dev.brighten.antivpn.utils;
@@ -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; package dev.brighten.antivpn.utils.config;
import java.util.*; 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; package dev.brighten.antivpn.utils.config;
import java.io.*; 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; package dev.brighten.antivpn.utils.config;
import lombok.AccessLevel; 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 package dev.brighten.antivpn.utils.json;
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.
*/
/** /**
* This provides static methods to convert comma delimited text into a * 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 package dev.brighten.antivpn.utils.json;
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.
*/
/** /**
* Convert a web browser cookie specification to a JSONObject and back. * Convert a web browser cookie specification to a JSONObject and back.
@@ -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 package dev.brighten.antivpn.utils.json;
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.
*/
import java.util.Iterator; import java.util.Iterator;
@@ -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 package dev.brighten.antivpn.utils.json;
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.
*/
import java.util.Iterator; import java.util.Iterator;

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