diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/de/igslandstuhl/database/server/WebServer.java b/src/main/java/de/igslandstuhl/database/server/WebServer.java index 875a1b0..7091055 100644 --- a/src/main/java/de/igslandstuhl/database/server/WebServer.java +++ b/src/main/java/de/igslandstuhl/database/server/WebServer.java @@ -24,6 +24,7 @@ import de.igslandstuhl.database.server.webserver.handlers.GetRequestHandler; import de.igslandstuhl.database.server.webserver.handlers.PostRequestHandler; +import de.igslandstuhl.database.server.webserver.handlers.SessionValidationResult; import de.igslandstuhl.database.server.webserver.requests.GetRequest; import de.igslandstuhl.database.server.webserver.requests.HttpHeader; import de.igslandstuhl.database.server.webserver.requests.PostRequest; @@ -146,7 +147,16 @@ void handlePost(String headerString, InputStream in, PrintStream out) throws IOE body = URLDecoder.decode(raw, bodyCharset.name()); } PostRequest parsedRequest = new PostRequest(postHeader, body, clientIp, secure); - HttpResponse response = Server.getInstance().getWebServer().getSessionManager().validateSession(parsedRequest) ? PostRequestHandler.getInstance().handlePostRequest(parsedRequest) : PostResponse.forbidden("Forbidden: session manipulation or ratelimit", parsedRequest); + + SessionValidationResult v = Server.getInstance().getWebServer().getSessionManager().validateSession(parsedRequest); + HttpResponse response; + switch(v) { + case OK -> response = PostRequestHandler.getInstance().handlePostRequest(parsedRequest); //OK + case RATE_LIMITED -> response = PostResponse.tooManyRequests("Too Many Requests", parsedRequest); //429 + case INVALID_SESSION -> response = PostResponse.unauthorized("Invalid Session", parsedRequest); //401 + default -> response = PostResponse.unauthorized("Invalid Session", parsedRequest); + } + response.respond(out); } diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/Status.java b/src/main/java/de/igslandstuhl/database/server/webserver/Status.java index 283aa9b..add178e 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/Status.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/Status.java @@ -16,6 +16,7 @@ public enum Status { METHOD_NOT_ALLOWED(405, "Method Not Allowed"), I_AM_A_TEAPOT(418, "I'm a teapot"), INTERNAL_SERVER_ERROR(500, "Internal Server Error"), + TOO_MANY_REQUESTS(429, "Too Many Requests") ; /** * The HTTP status code. diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java index 256a855..f85f759 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java @@ -21,13 +21,19 @@ private GetRequestHandler() {} public final HttpResponse handleRequest(GetRequest request) { SessionManager sessionManager = Server.getInstance().getWebServer().getSessionManager(); - if (!sessionManager.validateSession(request)) { - return GetResponse.forbidden(request); - } else { - String path = request.getPath(); - HttpHandler handler = Registry.getRequestHandlerRegistry().get(path); - return handler.handleHttpRequest(request); + + SessionValidationResult v = sessionManager.validateSession(request); + if(v != SessionValidationResult.OK){ + return switch (v){ + case RATE_LIMITED -> GetResponse.tooManyRequests(request); + case INVALID_SESSION -> GetResponse.unauthorized("Invalid Session", request); + default -> GetResponse.unauthorized("Invalid Session", request); + }; } + + String path = request.getPath(); + HttpHandler handler = Registry.getRequestHandlerRegistry().get(path); + return handler.handleHttpRequest(request); } public static GetResponse handleFileRequest(GetRequest request) { diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/SessionValidationResult.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/SessionValidationResult.java new file mode 100644 index 0000000..7221213 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/SessionValidationResult.java @@ -0,0 +1,5 @@ +package de.igslandstuhl.database.server.webserver.handlers; + +public enum SessionValidationResult { + OK, RATE_LIMITED, INVALID_SESSION +} diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java index 188e038..9658a11 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java @@ -45,6 +45,48 @@ public static GetResponse forbidden(HttpRequest request) { public static GetResponse unauthorized(HttpRequest request) { return new GetResponse(request, Status.UNAUTHORIZED, new ResourceLocation("html", "errors", "401.html"), ContentType.HTML, "", false); } + /** + * Returns a response for a GET request the user must be logged in for. + * This overload allows specifying a custom message (e.g. "Invalid Session") without leaking details. + * @param message The message to include in the response. + * @return the GetResponse object + */ + public static GetResponse unauthorized(String message, HttpRequest request) { + return new GetResponse( + request, + Status.UNAUTHORIZED, + new ResourceLocation("html", "errors", "401.html"), + ContentType.HTML, + message, + false + ); + } + /** + * Returns a response indicating that the client has sent too many requests in a given amount of time. + * This is used for rate limiting. + * @param message The message to include in the response. + * @return the GetResponse object + */ + public static GetResponse tooManyRequests(String message, HttpRequest request) { + return new GetResponse( + request, + Status.TOO_MANY_REQUESTS, + new ResourceLocation("html", "errors", "429.html"), + ContentType.HTML, + message, + false + ); + } + + /** + * Returns a response indicating that the client has sent too many requests in a given amount of time. + * This is used for rate limiting. + * @return the GetResponse object + */ + public static GetResponse tooManyRequests(HttpRequest request) { + return tooManyRequests("", request); + } + /** * The HTTP status of this response * @see Status diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java b/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java index 64b7044..b572963 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java @@ -227,6 +227,26 @@ public static PostResponse notFound(String message, PostRequest request) { public static PostResponse forbidden(String message, PostRequest request) { return new PostResponse(Status.FORBIDDEN, message, ContentType.TEXT_PLAIN, request); } + + /** + * Returns a response indicating that the client has sent too many requests in a given amount of time. + * This is used for rate limiting. + * @param message The error message to include in the response. + * @return A PostResponse object representing the too many requests response. + */ + public static PostResponse tooManyRequests(String message, PostRequest request) { + return new PostResponse(Status.TOO_MANY_REQUESTS, message, ContentType.TEXT_PLAIN, request); + } + + /** + * Returns a response indicating that the client has sent too many requests in a given amount of time. + * This is used for rate limiting. + * @return A PostResponse object representing the too many requests response. + */ + public static PostResponse tooManyRequests(PostRequest rq) { + return tooManyRequests("Too Many Requests", rq); + } + public static PostResponse redirect(String location, PostRequest request) { return new PostResponse(Status.FOUND, "", ContentType.TEXT_PLAIN, request, new String[] { "Location: " + location diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/sessions/SessionManager.java b/src/main/java/de/igslandstuhl/database/server/webserver/sessions/SessionManager.java index 01a2262..4c226ee 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/sessions/SessionManager.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/sessions/SessionManager.java @@ -7,6 +7,7 @@ import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.webserver.Cookie; +import de.igslandstuhl.database.server.webserver.handlers.SessionValidationResult; import de.igslandstuhl.database.server.webserver.requests.HttpRequest; public class SessionManager { @@ -26,6 +27,8 @@ public class SessionManager { private final int maximumInactivityDuration; private final int maxRequests; + + public SessionManager(int sessionExpireDuration, int maximumInactivityDuration, int maxRequests) { this.sessionExpireDuration = sessionExpireDuration; this.maximumInactivityDuration = maximumInactivityDuration; @@ -65,7 +68,7 @@ private void cleanSecondsJob() { } } - public boolean validateSession(HttpRequest request) { + public SessionValidationResult validateSession(HttpRequest request) { lastActivity.set(request, Instant.now()); Integer requests = requestCount.get(request); @@ -74,21 +77,21 @@ public boolean validateSession(HttpRequest request) { requestCount.set(request, count); if (count > maxRequests && !getSessionUser(request).isAdmin()) { System.out.println("Ratelimit!"); - return false; + return SessionValidationResult.RATE_LIMITED; } String userAgent = request.getUserAgent(); if (!getSession(request).getUserAgent().equals(userAgent)) { System.err.println("SEVERE WARNING: POTENTIAL ATTACK: faked session id (device changed), for user " + getSessionUser(request)); - return false; + return SessionValidationResult.INVALID_SESSION; } String ip = request.getIP(); if (!getSession(request).getIpAddress().equals(ip)) { System.err.println("SEVERE WARNING: POTENTIAL ATTACK: faked session id (ip address changed) for user " + getSessionUser(request)); - return false; + return SessionValidationResult.INVALID_SESSION; } - return true; + return SessionValidationResult.OK; } public Session getSession(UUID sessionUUID) {