From 1406b5935d81da289335d8a98fea2506774bfe3c Mon Sep 17 00:00:00 2001 From: Daniel DeGroff Date: Thu, 30 Oct 2025 13:25:25 -0600 Subject: [PATCH 1/2] Normalize usage of toUpperCase and toLowerCase using Locale.ROOT. Normalize the content type keys for withMaxRequestBodySize. --- src/main/java/io/fusionauth/http/Cookie.java | 3 ++- .../fusionauth/http/io/MultipartStream.java | 3 ++- .../fusionauth/http/server/HTTPRequest.java | 22 +++++++++---------- .../fusionauth/http/server/HTTPResponse.java | 13 ++++++----- .../http/server/HTTPServerConfiguration.java | 4 +++- .../http/server/internal/HTTPWorker.java | 4 ++-- .../io/fusionauth/http/util/HTTPTools.java | 6 +++-- .../java/io/fusionauth/http/FormDataTest.java | 4 +++- .../fusionauth/http/util/HTTPToolsTest.java | 8 +++++-- 9 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/main/java/io/fusionauth/http/Cookie.java b/src/main/java/io/fusionauth/http/Cookie.java index 1fd82c1b..8b9cca0e 100644 --- a/src/main/java/io/fusionauth/http/Cookie.java +++ b/src/main/java/io/fusionauth/http/Cookie.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -245,7 +246,7 @@ public void addAttribute(String name, String value) { return; } - switch (name.toLowerCase()) { + switch (name.toLowerCase(Locale.ROOT)) { case HTTPValues.CookieAttributes.DomainLower: domain = value; break; diff --git a/src/main/java/io/fusionauth/http/io/MultipartStream.java b/src/main/java/io/fusionauth/http/io/MultipartStream.java index 478c878f..283875c6 100644 --- a/src/main/java/io/fusionauth/http/io/MultipartStream.java +++ b/src/main/java/io/fusionauth/http/io/MultipartStream.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -264,7 +265,7 @@ private void readHeaders(Map headers) throws IOException, P var nextState = state.next(b); if (nextState != state) { switch (state) { - case HeaderName -> headerName = build.toString().toLowerCase(); + case HeaderName -> headerName = build.toString().toLowerCase(Locale.ROOT); case HeaderValue -> headers.put(headerName, HTTPTools.parseHeaderValue(build.toString())); } diff --git a/src/main/java/io/fusionauth/http/server/HTTPRequest.java b/src/main/java/io/fusionauth/http/server/HTTPRequest.java index 38773690..be5651ab 100644 --- a/src/main/java/io/fusionauth/http/server/HTTPRequest.java +++ b/src/main/java/io/fusionauth/http/server/HTTPRequest.java @@ -164,13 +164,13 @@ public void addCookies(Collection cookies) { } public void addHeader(String name, String value) { - name = name.toLowerCase(); + name = name.toLowerCase(Locale.ROOT); headers.computeIfAbsent(name, key -> new ArrayList<>()).add(value); decodeHeader(name, value); } public void addHeaders(String name, String... values) { - name = name.toLowerCase(); + name = name.toLowerCase(Locale.ROOT); headers.computeIfAbsent(name, key -> new ArrayList<>()).addAll(List.of(values)); for (String value : values) { @@ -179,7 +179,7 @@ public void addHeaders(String name, String... values) { } public void addHeaders(String name, Collection values) { - name = name.toLowerCase(); + name = name.toLowerCase(Locale.ROOT); headers.computeIfAbsent(name, key -> new ArrayList<>()).addAll(values); for (String value : values) { @@ -254,12 +254,12 @@ public Map getAttributes() { public String getBaseURL() { // Setting the wrong value in the X-Forwarded-Proto header seems to be a common issue that causes an exception during URI.create. // Assuming request.getScheme() is not the problem, and it is related to the proxy configuration. - String scheme = getScheme().toLowerCase(); + String scheme = getScheme().toLowerCase(Locale.ROOT); if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) { throw new IllegalArgumentException("The request scheme is invalid. Only http or https are valid schemes. The X-Forwarded-Proto header has a value of [" + getHeader(Headers.XForwardedProto) + "], this is likely an issue in your proxy configuration."); } - String serverName = getHost().toLowerCase(); + String serverName = getHost().toLowerCase(Locale.ROOT); int serverPort = getBaseURLServerPort(); String uri = scheme + "://" + serverName; @@ -381,7 +381,7 @@ public String getHeader(String name) { } public List getHeaders(String name) { - return headers.get(name.toLowerCase()); + return headers.get(name.toLowerCase(Locale.ROOT)); } public Map> getHeaders() { @@ -650,11 +650,11 @@ public Object removeAttribute(String name) { } public void removeHeader(String name) { - headers.remove(name.toLowerCase()); + headers.remove(name.toLowerCase(Locale.ROOT)); } public void removeHeader(String name, String... values) { - List actual = headers.get(name.toLowerCase()); + List actual = headers.get(name.toLowerCase(Locale.ROOT)); if (actual != null) { actual.removeAll(List.of(values)); } @@ -671,13 +671,13 @@ public void setAttribute(String name, Object value) { } public void setHeader(String name, String value) { - name = name.toLowerCase(); + name = name.toLowerCase(Locale.ROOT); this.headers.put(name, new ArrayList<>(List.of(value))); decodeHeader(name, value); } public void setHeaders(String name, String... values) { - name = name.toLowerCase(); + name = name.toLowerCase(Locale.ROOT); this.headers.put(name, new ArrayList<>(List.of(values))); for (String value : values) { @@ -686,7 +686,7 @@ public void setHeaders(String name, String... values) { } public void setHeaders(String name, Collection values) { - name = name.toLowerCase(); + name = name.toLowerCase(Locale.ROOT); this.headers.put(name, new ArrayList<>(values)); for (String value : values) { diff --git a/src/main/java/io/fusionauth/http/server/HTTPResponse.java b/src/main/java/io/fusionauth/http/server/HTTPResponse.java index 874111fd..56bcda31 100644 --- a/src/main/java/io/fusionauth/http/server/HTTPResponse.java +++ b/src/main/java/io/fusionauth/http/server/HTTPResponse.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; @@ -82,7 +83,7 @@ public void addHeader(String name, String value) { return; } - headers.computeIfAbsent(name.toLowerCase(), key -> new ArrayList<>()).add(value); + headers.computeIfAbsent(name.toLowerCase(Locale.ROOT), key -> new ArrayList<>()).add(value); } public void clearHeaders() { @@ -102,7 +103,7 @@ public void close() throws IOException { } public boolean containsHeader(String name) { - String key = name.toLowerCase(); + String key = name.toLowerCase(Locale.ROOT); return headers.containsKey(key) && !headers.get(key).isEmpty(); } @@ -174,12 +175,12 @@ public void setException(Throwable exception) { } public String getHeader(String name) { - String key = name.toLowerCase(); + String key = name.toLowerCase(Locale.ROOT); return headers.containsKey(key) && !headers.get(key).isEmpty() ? headers.get(key).getFirst() : null; } public List getHeaders(String key) { - return headers.get(key.toLowerCase()); + return headers.get(key.toLowerCase(Locale.ROOT)); } public Map> getHeadersMap() { @@ -260,7 +261,7 @@ public void removeCookie(String name) { */ public void removeHeader(String name) { if (name != null) { - headers.remove(name.toLowerCase()); + headers.remove(name.toLowerCase(Locale.ROOT)); } } @@ -304,7 +305,7 @@ public void setHeader(String name, String value) { return; } - headers.put(name.toLowerCase(), new ArrayList<>(List.of(value))); + headers.put(name.toLowerCase(Locale.ROOT), new ArrayList<>(List.of(value))); } /** diff --git a/src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java b/src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java index 966b9fc5..b04c168a 100644 --- a/src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java +++ b/src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -493,7 +494,8 @@ public HTTPServerConfiguration withMaxRequestBodySize(Map maxRe this.maxRequestBodySize.clear(); // Add back a default to ensure we always have a fallback, can still be modified by the incoming configuration. this.maxRequestBodySize.put("*", DefaultMaxRequestSizes.get("*")); - this.maxRequestBodySize.putAll(maxRequestBodySize); + // Store lower case keys + maxRequestBodySize.forEach((k, v) -> this.maxRequestBodySize.put(k.toLowerCase(Locale.ROOT), v)); return this; } diff --git a/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java b/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java index 8267a2fa..1ae36ffc 100644 --- a/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java +++ b/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java @@ -234,8 +234,8 @@ public void run() { logger.trace("[{}] Closing socket. Client closed the connection. Reason [{}].", Thread.currentThread().threadId(), e.getMessage()); closeSocketOnly(CloseSocketReason.Expected); } catch (HTTPProcessingException e) { - // Note that I am only tracing this, because this exception is mostly expected. Use closeSocketOnError so we can attempt to write a response. - logger.trace("[{}] Closing socket with status [{}]. An unhandled [{}] exception was taken. Reason [{}].", Thread.currentThread().threadId(), e.getStatus(), e.getClass().getSimpleName(), e.getMessage()); + // These are expected, but are things the client may want to know about. Use closeSocketOnError so we can attempt to write a response. + logger.debug("[{}] Closing socket with status [{}]. An unhandled [{}] exception was taken. Reason [{}].", Thread.currentThread().threadId(), e.getStatus(), e.getClass().getSimpleName(), e.getMessage()); closeSocketOnError(response, e.getStatus()); } catch (TooManyBytesToDrainException e) { // The request handler did not read the entire InputStream, we tried to drain it but there were more bytes remaining than the configured maximum. diff --git a/src/main/java/io/fusionauth/http/util/HTTPTools.java b/src/main/java/io/fusionauth/http/util/HTTPTools.java index 3e9625dd..d49b328b 100644 --- a/src/main/java/io/fusionauth/http/util/HTTPTools.java +++ b/src/main/java/io/fusionauth/http/util/HTTPTools.java @@ -26,6 +26,7 @@ import java.util.HexFormat; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -58,6 +59,7 @@ public static int getMaxRequestBodySize(String contentType, Map } // Exact match + contentType = contentType.toLowerCase(Locale.ROOT); Integer maximumSize = maxRequestBodySize.get(contentType); if (maximumSize != null) { return maximumSize; @@ -426,10 +428,10 @@ private static void parseHeaderParameter(char[] chars, int start, int end, Map 128k - .withConfiguration(config -> config.withMaxRequestBodySize(Map.of(ContentTypes.Form, 128 * 1024))) + // - Use a UC Content-Type to make sure it still works + .withConfiguration(config -> config.withMaxRequestBodySize(Map.of(ContentTypes.Form.toUpperCase(Locale.ROOT), 128 * 1024))) .expectResponse(""" HTTP/1.1 413 \r connection: close\r diff --git a/src/test/java/io/fusionauth/http/util/HTTPToolsTest.java b/src/test/java/io/fusionauth/http/util/HTTPToolsTest.java index e3081b09..50f2e479 100644 --- a/src/test/java/io/fusionauth/http/util/HTTPToolsTest.java +++ b/src/test/java/io/fusionauth/http/util/HTTPToolsTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import com.inversoft.json.ToString; @@ -64,6 +65,9 @@ public void getMaxRequestBodySize() { assertMaxConfiguredSize("text/css", 6, configuration); assertMaxConfiguredSize("text/html", 7, configuration); + // Mixed case + assertMaxConfiguredSize("Application/JSON", 3, configuration); + // We don't expect this at runtime, but ideally we won't explode. These would be invalid values. // - Some of these are legit Content-Type headers, but at runtime we will have already parsed the header so we do // not expect any attributes; @@ -283,7 +287,7 @@ private String hex(byte[] bytes) { List result = new ArrayList<>(); for (byte b : bytes) { - result.add(Integer.toHexString(0xFF & b).toUpperCase()); + result.add(Integer.toHexString(0xFF & b).toUpperCase(Locale.ROOT)); } return String.join(" ", result); } @@ -291,7 +295,7 @@ private String hex(byte[] bytes) { private String hex(String s) { List result = new ArrayList<>(); for (char ch : s.toCharArray()) { - result.add(Integer.toHexString(ch).toUpperCase()); + result.add(Integer.toHexString(ch).toUpperCase(Locale.ROOT)); } return String.join(" ", result); } From df075c79ab9ca08c2760ac5d1cdada86930d9d5d Mon Sep 17 00:00:00 2001 From: Daniel DeGroff Date: Thu, 30 Oct 2025 13:45:33 -0600 Subject: [PATCH 2/2] bump version --- README.md | 8 ++++---- build.savant | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 24e06ffe..af8750bd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ### Latest versions -* Latest stable version: `1.3.0` +* Latest stable version: `1.3.1` * Now with 100% more virtual threads! * Prior stable version `0.3.7` @@ -27,20 +27,20 @@ To add this library to your project, you can include this dependency in your Mav io.fusionauth java-http - 1.3.0 + 1.3.1 ``` If you are using Gradle, you can add this to your build file: ```groovy -implementation 'io.fusionauth:java-http:1.3.0' +implementation 'io.fusionauth:java-http:1.3.1' ``` If you are using Savant, you can add this to your build file: ```groovy -dependency(id: "io.fusionauth:java-http:1.3.0") +dependency(id: "io.fusionauth:java-http:1.3.1") ``` ## Examples Usages: diff --git a/build.savant b/build.savant index 8f0013d4..06b51d28 100644 --- a/build.savant +++ b/build.savant @@ -18,7 +18,7 @@ restifyVersion = "4.2.1" slf4jVersion = "2.0.17" testngVersion = "7.11.0" -project(group: "io.fusionauth", name: "java-http", version: "1.3.0", licenses: ["ApacheV2_0"]) { +project(group: "io.fusionauth", name: "java-http", version: "1.3.1", licenses: ["ApacheV2_0"]) { workflow { fetch { // Dependency resolution order: