From 6ef1bc5188765fbe23c8c2d4ec93a94f9f3bab85 Mon Sep 17 00:00:00 2001 From: Martin Stenhagen Date: Wed, 25 Feb 2026 16:48:04 +0100 Subject: [PATCH 01/10] Added public static final int SC_PAYLOAD_TOO_LARGE = 413; and Map.entry(SC_PAYLOAD_TOO_LARGE, "Payload Too Large") too HttpResponseBuilder --- src/main/java/org/example/http/HttpResponseBuilder.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/http/HttpResponseBuilder.java b/src/main/java/org/example/http/HttpResponseBuilder.java index 9b6ed2a7..5819dc82 100644 --- a/src/main/java/org/example/http/HttpResponseBuilder.java +++ b/src/main/java/org/example/http/HttpResponseBuilder.java @@ -24,6 +24,7 @@ public class HttpResponseBuilder { public static final int SC_UNAUTHORIZED = 401; public static final int SC_FORBIDDEN = 403; public static final int SC_NOT_FOUND = 404; + public static final int SC_PAYLOAD_TOO_LARGE = 413; // SERVER ERROR public static final int SC_INTERNAL_SERVER_ERROR = 500; @@ -58,7 +59,8 @@ public class HttpResponseBuilder { Map.entry(SC_INTERNAL_SERVER_ERROR, "Internal Server Error"), Map.entry(SC_BAD_GATEWAY, "Bad Gateway"), Map.entry(SC_SERVICE_UNAVAILABLE, "Service Unavailable"), - Map.entry(SC_GATEWAY_TIMEOUT, "Gateway Timeout") + Map.entry(SC_GATEWAY_TIMEOUT, "Gateway Timeout"), + Map.entry(SC_PAYLOAD_TOO_LARGE, "Payload Too Large") ); public void setStatusCode(int statusCode) { From 43f74fbc62b072b9a0aaa43fb7e7d60abb1da89a Mon Sep 17 00:00:00 2001 From: Martin Stenhagen Date: Fri, 27 Feb 2026 12:39:03 +0100 Subject: [PATCH 02/10] doFilter override and (empty for now) methods. --- .../filter/MaxRequestBodySizeFilter.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/main/java/org/example/filter/MaxRequestBodySizeFilter.java diff --git a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java new file mode 100644 index 00000000..295a4b73 --- /dev/null +++ b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java @@ -0,0 +1,70 @@ +package org.example.filter; + + +import org.example.http.HttpResponseBuilder; +import org.example.httpparser.HttpRequest; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * A filter that rejects requests with bodies larger than a configured byte limit. + * It primarily uses the Content-Length header (when present), and can also validate + * a parsed request body if available. + */ +public class MaxRequestBodySizeFilter implements Filter { + + private final long maxBytes; + + public MaxRequestBodySizeFilter(long maxBytes) { + if (maxBytes <= 0) { + throw new IllegalArgumentException("maxBytes must be >= 0"); + } + this.maxBytes = maxBytes; + } + @Override + public void init() { + + } + + @Override + public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) { + + if (!mayHaveBody(request.getMethod())) { + chain.doFilter(request, response); + return; + } + + Long contentLength = getHeaderAsLong(request.getHeaders(), "Content-Length"); + + if (contentLength != null && contentLength > maxBytes) { + reject(response, contentLength); + return; + } + + // fallback: if a body has already been parsed, validate it as well + String body = request.getBody(); + if (body != null && !body.isEmpty()) { + int bodyBytes = body.getBytes(StandardCharsets.UTF_8).length; + if (bodyBytes > maxBytes) { + reject(response, (long) bodyBytes); + return; + } + } + chain.doFilter(request, response); + } + + @Override + public void destroy() { + + } + + private boolean mayHaveBody(String method) { + } + + private Long getHeaderAsLong(Map headers, String s) { + } + + private void reject(HttpResponseBuilder response, Long contentLength) { + } +} From 44751adf2abc2d089d3d3e43d3855e6c751b3419 Mon Sep 17 00:00:00 2001 From: Martin Stenhagen Date: Fri, 27 Feb 2026 12:43:40 +0100 Subject: [PATCH 03/10] mayHaveBody method added --- .../java/org/example/filter/MaxRequestBodySizeFilter.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java index 295a4b73..f115fea7 100644 --- a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java +++ b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java @@ -60,6 +60,11 @@ public void destroy() { } private boolean mayHaveBody(String method) { + if (method == null) { + return false; + } + String m = method.trim().toUpperCase(); + return m.equals("POST") || m.equals("PUT") || m.equals("PATCH"); } private Long getHeaderAsLong(Map headers, String s) { From 2bbbe83cdac8cb612ace64e9075dc6c248a566d3 Mon Sep 17 00:00:00 2001 From: Martin Stenhagen Date: Fri, 27 Feb 2026 13:06:36 +0100 Subject: [PATCH 04/10] getHeaderAsLong-method added --- .../filter/MaxRequestBodySizeFilter.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java index f115fea7..92dd4767 100644 --- a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java +++ b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java @@ -45,9 +45,9 @@ public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterCh // fallback: if a body has already been parsed, validate it as well String body = request.getBody(); if (body != null && !body.isEmpty()) { - int bodyBytes = body.getBytes(StandardCharsets.UTF_8).length; - if (bodyBytes > maxBytes) { - reject(response, (long) bodyBytes); + int bodySizeInBytes = body.getBytes(StandardCharsets.UTF_8).length; + if (bodySizeInBytes > maxBytes) { + reject(response, (long) bodySizeInBytes); return; } } @@ -63,11 +63,31 @@ private boolean mayHaveBody(String method) { if (method == null) { return false; } - String m = method.trim().toUpperCase(); - return m.equals("POST") || m.equals("PUT") || m.equals("PATCH"); + String normalizedMethod = method.trim().toUpperCase(); + return normalizedMethod.equals("POST") || normalizedMethod.equals("PUT") || normalizedMethod.equals("PATCH"); } - private Long getHeaderAsLong(Map headers, String s) { + private Long getHeaderAsLong(Map headers, String headerName) { + if (headers == null || headerName == null) { + return null; + } + String rawHeaderValue = null; + for (Map.Entry headerEntry : headers.entrySet()) { + String currentHeaderName = headerEntry.getKey().toUpperCase(); + if (currentHeaderName.equalsIgnoreCase(headerName)) { + rawHeaderValue = headerEntry.getValue(); + break; + } + } + if (rawHeaderValue == null) { + return null; + } + try { + long parsedContentLength = Long.parseLong(rawHeaderValue.trim()); + return parsedContentLength < 0 ? null : parsedContentLength; + } catch (NumberFormatException ex) { + return null; + } } private void reject(HttpResponseBuilder response, Long contentLength) { From 2089bb913389e5e670c2d2af062b9b6e1866d440 Mon Sep 17 00:00:00 2001 From: Martin Stenhagen Date: Fri, 27 Feb 2026 13:16:39 +0100 Subject: [PATCH 05/10] reject-method added --- src/main/java/org/example/filter/MaxRequestBodySizeFilter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java index 92dd4767..6ef54f51 100644 --- a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java +++ b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java @@ -91,5 +91,7 @@ private Long getHeaderAsLong(Map headers, String headerName) { } private void reject(HttpResponseBuilder response, Long contentLength) { + response.setStatusCode(HttpResponseBuilder.SC_PAYLOAD_TOO_LARGE); + response.setBody("Payload too large: " + contentLength + "bytes (max " + maxBytes + ")"); } } From f3768e32a25448b082181b958b32aafb439f7bca Mon Sep 17 00:00:00 2001 From: Martin Stenhagen Date: Fri, 27 Feb 2026 13:24:42 +0100 Subject: [PATCH 06/10] added check statusCode == HttpResponseBuilder.SC_PAYLOAD_TOO_LARGE --- src/main/java/org/example/ConnectionHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/ConnectionHandler.java b/src/main/java/org/example/ConnectionHandler.java index 1fcc29fb..69dd2bbd 100644 --- a/src/main/java/org/example/ConnectionHandler.java +++ b/src/main/java/org/example/ConnectionHandler.java @@ -73,7 +73,8 @@ public void runConnectionHandler() throws IOException { int statusCode = response.getStatusCode(); if (statusCode == HttpResponseBuilder.SC_FORBIDDEN || - statusCode == HttpResponseBuilder.SC_BAD_REQUEST) { + statusCode == HttpResponseBuilder.SC_BAD_REQUEST || + statusCode == HttpResponseBuilder.SC_PAYLOAD_TOO_LARGE) { byte[] responseBytes = response.build(); client.getOutputStream().write(responseBytes); client.getOutputStream().flush(); From 3de21acaa9ea379a3ebeda8ec15d19e2ba2649c2 Mon Sep 17 00:00:00 2001 From: Martin Stenhagen Date: Fri, 27 Feb 2026 13:52:12 +0100 Subject: [PATCH 07/10] Updated AppConfig and ConnectionHandler to use the new Filter. --- .../java/org/example/ConnectionHandler.java | 5 +++ .../java/org/example/config/AppConfig.java | 36 ++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/example/ConnectionHandler.java b/src/main/java/org/example/ConnectionHandler.java index 69dd2bbd..72f8139f 100644 --- a/src/main/java/org/example/ConnectionHandler.java +++ b/src/main/java/org/example/ConnectionHandler.java @@ -2,6 +2,7 @@ import org.example.config.AppConfig; import org.example.filter.IpFilter; +import org.example.filter.MaxRequestBodySizeFilter; import org.example.httpparser.HttpParser; import org.example.httpparser.HttpRequest; import java.util.ArrayList; @@ -40,6 +41,10 @@ private List buildFilters() { if (Boolean.TRUE.equals(ipFilterConfig.enabled())) { list.add(createIpFilterFromConfig(ipFilterConfig)); } + AppConfig.MaxRequestBodyConfig maxBodyConfig = config.maxRequestBody(); + if (Boolean.TRUE.equals(maxBodyConfig.enabled())) { + list.add(new MaxRequestBodySizeFilter(maxBodyConfig.maxBytes())); + } // Add more filters here... return list; } diff --git a/src/main/java/org/example/config/AppConfig.java b/src/main/java/org/example/config/AppConfig.java index db689ed5..ae7beb7c 100644 --- a/src/main/java/org/example/config/AppConfig.java +++ b/src/main/java/org/example/config/AppConfig.java @@ -7,21 +7,27 @@ public record AppConfig( @JsonProperty("server") ServerConfig server, @JsonProperty("logging") LoggingConfig logging, - @JsonProperty("ipFilter") IpFilterConfig ipFilter + @JsonProperty("ipFilter") IpFilterConfig ipFilter, + @JsonProperty("maxRequestBody") MaxRequestBodyConfig maxRequestBody + ) { public static AppConfig defaults() { return new AppConfig( ServerConfig.defaults(), LoggingConfig.defaults(), - IpFilterConfig.defaults() + IpFilterConfig.defaults(), + MaxRequestBodyConfig.defaults() ); } public AppConfig withDefaultsApplied() { ServerConfig serverConfig = (server == null ? ServerConfig.defaults() : server.withDefaultsApplied()); LoggingConfig loggingConfig = (logging == null ? LoggingConfig.defaults() : logging.withDefaultsApplied()); - IpFilterConfig ipFilterConfig = (ipFilter == null ? IpFilterConfig.defaults() : ipFilter.withDefaultsApplied()); // ← LÄGG TILL - return new AppConfig(serverConfig, loggingConfig, ipFilterConfig); // ← UPPDATERA DENNA RAD + IpFilterConfig ipFilterConfig = (ipFilter == null ? IpFilterConfig.defaults() : ipFilter.withDefaultsApplied()); + MaxRequestBodyConfig maxRequestBodyConfig = + (maxRequestBody == null ? MaxRequestBodyConfig.defaults() : maxRequestBody.withDefaultsApplied()); + + return new AppConfig(serverConfig, loggingConfig, ipFilterConfig, maxRequestBodyConfig); } @JsonIgnoreProperties(ignoreUnknown = true) @@ -76,4 +82,26 @@ public IpFilterConfig withDefaultsApplied() { return new IpFilterConfig(e, m, blocked, allowed); } } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record MaxRequestBodyConfig( + @JsonProperty("enabled") Boolean enabled, + @JsonProperty("maxBytes") Long maxBytes + ) { + public static MaxRequestBodyConfig defaults() { + // välj en rimlig default för skolprojekt: 1 MiB + return new MaxRequestBodyConfig(false, 1_048_576L); + } + + public MaxRequestBodyConfig withDefaultsApplied() { + Boolean enabledFlag = (enabled == null) ? false : enabled; + long limit = (maxBytes == null) ? 1_048_576L : maxBytes; + + if (limit < 0) { + throw new IllegalArgumentException("maxRequestBody.maxBytes must be >= 0"); + } + + return new MaxRequestBodyConfig(enabledFlag, limit); + } + } } From 1be5e20e1421ef5b33d9fe68ee62b710b2acd302 Mon Sep 17 00:00:00 2001 From: Martin Stenhagen Date: Fri, 27 Feb 2026 14:14:06 +0100 Subject: [PATCH 08/10] Updated application.yml and created tests --- .../java/org/example/config/AppConfig.java | 1 - src/main/resources/application.yml | 4 + .../filter/MaxRequestBodySizeTest.java | 128 ++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/example/filter/MaxRequestBodySizeTest.java diff --git a/src/main/java/org/example/config/AppConfig.java b/src/main/java/org/example/config/AppConfig.java index ae7beb7c..7e345416 100644 --- a/src/main/java/org/example/config/AppConfig.java +++ b/src/main/java/org/example/config/AppConfig.java @@ -89,7 +89,6 @@ public record MaxRequestBodyConfig( @JsonProperty("maxBytes") Long maxBytes ) { public static MaxRequestBodyConfig defaults() { - // välj en rimlig default för skolprojekt: 1 MiB return new MaxRequestBodyConfig(false, 1_048_576L); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a57443be..1b997744 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,3 +10,7 @@ ipFilter: mode: "BLOCKLIST" blockedIps: [ ] allowedIps: [ ] + +maxRequestBody: + enabled: true + maxBytes: 1048576 diff --git a/src/test/java/org/example/filter/MaxRequestBodySizeTest.java b/src/test/java/org/example/filter/MaxRequestBodySizeTest.java new file mode 100644 index 00000000..67a6f933 --- /dev/null +++ b/src/test/java/org/example/filter/MaxRequestBodySizeTest.java @@ -0,0 +1,128 @@ +package org.example.filter; + +import org.example.http.HttpResponseBuilder; +import org.example.httpparser.HttpRequest; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +class MaxRequestBodySizeFilterTest { + + private static FilterChain spyChain(AtomicBoolean called) { + return (req, resp) -> called.set(true); + } + + @Test + void getRequest_shouldPassThrough() { + MaxRequestBodySizeFilter filter = new MaxRequestBodySizeFilter(10); + + HttpRequest request = new HttpRequest("GET", "/", "HTTP/1.1", Map.of(), null); + HttpResponseBuilder response = new HttpResponseBuilder(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + + filter.doFilter(request, response, spyChain(chainCalled)); + + assertTrue(chainCalled.get()); + assertEquals(HttpResponseBuilder.SC_OK, response.getStatusCode()); + } + + @Test + void postWithTooLargeContentLength_shouldRejectWith413() { + MaxRequestBodySizeFilter filter = new MaxRequestBodySizeFilter(10); + + HttpRequest request = new HttpRequest( + "POST", "/upload", "HTTP/1.1", + Map.of("Content-Length", "11"), + null + ); + HttpResponseBuilder response = new HttpResponseBuilder(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + + filter.doFilter(request, response, spyChain(chainCalled)); + + assertFalse(chainCalled.get()); + assertEquals(HttpResponseBuilder.SC_PAYLOAD_TOO_LARGE, response.getStatusCode()); + assertTrue(response.getBody().startsWith("Payload too large:"), "body should explain rejection"); + } + + @Test + void postWithContentLengthEqualToMax_shouldPassThrough() { + MaxRequestBodySizeFilter filter = new MaxRequestBodySizeFilter(10); + + HttpRequest request = new HttpRequest( + "POST", "/upload", "HTTP/1.1", + Map.of("Content-Length", "10"), + null + ); + HttpResponseBuilder response = new HttpResponseBuilder(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + + filter.doFilter(request, response, spyChain(chainCalled)); + + assertTrue(chainCalled.get()); + assertEquals(HttpResponseBuilder.SC_OK, response.getStatusCode()); + } + + @Test + void invalidContentLength_shouldBeIgnoredAndPassThrough() { + MaxRequestBodySizeFilter filter = new MaxRequestBodySizeFilter(10); + + HttpRequest request = new HttpRequest( + "POST", "/upload", "HTTP/1.1", + Map.of("Content-Length", "abc"), + null + ); + HttpResponseBuilder response = new HttpResponseBuilder(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + + filter.doFilter(request, response, spyChain(chainCalled)); + + assertTrue(chainCalled.get()); + assertEquals(HttpResponseBuilder.SC_OK, response.getStatusCode()); + } + + @Test + void contentLengthHeaderName_shouldBeCaseInsensitive() { + MaxRequestBodySizeFilter filter = new MaxRequestBodySizeFilter(10); + + HttpRequest request = new HttpRequest( + "POST", "/upload", "HTTP/1.1", + Map.of("content-length", "11"), // lowercase key + null + ); + HttpResponseBuilder response = new HttpResponseBuilder(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + + filter.doFilter(request, response, spyChain(chainCalled)); + + assertFalse(chainCalled.get()); + assertEquals(HttpResponseBuilder.SC_PAYLOAD_TOO_LARGE, response.getStatusCode()); + } + + @Test + void bodyFallback_shouldCountUtf8Bytes() { + // "€" is 3 bytes in UTF-8 + MaxRequestBodySizeFilter filter = new MaxRequestBodySizeFilter(2); + + HttpRequest request = new HttpRequest( + "POST", "/upload", "HTTP/1.1", + Map.of(), // no Content-Length + "€" + ); + HttpResponseBuilder response = new HttpResponseBuilder(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + + filter.doFilter(request, response, spyChain(chainCalled)); + + assertFalse(chainCalled.get()); + assertEquals(HttpResponseBuilder.SC_PAYLOAD_TOO_LARGE, response.getStatusCode()); + } + + @Test + void negativeMaxBytes_shouldThrow() { + assertThrows(IllegalArgumentException.class, () -> new MaxRequestBodySizeFilter(-1)); + } +} From 715913edc8cbee53715ccc9ea0f6bc496d7b0d4b Mon Sep 17 00:00:00 2001 From: Martin Stenhagen Date: Fri, 27 Feb 2026 14:21:44 +0100 Subject: [PATCH 09/10] Fixed maxBytes <= 0 to maxBytes < 0. Removed unnecessary .toUpperCase in getHeaderAsLong --- .../java/org/example/filter/MaxRequestBodySizeFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java index 6ef54f51..ac2d5dec 100644 --- a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java +++ b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java @@ -17,7 +17,7 @@ public class MaxRequestBodySizeFilter implements Filter { private final long maxBytes; public MaxRequestBodySizeFilter(long maxBytes) { - if (maxBytes <= 0) { + if (maxBytes < 0) { throw new IllegalArgumentException("maxBytes must be >= 0"); } this.maxBytes = maxBytes; @@ -73,7 +73,7 @@ private Long getHeaderAsLong(Map headers, String headerName) { } String rawHeaderValue = null; for (Map.Entry headerEntry : headers.entrySet()) { - String currentHeaderName = headerEntry.getKey().toUpperCase(); + String currentHeaderName = headerEntry.getKey(); if (currentHeaderName.equalsIgnoreCase(headerName)) { rawHeaderValue = headerEntry.getValue(); break; From fd96cdbec0faf6f02ebb40e643197a10e914fe16 Mon Sep 17 00:00:00 2001 From: Martin Stenhagen Date: Fri, 27 Feb 2026 15:15:00 +0100 Subject: [PATCH 10/10] After coderabbit feedback: Changed body from "" to null in ConnectionHandler to not give impressionthat body exists. Fixed this line: response.setBody("Payload too large: " + contentLength + " bytes (max " + maxBytes + ")"); Added --- src/main/java/org/example/ConnectionHandler.java | 2 +- src/main/java/org/example/filter/MaxRequestBodySizeFilter.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/ConnectionHandler.java b/src/main/java/org/example/ConnectionHandler.java index 72f8139f..b6a45582 100644 --- a/src/main/java/org/example/ConnectionHandler.java +++ b/src/main/java/org/example/ConnectionHandler.java @@ -68,7 +68,7 @@ public void runConnectionHandler() throws IOException { parser.getUri(), parser.getVersion(), parser.getHeadersMap(), - "" + null ); String clientIp = client.getInetAddress().getHostAddress(); diff --git a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java index ac2d5dec..8a05ffcc 100644 --- a/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java +++ b/src/main/java/org/example/filter/MaxRequestBodySizeFilter.java @@ -92,6 +92,6 @@ private Long getHeaderAsLong(Map headers, String headerName) { private void reject(HttpResponseBuilder response, Long contentLength) { response.setStatusCode(HttpResponseBuilder.SC_PAYLOAD_TOO_LARGE); - response.setBody("Payload too large: " + contentLength + "bytes (max " + maxBytes + ")"); + response.setBody("Payload too large: " + contentLength + " bytes (max " + maxBytes + ")"); } }