diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/RateLimitGuard.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/RateLimitGuard.java
new file mode 100644
index 000000000..af2eb0de4
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/RateLimitGuard.java
@@ -0,0 +1,84 @@
+/*
+ * 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 .
+ */
+package com.djrapitops.plan.delivery.webserver;
+
+import com.djrapitops.plan.utilities.dev.Untrusted;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Simple guard against DDoS attacks to single endpoint.
+ *
+ * This only protects against a DDoS that doesn't follow redirects.
+ *
+ * @author AuroraLS3
+ */
+public class RateLimitGuard {
+
+ private static final int ATTEMPT_LIMIT = 30;
+ private final Cache requests = Caffeine.newBuilder()
+ .expireAfterWrite(120, TimeUnit.SECONDS)
+ .build();
+ private final Cache lastRequestPath = Caffeine.newBuilder()
+ .expireAfterWrite(120, TimeUnit.SECONDS)
+ .build();
+
+ public boolean shouldPreventRequest(@Untrusted String accessor) {
+ Integer attempts = requests.getIfPresent(accessor);
+ if (attempts == null) return false;
+ // Too many attempts, forbid further attempts.
+ return attempts >= ATTEMPT_LIMIT;
+ }
+
+ public void increaseAttemptCount(@Untrusted String requestPath, @Untrusted String accessor) {
+ String previous = lastRequestPath.getIfPresent(accessor);
+ if (!Objects.equals(previous, requestPath)) {
+ resetAttemptCount(accessor);
+ }
+
+ Integer attempts = requests.getIfPresent(accessor);
+ if (attempts == null) {
+ attempts = 0;
+ }
+
+ lastRequestPath.put(accessor, requestPath);
+ requests.put(accessor, attempts + 1);
+ }
+
+ public void resetAttemptCount(@Untrusted String accessor) {
+ // previous request changed
+ requests.cleanUp();
+ requests.invalidate(accessor);
+ }
+
+ public static class Disabled extends RateLimitGuard {
+ @Override
+ public boolean shouldPreventRequest(String accessor) {
+ return false;
+ }
+
+ @Override
+ public void increaseAttemptCount(String requestPath, String accessor) { /* Disabled */ }
+
+ @Override
+ public void resetAttemptCount(String accessor) { /* Disabled */ }
+ }
+
+}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java
index 5a20d0ff5..1ed008067 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java
@@ -475,6 +475,14 @@ public class ResponseFactory {
.build();
}
+ public Response failedRateLimit403() {
+ return Response.builder()
+ .setMimeType(MimeType.HTML)
+ .setContent("403 Forbidden
You are being rate-limited.
")
+ .setStatus(403)
+ .build();
+ }
+
public Response ipWhitelist403(@Untrusted String accessor) {
return Response.builder()
.setMimeType(MimeType.HTML)
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/InternalRequest.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/InternalRequest.java
index 3c73a1ce8..63c839c5a 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/InternalRequest.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/InternalRequest.java
@@ -81,4 +81,6 @@ public interface InternalRequest {
}
return authenticationExtractor.extractAuthentication(this);
}
+
+ String getRequestedPath();
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/JettyInternalRequest.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/JettyInternalRequest.java
index f7f08d56c..52855fdd8 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/JettyInternalRequest.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/JettyInternalRequest.java
@@ -129,6 +129,11 @@ public class JettyInternalRequest implements InternalRequest {
return baseRequest.getRequestURI();
}
+ @Override
+ public String getRequestedPath() {
+ return baseRequest.getHttpURI().getDecodedPath();
+ }
+
@Override
public String toString() {
return "JettyInternalRequest{" +
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/RequestHandler.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/RequestHandler.java
index 97ce55c92..f2b49a3ae 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/RequestHandler.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/RequestHandler.java
@@ -19,6 +19,7 @@ package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.webserver.PassBruteForceGuard;
+import com.djrapitops.plan.delivery.webserver.RateLimitGuard;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.delivery.webserver.ResponseResolver;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
@@ -40,13 +41,15 @@ public class RequestHandler {
private final ResponseResolver responseResolver;
private final PassBruteForceGuard bruteForceGuard;
+ private final RateLimitGuard rateLimitGuard;
private final AccessLogger accessLogger;
@Inject
- public RequestHandler(WebserverConfiguration webserverConfiguration, ResponseFactory responseFactory, ResponseResolver responseResolver, AccessLogger accessLogger) {
+ public RequestHandler(WebserverConfiguration webserverConfiguration, ResponseFactory responseFactory, ResponseResolver responseResolver, RateLimitGuard rateLimitGuard, AccessLogger accessLogger) {
this.webserverConfiguration = webserverConfiguration;
this.responseFactory = responseFactory;
this.responseResolver = responseResolver;
+ this.rateLimitGuard = rateLimitGuard;
this.accessLogger = accessLogger;
bruteForceGuard = new PassBruteForceGuard();
@@ -64,11 +67,17 @@ public class RequestHandler {
.warnAboutWhitelistBlock(accessAddress, internalRequest.getRequestedURIString());
response = responseFactory.ipWhitelist403(accessAddress);
} else {
- try {
- request = internalRequest.toRequest();
- response = attemptToResolve(request, accessAddress);
- } catch (WebUserAuthException thrownByAuthentication) {
- response = processFailedAuthentication(internalRequest, accessAddress, thrownByAuthentication);
+ String requestedPath = internalRequest.getRequestedPath();
+ rateLimitGuard.increaseAttemptCount(requestedPath, accessAddress);
+ if (rateLimitGuard.shouldPreventRequest(accessAddress)) {
+ response = responseFactory.failedRateLimit403();
+ } else {
+ try {
+ request = internalRequest.toRequest();
+ response = attemptToResolve(request, accessAddress);
+ } catch (WebUserAuthException thrownByAuthentication) {
+ response = processFailedAuthentication(internalRequest, accessAddress, thrownByAuthentication);
+ }
}
}