Implement handbook request limiting

This commit is contained in:
KingRainbow44
2023-05-31 19:55:13 -04:00
parent a575a2b7f6
commit 8e11f53a2e
3 changed files with 107 additions and 27 deletions

View File

@@ -4,20 +4,12 @@ import ch.qos.logback.classic.Level;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.utils.*;
import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.Utils;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.lang.reflect.Field; import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import static emu.grasscutter.Grasscutter.config; import static emu.grasscutter.Grasscutter.*;
/** /**
* *when your JVM fails* * *when your JVM fails*
@@ -34,17 +26,18 @@ public class ConfigContainer {
* with the new dispatch server. * with the new dispatch server.
* Version 8 - 'server' is being added for enforcing handbook server * Version 8 - 'server' is being added for enforcing handbook server
* addresses. * addresses.
* Version 9 - 'limits' was added for handbook requests.
*/ */
private static int version() { private static int version() {
return 8; return 9;
} }
/** /**
* Attempts to update the server's existing configuration to the latest * Attempts to update the server's existing configuration.
*/ */
public static void updateConfig() { public static void updateConfig() {
try { // Check if the server is using a legacy config. try { // Check if the server is using a legacy config.
JsonObject configObject = JsonUtils.loadToClass(Grasscutter.configFile.toPath(), JsonObject.class); var configObject = JsonUtils.loadToClass(Grasscutter.configFile.toPath(), JsonObject.class);
if (!configObject.has("version")) { if (!configObject.has("version")) {
Grasscutter.getLogger().info("Updating legacy .."); Grasscutter.getLogger().info("Updating legacy ..");
Grasscutter.saveConfig(null); Grasscutter.saveConfig(null);
@@ -58,9 +51,9 @@ public class ConfigContainer {
return; return;
// Create a new configuration instance. // Create a new configuration instance.
ConfigContainer updated = new ConfigContainer(); var updated = new ConfigContainer();
// Update all configuration fields. // Update all configuration fields.
Field[] fields = ConfigContainer.class.getDeclaredFields(); var fields = ConfigContainer.class.getDeclaredFields();
Arrays.stream(fields).forEach(field -> { Arrays.stream(fields).forEach(field -> {
try { try {
field.set(updated, field.get(config)); field.set(updated, field.get(config));
@@ -73,7 +66,7 @@ public class ConfigContainer {
Grasscutter.saveConfig(updated); Grasscutter.saveConfig(updated);
Grasscutter.loadConfig(); Grasscutter.loadConfig();
} catch (Exception exception) { } catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to inject the updated ", exception); Grasscutter.getLogger().warn("Failed to save the updated configuration.", exception);
} }
} }
@@ -301,17 +294,31 @@ public class ConfigContainer {
public static class HandbookOptions { public static class HandbookOptions {
public boolean enable = false; public boolean enable = false;
public boolean allowCommands = true; public boolean allowCommands = true;
public int maxRequests = 10;
public int maxEntities = 100;
public Limits limits = new Limits();
public Server server = new Server(); public Server server = new Server();
public static class Limits {
/* Are rate limits checked? */
public boolean enabled = false;
/* The time for limits to expire. */
public int interval = 3;
/* The maximum amount of normal requests. */
public int maxRequests = 10;
/* The maximum amount of entities to be spawned in one request. */
public int maxEntities = 25;
}
public static class Server { public static class Server {
/* Are the server settings sent to the handbook? */
public boolean enforced = false; public boolean enforced = false;
/* The default server address for the handbook's authentication. */
public String address = "127.0.0.1"; public String address = "127.0.0.1";
/* The default server port for the handbook's authentication. */
public int port = 443; public int port = 443;
/* Should the defaults be enforced? */
public boolean canChange = true; public boolean canChange = true;
} }
} }

View File

@@ -1,23 +1,27 @@
package emu.grasscutter.server.http.documentation; package emu.grasscutter.server.http.documentation;
import static emu.grasscutter.config.Configuration.HANDBOOK;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest; import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.DispatchUtils; import emu.grasscutter.utils.*;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.objects.*;
import emu.grasscutter.utils.objects.HandbookBody;
import emu.grasscutter.utils.objects.HandbookBody.Action; import emu.grasscutter.utils.objects.HandbookBody.Action;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.ContentType; import io.javalin.http.*;
import io.javalin.http.Context;
import java.util.*;
import java.util.concurrent.*;
import static emu.grasscutter.config.Configuration.HANDBOOK;
/** Handles requests for the new GM Handbook. */ /** Handles requests for the new GM Handbook. */
public final class HandbookHandler implements Router { public final class HandbookHandler implements Router {
private String handbook; private String handbook;
private final boolean serve; private final boolean serve;
private final Map<String, Integer> currentRequests
= new ConcurrentHashMap<>();
/** /**
* Constructor for the handbook router. Enables serving the handbook if the handbook file is * Constructor for the handbook router. Enables serving the handbook if the handbook file is
* found. * found.
@@ -34,6 +38,17 @@ public final class HandbookHandler implements Router {
.replace("{{DETAILS_PORT}}", String.valueOf(server.port)) .replace("{{DETAILS_PORT}}", String.valueOf(server.port))
.replace("{{DETAILS_DISABLE}}", Boolean.toString(!server.canChange)); .replace("{{DETAILS_DISABLE}}", Boolean.toString(!server.canChange));
} }
// Create a new task to reset the request count.
if (HANDBOOK.limits.enabled) {
new Timer().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
currentRequests.clear();
}
}, 0, TimeUnit.SECONDS.toMillis(
HANDBOOK.limits.interval));
}
} }
@Override @Override
@@ -60,6 +75,32 @@ public final class HandbookHandler implements Router {
return HANDBOOK.enable && HANDBOOK.allowCommands; return HANDBOOK.enable && HANDBOOK.allowCommands;
} }
/**
* Checks the request against the normal request limits.
*
* @param ctx The Javalin request context.
* @return True if the request is within the normal limits.
*/
private boolean normalLimit(Context ctx) {
var limits = HANDBOOK.limits;
if (!limits.enabled) return true;
// Check the request count.
var address = Utils.address(ctx);
var count = this.currentRequests.getOrDefault(address, 0);
if (++count >= limits.maxRequests) {
// Respond to the request.
ctx.status(429).result(JObject.c()
.add("timestamp", System.currentTimeMillis())
.toString());
return false;
}
// Update the request count.
this.currentRequests.put(address, count);
return true;
}
/** /**
* Serves the handbook if it is found. * Serves the handbook if it is found.
* *
@@ -128,6 +169,9 @@ public final class HandbookHandler implements Router {
return; return;
} }
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class. // Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.GrantAvatar.class); var request = ctx.bodyAsClass(HandbookBody.GrantAvatar.class);
// Get the response. // Get the response.
@@ -148,6 +192,9 @@ public final class HandbookHandler implements Router {
return; return;
} }
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class. // Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.GiveItem.class); var request = ctx.bodyAsClass(HandbookBody.GiveItem.class);
// Get the response. // Get the response.
@@ -168,6 +215,9 @@ public final class HandbookHandler implements Router {
return; return;
} }
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class. // Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.TeleportTo.class); var request = ctx.bodyAsClass(HandbookBody.TeleportTo.class);
// Get the response. // Get the response.
@@ -188,8 +238,23 @@ public final class HandbookHandler implements Router {
return; return;
} }
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class. // Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.SpawnEntity.class); var request = ctx.bodyAsClass(HandbookBody.SpawnEntity.class);
// Check the entity limit.
var entityLimit = HANDBOOK.limits.enabled ?
Math.max(HANDBOOK.limits.maxEntities, 0) :
Long.MAX_VALUE;
if (request.getAmount() > entityLimit) {
ctx.status(400).result(JObject.c()
.add("timestamp", System.currentTimeMillis())
.add("error", "Entity limit exceeded.")
.toString());
return;
}
// Get the response. // Get the response.
var response = DispatchUtils.performHandbookAction(Action.SPAWN_ENTITY, request); var response = DispatchUtils.performHandbookAction(Action.SPAWN_ENTITY, request);
// Send the response. // Send the response.

View File

@@ -128,4 +128,12 @@ public final class JObject {
public Object json() { public Object json() {
return this.members; return this.members;
} }
/**
* @return A string representation of this object.
*/
@Override
public String toString() {
return JsonUtils.encode(this.gson());
}
} }