diff --git a/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Rfc6265Cookie.scala b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Rfc6265Cookie.scala new file mode 100644 index 000000000..983f2087d --- /dev/null +++ b/core/src/main/scala/com.snowplowanalytics.snowplow.collector.core/Rfc6265Cookie.scala @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2013-present Snowplow Analytics Ltd. + * All rights reserved. + * + * This software is made available by Snowplow Analytics, Ltd., + * under the terms of the Snowplow Limited Use License Agreement, Version 1.0 + * located at https://docs.snowplow.io/limited-use-license-1.0 + * BY INSTALLING, DOWNLOADING, ACCESSING, USING OR DISTRIBUTING ANY PORTION + * OF THE SOFTWARE, YOU AGREE TO THE TERMS OF SUCH LICENSE AGREEMENT. + */ +package com.snowplowanalytics.snowplow.collector.core + +object Rfc6265Cookie { + + // See https://www.ietf.org/rfc/rfc6265.txt + private val allowedChars = Set(0x21.toChar) ++ + Set(0x23.toChar to 0x2b.toChar: _*) ++ + Set(0x2d.toChar to 0x3a.toChar: _*) ++ + Set(0x3c.toChar to 0x5b.toChar: _*) ++ + Set(0x5d.toChar to 0x7e.toChar: _*) + + // Remove all the sub-parts (between two ';') that contain unauthorized characters + def parse(rawCookie: String): Option[String] = + rawCookie.replaceAll(" ", "").split(";").filter(_.forall(allowedChars.contains)).mkString(";") match { + case s if s.nonEmpty => Some(s) + case _ => None + } +} 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 9acff5b66..78121d16d 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 @@ -305,7 +305,7 @@ class Service[F[_]: Sync]( case ci"X-Forwarded-For" | ci"X-Real-Ip" | ci"Cookie" if spAnonymous.isDefined => None // FIXME: This is a temporary backport of old akka behaviour we will remove by // adapting enrich to support a CIString header names as per RFC7230#Section-3.2 - case ci"Cookie" => Some(s"Cookie: ${h.value}") + case ci"Cookie" => Rfc6265Cookie.parse(h.value).map(c => s"Cookie: $c") case _ => Some(h.toString()) } } diff --git a/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/Rfc6265CookieSpec.scala b/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/Rfc6265CookieSpec.scala new file mode 100644 index 000000000..59950e9a5 --- /dev/null +++ b/core/src/test/scala/com.snowplowanalytics.snowplow.collector.core/Rfc6265CookieSpec.scala @@ -0,0 +1,35 @@ +package com.snowplowanalytics.snowplow.collector.core + +import org.specs2.mutable.Specification + +class Rfc6265CookieSpec extends Specification { + val valid1 = "name=value" + val valid2 = "name1=value2" + val bothValid = s"$valid1;$valid2" + val invalid = "{\"key\": \"value\"}" + + "Rfc6265Cookie.parse" should { + "leave a valid cookie as is" in { + Rfc6265Cookie.parse(valid1) must beSome(valid1) + Rfc6265Cookie.parse(bothValid) must beSome(bothValid) + } + + "remove whitespaces" in { + Rfc6265Cookie.parse(s" $valid1 ") must beSome(valid1) + Rfc6265Cookie.parse("name = value") must beSome(valid1) + } + + "remove invalid parts" in { + Rfc6265Cookie.parse(s"$invalid;$valid1;$valid2") must beSome(bothValid) + Rfc6265Cookie.parse(s"$valid1;$invalid;$valid2") must beSome(bothValid) + Rfc6265Cookie.parse(s"$valid1;$valid2;$invalid") must beSome(bothValid) + } + + "return None if no valid part is left" in { + Rfc6265Cookie.parse(invalid) must beNone + Rfc6265Cookie.parse(s";$invalid;") must beNone + Rfc6265Cookie.parse(";") must beNone + Rfc6265Cookie.parse(";;") must beNone + } + } +} 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 164b30cf5..a40f1ba52 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 @@ -249,7 +249,7 @@ class ServiceSpec extends Specification { "Content-Type: application/json", "X-Forwarded-For: 192.0.2.3", "Access-Control-Allow-Credentials: true", - "Cookie: cookie=value; sp=dfdb716e-ecf9-4d00-8b10-44edfbc8a108", + "Cookie: cookie=value;sp=dfdb716e-ecf9-4d00-8b10-44edfbc8a108", "image/gif" ).asJava e.contentType shouldEqual "image/gif"