diff --git a/http/http-server-common/src/main/java/io/koraframework/http/server/common/HttpServerConfig.java b/http/http-server-common/src/main/java/io/koraframework/http/server/common/HttpServerConfig.java index 6ae0394ae..666ffd72e 100644 --- a/http/http-server-common/src/main/java/io/koraframework/http/server/common/HttpServerConfig.java +++ b/http/http-server-common/src/main/java/io/koraframework/http/server/common/HttpServerConfig.java @@ -48,4 +48,39 @@ default Size maxRequestBodySize() { } HttpServerTelemetryConfig telemetry(); + + HttpServerCorsConfig cors(); + + @ConfigValueExtractor + interface HttpServerCorsConfig { + + default boolean enabled() { + return false; + } + + @Nullable + default String allowOrigin() { + return null; + } + + default List allowHeaders() { + return List.of("*"); + } + + default List allowMethods() { + return List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"); + } + + default boolean allowCredentials() { + return true; + } + + default List exposeHeaders() { + return List.of(); + } + + default Duration maxAge() { + return Duration.ofHours(1); + } + } } diff --git a/http/http-server-common/src/test/java/io/koraframework/http/server/common/router/HttpServerHandlerProcessTests.java b/http/http-server-common/src/test/java/io/koraframework/http/server/common/router/HttpServerHandlerProcessTests.java index b664191f4..a7ef527a9 100644 --- a/http/http-server-common/src/test/java/io/koraframework/http/server/common/router/HttpServerHandlerProcessTests.java +++ b/http/http-server-common/src/test/java/io/koraframework/http/server/common/router/HttpServerHandlerProcessTests.java @@ -1,5 +1,13 @@ package io.koraframework.http.server.common.router; +import io.koraframework.http.server.common.$HttpServerConfig_HttpServerCorsConfig_ConfigValueExtractor; +import io.koraframework.http.server.common.telemetry.*; +import io.koraframework.http.server.common.telemetry.impl.NoopHttpServerTelemetry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; import io.koraframework.application.graph.All; import io.koraframework.common.util.Size; import io.koraframework.http.common.body.HttpBody; @@ -160,7 +168,8 @@ private HttpServerConfig config(boolean ignoreTrailingSlash) { new $HttpServerTelemetryConfig_HttpServerLoggingConfig_ConfigValueExtractor.HttpServerLoggingConfig_Defaults(), new $HttpServerTelemetryConfig_HttpServerMetricsConfig_ConfigValueExtractor.HttpServerMetricsConfig_Defaults(), new $HttpServerTelemetryConfig_HttpServerTracingConfig_ConfigValueExtractor.HttpServerTracingConfig_Defaults() - ) + ), + new $HttpServerConfig_HttpServerCorsConfig_ConfigValueExtractor.HttpServerCorsConfig_Defaults() ); } diff --git a/http/http-server-common/src/test/java/io/koraframework/http/server/common/router/HttpServerHandlerTest.java b/http/http-server-common/src/test/java/io/koraframework/http/server/common/router/HttpServerHandlerTest.java index 5f4484329..f4924c983 100644 --- a/http/http-server-common/src/test/java/io/koraframework/http/server/common/router/HttpServerHandlerTest.java +++ b/http/http-server-common/src/test/java/io/koraframework/http/server/common/router/HttpServerHandlerTest.java @@ -1,5 +1,10 @@ package io.koraframework.http.server.common.router; +import io.koraframework.http.server.common.$HttpServerConfig_HttpServerCorsConfig_ConfigValueExtractor; +import io.koraframework.http.server.common.telemetry.*; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import io.koraframework.common.util.Size; import io.koraframework.http.server.common.$HttpServerConfig_ConfigValueExtractor; import io.koraframework.http.server.common.HttpServerConfig; @@ -155,7 +160,8 @@ private HttpServerConfig config(boolean ignoreTrailingSlash) { new $HttpServerTelemetryConfig_HttpServerLoggingConfig_ConfigValueExtractor.HttpServerLoggingConfig_Defaults(), new $HttpServerTelemetryConfig_HttpServerMetricsConfig_ConfigValueExtractor.HttpServerMetricsConfig_Defaults(), new $HttpServerTelemetryConfig_HttpServerTracingConfig_ConfigValueExtractor.HttpServerTracingConfig_Defaults() - ) + ), + new $HttpServerConfig_HttpServerCorsConfig_ConfigValueExtractor.HttpServerCorsConfig_Defaults() ); } diff --git a/http/http-server-common/src/testFixtures/java/io/koraframework/http/server/common/HttpServerTestKit.java b/http/http-server-common/src/testFixtures/java/io/koraframework/http/server/common/HttpServerTestKit.java index 40db6703e..2d5621530 100644 --- a/http/http-server-common/src/testFixtures/java/io/koraframework/http/server/common/HttpServerTestKit.java +++ b/http/http-server-common/src/testFixtures/java/io/koraframework/http/server/common/HttpServerTestKit.java @@ -979,7 +979,8 @@ protected void startServer(boolean ignoreTrailingSlash, List config, Http null ); } + + @Test + void corsActualRequest() throws Exception { + var server = this.corsServer(); + try { + server.init(); + + var request = new Request.Builder() + .url("http://localhost:%d/".formatted(server.port())) + .get() + .header("Origin", "https://example.com") + .build(); + + try (var response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(200); + assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("https://example.com"); + assertThat(response.header("Access-Control-Allow-Credentials")).isEqualTo("true"); + assertThat(response.header("Vary")).isEqualTo("Origin"); + assertThat(response.body().string()).isEqualTo("ok"); + } + } finally { + server.release(); + } + } + + @Test + void corsPreflightRequest() throws Exception { + var server = this.corsServer(); + try { + server.init(); + + var request = new Request.Builder() + .url("http://localhost:%d/".formatted(server.port())) + .method("OPTIONS", null) + .header("Origin", "https://example.com") + .header("Access-Control-Request-Method", "POST") + .build(); + + try (var response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(204); + assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("https://example.com"); + assertThat(response.header("Access-Control-Allow-Methods")).isEqualTo("GET,POST,PUT,DELETE,PATCH,OPTIONS,HEAD"); + assertThat(response.body().string()).isEmpty(); + } + } finally { + server.release(); + } + } + + private UndertowHttpServer corsServer() { + var config = new TestHttpServerConfig(new TestHttpServerCorsConfig()); + var handler = new HttpServerHandler(List.of(new HttpServerRequestHandlerImpl( + "GET", + "/", + request -> HttpServerResponse.of(200, HttpBody.plaintext("ok")) + )), List.of(), config); + var processingHandler = new KoraRequestProcessingHttpHandler(NoopHttpServerTelemetry.INSTANCE, handler); + return new UndertowHttpServer( + "test-cors", + valueOf(new KoraCorsHttpHandler(processingHandler, config.cors())), + null, + valueOf(config), + null, + null + ); + } + + private record TestHttpServerConfig(HttpServerCorsConfig cors) implements HttpServerConfig { + @Override + public int port() { + return 0; + } + + @Override + public Duration socketReadTimeout() { + return Duration.ofSeconds(1); + } + + @Override + public Duration socketWriteTimeout() { + return Duration.ofSeconds(1); + } + + @Override + public Duration shutdownWait() { + return Duration.ofMillis(1); + } + + @Override + public HttpServerTelemetryConfig telemetry() { + return new $HttpServerTelemetryConfig_ConfigValueExtractor.HttpServerTelemetryConfig_Impl( + new $HttpServerTelemetryConfig_HttpServerLoggingConfig_ConfigValueExtractor.HttpServerLoggingConfig_Defaults(), + new $HttpServerTelemetryConfig_HttpServerMetricsConfig_ConfigValueExtractor.HttpServerMetricsConfig_Defaults(), + new $HttpServerTelemetryConfig_HttpServerTracingConfig_ConfigValueExtractor.HttpServerTracingConfig_Defaults() + ); + } + + @Override + public Size maxRequestBodySize() { + return Size.of(1, Size.Type.GiB); + } + } + + private static final class TestHttpServerCorsConfig implements HttpServerConfig.HttpServerCorsConfig { + @Override + public boolean enabled() { + return true; + } + } }