Added mongodb and option to remove spoiled cached responses

This commit is contained in:
2025-06-05 11:35:15 -04:00
parent c191fbccfa
commit edd255e29d
15 changed files with 390 additions and 109 deletions
@@ -3,7 +3,7 @@ package dev.brighten.antivpn.bukkit;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.bukkit.command.BukkitCommand;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.Database;
import dev.brighten.antivpn.database.sqllite.LiteDatabase;
import lombok.Getter;
import org.bstats.bukkit.Metrics;
@@ -114,7 +114,7 @@ public class BukkitPlugin extends JavaPlugin {
}
private String getDatabaseType() {
VPNDatabase database = AntiVPN.getInstance().getDatabase();
Database database = AntiVPN.getInstance().getDatabase();
if(database instanceof LiteDatabase) {
return "SQLLite";
@@ -3,7 +3,7 @@ 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.Database;
import dev.brighten.antivpn.database.sqllite.LiteDatabase;
import net.md_5.bungee.api.plugin.Plugin;
import org.bstats.bungeecord.Metrics;
@@ -47,7 +47,7 @@ public class BungeePlugin extends Plugin {
}
private String getDatabaseType() {
VPNDatabase database = AntiVPN.getInstance().getDatabase();
Database database = AntiVPN.getInstance().getDatabase();
if(database instanceof LiteDatabase) {
return "SQLLite";
+8 -2
View File
@@ -80,8 +80,8 @@
</excludes>
</relocation>
<relocation>
<pattern>com.mongodb</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.mongodb</shadedPattern>
<pattern>com.mongodb.client</pattern>
<shadedPattern>dev.brighten.antivpn.shaded.com.mongodb.client</shadedPattern>
<excludes>
<!-- Exclude annotation values from relocation -->
<exclude>dev.brighten.antivpn.depends.Relocate</exclude>
@@ -154,6 +154,12 @@
<artifactId>annotations</artifactId>
<version>24.0.1</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>5.5.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
@@ -5,7 +5,8 @@ import dev.brighten.antivpn.api.VPNConfig;
import dev.brighten.antivpn.api.VPNExecutor;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.command.impl.AntiVPNCommand;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.Database;
import dev.brighten.antivpn.database.mongodb.MongoDatabase;
import dev.brighten.antivpn.database.postgres.PostgresDatabase;
import dev.brighten.antivpn.database.sqllite.LiteDatabase;
import dev.brighten.antivpn.database.sqllite.version.Version;
@@ -42,13 +43,17 @@ import java.util.List;
relocations = {
@Relocate(from = "org\\.postgresql", to = "dev.brighten.antivpn.shaded.org.postgresql")
})
@MavenLibrary(groupId = "com\\.mongodb", artifactId = "driver-sync", version = "5.5.0",
relocations = {
@Relocate(from = "com\\.mongodb.client", to = "dev.brighten.antivpn.shaded.com.mongodb.client")
})
public class AntiVPN {
private static AntiVPN INSTANCE;
private VPNConfig vpnConfig;
private VPNExecutor executor;
private PlayerExecutor playerExecutor;
private VPNDatabase database;
private Database database;
private MessageHandler messageHandler;
private Configuration config;
private List<Command> commands = new ArrayList<>();
@@ -94,6 +99,7 @@ public class AntiVPN {
INSTANCE.database = switch (INSTANCE.vpnConfig.getDatabaseType().toLowerCase()) {
case "sqlite", "sqllite" -> new LiteDatabase();
case "postgresql", "postgres" -> new PostgresDatabase();
case "mongo", "mongodb" -> new MongoDatabase();
default ->
throw new IllegalStateException("Unexpected database type set at config.yml 'database.type': \""
+ INSTANCE.vpnConfig.getDatabaseType().toLowerCase() + "\"!" +
@@ -0,0 +1,39 @@
package dev.brighten.antivpn.database;
import dev.brighten.antivpn.web.objects.VPNResponse;
import java.util.Optional;
import java.util.UUID;
@SuppressWarnings({"unused"})
public interface Database {
Optional<VPNResponse> getStoredResponse(String ip);
void cacheResponse(VPNResponse toCache);
void deleteResponse(String ip);
void clearResponses();
void clearOutdatedResponses();
boolean isWhitelisted(UUID uuid);
boolean isWhitelisted(String ip);
void addWhitelist(UUID uuid);
void addWhitelist(String cidr);
void removeWhitelist(UUID uuid);
void removeWhitelist(String cidr);
boolean getAlertsState(UUID uuid);
void updateAlertsState(UUID uuid, boolean state);
void init();
void shutdown();
}
@@ -1,88 +0,0 @@
package dev.brighten.antivpn.database;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.sqllite.version.Version;
import dev.brighten.antivpn.web.objects.VPNResponse;
import org.intellij.lang.annotations.Language;
import java.sql.*;
import java.util.Optional;
import java.util.UUID;
@SuppressWarnings({"unused", "SqlSourceToSinkFlow"})
public interface VPNDatabase {
Optional<VPNResponse> getStoredResponse(String ip);
void cacheResponse(VPNResponse toCache);
void deleteResponse(String ip);
void clearResponses();
boolean isWhitelisted(UUID uuid);
boolean isWhitelisted(String ip);
void addWhitelist(UUID uuid);
void addWhitelist(String cidr);
void removeWhitelist(UUID uuid);
void removeWhitelist(String cidr);
boolean getAlertsState(UUID uuid);
void updateAlertsState(UUID uuid, boolean state);
void init();
default void setupTable() throws SQLException {
try(Connection connection = connection()) {
Statement statement = connection.createStatement();
statement.execute("CREATE TABLE IF NOT EXISTS vpn_responses (ip TEXT, response TEXT)");
statement.execute("CREATE TABLE IF NOT EXISTS whitelist (uuid TEXT, minimum NUMERIC, maximum NUMERIC)");
statement.execute("CREATE TABLE IF NOT EXISTS alerts (uuid TEXT)");
statement.execute("CREATE TABLE IF NOT EXISTS version (version INTEGER PRIMARY KEY, updated BOOLEAN)");
// Run through updates
for (Version version : Version.versions) {
try(ResultSet result = query("SELECT * FROM version WHERE version = ?",
version.versionNumber())) {
if(!result.next()) {
version.update(this);
statement("INSERT INTO version (version, updated) VALUES (?, ?)",
version.versionNumber(), true);
}
} catch (SQLException e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
}
}
default ResultSet query(@Language("SQL") String query, Object... args) throws SQLException {
try(Connection connection = connection()) {
PreparedStatement pstmt = connection.prepareStatement(query);
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
return pstmt.executeQuery();
}
}
default void statement(@Language("SQL") String query, Object... args) throws SQLException {
try(Connection connection = connection()) {
PreparedStatement pstmt = connection.prepareStatement(query);
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
pstmt.execute();
}
}
Connection connection();
void shutdown();
}
@@ -0,0 +1,229 @@
package dev.brighten.antivpn.database.mongodb;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.UpdateOptions;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.Database;
import dev.brighten.antivpn.database.mongodb.records.AlertsUser;
import dev.brighten.antivpn.database.mongodb.records.CidrWhitelist;
import dev.brighten.antivpn.database.mongodb.records.UserIpResponse;
import dev.brighten.antivpn.database.mongodb.records.UserWhitelist;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.utils.IpUtils;
import dev.brighten.antivpn.utils.json.JSONException;
import dev.brighten.antivpn.web.objects.VPNResponse;
import org.bson.Document;
import java.math.BigDecimal;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class MongoDatabase implements Database {
private MongoCollection<AlertsUser> alertsCollection;
private MongoCollection<CidrWhitelist> cidrWhitelistCollection;
private MongoCollection<UserWhitelist> userWhitelistCollection;
private MongoCollection<UserIpResponse> vpnResponseCollection;
@Override
public Optional<VPNResponse> getStoredResponse(String ip) {
UserIpResponse response = vpnResponseCollection.find(Filters.eq("ip", ip)).first();
if(response == null) {
return Optional.empty();
}
try {
return Optional.of(response.getVpnResponse());
} catch (JSONException e) {
AntiVPN.getInstance().getExecutor().logException("Could not convert vpn response from JSON String to DTO " +
"for address: " + ip, e);
return Optional.empty();
}
}
@Override
public void cacheResponse(VPNResponse toCache) {
try {
UserIpResponse response = new UserIpResponse(toCache.getIp(), new Date(), toCache.toJson().toString());
vpnResponseCollection.updateOne(Filters.eq("ip", toCache.getIp()),
new Document("$set", response),
new UpdateOptions().upsert(true));
} catch (JSONException e) {
AntiVPN.getInstance().getExecutor().log(Level.SEVERE, "An error occurred while caching response", e);
}
}
@Override
public void deleteResponse(String ip) {
vpnResponseCollection.deleteOne(Filters.eq("ip", ip));
}
@Override
public void clearResponses() {
//Clears all documents within the collection
var result = vpnResponseCollection.deleteMany(new Document());
AntiVPN.getInstance().getExecutor().log(Level.INFO, "VPN responses have been cleared (count=%s)", result.getDeletedCount());
}
@Override
public void clearOutdatedResponses() {
var result = vpnResponseCollection.deleteMany(Filters.lte("date",
new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMicros(7))));
AntiVPN.getInstance().getExecutor().log(Level.INFO, "Cleared outdated responses (count=%s)",
result.getDeletedCount());
}
@Override
public boolean isWhitelisted(UUID uuid) {
UserWhitelist whitelist = userWhitelistCollection.find(Filters.eq("uuid", uuid)).first();
return whitelist != null;
}
@Override
public boolean isWhitelisted(String ip) {
Optional<BigDecimal> ipDec = IpUtils.getIpDecimal(ip);
if(ipDec.isEmpty()) {
AntiVPN.getInstance().getExecutor().log(Level.WARNING, "Could not check for whitelist on IP %s since" +
" it cannot be converted to decimal!", ip);
return false;
}
BigDecimal decimal = ipDec.get();
CidrWhitelist whitelist = cidrWhitelistCollection.find(Filters.and(
Filters.lte("start", decimal),
Filters.gte("end", decimal)
)).first();
return whitelist != null;
}
@Override
public void addWhitelist(UUID uuid) {
UserWhitelist whitelist = new UserWhitelist(uuid);
userWhitelistCollection.insertOne(whitelist);
}
@Override
public void addWhitelist(String cidr) {
try {
var cidrObj = new CIDRUtils(cidr);
Optional<BigDecimal> start = IpUtils.getIpDecimal(cidrObj.getStartAddress().getHostAddress()),
end = IpUtils.getIpDecimal(cidrObj.getEndAddress().getHostAddress());
if(start.isEmpty() || end.isEmpty()) {
AntiVPN.getInstance().getExecutor().log(Level.WARNING, "Could not whitelist cidr %s since " +
"it is missing either a start or end address", cidr);
return;
}
CidrWhitelist whitelist = new CidrWhitelist(start.get(), end.get());
cidrWhitelistCollection.insertOne(whitelist);
} catch (UnknownHostException e) {
AntiVPN.getInstance().getExecutor().logException("Could not whitelist cidr: " + cidr, e);
}
}
@Override
public void removeWhitelist(UUID uuid) {
var result = userWhitelistCollection.deleteMany(Filters.eq("uuid", uuid.toString()));
AntiVPN.getInstance().getExecutor().log(Level.INFO, "Removed whitelist for uuid %s (count=%s)",
uuid, result.getDeletedCount());
}
@Override
public void removeWhitelist(String cidr) {
try {
var cidrObj = new CIDRUtils(cidr);
Optional<BigDecimal> start = IpUtils.getIpDecimal(cidrObj.getStartAddress().getHostAddress());
Optional<BigDecimal> end = IpUtils.getIpDecimal(cidrObj.getEndAddress().getHostAddress());
if(start.isEmpty() || end.isEmpty()) {
AntiVPN.getInstance().getExecutor().log(Level.WARNING, "Could not remove cidr %s from whitelist" +
" since it is missing either a start or end address.", cidr);
return;
}
var result = cidrWhitelistCollection.deleteMany(Filters.and(Filters.eq("start", start.get()),
Filters.eq("end", end.get())));
AntiVPN.getInstance().getExecutor().log(Level.INFO, "Removed cidr %s from whitelist (count=%s).",
cidr, result.getDeletedCount());
} catch (UnknownHostException e) {
AntiVPN.getInstance().getExecutor().logException("Could not remove whitelist for CIDR: " + cidr, e);
}
}
@Override
public boolean getAlertsState(UUID uuid) {
AlertsUser alertsUser = alertsCollection.find(Filters.eq("uuid", uuid)).first();
if(alertsUser == null) {
return false;
}
return alertsUser.state();
}
@Override
public void updateAlertsState(UUID uuid, boolean state) {
alertsCollection.updateOne(Filters.eq("uuid", uuid),
new Document("$set", new AlertsUser(uuid, state)),
new UpdateOptions().upsert(true));
}
@Override
public void init() {
String connectionUrl;
if(AntiVPN.getInstance().getVpnConfig().getMongoURL().isEmpty()) {
String databaseName = AntiVPN.getInstance().getVpnConfig().getDatabaseName();
String username = AntiVPN.getInstance().getVpnConfig().getUsername();
String password = AntiVPN.getInstance().getVpnConfig().getPassword();
String ip = AntiVPN.getInstance().getVpnConfig().getIp();
int port = AntiVPN.getInstance().getVpnConfig().getPort();
connectionUrl = String.format("mongodb+srv://" +
"%s:%s>@%s:%s/%s?connectTimeoutMS=2000",
username, password, ip, port, databaseName);
} else {
connectionUrl = AntiVPN.getInstance()
.getVpnConfig().getMongoURL();
}
try(MongoClient mongoClient = MongoClients.create(connectionUrl)) {
var database = mongoClient.getDatabase(AntiVPN.getInstance().getVpnConfig().getDatabaseName());
userWhitelistCollection = database.getCollection("whitelist", UserWhitelist.class);
cidrWhitelistCollection = database.getCollection("cidrWhitelist", CidrWhitelist.class);
alertsCollection = database.getCollection("alerts", AlertsUser.class);
vpnResponseCollection = database.getCollection("responses", UserIpResponse.class);
}
}
@Override
public void shutdown() {
userWhitelistCollection = null;
cidrWhitelistCollection = null;
alertsCollection = null;
vpnResponseCollection = null;
}
}
@@ -0,0 +1,6 @@
package dev.brighten.antivpn.database.mongodb.records;
import java.util.UUID;
public record AlertsUser(UUID uuid, boolean state) {
}
@@ -0,0 +1,6 @@
package dev.brighten.antivpn.database.mongodb.records;
import java.math.BigDecimal;
public record CidrWhitelist(BigDecimal start, BigDecimal end) {
}
@@ -0,0 +1,13 @@
package dev.brighten.antivpn.database.mongodb.records;
import dev.brighten.antivpn.utils.json.JSONException;
import dev.brighten.antivpn.web.objects.VPNResponse;
import java.util.Date;
public record UserIpResponse(String ip, Date queried, String response) {
public VPNResponse getVpnResponse() throws JSONException {
return VPNResponse.fromJson(response);
}
}
@@ -0,0 +1,6 @@
package dev.brighten.antivpn.database.mongodb.records;
import java.util.UUID;
public record UserWhitelist(UUID uuid) {
}
@@ -1,20 +1,23 @@
package dev.brighten.antivpn.database.sqllite;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.sqllite.version.Version;
import dev.brighten.antivpn.utils.CIDRUtils;
import dev.brighten.antivpn.utils.IpUtils;
import dev.brighten.antivpn.utils.StringUtil;
import dev.brighten.antivpn.utils.json.JSONException;
import dev.brighten.antivpn.web.objects.VPNResponse;
import org.intellij.lang.annotations.Language;
import java.math.BigDecimal;
import java.net.UnknownHostException;
import java.sql.*;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class LiteDatabase implements VPNDatabase {
@SuppressWarnings("SqlSourceToSinkFlow")
public class LiteDatabase implements dev.brighten.antivpn.database.Database {
protected Connection connection;
@@ -36,7 +39,7 @@ public class LiteDatabase implements VPNDatabase {
String hashedIp = StringUtil.getHash(toCache.getIp());
try {
statement("INSERT INTO vpn_responses (ip, response) VALUES (?, ?)", hashedIp, jsonResponse);
statement("INSERT INTO vpn_responses (ip, date, response) VALUES (?, ?, ?)", hashedIp, System.currentTimeMillis(), jsonResponse);
} catch (SQLException e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
@@ -63,6 +66,17 @@ public class LiteDatabase implements VPNDatabase {
}
}
@Override
public void clearOutdatedResponses() {
long cutoffTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7);
try {
statement("DELETE FROM vpn_responses WHERE date < ?", cutoffTime);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isWhitelisted(UUID uuid) {
try(ResultSet result = query("SELECT * FROM whitelist WHERE uuid = ?", uuid.toString())) {
@@ -194,12 +208,44 @@ public class LiteDatabase implements VPNDatabase {
}
}
@Override
public void setupTable() throws SQLException {
VPNDatabase.super.setupTable();
// Run through updates
for (Version version : Version.versions) {
try(ResultSet result = query("SELECT * FROM version WHERE version = ?",
version.versionNumber())) {
if(!result.next()) {
version.update(this);
statement("INSERT INTO version (version, updated) VALUES (?, ?)",
version.versionNumber(), true);
}
} catch (SQLException e) {
AntiVPN.getInstance().getExecutor().logException(e);
}
}
}
protected ResultSet query(@Language("SQL") String query, Object... args) throws SQLException {
try(Connection connection = connection()) {
PreparedStatement pstmt = connection.prepareStatement(query);
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
return pstmt.executeQuery();
}
}
protected void statement(@Language("SQL") String query, Object... args) throws SQLException {
try(Connection connection = connection()) {
PreparedStatement pstmt = connection.prepareStatement(query);
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
pstmt.execute();
}
}
@Override
public Connection connection() {
return connection;
}
@@ -1,6 +1,6 @@
package dev.brighten.antivpn.database.sqllite.version;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.sqllite.LiteDatabase;
import dev.brighten.antivpn.database.sqllite.version.impl.First;
import java.sql.SQLException;
@@ -8,7 +8,7 @@ import java.util.ArrayList;
import java.util.List;
public interface Version {
void update(VPNDatabase database) throws SQLException;
void update(LiteDatabase database) throws SQLException;
int versionNumber();
List<Version> versions = new ArrayList<>();
@@ -1,13 +1,25 @@
package dev.brighten.antivpn.database.sqllite.version.impl;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.sqllite.LiteDatabase;
import dev.brighten.antivpn.database.sqllite.version.Version;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
public class First implements Version {
@Override
public void update(VPNDatabase database) {
public void update(LiteDatabase database) {
try(Connection connection = database.connection()) {
Statement statement = connection.createStatement();
statement.execute("CREATE TABLE IF NOT EXISTS vpn_responses (ip TEXT, date INTEGER, response TEXT)");
statement.execute("CREATE TABLE IF NOT EXISTS whitelist (uuid TEXT, minimum NUMERIC, maximum NUMERIC)");
statement.execute("CREATE TABLE IF NOT EXISTS alerts (uuid TEXT)");
statement.execute("CREATE TABLE IF NOT EXISTS version (version INTEGER PRIMARY KEY, updated BOOLEAN)");
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
@@ -9,7 +9,7 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import dev.brighten.antivpn.AntiVPN;
import dev.brighten.antivpn.command.Command;
import dev.brighten.antivpn.database.VPNDatabase;
import dev.brighten.antivpn.database.Database;
import dev.brighten.antivpn.database.sqllite.LiteDatabase;
import dev.brighten.antivpn.velocity.command.VelocityCommand;
import lombok.Getter;
@@ -84,7 +84,7 @@ public class VelocityPlugin {
}
private String getDatabaseType() {
VPNDatabase database = AntiVPN.getInstance().getDatabase();
Database database = AntiVPN.getInstance().getDatabase();
if(database instanceof LiteDatabase) {
return "SQLLite";