From e207352038e171e899695c895108fcca833ef70b Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 18 Jan 2018 16:55:36 -0800 Subject: Respond with correct cors headers for all rejections --- src/main/scala/xyz/driver/core/app/DriverApp.scala | 38 ++++++++++++++++------ 1 file changed, 28 insertions(+), 10 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/core/app/DriverApp.scala b/src/main/scala/xyz/driver/core/app/DriverApp.scala index df80c3d..bc9d634 100644 --- a/src/main/scala/xyz/driver/core/app/DriverApp.scala +++ b/src/main/scala/xyz/driver/core/app/DriverApp.scala @@ -223,6 +223,25 @@ class DriverApp( } object DriverApp { + def defaultCorsHeaders: Directive0 = { + optionalHeaderValueByType[Origin](()) flatMap { originHeader => + respondWithHeaders( + List[HttpHeader]( + allowOrigin(originHeader), + `Access-Control-Allow-Headers`(AllowedHeaders: _*), + `Access-Control-Expose-Headers`(AllowedHeaders: _*) + )) + } + } + + def corsAllowedMethodHeaders(methods: scala.collection.immutable.Seq[HttpMethod]): Directive0 = { + respondWithHeaders( + List[HttpHeader]( + Allow(methods), + `Access-Control-Allow-Methods`(methods) + )) + } + implicit def rejectionHandler: RejectionHandler = RejectionHandler .newBuilder() @@ -230,20 +249,19 @@ object DriverApp { val methods = rejections map (_.supported) lazy val names = methods map (_.name) mkString ", " - options { ctx => - optionalHeaderValueByType[Origin](()) { originHeader => - respondWithHeaders(List[HttpHeader]( - Allow(methods), - `Access-Control-Allow-Methods`(methods), - allowOrigin(originHeader), - `Access-Control-Allow-Headers`(AllowedHeaders: _*), - `Access-Control-Expose-Headers`(AllowedHeaders: _*) - )) { + options { + defaultCorsHeaders { + corsAllowedMethodHeaders(methods) { complete(s"Supported methods: $names.") } - }(ctx) + } } ~ complete(MethodNotAllowed -> s"HTTP method not allowed, supported methods: $names!") } + .handleAll[Rejection] { rejections => + defaultCorsHeaders { + RejectionHandler.default(rejections).getOrElse(reject) + } + } .result() } -- cgit v1.2.3 From b7e059da67bee48986b3165c94835f32b84cf76d Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Mon, 22 Jan 2018 11:25:35 -0800 Subject: Move directives to rest package --- src/main/scala/xyz/driver/core/app/DriverApp.scala | 28 ++++------------------ src/main/scala/xyz/driver/core/rest/package.scala | 21 +++++++++++++++- 2 files changed, 25 insertions(+), 24 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/core/app/DriverApp.scala b/src/main/scala/xyz/driver/core/app/DriverApp.scala index bc9d634..d95e254 100644 --- a/src/main/scala/xyz/driver/core/app/DriverApp.scala +++ b/src/main/scala/xyz/driver/core/app/DriverApp.scala @@ -223,25 +223,6 @@ class DriverApp( } object DriverApp { - def defaultCorsHeaders: Directive0 = { - optionalHeaderValueByType[Origin](()) flatMap { originHeader => - respondWithHeaders( - List[HttpHeader]( - allowOrigin(originHeader), - `Access-Control-Allow-Headers`(AllowedHeaders: _*), - `Access-Control-Expose-Headers`(AllowedHeaders: _*) - )) - } - } - - def corsAllowedMethodHeaders(methods: scala.collection.immutable.Seq[HttpMethod]): Directive0 = { - respondWithHeaders( - List[HttpHeader]( - Allow(methods), - `Access-Control-Allow-Methods`(methods) - )) - } - implicit def rejectionHandler: RejectionHandler = RejectionHandler .newBuilder() @@ -250,8 +231,8 @@ object DriverApp { lazy val names = methods map (_.name) mkString ", " options { - defaultCorsHeaders { - corsAllowedMethodHeaders(methods) { + respondWithCorsHeaders { + respondWithCorsAllowedMethodHeaders(methods) { complete(s"Supported methods: $names.") } } @@ -259,9 +240,10 @@ object DriverApp { complete(MethodNotAllowed -> s"HTTP method not allowed, supported methods: $names!") } .handleAll[Rejection] { rejections => - defaultCorsHeaders { - RejectionHandler.default(rejections).getOrElse(reject) + respondWithCorsHeaders { + reject(rejections: _*) } } .result() + .seal } diff --git a/src/main/scala/xyz/driver/core/rest/package.scala b/src/main/scala/xyz/driver/core/rest/package.scala index 77b916a..88f78d9 100644 --- a/src/main/scala/xyz/driver/core/rest/package.scala +++ b/src/main/scala/xyz/driver/core/rest/package.scala @@ -3,7 +3,7 @@ package xyz.driver.core.rest import java.net.InetAddress import akka.http.scaladsl.marshalling.{ToEntityMarshaller, ToResponseMarshallable} -import akka.http.scaladsl.model.headers.{HttpOriginRange, Origin, `Access-Control-Allow-Origin`} +import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.model._ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ @@ -110,6 +110,25 @@ object `package` { } } + def respondWithCorsHeaders: Directive0 = { + optionalHeaderValueByType[Origin](()) flatMap { originHeader => + respondWithHeaders( + List[HttpHeader]( + allowOrigin(originHeader), + `Access-Control-Allow-Headers`(AllowedHeaders: _*), + `Access-Control-Expose-Headers`(AllowedHeaders: _*) + )) + } + } + + def respondWithCorsAllowedMethodHeaders(methods: scala.collection.immutable.Seq[HttpMethod]): Directive0 = { + respondWithHeaders( + List[HttpHeader]( + Allow(methods), + `Access-Control-Allow-Methods`(methods) + )) + } + def extractServiceContext(request: HttpRequest, remoteAddress: RemoteAddress): ServiceRequestContext = new ServiceRequestContext( extractTrackingId(request), -- cgit v1.2.3 From 32496bbc8f64f84c8b9bd8b567aa8cc13343414b Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 25 Jan 2018 14:40:50 -0800 Subject: Refactor DriverRoute to use shared directives --- src/main/scala/xyz/driver/core/rest/DriverRoute.scala | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index 4c483c6..5f961b6 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -24,17 +24,9 @@ trait DriverRoute { } protected def defaultResponseHeaders: Directive0 = { - (extractRequest & optionalHeaderValueByType[Origin](())) tflatMap { - case (request, originHeader) => - val tracingHeader = RawHeader(ContextHeaders.TrackingIdHeader, rest.extractTrackingId(request)) - val responseHeaders = List[HttpHeader]( - tracingHeader, - allowOrigin(originHeader), - `Access-Control-Allow-Headers`(AllowedHeaders: _*), - `Access-Control-Expose-Headers`(AllowedHeaders: _*) - ) - - respondWithHeaders(responseHeaders) + extractRequest flatMap { request => + val tracingHeader = RawHeader(ContextHeaders.TrackingIdHeader, rest.extractTrackingId(request)) + respondWithHeader(tracingHeader) & respondWithCorsHeaders } } -- cgit v1.2.3 From a4b2648a288110350c0ff8dc784626668112ab84 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Fri, 26 Jan 2018 11:43:52 -0800 Subject: Remove rejection handler, respond with default set of allowed methods and origins to all options requests in DriverRoute --- src/main/resources/reference.conf | 14 +++++ src/main/scala/xyz/driver/core/app/DriverApp.scala | 33 ++---------- src/main/scala/xyz/driver/core/app/module.scala | 5 +- .../scala/xyz/driver/core/rest/DriverRoute.scala | 57 +++++++++++++++++++- src/main/scala/xyz/driver/core/rest/package.scala | 25 +++++---- .../scala/xyz/driver/core/rest/DriverAppTest.scala | 60 +++++++++++++++++----- .../xyz/driver/core/rest/DriverRouteTest.scala | 4 +- 7 files changed, 140 insertions(+), 58 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 16dcfda..aed7b12 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -10,6 +10,20 @@ application { baseUrl: "localhost:8080" environment: "local_testing" + + cors { + allowedMethods: ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"] + allowedOrigins: [ + { + scheme: http + hostSuffix: localhost + }, + { + scheme: https + hostSuffix: example.com + } + ] + } } # Settings about the auto-generated REST API documentation. diff --git a/src/main/scala/xyz/driver/core/app/DriverApp.scala b/src/main/scala/xyz/driver/core/app/DriverApp.scala index d95e254..a593893 100644 --- a/src/main/scala/xyz/driver/core/app/DriverApp.scala +++ b/src/main/scala/xyz/driver/core/app/DriverApp.scala @@ -2,7 +2,6 @@ package xyz.driver.core.app import akka.actor.ActorSystem import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.server.Directives._ @@ -42,7 +41,6 @@ class DriverApp( port: Int = 8080, tracer: Tracer = NoTracer)(implicit actorSystem: ActorSystem, executionContext: ExecutionContext) { self => - import DriverApp._ implicit private lazy val materializer: ActorMaterializer = ActorMaterializer()(actorSystem) private lazy val http: HttpExt = Http()(actorSystem) @@ -73,8 +71,9 @@ class DriverApp( val swaggerRoute = swaggerService.routes ~ swaggerService.swaggerUI val versionRt = versionRoute(version, gitHash, time.currentTime()) val basicRoutes = new DriverRoute { - override def log: Logger = self.log - override def route: Route = versionRt ~ healthRoute ~ swaggerRoute + override def log: Logger = self.log + override def config: Config = xyz.driver.core.config.loadDefaultConfig + override def route: Route = versionRt ~ healthRoute ~ swaggerRoute } val combinedRoute = modules.map(_.route).foldLeft(basicRoutes.routeWithDefaults)(_ ~ _) @@ -221,29 +220,3 @@ class DriverApp( }) } } - -object DriverApp { - implicit def rejectionHandler: RejectionHandler = - RejectionHandler - .newBuilder() - .handleAll[MethodRejection] { rejections => - val methods = rejections map (_.supported) - lazy val names = methods map (_.name) mkString ", " - - options { - respondWithCorsHeaders { - respondWithCorsAllowedMethodHeaders(methods) { - complete(s"Supported methods: $names.") - } - } - } ~ - complete(MethodNotAllowed -> s"HTTP method not allowed, supported methods: $names!") - } - .handleAll[Rejection] { rejections => - respondWithCorsHeaders { - reject(rejections: _*) - } - } - .result() - .seal -} diff --git a/src/main/scala/xyz/driver/core/app/module.scala b/src/main/scala/xyz/driver/core/app/module.scala index 7be38eb..0a255fb 100644 --- a/src/main/scala/xyz/driver/core/app/module.scala +++ b/src/main/scala/xyz/driver/core/app/module.scala @@ -30,8 +30,9 @@ class EmptyModule extends Module { class SimpleModule(override val name: String, theRoute: Route, routeType: Type) extends Module { private val driverRoute: DriverRoute = new DriverRoute { - override def route: Route = theRoute - override val log: Logger = xyz.driver.core.logging.NoLogger + override def route: Route = theRoute + override val config: Config = xyz.driver.core.config.loadDefaultConfig + override val log: Logger = xyz.driver.core.logging.NoLogger } override def route: Route = driverRoute.routeWithDefaults diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index 5f961b6..5647818 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -7,6 +7,7 @@ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{Directive0, ExceptionHandler, RequestContext, Route} +import com.typesafe.config.Config import com.typesafe.scalalogging.Logger import org.slf4j.MDC import xyz.driver.core.rest @@ -16,17 +17,69 @@ import scala.compat.Platform.ConcurrentModificationException trait DriverRoute { def log: Logger + def config: Config def route: Route def routeWithDefaults: Route = { - (defaultResponseHeaders & handleExceptions(ExceptionHandler(exceptionHandler)))(route) + (defaultResponseHeaders & handleExceptions(ExceptionHandler(exceptionHandler))) { + route ~ defaultOptionsRoute + } + } + + protected lazy val allowedCorsDomainSuffixes: Set[HttpOrigin] = { + import scala.collection.JavaConverters._ + config + .getConfigList("application.cors.allowedOrigins") + .asScala + .map { c => + HttpOrigin(c.getString("scheme"), Host(c.getString("hostSuffix"))) + }(scala.collection.breakOut) + } + + protected lazy val defaultCorsAllowedMethods: Set[HttpMethod] = { + import scala.collection.JavaConverters._ + config.getStringList("application.cors.allowedMethods").asScala.toSet.flatMap(HttpMethods.getForKey) + } + + protected lazy val defaultCorsAllowedOrigin: Origin = + Origin(allowedCorsDomainSuffixes.to[collection.immutable.Seq]) + + protected def corsAllowedOriginHeader(origin: Option[Origin]): HttpHeader = { + val allowedOrigin = + origin + .filter { requestOrigin => + allowedCorsDomainSuffixes.exists { allowedOriginSuffix => + requestOrigin.origins.exists(o => + o.scheme == allowedOriginSuffix.scheme && + o.host.host.address.endsWith(allowedOriginSuffix.host.host.address())) + } + } + .getOrElse(defaultCorsAllowedOrigin) + + `Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigin.origins: _*)) + } + + protected def respondWithAllCorsHeaders: Directive0 = { + respondWithCorsAllowedHeaders tflatMap { _ => + respondWithCorsAllowedMethodHeaders(defaultCorsAllowedMethods) tflatMap { _ => + optionalHeaderValueByType[Origin](()) flatMap { origin => + respondWithHeader(corsAllowedOriginHeader(origin)) + } + } + } + } + + protected def defaultOptionsRoute: Route = options { + respondWithAllCorsHeaders { + complete("OK") + } } protected def defaultResponseHeaders: Directive0 = { extractRequest flatMap { request => val tracingHeader = RawHeader(ContextHeaders.TrackingIdHeader, rest.extractTrackingId(request)) - respondWithHeader(tracingHeader) & respondWithCorsHeaders + respondWithHeader(tracingHeader) & respondWithAllCorsHeaders } } diff --git a/src/main/scala/xyz/driver/core/rest/package.scala b/src/main/scala/xyz/driver/core/rest/package.scala index 88f78d9..5fd9417 100644 --- a/src/main/scala/xyz/driver/core/rest/package.scala +++ b/src/main/scala/xyz/driver/core/rest/package.scala @@ -110,22 +110,25 @@ object `package` { } } - def respondWithCorsHeaders: Directive0 = { - optionalHeaderValueByType[Origin](()) flatMap { originHeader => - respondWithHeaders( - List[HttpHeader]( - allowOrigin(originHeader), - `Access-Control-Allow-Headers`(AllowedHeaders: _*), - `Access-Control-Expose-Headers`(AllowedHeaders: _*) - )) + def respondWithCorsAllowedHeaders: Directive0 = { + respondWithHeaders( + List[HttpHeader]( + `Access-Control-Allow-Headers`(AllowedHeaders: _*), + `Access-Control-Expose-Headers`(AllowedHeaders: _*) + )) + } + + def respondWithCorsAllowedOriginHeaders(origin: Origin): Directive0 = { + respondWithHeader { + `Access-Control-Allow-Origin`(HttpOriginRange(origin.origins: _*)) } } - def respondWithCorsAllowedMethodHeaders(methods: scala.collection.immutable.Seq[HttpMethod]): Directive0 = { + def respondWithCorsAllowedMethodHeaders(methods: Set[HttpMethod]): Directive0 = { respondWithHeaders( List[HttpHeader]( - Allow(methods), - `Access-Control-Allow-Methods`(methods) + Allow(methods.to[collection.immutable.Seq]), + `Access-Control-Allow-Methods`(methods.to[collection.immutable.Seq]) )) } diff --git a/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala b/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala index f5602be..991d7c5 100644 --- a/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala +++ b/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala @@ -1,7 +1,7 @@ package xyz.driver.core.rest import akka.http.scaladsl.model.headers._ -import akka.http.scaladsl.model.{HttpMethods, StatusCodes} +import akka.http.scaladsl.model.{HttpMethod, HttpMethods, StatusCodes} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import akka.http.scaladsl.settings.RoutingSettings @@ -15,8 +15,9 @@ import scala.reflect.runtime.universe._ class DriverAppTest extends FlatSpec with ScalatestRouteTest with Matchers { class TestRoute extends DriverRoute { - override def log: Logger = xyz.driver.core.logging.NoLogger - override def route: Route = path("api" / "v1" / "test")(post(complete("OK"))) + override def log: Logger = xyz.driver.core.logging.NoLogger + override def config: Config = xyz.driver.core.config.loadDefaultConfig + override def route: Route = path("api" / "v1" / "test")(post(complete("OK"))) } val module: Module = new Module { @@ -30,29 +31,64 @@ class DriverAppTest extends FlatSpec with ScalatestRouteTest with Matchers { appName = "test-app", version = "0.1", gitHash = "deadb33f", - modules = Seq(module) + modules = Seq(module), + log = xyz.driver.core.logging.NoLogger ) val config: Config = xyz.driver.core.config.loadDefaultConfig val routingSettings: RoutingSettings = RoutingSettings(config) - val appRoute: Route = - Route.seal(app.appRoute)(routingSettings = routingSettings, rejectionHandler = DriverApp.rejectionHandler) + val appRoute: Route = Route.seal(app.appRoute)(routingSettings = routingSettings) + + val allowedMethods: collection.immutable.Seq[HttpMethod] = { + import scala.collection.JavaConverters._ + config + .getStringList("application.cors.allowedMethods") + .asScala + .flatMap(HttpMethods.getForKey) + .to[collection.immutable.Seq] + } + + val allowedOrigin: Origin = { + import scala.collection.JavaConverters._ + Origin( + config + .getConfigList("application.cors.allowedOrigins") + .asScala + .map { c => + HttpOrigin(c.getString("scheme"), Host(c.getString("hostSuffix"))) + }(scala.collection.breakOut): _*) + } "DriverApp" should "respond with the correct CORS headers for the swagger OPTIONS route" in { Options(s"/api-docs/swagger.json") ~> appRoute ~> check { status shouldBe StatusCodes.OK - info(response.toString()) - headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange.*)) - headers should contain(`Access-Control-Allow-Methods`(HttpMethods.GET)) + headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigin.origins: _*))) + header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods } } it should "respond with the correct CORS headers for the test route" in { Options(s"/api/v1/test") ~> appRoute ~> check { status shouldBe StatusCodes.OK - info(response.toString()) - headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange.*)) - headers should contain(`Access-Control-Allow-Methods`(HttpMethods.GET, HttpMethods.POST)) + headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigin.origins: _*))) + header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods + } + } + + it should "allow subdomains of allowed origin suffixes" in { + Options(s"/api/v1/test").withHeaders(Origin(HttpOrigin("https", Host("foo.example.com")))) ~> appRoute ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOrigin("https", Host("foo.example.com")))) + header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods + } + } + + it should "respond with default domains for invalid origins" in { + Options(s"/api/v1/test") + .withHeaders(Origin(HttpOrigin("https", Host("invalid.foo.bar.com")))) ~> appRoute ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigin.origins: _*))) + header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods } } } diff --git a/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala b/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala index f402261..c763dda 100644 --- a/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala +++ b/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala @@ -4,6 +4,7 @@ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.Directives.{complete => akkaComplete} import akka.http.scaladsl.server.Route import akka.http.scaladsl.testkit.ScalatestRouteTest +import com.typesafe.config.Config import com.typesafe.scalalogging.Logger import org.scalatest.{AsyncFlatSpec, Matchers} import xyz.driver.core.logging.NoLogger @@ -13,7 +14,8 @@ import scala.concurrent.Future class DriverRouteTest extends AsyncFlatSpec with ScalatestRouteTest with Matchers { class TestRoute(override val route: Route) extends DriverRoute { - override def log: Logger = NoLogger + override def log: Logger = NoLogger + override def config: Config = xyz.driver.core.config.loadDefaultConfig } "DriverRoute" should "respond with 200 OK for a basic route" in { -- cgit v1.2.3 From 7f2e90c278a28c49da5c736b0977466fed8e96cc Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Fri, 26 Jan 2018 15:48:04 -0800 Subject: Remove default config values --- src/main/resources/reference.conf | 11 +-- .../scala/xyz/driver/core/rest/DriverRoute.scala | 3 +- .../scala/xyz/driver/core/rest/DriverAppTest.scala | 94 ---------------------- .../xyz/driver/core/rest/DriverRouteTest.scala | 65 +++++++++++++-- 4 files changed, 62 insertions(+), 111 deletions(-) delete mode 100644 src/test/scala/xyz/driver/core/rest/DriverAppTest.scala (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index aed7b12..238ac68 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -13,16 +13,7 @@ application { cors { allowedMethods: ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"] - allowedOrigins: [ - { - scheme: http - hostSuffix: localhost - }, - { - scheme: https - hostSuffix: example.com - } - ] + allowedOrigins: [] } } diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index 5647818..1fe5e3f 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -42,8 +42,9 @@ trait DriverRoute { config.getStringList("application.cors.allowedMethods").asScala.toSet.flatMap(HttpMethods.getForKey) } - protected lazy val defaultCorsAllowedOrigin: Origin = + protected lazy val defaultCorsAllowedOrigin: Origin = { Origin(allowedCorsDomainSuffixes.to[collection.immutable.Seq]) + } protected def corsAllowedOriginHeader(origin: Option[Origin]): HttpHeader = { val allowedOrigin = diff --git a/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala b/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala deleted file mode 100644 index 991d7c5..0000000 --- a/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala +++ /dev/null @@ -1,94 +0,0 @@ -package xyz.driver.core.rest - -import akka.http.scaladsl.model.headers._ -import akka.http.scaladsl.model.{HttpMethod, HttpMethods, StatusCodes} -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import akka.http.scaladsl.settings.RoutingSettings -import akka.http.scaladsl.testkit.ScalatestRouteTest -import com.typesafe.config.Config -import com.typesafe.scalalogging.Logger -import org.scalatest.{FlatSpec, Matchers} -import xyz.driver.core.app.{DriverApp, Module} - -import scala.reflect.runtime.universe._ - -class DriverAppTest extends FlatSpec with ScalatestRouteTest with Matchers { - class TestRoute extends DriverRoute { - override def log: Logger = xyz.driver.core.logging.NoLogger - override def config: Config = xyz.driver.core.config.loadDefaultConfig - override def route: Route = path("api" / "v1" / "test")(post(complete("OK"))) - } - - val module: Module = new Module { - val testRoute = new TestRoute - override def route: Route = testRoute.routeWithDefaults - override def routeTypes: Seq[Type] = Seq(typeOf[TestRoute]) - override val name: String = "test-module" - } - - val app: DriverApp = new DriverApp( - appName = "test-app", - version = "0.1", - gitHash = "deadb33f", - modules = Seq(module), - log = xyz.driver.core.logging.NoLogger - ) - - val config: Config = xyz.driver.core.config.loadDefaultConfig - val routingSettings: RoutingSettings = RoutingSettings(config) - val appRoute: Route = Route.seal(app.appRoute)(routingSettings = routingSettings) - - val allowedMethods: collection.immutable.Seq[HttpMethod] = { - import scala.collection.JavaConverters._ - config - .getStringList("application.cors.allowedMethods") - .asScala - .flatMap(HttpMethods.getForKey) - .to[collection.immutable.Seq] - } - - val allowedOrigin: Origin = { - import scala.collection.JavaConverters._ - Origin( - config - .getConfigList("application.cors.allowedOrigins") - .asScala - .map { c => - HttpOrigin(c.getString("scheme"), Host(c.getString("hostSuffix"))) - }(scala.collection.breakOut): _*) - } - - "DriverApp" should "respond with the correct CORS headers for the swagger OPTIONS route" in { - Options(s"/api-docs/swagger.json") ~> appRoute ~> check { - status shouldBe StatusCodes.OK - headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigin.origins: _*))) - header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods - } - } - - it should "respond with the correct CORS headers for the test route" in { - Options(s"/api/v1/test") ~> appRoute ~> check { - status shouldBe StatusCodes.OK - headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigin.origins: _*))) - header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods - } - } - - it should "allow subdomains of allowed origin suffixes" in { - Options(s"/api/v1/test").withHeaders(Origin(HttpOrigin("https", Host("foo.example.com")))) ~> appRoute ~> check { - status shouldBe StatusCodes.OK - headers should contain(`Access-Control-Allow-Origin`(HttpOrigin("https", Host("foo.example.com")))) - header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods - } - } - - it should "respond with default domains for invalid origins" in { - Options(s"/api/v1/test") - .withHeaders(Origin(HttpOrigin("https", Host("invalid.foo.bar.com")))) ~> appRoute ~> check { - status shouldBe StatusCodes.OK - headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigin.origins: _*))) - header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods - } - } -} diff --git a/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala b/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala index c763dda..60056b7 100644 --- a/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala +++ b/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala @@ -1,10 +1,11 @@ package xyz.driver.core.rest -import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.{HttpMethod, StatusCodes} +import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.server.Directives.{complete => akkaComplete} -import akka.http.scaladsl.server.Route +import akka.http.scaladsl.server.{Directives, Route} import akka.http.scaladsl.testkit.ScalatestRouteTest -import com.typesafe.config.Config +import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.scalalogging.Logger import org.scalatest.{AsyncFlatSpec, Matchers} import xyz.driver.core.logging.NoLogger @@ -12,10 +13,24 @@ import xyz.driver.core.rest.errors._ import scala.concurrent.Future -class DriverRouteTest extends AsyncFlatSpec with ScalatestRouteTest with Matchers { +class DriverRouteTest extends AsyncFlatSpec with ScalatestRouteTest with Matchers with Directives { class TestRoute(override val route: Route) extends DriverRoute { - override def log: Logger = NoLogger - override def config: Config = xyz.driver.core.config.loadDefaultConfig + override def log: Logger = NoLogger + override def config: Config = + ConfigFactory.parseString(""" + |application { + | cors { + | allowedMethods: ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"] + | allowedOrigins: [{scheme: https, hostSuffix: example.com}] + | } + |} + """.stripMargin) + } + + val allowedOrigins = Set(HttpOrigin("https", Host("example.com"))) + val allowedMethods: collection.immutable.Seq[HttpMethod] = { + import akka.http.scaladsl.model.HttpMethods._ + collection.immutable.Seq(GET, PUT, POST, PATCH, DELETE, OPTIONS) } "DriverRoute" should "respond with 200 OK for a basic route" in { @@ -88,4 +103,42 @@ class DriverRouteTest extends AsyncFlatSpec with ScalatestRouteTest with Matcher responseAs[String] shouldBe "Database access error" } } + + it should "respond with the correct CORS headers for the swagger OPTIONS route" in { + val route = new TestRoute(get(akkaComplete(StatusCodes.OK))) + Options(s"/api-docs/swagger.json") ~> route.routeWithDefaults ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigins.toSeq: _*))) + header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods + } + } + + it should "respond with the correct CORS headers for the test route" in { + val route = new TestRoute(get(akkaComplete(StatusCodes.OK))) + Options(s"/api/v1/test") ~> route.routeWithDefaults ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigins.toSeq: _*))) + header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods + } + } + + it should "allow subdomains of allowed origin suffixes" in { + val route = new TestRoute(get(akkaComplete(StatusCodes.OK))) + Options(s"/api/v1/test") + .withHeaders(Origin(HttpOrigin("https", Host("foo.example.com")))) ~> route.routeWithDefaults ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOrigin("https", Host("foo.example.com")))) + header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods + } + } + + it should "respond with default domains for invalid origins" in { + val route = new TestRoute(get(akkaComplete(StatusCodes.OK))) + Options(s"/api/v1/test") + .withHeaders(Origin(HttpOrigin("https", Host("invalid.foo.bar.com")))) ~> route.routeWithDefaults ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigins.toSeq: _*))) + header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods + } + } } -- cgit v1.2.3 From 04db4e857fceeb15196d8f13d63e987ad214be38 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 8 Feb 2018 13:02:07 -0800 Subject: Move rejection handler to DriverRoute --- src/main/resources/reference.conf | 17 +++++++++++++++-- src/main/scala/xyz/driver/core/rest/DriverRoute.scala | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 238ac68..f903234 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -2,7 +2,7 @@ # Default settings for driver core. Any settings defined by users of # # this library will take precedence. See the documentation of the # # Typesafe Config Library (https://github.com/lightbend/config) for # -# more information. # +# more information. # ###################################################################### # This scope is for general settings related to the execution of a @@ -13,7 +13,20 @@ application { cors { allowedMethods: ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"] - allowedOrigins: [] + allowedOrigins: [ + { + scheme: http + hostSuffix: localhost + }, + { + scheme: https + hostSuffix: driver.xyz + }, + { + scheme: https + hostSuffix: driver.network + } + ] } } diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index 1fe5e3f..5e629be 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -6,7 +6,7 @@ import akka.http.scaladsl.model._ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.{Directive0, ExceptionHandler, RequestContext, Route} +import akka.http.scaladsl.server._ import com.typesafe.config.Config import com.typesafe.scalalogging.Logger import org.slf4j.MDC @@ -22,7 +22,7 @@ trait DriverRoute { def route: Route def routeWithDefaults: Route = { - (defaultResponseHeaders & handleExceptions(ExceptionHandler(exceptionHandler))) { + (defaultResponseHeaders & handleRejections(rejectionHandler) & handleExceptions(ExceptionHandler(exceptionHandler))) { route ~ defaultOptionsRoute } } @@ -84,6 +84,19 @@ trait DriverRoute { } } + protected def rejectionHandler: RejectionHandler = + RejectionHandler + .newBuilder() + .handle { + case rejection => + respondWithAllCorsHeaders { + RejectionHandler + .default(scala.collection.immutable.Seq(rejection)) + .getOrElse(complete("OK")) + } + } + .result() + /** * Override me for custom exception handling * -- cgit v1.2.3 From a499e0308cd1a9be29c21c4d453c8a7aa7fa25f1 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Tue, 20 Feb 2018 11:39:11 -0800 Subject: Use config from outer scope in DriverApp --- src/main/scala/xyz/driver/core/app/DriverApp.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/core/app/DriverApp.scala b/src/main/scala/xyz/driver/core/app/DriverApp.scala index a593893..1ded4dd 100644 --- a/src/main/scala/xyz/driver/core/app/DriverApp.scala +++ b/src/main/scala/xyz/driver/core/app/DriverApp.scala @@ -72,7 +72,7 @@ class DriverApp( val versionRt = versionRoute(version, gitHash, time.currentTime()) val basicRoutes = new DriverRoute { override def log: Logger = self.log - override def config: Config = xyz.driver.core.config.loadDefaultConfig + override def config: Config = self.config override def route: Route = versionRt ~ healthRoute ~ swaggerRoute } val combinedRoute = modules.map(_.route).foldLeft(basicRoutes.routeWithDefaults)(_ ~ _) -- cgit v1.2.3 From 2059634d2fa2c28ddf2b992bc36ab3d96f3c2512 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 22 Feb 2018 11:26:46 -0800 Subject: Call .seal on RejectionHandler --- src/main/scala/xyz/driver/core/rest/DriverRoute.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index 5e629be..58a4143 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -90,12 +90,11 @@ trait DriverRoute { .handle { case rejection => respondWithAllCorsHeaders { - RejectionHandler - .default(scala.collection.immutable.Seq(rejection)) - .getOrElse(complete("OK")) + RejectionHandler.default(collection.immutable.Seq(rejection)).get } } .result() + .seal /** * Override me for custom exception handling -- cgit v1.2.3