Skip to content

Commit

Permalink
Add crossdomain.xml support (close #399)
Browse files Browse the repository at this point in the history
crossdomain.xml provides cross-domain functionality to Adobe Flash/Flex, but these days Adobe Acrobat. The functionality is required when a tracker request is embedded in a pdf. In this case, when user opens up a PDF file with a script hosted on domain a.com, the script will fetch the crossdomain.xml policy from domain b.com to check whether the endpoint can be accessed and that's used by Adobe to conditionally issue requests.
  • Loading branch information
peel committed Nov 23, 2023
1 parent 1a45280 commit fff86e1
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down Expand Up @@ -155,6 +155,26 @@ class Service[F[_]: Sync](
)
}

def crossdomainResponse: F[Response[F]] = Sync[F].pure {
val policy =
config
.crossDomain
.domains
.map(d => s"""<allow-access-from domain="${d}" secure="${config.crossDomain.secure}" />""")
.mkString("\n")

val xml = s"""<?xml version="1.0"?>
|<cross-domain-policy>
|${policy}
|</cross-domain-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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}

Expand Down Expand Up @@ -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"/")
Expand All @@ -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)
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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("""<?xml version="1.0"?>""")
body must contain("<cross-domain-policy>")
body must endWith("</cross-domain-policy>")
}
}
}

0 comments on commit fff86e1

Please sign in to comment.