Skip to content

Commit

Permalink
Add CORS route to DevConsole
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Nov 22, 2022
1 parent 6419840 commit 7169469
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 8 deletions.
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,125 @@
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 testPreflightHttpsLocalhostOrigin() {
String origin = "https://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 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/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 testSimpleRequestHttpsLocalhostOrigin() {
String origin = "https://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 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();

/**
* 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.quarkus.vertx.http.runtime.devmode;

import java.util.List;
import java.util.Optional;

import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;

import io.quarkus.vertx.http.runtime.cors.CORSConfig;
import io.quarkus.vertx.http.runtime.cors.CORSFilter;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.web.RoutingContext;

public class DevConsoleCORSFilter implements Handler<RoutingContext> {
private static final Logger LOG = Logger.getLogger(DevConsoleCORSFilter.class);

private static final String HTTP_PORT_CONFIG_PROP = "quarkus.http.port";
private static final String LOCAL_HOST = "localhost";
private static final String HTTP_LOCAL_HOST = "http://" + LOCAL_HOST;
private static final String HTTPS_LOCAL_HOST = "https://" + LOCAL_HOST;

public DevConsoleCORSFilter() {
}

private static CORSFilter corsFilter() {
int httpPort = ConfigProvider.getConfig().getValue(HTTP_PORT_CONFIG_PROP, int.class);
CORSConfig config = new CORSConfig();
config.origins = Optional.of(List.of(HTTP_LOCAL_HOST + ":" + httpPort, HTTPS_LOCAL_HOST + ":" + httpPort));
return new CORSFilter(config);
}

@Override
public void handle(RoutingContext event) {
HttpServerRequest request = event.request();
HttpServerResponse response = event.response();
String origin = request.getHeader(HttpHeaders.ORIGIN);
if (origin == null) {
corsFilter().handle(event);
} else {
if (origin.startsWith(HTTP_LOCAL_HOST) || origin.startsWith(HTTPS_LOCAL_HOST)) {
corsFilter().handle(event);
} else {
LOG.errorf("Only localhost origin is allowed, but Origin header value is: %s", origin);
response.setStatusCode(403);
response.setStatusMessage("CORS Rejected - Invalid origin");
response.end();
}
}
}
}

0 comments on commit 7169469

Please sign in to comment.