Skip to content

Commit

Permalink
Whitelist bounce gathering (#3511)
Browse files Browse the repository at this point in the history
* Store bounced whitelist logins

* Add allowlist bounce endpoint

* Restore locale file indent from master branch

* Add UI for allowlist

* Update locale

* Fix sonar detected bug and implement database tests

Affects issues:
- Close #2233
  • Loading branch information
AuroraLS3 authored Mar 10, 2024
1 parent 24e6af2 commit 8116063
Show file tree
Hide file tree
Showing 45 changed files with 871 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.transactions.events.BanStatusTransaction;
import com.djrapitops.plan.storage.database.transactions.events.KickStoreTransaction;
import com.djrapitops.plan.storage.database.transactions.events.StoreAllowlistBounceTransaction;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import org.bukkit.event.EventHandler;
Expand Down Expand Up @@ -82,6 +83,11 @@ public void onPlayerLogin(PlayerLoginEvent event) {
UUID playerUUID = event.getPlayer().getUniqueId();
ServerUUID serverUUID = serverInfo.getServerUUID();
boolean banned = PlayerLoginEvent.Result.KICK_BANNED == event.getResult();
boolean notWhitelisted = PlayerLoginEvent.Result.KICK_WHITELIST == event.getResult();

if (notWhitelisted) {
dbSystem.getDatabase().executeTransaction(new StoreAllowlistBounceTransaction(playerUUID, event.getPlayer().getName(), serverUUID, System.currentTimeMillis()));
}

String address = event.getHostname();
if (!address.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public enum WebPermission implements Supplier<String>, Lang {
PAGE_SERVER_PERFORMANCE_OVERVIEW("See Performance numbers"),
PAGE_SERVER_PLUGIN_HISTORY("See Plugin History"),
PAGE_SERVER_PLUGINS("See Plugins -tabs of servers"),
PAGE_SERVER_ALLOWLIST_BOUNCE("See list of Game allowlist bounces"),

PAGE_PLAYER("See all of player page"),
PAGE_PLAYER_OVERVIEW("See Player Overview -tab"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.domain.datatransfer;

import com.djrapitops.plan.utilities.dev.Untrusted;

import java.util.Objects;
import java.util.UUID;

/**
* Represents an event where player bounced off the whitelist.
*
* @author AuroraLS3
*/
public class AllowlistBounce {

private final UUID playerUUID;
@Untrusted
private final String playerName;
private final int count;
private final long lastTime;

public AllowlistBounce(UUID playerUUID, String playerName, int count, long lastTime) {
this.playerUUID = playerUUID;
this.playerName = playerName;
this.count = count;
this.lastTime = lastTime;
}

public UUID getPlayerUUID() {
return playerUUID;
}

public String getPlayerName() {
return playerName;
}

public int getCount() {
return count;
}

public long getLastTime() {
return lastTime;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AllowlistBounce bounce = (AllowlistBounce) o;
return getCount() == bounce.getCount() && getLastTime() == bounce.getLastTime() && Objects.equals(getPlayerUUID(), bounce.getPlayerUUID()) && Objects.equals(getPlayerName(), bounce.getPlayerName());
}

@Override
public int hashCode() {
return Objects.hash(getPlayerUUID(), getPlayerName(), getCount(), getLastTime());
}

@Override
public String toString() {
return "AllowlistBounce{" +
"playerUUID=" + playerUUID +
", playerName='" + playerName + '\'' +
", count=" + count +
", lastTime=" + lastTime +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public enum DataID {
JOIN_ADDRESSES_BY_DAY,
PLAYER_RETENTION,
PLAYER_JOIN_ADDRESSES,
PLAYER_ALLOWLIST_BOUNCES,
;

public String of(ServerUUID serverUUID) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;

import com.djrapitops.plan.delivery.domain.auth.WebPermission;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.cache.AsyncJSONResolverService;
import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.objects.AllowlistQueries;
import com.djrapitops.plan.storage.database.queries.objects.SessionQueries;
import com.djrapitops.plan.utilities.dev.Untrusted;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.jetbrains.annotations.Nullable;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Map;
import java.util.Optional;

/**
* Response resolver to get game allowlist bounces.
*
* @author AuroraLS3
*/
@Singleton
@Path("/v1/gameAllowlistBounces")
public class AllowlistJSONResolver extends JSONResolver {

private final DBSystem dbSystem;
private final Identifiers identifiers;
private final AsyncJSONResolverService jsonResolverService;

@Inject
public AllowlistJSONResolver(DBSystem dbSystem, Identifiers identifiers, AsyncJSONResolverService jsonResolverService) {
this.dbSystem = dbSystem;
this.identifiers = identifiers;
this.jsonResolverService = jsonResolverService;
}

@Override
public Formatter<Long> getHttpLastModifiedFormatter() {return jsonResolverService.getHttpLastModifiedFormatter();}

@Override
public boolean canAccess(@Untrusted Request request) {
WebUser user = request.getUser().orElse(new WebUser(""));

return user.hasPermission(WebPermission.PAGE_SERVER_ALLOWLIST_BOUNCE);
}

@GET
@Operation(
description = "Get allowlist bounce data for server",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
@ApiResponse(responseCode = "400", description = "If 'server' parameter is not an existing server")
},
parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for (optional)", examples = {
@ExampleObject("Server 1"),
@ExampleObject("1"),
@ExampleObject("1fb39d2a-eb82-4868-b245-1fad17d823b3"),
}),
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(@Untrusted Request request) {
return Optional.of(getResponse(request));
}

private Response getResponse(@Untrusted Request request) {
JSONStorage.StoredJSON result = getStoredJSON(request);
return getCachedOrNewResponse(request, result);
}

@Nullable
private JSONStorage.StoredJSON getStoredJSON(Request request) {
Optional<Long> timestamp = Identifiers.getTimestamp(request);

ServerUUID serverUUID = identifiers.getServerUUID(request);
Database database = dbSystem.getDatabase();
return jsonResolverService.resolve(timestamp, DataID.PLAYER_ALLOWLIST_BOUNCES, serverUUID,
theUUID -> Map.of(
"allowlist_bounces", database.query(AllowlistQueries.getBounces(serverUUID)),
"last_seen_by_uuid", database.query(SessionQueries.lastSeen(serverUUID))
)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public RootJSONResolver(
RetentionJSONResolver retentionJSONResolver,
PlayerJoinAddressJSONResolver playerJoinAddressJSONResolver,
PluginHistoryJSONResolver pluginHistoryJSONResolver,
AllowlistJSONResolver allowlistJSONResolver,

PreferencesJSONResolver preferencesJSONResolver,
StorePreferencesJSONResolver storePreferencesJSONResolver,
Expand Down Expand Up @@ -129,7 +130,8 @@ public RootJSONResolver(
.add("extensionData", extensionJSONResolver)
.add("retention", retentionJSONResolver)
.add("joinAddresses", playerJoinAddressJSONResolver)
.add("preferences", preferencesJSONResolver);
.add("preferences", preferencesJSONResolver)
.add("gameAllowlistBounces", allowlistJSONResolver);

this.webServer = webServer;
// These endpoints require authentication to be enabled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@ public enum HtmlLang implements Lang {
LABEL_TABLE_SHOW_PER_PAGE("html.label.table.showPerPage", "Show per page"),
LABEL_EXPORT("html.label.export", "Export"),

LABEL_ALLOWLIST("html.label.allowlist", "Allowlist"),
LABEL_ALLOWLIST_BOUNCES("html.label.allowlistBounces", "Allowlist Bounces"),
LABEL_ATTEMPTS("html.label.attempts", "Attempts"),
LABEL_LAST_KNOWN_ATTEMPT("html.label.lastKnownAttempt", "Last Known Attempt"),
LABEL_PREVIOUS_ATTEMPT("html.label.lastBlocked", "Last Blocked"),
LABEL_LAST_ALLOWED_LOGIN("html.label.lastAllowed", "Last Allowed"),
LABEL_BLOCKED("html.label.blocked", "Blocked"),
LABEL_ALLOWED("html.label.allowed", "Allowed"),

LOGIN_LOGIN("html.login.login", "Login"),
LOGIN_LOGOUT("html.login.logout", "Logout"),
LOGIN_USERNAME("html.login.username", "Username"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ public void downloadDriver() {
}
}

public static ThreadLocal<StackTraceElement[]> getTransactionOrigin() {
return TRANSACTION_ORIGIN;
}

@Override
public void init() {
List<Runnable> unfinishedTransactions = forceCloseTransactionExecutor();
Expand Down Expand Up @@ -187,22 +191,6 @@ protected boolean attemptToCloseTransactionExecutor() {
return true;
}

protected List<Runnable> forceCloseTransactionExecutor() {
if (transactionExecutor == null || transactionExecutor.isShutdown() || transactionExecutor.isTerminated()) {
return Collections.emptyList();
}
try {
List<Runnable> unfinished = transactionExecutor.shutdownNow();
int unfinishedCount = unfinished.size();
if (unfinishedCount > 0) {
logger.warn(unfinishedCount + " unfinished database transactions were not executed.");
}
return unfinished;
} finally {
logger.info(locale.getString(PluginLang.DISABLED_WAITING_TRANSACTIONS_COMPLETE));
}
}

Patch[] patches() {
return new Patch[]{
new Version10Patch(),
Expand Down Expand Up @@ -313,6 +301,22 @@ public void run() {
*/
public abstract void setupDataSource();

protected List<Runnable> forceCloseTransactionExecutor() {
if (transactionExecutor == null || transactionExecutor.isShutdown() || transactionExecutor.isTerminated()) {
return Collections.emptyList();
}
try {
List<Runnable> unfinished = transactionExecutor.shutdownNow();
int unfinishedCount = unfinished.size();
if (unfinishedCount > 0) {
logger.warn(unfinishedCount + " unfinished database transactions were not executed.");
}
return unfinished;
} finally {
logger.info(locale.getString(PluginLang.DISABLED_WAITING_TRANSACTIONS_COMPLETE));
}
}

@Override
public void close() {
// SQLiteDB Overrides this, so any additions to this should also be reflected there.
Expand All @@ -326,13 +330,6 @@ public void close() {
setState(State.CLOSED);
}

protected void unloadDriverClassloader() {
// Unloading class loader using close() causes issues when reloading.
// It is better to leak this memory than crash the plugin on reload.

driverClassLoader = null;
}

public abstract Connection getConnection() throws SQLException;

public abstract void returnToPool(Connection connection);
Expand All @@ -346,8 +343,11 @@ public <T> T queryWithinTransaction(Query<T> query, Transaction transaction) {
return accessLock.performDatabaseOperation(() -> query.executeQuery(this), transaction);
}

public static ThreadLocal<StackTraceElement[]> getTransactionOrigin() {
return TRANSACTION_ORIGIN;
protected void unloadDriverClassloader() {
// Unloading class loader using close() causes issues when reloading.
// It is better to leak this memory than crash the plugin on reload.

driverClassLoader = null;
}

@Override
Expand Down
Loading

0 comments on commit 8116063

Please sign in to comment.