Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CORS route to DevConsole #29342

Merged
merged 1 commit into from
Nov 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.deployment.webjar.WebJarBuildItem;
import io.quarkus.vertx.http.deployment.webjar.WebJarResultsBuildItem;
import io.quarkus.vertx.http.runtime.devmode.DevConsoleCORSFilter;
import io.quarkus.vertx.http.runtime.devmode.DevConsoleFilter;
import io.quarkus.vertx.http.runtime.devmode.DevConsoleRecorder;
import io.quarkus.vertx.http.runtime.devmode.RedirectHandler;
Expand Down Expand Up @@ -438,6 +439,7 @@ public DevConsoleTemplateInfoBuildItem config(List<DevServiceDescriptionBuildIte
@Consume(LoggingSetupBuildItem.class)
@BuildStep(onlyIf = IsDevelopment.class)
public void setupDevConsoleRoutes(
DevUIConfig devUIConfig,
DevConsoleRecorder recorder,
LogStreamRecorder logStreamRecorder,
List<DevConsoleRouteBuildItem> routes,
Expand Down Expand Up @@ -472,6 +474,12 @@ public void setupDevConsoleRoutes(
// if the handler is a proxy, then that means it's been produced by a recorder and therefore belongs in the regular runtime Vert.x instance
// otherwise this is handled in the setupDeploymentSideHandling method
if (!i.isDeploymentSide()) {
if (devUIConfig.cors.enabled) {
routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder()
.route("dev/*")
.handler(new DevConsoleCORSFilter())
.build());
}
NonApplicationRootPathBuildItem.Builder builder = nonApplicationRootPathBuildItem.routeBuilder()
.routeFunction(
"dev/" + groupAndArtifact.getKey() + "." + groupAndArtifact.getValue() + "/" + i.getPath(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.vertx.http.deployment.devmode.console;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigRoot;

Expand All @@ -12,4 +13,19 @@ public class DevUIConfig {
@ConfigItem(defaultValue = "50")
public int historySize;

/**
* CORS configuration.
*/
public Cors cors = new Cors();

@ConfigGroup
public static class Cors {

/**
* Enable CORS filter.
*/
@ConfigItem(defaultValue = "true")
public boolean enabled = true;
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.vertx.http.cors;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.nullValue;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -28,6 +29,7 @@ void corsMatchingOrigin() {
.when()
.options("/test").then()
.statusCode(200)
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Credentials", "true");
}

Expand All @@ -42,7 +44,8 @@ void corsNotMatchingOrigin() {
.header("Access-Control-Request-Headers", headers)
.when()
.options("/test").then()
.statusCode(200)
.statusCode(403)
.header("Access-Control-Allow-Origin", nullValue())
.header("Access-Control-Allow-Credentials", "false");
}

Expand All @@ -58,6 +61,7 @@ void corsMatchingOriginWithWildcard() {
.when()
.options("/test").then()
.statusCode(200)
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Credentials", "false");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package io.quarkus.vertx.http.devconsole;

import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusDevModeTest;
import io.restassured.RestAssured;

public class DevConsoleCorsTest {

@RegisterExtension
static final QuarkusDevModeTest config = new QuarkusDevModeTest()
.withEmptyApplication();

@Test
public void testPreflightHttpLocalhostOrigin() {
String origin = "http://localhost:8080";
String methods = "GET,POST";
RestAssured.given()
.header("Origin", origin)
.header("Access-Control-Request-Method", methods)
.when()
.options("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(200)
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Methods", methods)
.body(emptyOrNullString());
}

@Test
public void testPreflightHttpLocalhostIpOrigin() {
String origin = "http://127.0.0.1:8080";
String methods = "GET,POST";
RestAssured.given()
.header("Origin", origin)
.header("Access-Control-Request-Method", methods)
.when()
.options("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(200)
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Methods", methods)
.body(emptyOrNullString());
}

@Test
public void testPreflightHttpsLocalhostOrigin() {
String origin = "https://localhost:8443";
String methods = "GET,POST";
RestAssured.given()
.header("Origin", origin)
.header("Access-Control-Request-Method", methods)
.when()
.options("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(200)
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Methods", methods)
.body(emptyOrNullString());
}

@Test
public void testPreflightHttpsLocalhostIpOrigin() {
String origin = "https://127.0.0.1:8443";
String methods = "GET,POST";
RestAssured.given()
.header("Origin", origin)
.header("Access-Control-Request-Method", methods)
.when()
.options("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(200)
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Methods", methods)
.body(emptyOrNullString());
}

@Test
public void testPreflightNonLocalhostOrigin() {
String methods = "GET,POST";
RestAssured.given()
.header("Origin", "https://quarkus.io/http://localhost")
.header("Access-Control-Request-Method", methods)
.when()
.options("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(403)
.header("Access-Control-Allow-Origin", nullValue())
.header("Access-Control-Allow-Methods", nullValue())
.body(emptyOrNullString());
}

@Test
public void testPreflightBadLocalhostOrigin() {
String methods = "GET,POST";
RestAssured.given()
.header("Origin", "http://localhost:8080/devui")
.header("Access-Control-Request-Method", methods)
.when()
.options("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(403)
.header("Access-Control-Allow-Origin", nullValue())
.body(emptyOrNullString());
}

@Test
public void testPreflightBadLocalhostIpOrigin() {
String methods = "GET,POST";
RestAssured.given()
.header("Origin", "http://127.0.0.1:8080/devui")
.header("Access-Control-Request-Method", methods)
.when()
.options("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(403)
.header("Access-Control-Allow-Origin", nullValue())
.body(emptyOrNullString());
}

@Test
public void testPreflightLocalhostOriginWithoutPort() {
String methods = "GET,POST";
RestAssured.given()
.header("Origin", "http://localhost")
.header("Access-Control-Request-Method", methods)
.when()
.options("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(403)
.header("Access-Control-Allow-Origin", nullValue())
.body(emptyOrNullString());
}

@Test
public void testSimpleRequestHttpLocalhostOrigin() {
String origin = "http://localhost:8080";
RestAssured.given()
.header("Origin", origin)
.when()
.get("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(200)
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Methods", nullValue())
.body(not(emptyOrNullString()));
}

@Test
public void testSimpleRequestHttpLocalhostIpOrigin() {
String origin = "http://127.0.0.1:8080";
RestAssured.given()
.header("Origin", origin)
.when()
.get("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(200)
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Methods", nullValue())
.body(not(emptyOrNullString()));
}

@Test
public void testSimpleRequestHttpsLocalhostOrigin() {
String origin = "https://localhost:8443";
RestAssured.given()
.header("Origin", origin)
.when()
.get("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(200)
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Methods", nullValue())
.body(not(emptyOrNullString()));
}

@Test
public void testSimpleRequestHttpsLocalhostIpOrigin() {
String origin = "https://127.0.0.1:8443";
RestAssured.given()
.header("Origin", origin)
.when()
.get("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(200)
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Methods", nullValue())
.body(not(emptyOrNullString()));
}

@Test
public void testSimpleRequestNonLocalhostOrigin() {
RestAssured.given()
.header("Origin", "https://quarkus.io/http://localhost")
.when()
.get("q/dev/io.quarkus.quarkus-vertx-http/config").then()
.statusCode(403)
.header("Access-Control-Allow-Origin", nullValue())
.body(emptyOrNullString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class CORSConfig {
*/
@ConfigItem
@ConvertWith(TrimmedStringConverter.class)
public Optional<List<String>> origins;
public Optional<List<String>> origins = Optional.empty();
sberyozkin marked this conversation as resolved.
Show resolved Hide resolved

/**
* HTTP methods allowed for CORS
Expand All @@ -36,7 +36,7 @@ public class CORSConfig {
*/
@ConfigItem
@ConvertWith(TrimmedStringConverter.class)
public Optional<List<String>> methods;
public Optional<List<String>> methods = Optional.empty();

/**
* HTTP headers allowed for CORS
Expand All @@ -48,7 +48,7 @@ public class CORSConfig {
*/
@ConfigItem
@ConvertWith(TrimmedStringConverter.class)
public Optional<List<String>> headers;
public Optional<List<String>> headers = Optional.empty();

/**
* HTTP headers exposed in CORS
Expand All @@ -59,14 +59,14 @@ public class CORSConfig {
*/
@ConfigItem
@ConvertWith(TrimmedStringConverter.class)
public Optional<List<String>> exposedHeaders;
public Optional<List<String>> exposedHeaders = Optional.empty();

/**
* The `Access-Control-Max-Age` response header value indicating
* how long the results of a pre-flight request can be cached.
*/
@ConfigItem
public Optional<Duration> accessControlMaxAge;
public Optional<Duration> accessControlMaxAge = Optional.empty();

/**
* The `Access-Control-Allow-Credentials` header is used to tell the
Expand All @@ -77,7 +77,7 @@ public class CORSConfig {
* there is a match with the precise `Origin` header and that header is not '*'.
*/
@ConfigItem
public Optional<Boolean> accessControlAllowCredentials;
public Optional<Boolean> accessControlAllowCredentials = Optional.empty();

@Override
public String toString() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,11 @@ public void handle(RoutingContext event) {
String.join(",", exposedHeaders.orElse(Collections.emptyList())));
}

if (request.method().equals(HttpMethod.OPTIONS) && (requestedHeaders != null || requestedMethods != null)) {
if (!allowsOrigin) {
response.setStatusCode(403);
response.setStatusMessage("CORS Rejected - Invalid origin");
response.end();
} else if (request.method().equals(HttpMethod.OPTIONS) && (requestedHeaders != null || requestedMethods != null)) {
if (corsConfig.accessControlMaxAge.isPresent()) {
response.putHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE,
String.valueOf(corsConfig.accessControlMaxAge.get().getSeconds()));
Expand Down
Loading