From 19bb8300a6b501f564140bcc9a1955c729c803ba Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Fri, 17 Jul 2026 09:12:08 +0100 Subject: [PATCH] Honor proxy headers for Swagger server origin --- .../adapter/httpserver/HttpRequestOrigin.java | 74 ++++++++++++++++ .../ThingifierAutoDocGenRouting.java | 9 +- .../httpserver/HttpRequestOriginTest.java | 87 +++++++++++++++++++ 3 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/httpserver/HttpRequestOrigin.java create mode 100644 thingifier/src/test/java/uk/co/compendiumdev/thingifier/adapter/httpserver/HttpRequestOriginTest.java diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/httpserver/HttpRequestOrigin.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/httpserver/HttpRequestOrigin.java new file mode 100644 index 00000000..86245f09 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/httpserver/HttpRequestOrigin.java @@ -0,0 +1,74 @@ +package uk.co.compendiumdev.thingifier.adapter.httpserver; + +final class HttpRequestOrigin { + + private HttpRequestOrigin() {} + + static String from(final HttpServerRequest request) { + return "%s://%s".formatted(schemeFrom(request), hostFrom(request)); + } + + private static String schemeFrom(final HttpServerRequest request) { + final String forwardedProto = forwardedHeaderValue(request.header("Forwarded"), "proto"); + if (hasText(forwardedProto)) { + return forwardedProto; + } + + final String proxyProto = firstHeaderValue(request.header("X-Forwarded-Proto")); + if (hasText(proxyProto)) { + return proxyProto; + } + + return request.scheme(); + } + + private static String hostFrom(final HttpServerRequest request) { + final String forwardedHost = forwardedHeaderValue(request.header("Forwarded"), "host"); + if (hasText(forwardedHost)) { + return forwardedHost; + } + + final String proxyHost = firstHeaderValue(request.header("X-Forwarded-Host")); + if (hasText(proxyHost)) { + return proxyHost; + } + + return request.host(); + } + + private static String forwardedHeaderValue(final String header, final String key) { + final String firstValue = firstHeaderValue(header); + if (!hasText(firstValue)) { + return ""; + } + + final String prefix = "%s=".formatted(key); + final String[] parts = firstValue.split(";"); + for (final String part : parts) { + final String trimmed = part.trim(); + if (trimmed.toLowerCase().startsWith(prefix)) { + return unquote(trimmed.substring(prefix.length()).trim()); + } + } + + return ""; + } + + private static String firstHeaderValue(final String header) { + if (!hasText(header)) { + return ""; + } + return header.split(",", 2)[0].trim(); + } + + private static String unquote(final String value) { + if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) { + return value.substring(1, value.length() - 1); + } + return value; + } + + private static boolean hasText(final String value) { + return value != null && !value.trim().isEmpty(); + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/httpserver/ThingifierAutoDocGenRouting.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/httpserver/ThingifierAutoDocGenRouting.java index 426fdf9f..27b8d810 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/httpserver/ThingifierAutoDocGenRouting.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/httpserver/ThingifierAutoDocGenRouting.java @@ -52,7 +52,7 @@ public ThingifierAutoDocGenRouting( response.type("application/json"); response.status(200); return new Swaggerizer(apiDefn) - .asJsonWithPreferredServer(requestOrigin(request)); + .asJsonWithPreferredServer(HttpRequestOrigin.from(request)); }); get( @@ -99,11 +99,8 @@ public ThingifierAutoDocGenRouting( // TODO: the swaggerizer could be stored at a class level and allow caching to // be used for the output return new Swaggerizer(apiDefn) - .asJsonWithPreferredServer(permissive != null, requestOrigin(request)); + .asJsonWithPreferredServer( + permissive != null, HttpRequestOrigin.from(request)); }); } - - private String requestOrigin(final HttpServerRequest request) { - return "%s://%s".formatted(request.scheme(), request.host()); - } } diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/adapter/httpserver/HttpRequestOriginTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/adapter/httpserver/HttpRequestOriginTest.java new file mode 100644 index 00000000..b921b78a --- /dev/null +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/adapter/httpserver/HttpRequestOriginTest.java @@ -0,0 +1,87 @@ +package uk.co.compendiumdev.thingifier.adapter.httpserver; + +import java.lang.reflect.Proxy; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class HttpRequestOriginTest { + + @Test + void usesDirectRequestOriginWhenProxyHeadersAreAbsent() { + final HttpServerRequest request = request("http", "localhost:4567"); + + Assertions.assertEquals("http://localhost:4567", HttpRequestOrigin.from(request)); + } + + @Test + void usesForwardedProtoAndHostWhenPresent() { + final HttpServerRequest request = + request( + "http", + "internal:4567", + "X-Forwarded-Proto", + "https", + "X-Forwarded-Host", + "apichallenges.eviltester.com"); + + Assertions.assertEquals( + "https://apichallenges.eviltester.com", HttpRequestOrigin.from(request)); + } + + @Test + void usesStandardForwardedHeaderWhenPresent() { + final HttpServerRequest request = + request( + "http", + "internal:4567", + "Forwarded", + "for=192.0.2.60;proto=https;host=\"apichallenges.eviltester.com\""); + + Assertions.assertEquals( + "https://apichallenges.eviltester.com", HttpRequestOrigin.from(request)); + } + + private HttpServerRequest request(final String scheme, final String host) { + return request(scheme, host, "", ""); + } + + private HttpServerRequest request( + final String scheme, + final String host, + final String firstHeaderName, + final String firstHeaderValue) { + return request(scheme, host, firstHeaderName, firstHeaderValue, "", ""); + } + + private HttpServerRequest request( + final String scheme, + final String host, + final String firstHeaderName, + final String firstHeaderValue, + final String secondHeaderName, + final String secondHeaderValue) { + return (HttpServerRequest) + Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] {HttpServerRequest.class}, + (proxy, method, args) -> { + if ("scheme".equals(method.getName())) { + return scheme; + } + if ("host".equals(method.getName())) { + return host; + } + if ("header".equals(method.getName())) { + final String requestedHeader = (String) args[0]; + if (requestedHeader.equals(firstHeaderName)) { + return firstHeaderValue; + } + if (requestedHeader.equals(secondHeaderName)) { + return secondHeaderValue; + } + return null; + } + throw new UnsupportedOperationException(method.getName()); + }); + } +}