diff --git a/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/RequestHandler.java b/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/RequestHandler.java index bca3ef3b7..89a7f44c6 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/RequestHandler.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/RequestHandler.java @@ -25,11 +25,14 @@ import com.djrapitops.plan.system.webserver.auth.Authentication; import com.djrapitops.plan.system.webserver.auth.BasicAuthentication; import com.djrapitops.plan.system.webserver.response.PromptAuthorizationResponse; import com.djrapitops.plan.system.webserver.response.Response; -import com.djrapitops.plugin.benchmarking.Timings; +import com.djrapitops.plan.system.webserver.response.ResponseFactory; +import com.djrapitops.plan.system.webserver.response.errors.ForbiddenResponse; import com.djrapitops.plugin.logging.L; import com.djrapitops.plugin.logging.console.PluginLogger; import com.djrapitops.plugin.logging.error.ErrorHandler; import com.djrapitops.plugin.utilities.Verify; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; @@ -37,6 +40,8 @@ import com.sun.net.httpserver.HttpHandler; import javax.inject.Inject; import javax.inject.Singleton; import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; /** * HttpHandler for WebServer request management. @@ -51,10 +56,14 @@ public class RequestHandler implements HttpHandler { private final Theme theme; private final DBSystem dbSystem; private final ResponseHandler responseHandler; - private final Timings timings; + private final ResponseFactory responseFactory; private final PluginLogger logger; private final ErrorHandler errorHandler; + private final Cache failedLoginAttempts = Caffeine.newBuilder() + .expireAfterWrite(90, TimeUnit.SECONDS) + .build(); + @Inject RequestHandler( Locale locale, @@ -62,7 +71,7 @@ public class RequestHandler implements HttpHandler { Theme theme, DBSystem dbSystem, ResponseHandler responseHandler, - Timings timings, + ResponseFactory responseFactory, PluginLogger logger, ErrorHandler errorHandler ) { @@ -71,7 +80,7 @@ public class RequestHandler implements HttpHandler { this.theme = theme; this.dbSystem = dbSystem; this.responseHandler = responseHandler; - this.timings = timings; + this.responseFactory = responseFactory; this.logger = logger; this.errorHandler = errorHandler; } @@ -84,7 +93,16 @@ public class RequestHandler implements HttpHandler { request.setAuth(getAuthorization(requestHeaders)); try { - Response response = responseHandler.getResponse(request); + Response response = shouldPreventRequest(request.getRemoteAddress()) // Forbidden response (Optional) + .orElse(responseHandler.getResponse(request)); // Or the actual requested response + + // Increase attempt count and block if too high + Optional forbid = handlePasswordBruteForceAttempts(request, response); + if (forbid.isPresent()) { + response = forbid.get(); + } + + // Authentication failed, but was not blocked if (response instanceof PromptAuthorizationResponse) { responseHeaders.set("WWW-Authenticate", response.getHeader("WWW-Authenticate").orElse("Basic realm=\"Plan WebUser (/plan register)\";")); } @@ -101,6 +119,52 @@ public class RequestHandler implements HttpHandler { } } + private Optional shouldPreventRequest(String accessor) { + Integer attempts = failedLoginAttempts.getIfPresent(accessor); + if (attempts == null) { + attempts = 0; + } + + // Too many attempts, forbid further attempts. + if (attempts >= 5) { + return createForbiddenResponse(); + } + return Optional.empty(); + } + + private Optional handlePasswordBruteForceAttempts(Request request, Response response) { + if (request.getAuth().isPresent() && response instanceof PromptAuthorizationResponse) { + // Authentication was attempted, but failed so new attempt is going to be given if not forbidden + + failedLoginAttempts.cleanUp(); + + String accessor = request.getRemoteAddress(); + Integer attempts = failedLoginAttempts.getIfPresent(accessor); + if (attempts == null) { + attempts = 0; + } + + // Too many attempts, forbid further attempts. + if (attempts >= 5) { + logger.warn(accessor + " failed to login 5 times. Their access is blocked for 90 seconds."); + return createForbiddenResponse(); + } + + // Attempts only increased if less than 5 attempts to prevent frustration from the cache value not + // getting removed. + failedLoginAttempts.put(accessor, attempts + 1); + } else if (!(response instanceof PromptAuthorizationResponse) && !(response instanceof ForbiddenResponse)) { + // Successful login + failedLoginAttempts.invalidate(request.getRemoteAddress()); + } + // First connection, no authentication headers present. + return Optional.empty(); + } + + private Optional createForbiddenResponse() { + return Optional.of(responseFactory.forbidden403("You have too many failed login attempts. Please wait 2 minutes until attempting again.")); + } + private Authentication getAuthorization(Headers requestHeaders) { List authorization = requestHeaders.get("Authorization"); if (Verify.isEmpty(authorization)) {