Implement handbook request limiting

This commit is contained in:
KingRainbow44 2023-05-31 19:55:13 -04:00
parent a575a2b7f6
commit 8e11f53a2e
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
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.annotations.SerializedName;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.Utils;
import emu.grasscutter.utils.*;
import lombok.NoArgsConstructor;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.*;
import static emu.grasscutter.Grasscutter.config;
import static emu.grasscutter.Grasscutter.*;
/**
* *when your JVM fails*
@ -34,17 +26,18 @@ public class ConfigContainer {
* with the new dispatch server.
* Version 8 - 'server' is being added for enforcing handbook server
* addresses.
* Version 9 - 'limits' was added for handbook requests.
*/
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() {
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")) {
Grasscutter.getLogger().info("Updating legacy ..");
Grasscutter.saveConfig(null);
@ -58,9 +51,9 @@ public class ConfigContainer {
return;
// Create a new configuration instance.
ConfigContainer updated = new ConfigContainer();
var updated = new ConfigContainer();
// Update all configuration fields.
Field[] fields = ConfigContainer.class.getDeclaredFields();
var fields = ConfigContainer.class.getDeclaredFields();
Arrays.stream(fields).forEach(field -> {
try {
field.set(updated, field.get(config));
@ -73,7 +66,7 @@ public class ConfigContainer {
Grasscutter.saveConfig(updated);
Grasscutter.loadConfig();
} 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 boolean enable = false;
public boolean allowCommands = true;
public int maxRequests = 10;
public int maxEntities = 100;
public Limits limits = new Limits();
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 {
/* Are the server settings sent to the handbook? */
public boolean enforced = false;
/* The default server address for the handbook's authentication. */
public String address = "127.0.0.1";
/* The default server port for the handbook's authentication. */
public int port = 443;
/* Should the defaults be enforced? */
public boolean canChange = true;
}
}

View File

@ -1,23 +1,27 @@
package emu.grasscutter.server.http.documentation;
import static emu.grasscutter.config.Configuration.HANDBOOK;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.DispatchUtils;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.objects.HandbookBody;
import emu.grasscutter.utils.*;
import emu.grasscutter.utils.objects.*;
import emu.grasscutter.utils.objects.HandbookBody.Action;
import io.javalin.Javalin;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.*;
import java.util.*;
import java.util.concurrent.*;
import static emu.grasscutter.config.Configuration.HANDBOOK;
/** Handles requests for the new GM Handbook. */
public final class HandbookHandler implements Router {
private String handbook;
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
* found.
@ -34,6 +38,17 @@ public final class HandbookHandler implements Router {
.replace("{{DETAILS_PORT}}", String.valueOf(server.port))
.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
@ -60,6 +75,32 @@ public final class HandbookHandler implements Router {
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.
*
@ -128,6 +169,9 @@ public final class HandbookHandler implements Router {
return;
}
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.GrantAvatar.class);
// Get the response.
@ -148,6 +192,9 @@ public final class HandbookHandler implements Router {
return;
}
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.GiveItem.class);
// Get the response.
@ -168,6 +215,9 @@ public final class HandbookHandler implements Router {
return;
}
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.TeleportTo.class);
// Get the response.
@ -188,8 +238,23 @@ public final class HandbookHandler implements Router {
return;
}
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a 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.
var response = DispatchUtils.performHandbookAction(Action.SPAWN_ENTITY, request);
// Send the response.

View File

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