Implementing database versioning for CIDR whitelisting

This commit is contained in:
2026-01-05 09:21:32 -05:00
parent 7913676323
commit 9f7f4b40f0
19 changed files with 551 additions and 213 deletions
@@ -28,16 +28,6 @@ public class BungeeListener extends VPNExecutor implements Listener {
.registerListener(BungeePlugin.pluginInstance.getPlugin(), this);
}
@Override
public void shutdown() {
if(cacheResetTask != null) {
cacheResetTask.cancel();
cacheResetTask = null;
}
BungeePlugin.pluginInstance.getProxy().getPluginManager().unregisterListener(this);
super.shutdown();
}
@Override
public void log(Level level, String log, Object... objects) {
BungeePlugin.pluginInstance.getProxy().getLogger().log(Level.INFO, String.format(log, objects));
@@ -190,7 +190,7 @@ public class AntiVPN {
executor.log("Failed to deregister H2 driver: " + e.getMessage());
}
}
VPNExecutor.threadExecutor.shutdown();
AntiVPN.getInstance().getExecutor().getThreadExecutor().shutdown();
if(database != null) database.shutdown();
}
@@ -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.api;
import com.github.benmanes.caffeine.cache.Cache;
@@ -39,7 +55,7 @@ public abstract class APIPlayer {
public void updateAlertsState() {
//Updating into database so its synced across servers and saved on logout.
AntiVPN.getInstance().getDatabase().updateAlertsState(uuid, alertsEnabled);
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> AntiVPN.getInstance().getDatabase().updateAlertsState(uuid, alertsEnabled));
}
public CheckResult checkPlayer(Consumer<CheckResult> onKick) {
@@ -81,7 +97,7 @@ public abstract class APIPlayer {
// the state, it will kick.
&& AntiVPN.getInstance().getVpnConfig().getCountryList()
.contains(result.getCountryCode())
!= AntiVPN.getInstance().getVpnConfig().isWhitelistCountries()) {
!= AntiVPN.getInstance().getVpnConfig().getWhitelistCountries()) {
//Using our built in kicking system if no commands are configured
checkResult = new CheckResult(result, ResultType.DENIED_COUNTRY);
} else if (result.isProxy()) {
@@ -1,7 +1,24 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.api;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.utils.ConfigDefault;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Collections;
@@ -52,37 +69,50 @@ public class VPNConfig {
defCountrylist = new ConfigDefault<>(new ArrayList<>(), "countries.list",
AntiVPN.getInstance());
private String license, kickMessage, databaseType, databaseName, mongoURL, username, password, ip, alertMsg,
countryVanillaKickReason;
private List<String> prefixWhitelists, commands, countryList, countryKickCommands;
@Getter
private String license;
@Getter
private String kickMessage;
@Getter
private String databaseType;
@Getter
private String databaseName;
private String mongoURL;
@Getter
private String username;
@Getter
private String password;
@Getter
private String ip;
private String alertMsg;
@Getter
private String countryVanillaKickReason;
@Getter
private List<String> prefixWhitelists;
private List<String> commands;
@Getter
private List<String> countryList;
private List<String> countryKickCommands;
private int port;
private boolean cacheResults, databaseEnabled, useCredentials, commandsEnabled, kickPlayers, alertToStaff,
metrics, whitelistCountries;
private boolean cacheResults;
@Getter
private boolean databaseEnabled;
private boolean useCredentials;
private boolean commandsEnabled;
@Getter
private boolean kickPlayers;
private boolean alertToStaff;
private boolean metrics;
private boolean whitelistCountries;
/**
* License from https://funkemunky.cc/shop to be used for more queries.
* @return String
*/
public String getLicense() {
return license;
}
/**
* If true, results will be cached to reduce queries to https://funkemunky.cc
* If true, results will be cached to reduce queries to <a href="https://funkemunky.cc">...</a>
* @return boolean
*/
public boolean cachedResults() {
return cacheResults;
}
/**
* Will be used for vanilla kick message when {@link VPNConfig#runCommands()} is true.
* @return String
*/
public String getKickString() {
return kickMessage;
}
/**
* Message to send staff on proxy detection.
* @return String
@@ -115,31 +145,6 @@ public class VPNConfig {
return commands;
}
/**
* If false, no commands nor kick will be run on proxy detection.
* @return boolean
*/
public boolean kickPlayersOnDetect() {
return kickPlayers;
}
/**
* Returns Strings of which are checked against the beginning of player names. Used to
* allow Geyser-connected players to join.
* @return List
*/
public List<String> getPrefixWhitelists() {
return prefixWhitelists;
}
/**
* Returns true if we want to use a database
* @return boolean
*/
public boolean isDatabaseEnabled() {
return databaseEnabled;
}
/**
* Whether or not the database we want to connect to requires credentials.
* @return boolean
@@ -156,59 +161,11 @@ public class VPNConfig {
return mongoURL;
}
/**
* Database type. Either MySQL and Mongo.
* @return String
*/
public String getDatabaseType() {
return databaseType;
}
/**
* Database name
* @return String
*/
public String getDatabaseName() {
return databaseName;
}
/**
* Database username
* @return String
*/
public String getUsername() {
return username;
}
/**
* Database Password
* @return String
*/
public String getPassword() {
return password;
}
/**
* Database IP
* @return String
*/
public String getIp() {
return ip;
}
/**
* Returns the list of ISO country codes we need to check.
* @return List
*/
public List<String> countryList() {
return countryList;
}
/**
* If true, we only allow the {@link VPNConfig#countryKickCommands()}. If false, we blacklist them.
* @return boolean
*/
public boolean whitelistCountries() {
public boolean getWhitelistCountries() {
return whitelistCountries;
}
@@ -220,14 +177,6 @@ public class VPNConfig {
return countryKickCommands;
}
/**
* Returns the vanilla kick reason for bad country locations
* @return String
*/
public String countryVanillaKickReason() {
return countryVanillaKickReason;
}
/**
* Gets the port based on configuration. If {@link VPNConfig#port} is -1, will get default port
* based on {@link VPNConfig#getDatabaseType()} lowerCase().
@@ -251,7 +200,7 @@ public class VPNConfig {
/**
* If true, https://bstats.org metrics will be collected to improve KauriVPN.
* If true, <a href="https://bstats.org">...</a> metrics will be collected to improve KauriVPN.
* @return boolean
*/
public boolean metrics() {
@@ -17,7 +17,8 @@ import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public abstract class VPNExecutor {
public static ScheduledExecutorService threadExecutor = Executors.newScheduledThreadPool(2);
@Getter
private ScheduledExecutorService threadExecutor = Executors.newScheduledThreadPool(2);
@Getter
private final Set<UUID> whitelisted = Collections.synchronizedSet(new HashSet<>());
@@ -75,12 +76,12 @@ public abstract class VPNExecutor {
StringUtil.varReplace(dev.brighten.antivpn.AntiVPN.getInstance().getVpnConfig()
.alertMessage(), player, result.response()))));
if(AntiVPN.getInstance().getVpnConfig().kickPlayersOnDetect()) {
if(AntiVPN.getInstance().getVpnConfig().isKickPlayers()) {
switch (result.resultType()) {
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()
.countryVanillaKickReason(), player, result.response()));
.getCountryVanillaKickReason(), player, result.response()));
}
}
@@ -1,6 +1,23 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.command.impl;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.CommandExecutor;
@@ -45,7 +62,7 @@ public class ClearCacheCommand extends Command {
@Override
public String execute(CommandExecutor executor, String[] args) {
AntiVPN.getInstance().getDatabase().clearResponses();
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> AntiVPN.getInstance().getDatabase().clearResponses());
return "&aCleared all cached API response information!";
}
@@ -1,3 +1,19 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.local;
import com.github.benmanes.caffeine.cache.Cache;
@@ -7,6 +23,7 @@ import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.sql.utils.MySQL;
import dev.brighten.antivpn.database.sql.utils.Query;
import dev.brighten.antivpn.database.version.H2Version;
import dev.brighten.antivpn.web.objects.VPNResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@@ -30,7 +47,7 @@ public class H2VPN implements VPNDatabase {
public H2VPN() {
VPNExecutor.threadExecutor.scheduleAtFixedRate(() -> {
AntiVPN.getInstance().getExecutor().getThreadExecutor().scheduleAtFixedRate(() -> {
if(!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return;
//Refreshing whitelisted players
@@ -143,14 +160,18 @@ public class H2VPN implements VPNDatabase {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed())
return;
if (whitelisted) {
if (!isWhitelisted(uuid)) {
Query.prepare("insert into `whitelisted` (`uuid`) values (?)").append(uuid.toString()).execute();
try {
if (whitelisted) {
if (!isWhitelisted(uuid)) {
Query.prepare("insert into `whitelisted` (`uuid`) values (?)").append(uuid.toString()).execute();
}
AntiVPN.getInstance().getExecutor().getWhitelisted().add(uuid);
} else {
Query.prepare("delete from `whitelisted` where `uuid` = ?").append(uuid.toString()).execute();
AntiVPN.getInstance().getExecutor().getWhitelisted().remove(uuid);
}
AntiVPN.getInstance().getExecutor().getWhitelisted().add(uuid);
} else {
Query.prepare("delete from `whitelisted` where `uuid` = ?").append(uuid.toString()).execute();
AntiVPN.getInstance().getExecutor().getWhitelisted().remove(uuid);
} catch (SQLException e) {
AntiVPN.getInstance().getExecutor().logException("Could not set whitelist for uuid '" + uuid + "' due to SQL error.", e);
}
}
@@ -159,14 +180,18 @@ public class H2VPN implements VPNDatabase {
if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed())
return;
if(whitelisted) {
if(!isWhitelisted(ip)) {
Query.prepare("insert into `whitelisted-ips` (`ip`) values (?)").append(ip).execute();
try {
if(whitelisted) {
if(!isWhitelisted(ip)) {
Query.prepare("insert into `whitelisted-ips` (`ip`) values (?)").append(ip).execute();
}
AntiVPN.getInstance().getExecutor().getWhitelistedIps().add(ip);
} else {
Query.prepare("delete from `whitelisted-ips` where `ip` = ?").append(ip).execute();
AntiVPN.getInstance().getExecutor().getWhitelistedIps().remove(ip);
}
AntiVPN.getInstance().getExecutor().getWhitelistedIps().add(ip);
} else {
Query.prepare("delete from `whitelisted-ips` where `ip` = ?").append(ip).execute();
AntiVPN.getInstance().getExecutor().getWhitelistedIps().remove(ip);
} catch (SQLException e) {
AntiVPN.getInstance().getExecutor().logException("Could not set whitelist for ip '" + ip + "' due to SQL error.", e);
}
}
@@ -174,8 +199,12 @@ public class H2VPN implements VPNDatabase {
public List<UUID> getAllWhitelisted() {
List<UUID> uuids = new ArrayList<>();
if(!MySQL.isClosed()) Query.prepare("select uuid from `whitelisted`")
.execute(set -> uuids.add(UUID.fromString(set.getString("uuid"))));
try {
if(!MySQL.isClosed()) Query.prepare("select uuid from `whitelisted`")
.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;
}
@@ -184,8 +213,12 @@ public class H2VPN implements VPNDatabase {
public List<String> getAllWhitelistedIps() {
List<String> ips = new ArrayList<>();
if(!MySQL.isClosed()) Query.prepare("select `ip` from `whitelisted-ips`")
try {
if(!MySQL.isClosed()) Query.prepare("select `ip` from `whitelisted-ips`")
.execute(set -> ips.add(set.getString("ip")));
} catch (SQLException e) {
AntiVPN.getInstance().getExecutor().logException("Could not get all whitelisted ips due to SQL error.", e);
}
return ips;
}
@@ -194,28 +227,28 @@ public class H2VPN implements VPNDatabase {
public void getStoredResponseAsync(String ip, Consumer<Optional<VPNResponse>> result) {
if(MySQL.isClosed()) return;
VPNExecutor.threadExecutor.execute(() -> result.accept(getStoredResponse(ip)));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(getStoredResponse(ip)));
}
@Override
public void isWhitelistedAsync(UUID uuid, Consumer<Boolean> result) {
if(MySQL.isClosed()) return;
VPNExecutor.threadExecutor.execute(() -> result.accept(isWhitelisted(uuid)));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(isWhitelisted(uuid)));
}
@Override
public void isWhitelistedAsync(String ip, Consumer<Boolean> result) {
if(MySQL.isClosed()) return;
VPNExecutor.threadExecutor.execute(() -> result.accept(isWhitelisted(ip)));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(isWhitelisted(ip)));
}
@Override
public void alertsState(UUID uuid, Consumer<Boolean> result) {
if(MySQL.isClosed()) return;
VPNExecutor.threadExecutor.execute(() -> {
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> {
try(ResultSet set = Query.prepare("select * from `alerts` where `uuid` = ? limit 1")
.append(uuid.toString()).executeQuery()) {
@@ -235,22 +268,35 @@ public class H2VPN implements VPNDatabase {
//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) {
Query.prepare("insert into `alerts` (`uuid`) values (?)").append(uuid.toString())
.execute();
try {
Query.prepare("insert into `alerts` (`uuid`) values (?)").append(uuid.toString())
.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 VPNExecutor.threadExecutor.execute(() ->
} else {
try {
Query.prepare("delete from `alerts` where `uuid` = ?")
.append(uuid.toString())
.execute());
.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;
VPNExecutor.threadExecutor.execute(() -> Query.prepare("delete from `responses`").execute());
try {
Query.prepare("delete from `responses`").execute();
} catch (SQLException e) {
AntiVPN.getInstance().getExecutor().logException("There was a problem clearing responses.", e);
}
}
@Override
@@ -259,6 +305,15 @@ public class H2VPN implements VPNDatabase {
return;
AntiVPN.getInstance().getExecutor().log("Initializing H2...");
MySQL.initH2();
try {
for (H2Version version : H2Version.versions) {
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...");
@@ -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.database.local.version;
import dev.brighten.antivpn.AntiVPN;
@@ -5,9 +21,13 @@ import dev.brighten.antivpn.database.local.H2VPN;
import dev.brighten.antivpn.database.sql.utils.Query;
import dev.brighten.antivpn.database.version.H2Version;
import java.sql.ResultSet;
import java.sql.SQLException;
public class First implements H2Version {
@Override
public void update(H2VPN database) throws Exception {
public void update(H2VPN database) throws SQLException {
backupDatabase();
Query.prepare("create table if not exists `whitelisted` (`uuid` varchar(36) not null)").execute();
Query.prepare("create table if not exists `whitelisted-ips` (`ip` varchar(45) not null)").execute();
Query.prepare("create table if not exists `responses` (`ip` varchar(45) not null, `asn` varchar(12),"
@@ -15,17 +35,15 @@ public class First implements H2Version {
+ "`method` varchar(32), `isp` text, `proxy` boolean, `cached` boolean, `inserted` timestamp,"
+ "`latitude` double, `longitude` double)").execute();
Query.prepare("create table if not exists `alerts` (`uuid` varchar(36) not null)").execute();
Query.prepare("create table if not exists `database_version` (`version` int)").execute();
Query.prepare("insert into `database_version` (`version`) values (?)").append(versionNumber()).execute();
AntiVPN.getInstance().getExecutor().log("Creating indexes...");
try {
Query.prepare("create index if not exists `uuid_1` on `whitelisted` (`uuid`)").execute();
Query.prepare("create index if not exists `ip_1` on `responses` (`ip`)").execute();
Query.prepare("create index if not exists `proxy_1` on `responses` (`proxy`)").execute();
Query.prepare("create index if not exists `inserted_1` on `responses` (`inserted`)").execute();
Query.prepare("create index if not exists `ip_1` on `whitelisted-ips` (`ip`)").execute();
} catch (Exception e) {
System.err.println("MySQL Excepton created" + e.getMessage());
}
Query.prepare("create index if not exists `uuid_1` on `whitelisted` (`uuid`)").execute();
Query.prepare("create index if not exists `ip_1` on `responses` (`ip`)").execute();
Query.prepare("create index if not exists `proxy_1` on `responses` (`proxy`)").execute();
Query.prepare("create index if not exists `inserted_1` on `responses` (`inserted`)").execute();
Query.prepare("create index if not exists `ip_1` on `whitelisted-ips` (`ip`)").execute();
}
@Override
@@ -35,6 +53,10 @@ public class First implements H2Version {
@Override
public boolean needsUpdate(H2VPN database) {
return false;
try(ResultSet set = Query.prepare("select * from `database_version` where version = 0").executeQuery()) {
return set.getFetchSize() == 0;
} catch (SQLException e) {
return true;
}
}
}
@@ -0,0 +1,122 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.brighten.antivpn.database.local.version;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.local.H2VPN;
import dev.brighten.antivpn.database.sql.utils.Query;
import dev.brighten.antivpn.database.version.H2Version;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.utils.IpUtils;
import java.net.UnknownHostException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class Second implements H2Version {
@Override
public void update(H2VPN database) throws SQLException {
backupDatabase();
List<String> whitelistedIps = new ArrayList<>();
try (var set = Query.prepare("SELECT * FROM `whitelisted-ips`").executeQuery()) {
while (set.next()) {
whitelistedIps.add(set.getString("ip"));
}
}
try {
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();
Query.prepare("CREATE INDEX idx_ip_range ON `whitelisted-ranges` (ip_start, ip_end)").execute();
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(IpUtils.getIpDecimal(cidr.getStartAddress().getHostAddress()).orElseThrow())
.append(IpUtils.getIpDecimal(cidr.getEndAddress().getHostAddress()).orElseThrow())
.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!");
}
}
Query.prepare("DROP INDEX ip_1 on `whitelisted-ips`").execute();
Query.prepare("DROP TABLE `whitelisted-ips`").execute();
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());
rollback(whitelistedIps);
throw e;
}
}
private void rollback(List<String> ipAddresses) throws SQLException {
AntiVPN.getInstance().getExecutor().log("Rolling back to version 0...");
Query.prepare("DROP INDEX idx_ip_range ON `whitelisted-ranges`").execute();
Query.prepare("DROP TABLE `whitelisted-ranges`").execute();
Query.prepare("DELETE FROM `database_version` WHERE version = ?").append(versionNumber()).execute();
Query.prepare("CREATE TABLE IF NOT EXISTS `whitelisted-ips` (`ip` VARCHAR(45) NOT NULL)")
.execute();
Query.prepare("create index if not exists `ip_1` on `whitelisted-ips` (`ip`)").execute();
Query.prepare("DELETE FROM `whitelisted-ips`").execute();
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(H2VPN database) {
try (ResultSet set = Query.prepare("select * from `database_version` where version = 1").executeQuery()) {
return set.getFetchSize() == 0;
} catch (SQLException e) {
return true;
}
}
}
@@ -32,7 +32,7 @@ public class MongoVPN implements VPNDatabase {
.build();
public MongoVPN() {
VPNExecutor.threadExecutor.scheduleAtFixedRate(() -> {
AntiVPN.getInstance().getExecutor().getThreadExecutor().scheduleAtFixedRate(() -> {
if(!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) return;
//Refreshing whitelisted players
@@ -55,7 +55,7 @@ public class MongoVPN implements VPNDatabase {
long lastUpdate = rdoc.get("lastAccess", 0L);
if(System.currentTimeMillis() - lastUpdate > TimeUnit.HOURS.toMillis(1)) {
VPNExecutor.threadExecutor.execute(() -> deleteResponse(ip));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> deleteResponse(ip));
return null;
}
@@ -101,7 +101,7 @@ public class MongoVPN implements VPNDatabase {
cachedResponses.put(toCache.getIp(), toCache);
VPNExecutor.threadExecutor.execute(() -> {
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> {
Bson update = new Document("$set", rdoc);
cacheDocument.updateOne(Filters.eq("ip", toCache.getIp()), update,
new UpdateOptions().upsert(true));
@@ -133,10 +133,10 @@ public class MongoVPN implements VPNDatabase {
Document wdoc = new Document("setting", "whitelist");
wdoc.put("uuid", uuid.toString());
AntiVPN.getInstance().getExecutor().getWhitelisted().add(uuid);
VPNExecutor.threadExecutor.execute(() -> settingsDocument.insertOne(wdoc));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> settingsDocument.insertOne(wdoc));
} else {
AntiVPN.getInstance().getExecutor().getWhitelisted().remove(uuid);
VPNExecutor.threadExecutor.execute(() -> settingsDocument.deleteMany(Filters
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> settingsDocument.deleteMany(Filters
.and(
Filters.eq("setting", "whitelist"),
Filters.eq("uuid", uuid.toString()))));
@@ -149,10 +149,10 @@ public class MongoVPN implements VPNDatabase {
Document wdoc = new Document("setting", "whitelist").append("ip", ip);
AntiVPN.getInstance().getExecutor().getWhitelistedIps().add(ip);
VPNExecutor.threadExecutor.execute(() -> settingsDocument.insertOne(wdoc));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> settingsDocument.insertOne(wdoc));
} else {
AntiVPN.getInstance().getExecutor().getWhitelistedIps().remove(ip);
VPNExecutor.threadExecutor.execute(() -> settingsDocument.deleteMany(Filters
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> settingsDocument.deleteMany(Filters
.and(
Filters.eq("setting", "whitelist"),
Filters.eq("ip", ip))));
@@ -179,29 +179,29 @@ public class MongoVPN implements VPNDatabase {
@Override
public void getStoredResponseAsync(String ip, Consumer<Optional<VPNResponse>> result) {
VPNExecutor.threadExecutor.execute(() -> result.accept(getStoredResponse(ip)));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(getStoredResponse(ip)));
}
@Override
public void isWhitelistedAsync(UUID uuid, Consumer<Boolean> result) {
VPNExecutor.threadExecutor.execute(() -> result.accept(isWhitelisted(uuid)));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(isWhitelisted(uuid)));
}
@Override
public void isWhitelistedAsync(String ip, Consumer<Boolean> result) {
VPNExecutor.threadExecutor.execute(() -> result.accept(isWhitelisted(ip)));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(isWhitelisted(ip)));
}
@Override
public void alertsState(UUID uuid, Consumer<Boolean> result) {
VPNExecutor.threadExecutor.execute(() -> result.accept(settingsDocument
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(settingsDocument
.find(Filters.and(Filters.eq("setting", "alerts"),
Filters.eq("uuid", uuid.toString()))).first() != null));
}
@Override
public void updateAlertsState(UUID uuid, boolean state) {
VPNExecutor.threadExecutor.execute(() -> {
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> {
settingsDocument.deleteMany(Filters.and(Filters.eq("setting", "alerts"),
Filters.eq("uuid", uuid.toString())));
if(state) {
@@ -29,7 +29,7 @@ public class MySqlVPN implements VPNDatabase {
public MySqlVPN() {
VPNExecutor.threadExecutor.scheduleAtFixedRate(() -> {
AntiVPN.getInstance().getExecutor().getThreadExecutor().scheduleAtFixedRate(() -> {
if(!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return;
//Refreshing whitelisted players
@@ -63,7 +63,7 @@ public class MySqlVPN implements VPNDatabase {
rs.getTimestamp("inserted").getTime(), -1);
if(System.currentTimeMillis() - responseFromDoc.getLastAccess() > TimeUnit.HOURS.toMillis(1)) {
VPNExecutor.threadExecutor.execute(() -> deleteResponse(ip));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> deleteResponse(ip));
return null;
}
@@ -190,28 +190,28 @@ public class MySqlVPN implements VPNDatabase {
public void getStoredResponseAsync(String ip, Consumer<Optional<VPNResponse>> result) {
if(MySQL.isClosed()) return;
VPNExecutor.threadExecutor.execute(() -> result.accept(getStoredResponse(ip)));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(getStoredResponse(ip)));
}
@Override
public void isWhitelistedAsync(UUID uuid, Consumer<Boolean> result) {
if(MySQL.isClosed()) return;
VPNExecutor.threadExecutor.execute(() -> result.accept(isWhitelisted(uuid)));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(isWhitelisted(uuid)));
}
@Override
public void isWhitelistedAsync(String ip, Consumer<Boolean> result) {
if(MySQL.isClosed()) return;
VPNExecutor.threadExecutor.execute(() -> result.accept(isWhitelisted(ip)));
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> result.accept(isWhitelisted(ip)));
}
@Override
public void alertsState(UUID uuid, Consumer<Boolean> result) {
if(MySQL.isClosed()) return;
VPNExecutor.threadExecutor.execute(() -> {
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> {
try(ResultSet set = Query.prepare("select * from `alerts` where `uuid` = ? limit 1")
.append(uuid.toString()).executeQuery()) {
@@ -237,7 +237,7 @@ public class MySqlVPN implements VPNDatabase {
} //No need to insert again of already enabled
});
//Removing any uuid from the alerts table will disable alerts globally.
} else VPNExecutor.threadExecutor.execute(() ->
} else AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() ->
Query.prepare("delete from `alerts` where `uuid` = ?")
.append(uuid.toString())
.execute());
@@ -247,7 +247,7 @@ public class MySqlVPN implements VPNDatabase {
public void clearResponses() {
if(MySQL.isClosed()) return;
VPNExecutor.threadExecutor.execute(() -> Query.prepare("delete from `responses`").execute());
AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> Query.prepare("delete from `responses`").execute());
}
@Override
@@ -7,15 +7,14 @@ import java.sql.*;
import java.util.UUID;
public class ExecutableStatement {
private PreparedStatement statement;
private final PreparedStatement statement;
private int pos = 1;
public ExecutableStatement(PreparedStatement statement) {
this.statement = statement;
}
@SneakyThrows
public Integer execute() {
public int execute() throws SQLException {
try {
return statement.executeUpdate();
} finally {
@@ -23,34 +22,30 @@ public class ExecutableStatement {
}
}
@SneakyThrows
public void execute(ResultSetIterator iterator) {
ResultSet rs = null;
try {
rs = statement.executeQuery();
public void execute(ResultSetIterator iterator) throws SQLException {
try(var rs = statement.executeQuery()) {
while (rs.next()) iterator.next(rs);
} finally {
MiscUtils.close(statement, rs);
MiscUtils.close(statement);
}
}
@SneakyThrows
public void executeSingle(ResultSetIterator iterator) {
ResultSet rs = null;
public int[] executeBatch() throws SQLException {
try {
return statement.executeBatch();
} finally {
MiscUtils.close(statement);
}
}
public ResultSet executeQuery() throws SQLException {
try {
rs = statement.executeQuery();
if (rs.next()) iterator.next(rs);
else iterator.next(null);
return statement.executeQuery();
} finally {
MiscUtils.close(statement, rs);
MiscUtils.close(statement);
}
}
@SneakyThrows
public ResultSet executeQuery() {
return statement.executeQuery();
}
@SneakyThrows
public ExecutableStatement append(Object obj) {
statement.setObject(pos++, obj);
@@ -135,4 +130,10 @@ public class ExecutableStatement {
statement.setBytes(pos++, obj);
return this;
}
@SneakyThrows
public ExecutableStatement addBatch() {
statement.addBatch();
return this;
}
}
@@ -81,7 +81,7 @@ public class MySQL {
}
}
private static void backupOldDB(File dbFile, File dataFolder) {
public static void backupOldDB(File dbFile, File dataFolder) {
if (dbFile.exists()) {
try {
// Optional: Make backup first
@@ -91,7 +91,7 @@ public class MySQL {
} else {
AntiVPN.getInstance().getExecutor().log("Backup directory already exists");
}
File backupFile = new File(backupDir, "database.mv.db.backup_" + System.currentTimeMillis());
File backupFile = new File(backupDir, dbFile.getName() + ".backup_" + System.currentTimeMillis());
Files.copy(dbFile.toPath(), backupFile.toPath());
// Actually delete the file
@@ -14,7 +14,10 @@ public class Query {
Query.conn = conn;
}
@SuppressWarnings("SqlSourceToSinkFlow")
public static ExecutableStatement prepare(@Language("SQL") String sql) throws SQLException {
return new ExecutableStatement(conn.prepareStatement(sql));
}
}
@@ -1,7 +1,8 @@
package dev.brighten.antivpn.database.sql.utils;
import java.sql.ResultSet;
import java.sql.SQLException;
public interface ResultSetIterator {
void next(ResultSet rs) throws Exception;
void next(ResultSet rs) throws SQLException;
}
@@ -1,15 +1,49 @@
/*
* Copyright 2026 Dawson Hessler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF 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.AntiVPN;
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.sql.utils.MySQL;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public interface H2Version extends Version<H2VPN> {
List<H2Version> versions = new ArrayList<>();
H2Version[] versions = new H2Version[] {new First(), new Second()};
static void registerVersions() {
default void backupDatabase() {
File dataFolder = new File(AntiVPN.getInstance().getPluginFolder(), "databases");
if(!dataFolder.exists()) {
return;
}
List<File> files = new ArrayList<>(List.of(Optional.ofNullable(dataFolder.listFiles()).orElse(new File[0])));
files.sort(Comparator.comparingLong(File::lastModified));
for (File file : files) {
MySQL.backupOldDB(file, dataFolder);
}
}
}
@@ -1,7 +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.database.version;
import java.sql.SQLException;
public interface Version<DB> {
void update(DB database) throws Exception;
void update(DB database) throws SQLException;
int versionNumber();
boolean needsUpdate(DB database);
}
@@ -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());
}
}
@@ -49,7 +49,7 @@ public class VelocityListener extends VPNExecutor {
LegacyComponentSerializer.builder()
.character('&')
.build().deserialize(AntiVPN.getInstance().getVpnConfig()
.countryVanillaKickReason()
.getCountryVanillaKickReason()
.replace("%player%", event.getPlayer().getUsername())
.replace("%country%", instantResult.response().getCountryName())
.replace("%code%", instantResult.response().getCountryCode()))));
@@ -59,7 +59,7 @@ public class VelocityListener extends VPNExecutor {
event.setResult(ResultedEvent.ComponentResult.denied(LegacyComponentSerializer.builder()
.character('&')
.build().deserialize(AntiVPN.getInstance().getVpnConfig()
.getKickString()
.getKickMessage()
.replace("%player%", event.getPlayer().getUsername())
.replace("%country%", instantResult.response().getCountryName())
.replace("%code%", instantResult.response().getCountryCode()))));
@@ -97,14 +97,14 @@ public class VelocityListener extends VPNExecutor {
//In case the user wants to run their own commands instead of using the
// built in kicking
if(AntiVPN.getInstance().getVpnConfig().kickPlayersOnDetect()) {
if(AntiVPN.getInstance().getVpnConfig().isKickPlayers()) {
switch (checkResult.resultType()) {
case DENIED_PROXY -> VelocityPlugin.INSTANCE.getServer().getScheduler()
.buildTask(VelocityPlugin.INSTANCE.getPluginInstance(), () ->
event.getPlayer().disconnect(LegacyComponentSerializer.builder()
.character('&')
.build().deserialize(AntiVPN.getInstance().getVpnConfig()
.getKickString()
.getKickMessage()
.replace("%player%", event.getPlayer().getUsername())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode()))))
@@ -114,7 +114,7 @@ public class VelocityListener extends VPNExecutor {
event.getPlayer().disconnect(LegacyComponentSerializer.builder()
.character('&')
.build().deserialize(AntiVPN.getInstance().getVpnConfig()
.countryVanillaKickReason()
.getCountryVanillaKickReason()
.replace("%player%", event.getPlayer().getUsername())
.replace("%country%", result.getCountryName())
.replace("%code%", result.getCountryCode()))))