mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2026-02-06 01:57:31 +01:00
Implement handbook request limiting
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user