diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 4ad566183..a9961c6f1 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -52,6 +52,7 @@ cors { accessControlMaxAge = 60 minutes + allowedCorsHeaders = ["X-Example"] } streams { diff --git a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Config.scala b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Config.scala index 5d2d335a3..63718dc2a 100644 --- a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Config.scala +++ b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Config.scala @@ -95,7 +95,8 @@ object Config { ) case class CORS( - accessControlMaxAge: FiniteDuration + accessControlMaxAge: FiniteDuration, + allowedCorsHeaders: List[String] ) case class Streams[+SinkConfig]( 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 317bc3020..c91d0d839 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 @@ -17,6 +17,7 @@ import org.apache.commons.codec.binary.Base64 import scala.concurrent.duration._ import scala.jdk.CollectionConverters._ +import cats.data.NonEmptyList import cats.effect.{Clock, Sync} import cats.implicits._ @@ -139,11 +140,13 @@ class Service[F[_]: Sync]( } override def preflightResponse(req: Request[F]): F[Response[F]] = Sync[F].pure { + val allowedHeaders = (List("Content-Type", "SP-Anonymous") ++ config.cors.allowedCorsHeaders).map(CIString(_)) + val corsHeaders = NonEmptyList.fromListUnsafe(allowedHeaders) Response[F]( headers = Headers( accessControlAllowOriginHeader(req), `Access-Control-Allow-Credentials`(), - `Access-Control-Allow-Headers`(ci"Content-Type", ci"SP-Anonymous"), + `Access-Control-Allow-Headers`(corsHeaders), `Access-Control-Max-Age`.Cache(config.cors.accessControlMaxAge.toSeconds).asInstanceOf[`Access-Control-Max-Age`] ) ) 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 8d3bf9f45..b39cc9b1a 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 @@ -415,7 +415,7 @@ class ServiceSpec extends Specification { val expected = Headers( Header.Raw(ci"Access-Control-Allow-Origin", "*"), `Access-Control-Allow-Credentials`(), - `Access-Control-Allow-Headers`(ci"Content-Type", ci"SP-Anonymous"), + `Access-Control-Allow-Headers`(ci"Content-Type", ci"SP-Anonymous", ci"X-Howdy"), `Access-Control-Max-Age`.Cache(3600).asInstanceOf[`Access-Control-Max-Age`] ) service.preflightResponse(Request[IO]()).unsafeRunSync().headers shouldEqual expected diff --git a/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/TestUtils.scala b/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/TestUtils.scala index 1c0a5da72..6aa14400b 100644 --- a/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/TestUtils.scala +++ b/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/TestUtils.scala @@ -74,7 +74,7 @@ object TestUtils { Map.empty[String, String], "" ), - cors = CORS(60.minutes), + cors = CORS(60.minutes, List("X-Howdy")), streams = Streams( good = SinkConfig( name = "raw", diff --git a/examples/config.kafka.extended.hocon b/examples/config.kafka.extended.hocon index 095118c22..44a29bb24 100644 --- a/examples/config.kafka.extended.hocon +++ b/examples/config.kafka.extended.hocon @@ -168,6 +168,9 @@ collector { # The Access-Control-Max-Age response header indicates how long the results of a preflight # request can be cached. -1 seconds disables the cache. Chromium max is 10m, Firefox is 24h. accessControlMaxAge = 60 minutes + # The allowedCorsHeaders response header allows non-safelisted CORS headers to be returned + # as part of OPTIONS (preflight) requests + allowedCorsHeaders = ["X-Example"] } streams { diff --git a/examples/config.kinesis.extended.hocon b/examples/config.kinesis.extended.hocon index 0efd17d72..57fe3f1a9 100644 --- a/examples/config.kinesis.extended.hocon +++ b/examples/config.kinesis.extended.hocon @@ -168,6 +168,9 @@ collector { # The Access-Control-Max-Age response header indicates how long the results of a preflight # request can be cached. -1 seconds disables the cache. Chromium max is 10m, Firefox is 24h. accessControlMaxAge = 60 minutes + # The allowedCorsHeaders response header allows non-safelisted CORS headers to be returned + # as part of OPTIONS (preflight) requests + allowedCorsHeaders = ["X-Example"] } streams { diff --git a/examples/config.nsq.extended.hocon b/examples/config.nsq.extended.hocon index f2925ad6d..6946f52b5 100644 --- a/examples/config.nsq.extended.hocon +++ b/examples/config.nsq.extended.hocon @@ -168,6 +168,9 @@ collector { # The Access-Control-Max-Age response header indicates how long the results of a preflight # request can be cached. -1 seconds disables the cache. Chromium max is 10m, Firefox is 24h. accessControlMaxAge = 60 minutes + # The allowedCorsHeaders response header allows non-safelisted CORS headers to be returned + # as part of OPTIONS (preflight) requests + allowedCorsHeaders = ["X-Example"] } streams { diff --git a/examples/config.pubsub.extended.hocon b/examples/config.pubsub.extended.hocon index 0d4fcc439..ca921c4a5 100644 --- a/examples/config.pubsub.extended.hocon +++ b/examples/config.pubsub.extended.hocon @@ -168,6 +168,9 @@ collector { # The Access-Control-Max-Age response header indicates how long the results of a preflight # request can be cached. -1 seconds disables the cache. Chromium max is 10m, Firefox is 24h. accessControlMaxAge = 60 minutes + # The allowedCorsHeaders response header allows non-safelisted CORS headers to be returned + # as part of OPTIONS (preflight) requests + allowedCorsHeaders = ["X-Example"] } streams { diff --git a/examples/config.sqs.extended.hocon b/examples/config.sqs.extended.hocon index 65a58db79..42dad4d07 100644 --- a/examples/config.sqs.extended.hocon +++ b/examples/config.sqs.extended.hocon @@ -159,6 +159,9 @@ collector { # The Access-Control-Max-Age response header indicates how long the results of a preflight # request can be cached. -1 seconds disables the cache. Chromium max is 10m, Firefox is 24h. accessControlMaxAge = 60 minutes + # The allowedCorsHeaders response header allows non-safelisted CORS headers to be returned + # as part of OPTIONS (preflight) requests + allowedCorsHeaders = ["X-Example"] } streams { diff --git a/examples/config.stdout.extended.hocon b/examples/config.stdout.extended.hocon index 42593a3d7..e00d2ee4e 100644 --- a/examples/config.stdout.extended.hocon +++ b/examples/config.stdout.extended.hocon @@ -168,6 +168,9 @@ collector { # The Access-Control-Max-Age response header indicates how long the results of a preflight # request can be cached. -1 seconds disables the cache. Chromium max is 10m, Firefox is 24h. accessControlMaxAge = 60 minutes + # The allowedCorsHeaders response header allows non-safelisted CORS headers to be returned + # as part of OPTIONS (preflight) requests + allowedCorsHeaders = ["X-Example"] } streams { diff --git a/kafka/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/KafkaConfigSpec.scala b/kafka/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/KafkaConfigSpec.scala index 3b402dafc..6ffb0ae65 100644 --- a/kafka/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/KafkaConfigSpec.scala +++ b/kafka/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/KafkaConfigSpec.scala @@ -101,7 +101,7 @@ object KafkaConfigSpec { headers = Map.empty[String, String], body = "" ), - cors = Config.CORS(1.hour), + cors = Config.CORS(1.hour, List("X-Example")), monitoring = Config.Monitoring( Config.Metrics( Config.Statsd(false, "localhost", 8125, 10.seconds, "snowplow.collector", Map.empty) diff --git a/kinesis/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/sinks/KinesisConfigSpec.scala b/kinesis/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/sinks/KinesisConfigSpec.scala index 1574ee9c5..38a84b203 100644 --- a/kinesis/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/sinks/KinesisConfigSpec.scala +++ b/kinesis/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/sinks/KinesisConfigSpec.scala @@ -111,7 +111,7 @@ object KinesisConfigSpec { headers = Map.empty[String, String], body = "" ), - cors = Config.CORS(1.hour), + cors = Config.CORS(1.hour, List("X-Example")), monitoring = Config.Monitoring( Config.Metrics( Config.Statsd(false, "localhost", 8125, 10.seconds, "snowplow.collector", Map.empty) diff --git a/nsq/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/NsqConfigSpec.scala b/nsq/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/NsqConfigSpec.scala index 8cc536d12..ca6360ef8 100644 --- a/nsq/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/NsqConfigSpec.scala +++ b/nsq/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/NsqConfigSpec.scala @@ -100,7 +100,7 @@ object NsqConfigSpec { headers = Map.empty[String, String], body = "" ), - cors = Config.CORS(1.hour), + cors = Config.CORS(1.hour, List("X-Example")), monitoring = Config.Monitoring( Config.Metrics( Config.Statsd(false, "localhost", 8125, 10.seconds, "snowplow.collector", Map.empty) diff --git a/pubsub/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/ConfigSpec.scala b/pubsub/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/ConfigSpec.scala index 1392133de..0b59a1522 100644 --- a/pubsub/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/ConfigSpec.scala +++ b/pubsub/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/ConfigSpec.scala @@ -100,7 +100,7 @@ object ConfigSpec { headers = Map.empty[String, String], body = "" ), - cors = Config.CORS(1.hour), + cors = Config.CORS(1.hour, List("X-Example")), monitoring = Config.Monitoring( Config.Metrics( Config.Statsd(false, "localhost", 8125, 10.seconds, "snowplow.collector", Map.empty) diff --git a/sqs/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/SqsConfigSpec.scala b/sqs/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/SqsConfigSpec.scala index 86a8a8d76..3b48b57d9 100644 --- a/sqs/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/SqsConfigSpec.scala +++ b/sqs/src/test/scala/com.snowplowanalytics.snowplow.collectors.scalastream/SqsConfigSpec.scala @@ -101,7 +101,7 @@ object SqsConfigSpec { headers = Map.empty[String, String], body = "" ), - cors = Config.CORS(1.hour), + cors = Config.CORS(1.hour, List("X-Example")), monitoring = Config.Monitoring( Config.Metrics( Config.Statsd(false, "localhost", 8125, 10.seconds, "snowplow.collector", Map.empty)