diff --git a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Routes.scala b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Routes.scala index 8160b285e..0e1e7f2ef 100644 --- a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Routes.scala +++ b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Routes.scala @@ -15,8 +15,12 @@ import org.http4s.dsl.Http4sDsl import org.http4s.implicits._ import com.comcast.ip4s.Dns -class Routes[F[_]: Sync](enableDefaultRedirect: Boolean, enableRootResponse: Boolean, service: IService[F]) - extends Http4sDsl[F] { +class Routes[F[_]: Sync]( + enableDefaultRedirect: Boolean, + enableRootResponse: Boolean, + enableCrossdomainTracking: Boolean, + service: IService[F] +) extends Http4sDsl[F] { implicit val dns: Dns[F] = Dns.forSync[F] @@ -83,8 +87,13 @@ class Routes[F[_]: Sync](enableDefaultRedirect: Boolean, enableRootResponse: Boo service.rootResponse } + private val crossdomainRoute = HttpRoutes.of[F] { + case GET -> Root / "crossdomain.xml" if enableCrossdomainTracking => + service.crossdomainResponse + } + val value: HttpApp[F] = { - val routes = healthRoutes <+> corsRoute <+> cookieRoutes <+> rootRoute + val routes = healthRoutes <+> corsRoute <+> cookieRoutes <+> rootRoute <+> crossdomainRoute val res = if (enableDefaultRedirect) routes else rejectRedirect <+> routes res.orNotFound } diff --git a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Run.scala b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Run.scala index 6a3ec65c7..e8f1fe4db 100644 --- a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Run.scala +++ b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Run.scala @@ -79,7 +79,7 @@ object Run { appInfo ) httpServer = HttpServer.build[F]( - new Routes[F](config.enableDefaultRedirect, config.rootResponse.enabled, collectorService).value, + new Routes[F](config.enableDefaultRedirect, config.rootResponse.enabled, config.crossDomain.enabled, collectorService).value, if (config.ssl.enable) config.ssl.port else config.port, config.ssl.enable, config.networking diff --git a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Service.scala b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Service.scala index 52b8f232c..0e62b4b0f 100644 --- a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Service.scala +++ b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Service.scala @@ -44,12 +44,12 @@ trait IService[F[_]] { def determinePath(vendor: String, version: String): String def sinksHealthy: F[Boolean] def rootResponse: F[Response[F]] + def crossdomainResponse: F[Response[F]] } object Service { // Contains an invisible pixel to return for `/i` requests. - val pixel = Base64.decodeBase64("R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==") - + val pixel = Base64.decodeBase64("R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==") val spAnonymousNuid = "00000000-0000-0000-0000-000000000000" } @@ -155,6 +155,26 @@ class Service[F[_]: Sync]( ) } + def crossdomainResponse: F[Response[F]] = Sync[F].pure { + val policy = + config + .crossDomain + .domains + .map(d => s"""""") + .mkString("\n") + + val xml = s""" + | + |${policy} + |""".stripMargin + + Response[F]( + status = Ok, + body = Stream.emit(xml).through(fs2.text.utf8.encode), + headers = Headers(`Content-Type`(MediaType.text.xml)) + ) + } + def extractHeader(req: Request[F], headerName: String): Option[String] = req.headers.get(CIString(headerName)).map(_.head.value) diff --git a/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/RoutesSpec.scala b/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/RoutesSpec.scala index ffc6de9d0..af2f04e0e 100644 --- a/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/RoutesSpec.scala +++ b/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/RoutesSpec.scala @@ -37,6 +37,9 @@ class RoutesSpec extends Specification { override def rootResponse: IO[Response[IO]] = IO.pure(Response(status = Ok, body = Stream.emit("root").through(text.utf8.encode))) + override def crossdomainResponse: IO[Response[IO]] = + IO.pure(Response(status = Ok, body = Stream.empty)) + override def cookie( body: IO[Option[String]], path: String, @@ -62,9 +65,13 @@ class RoutesSpec extends Specification { override def sinksHealthy: IO[Boolean] = IO.pure(true) } - def createTestServices(enabledDefaultRedirect: Boolean = true, enableRootResponse: Boolean = false) = { + def createTestServices( + enabledDefaultRedirect: Boolean = true, + enableRootResponse: Boolean = false, + enableCrossdomainTracking: Boolean = false + ) = { val service = new TestService() - val routes = new Routes(enabledDefaultRedirect, enableRootResponse, service).value + val routes = new Routes(enabledDefaultRedirect, enableRootResponse, enableCrossdomainTracking, service).value (service, routes) } @@ -244,7 +251,7 @@ class RoutesSpec extends Specification { test(Method.POST) } - "respond to the root route" in { + "respond to root route" in { "enabled return the response" in { val (_, routes) = createTestServices(enableRootResponse = true) val request = Request[IO](method = Method.GET, uri = uri"/") @@ -261,6 +268,24 @@ class RoutesSpec extends Specification { response.status must beEqualTo(Status.NotFound) } } + + "respond to crossdomain route" in { + "enabled return the response" in { + val (_, routes) = createTestServices(enableCrossdomainTracking = true) + val request = Request[IO](method = Method.GET, uri = uri"/crossdomain.xml") + val response = routes.run(request).unsafeRunSync() + + response.status must beEqualTo(Status.Ok) + } + "disabled return NotFound" in { + val (_, routes) = createTestServices(enableCrossdomainTracking = false) + val request = Request[IO](method = Method.GET, uri = uri"/crossdomain.xml") + val response = routes.run(request).unsafeRunSync() + + response.status must beEqualTo(Status.NotFound) + } + } + } } diff --git a/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/ServiceSpec.scala b/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/ServiceSpec.scala index c431eb75f..d3024294c 100644 --- a/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/ServiceSpec.scala +++ b/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/ServiceSpec.scala @@ -1016,5 +1016,13 @@ class ServiceSpec extends Specification { service.determinePath(vendor, version3) shouldEqual expected3 } } + + "crossdomainResponse" in { + val response = service.crossdomainResponse.unsafeRunSync() + val body = response.body.compile.toList.unsafeRunSync().map(_.toChar).mkString + body must startWith("""""") + body must contain("") + body must endWith("") + } } }