Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 3.1.0 #410

Merged
merged 2 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Release 3.0.1 (2024-01-10)
--------------------------
Add an option to send HSTS header (#408)

Release 3.0.1 (2024-01-10)
--------------------------
Remove unnecessary argument (#407)
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@
port = 443
}

hsts {
enable = false
maxAge = 365 days
}

networking {
maxConnections = 1024
idleTimeout = 610 seconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ case class Config[+SinkConfig](
monitoring: Config.Monitoring,
telemetry: Config.Telemetry,
ssl: Config.SSL,
hsts: Config.HSTS,
networking: Config.Networking,
enableDefaultRedirect: Boolean,
redirectDomains: Set[String],
Expand Down Expand Up @@ -133,6 +134,11 @@ object Config {
port: Int
)

case class HSTS(
enable: Boolean,
maxAge: FiniteDuration
)

final case class Telemetry(
// General params
disable: Boolean,
Expand Down Expand Up @@ -188,6 +194,7 @@ object Config {
implicit val metrics = deriveDecoder[Metrics]
implicit val monitoring = deriveDecoder[Monitoring]
implicit val ssl = deriveDecoder[SSL]
implicit val hsts = deriveDecoder[HSTS]
implicit val telemetry = deriveDecoder[Telemetry]
implicit val networking = deriveDecoder[Networking]
implicit val sinkConfig = newDecoder[SinkConfig].or(legacyDecoder[SinkConfig])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import cats.implicits._
import com.avast.datadog4s.api.Tag
import com.avast.datadog4s.extension.http4s.DatadogMetricsOps
import com.avast.datadog4s.{StatsDMetricFactory, StatsDMetricFactoryConfig}
import org.http4s.HttpRoutes
import org.http4s.{HttpApp, HttpRoutes}
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.headers.`Strict-Transport-Security`
import org.http4s.server.Server
import org.http4s.server.middleware.Metrics
import org.http4s.server.middleware.{HSTS, Metrics}
import org.typelevel.log4cats.Logger
import org.typelevel.log4cats.slf4j.Slf4jLogger

Expand All @@ -33,12 +34,13 @@ object HttpServer {
routes: HttpRoutes[F],
port: Int,
secure: Boolean,
hsts: Config.HSTS,
networking: Config.Networking,
metricsConfig: Config.Metrics
): Resource[F, Server] =
for {
withMetricsMiddleware <- createMetricsMiddleware(routes, metricsConfig)
server <- buildBlazeServer[F](withMetricsMiddleware, port, secure, networking)
server <- buildBlazeServer[F](withMetricsMiddleware, port, secure, hsts, networking)
} yield server

private def createMetricsMiddleware[F[_]: Async](
Expand All @@ -60,16 +62,22 @@ object HttpServer {
StatsDMetricFactoryConfig(Some(metricsConfig.statsd.prefix), server, defaultTags = tags)
}

private[core] def hstsMiddleware[F[_]: Async](hsts: Config.HSTS, routes: HttpApp[F]): HttpApp[F] =
if (hsts.enable)
HSTS(routes, `Strict-Transport-Security`.unsafeFromDuration(hsts.maxAge))
else routes

private def buildBlazeServer[F[_]: Async](
routes: HttpRoutes[F],
port: Int,
secure: Boolean,
hsts: Config.HSTS,
networking: Config.Networking
): Resource[F, Server] =
Resource.eval(Logger[F].info("Building blaze server")) >>
BlazeServerBuilder[F]
.bindSocketAddress(new InetSocketAddress(port))
.withHttpApp(routes.orNotFound)
.withHttpApp(hstsMiddleware(hsts, routes.orNotFound))
.withIdleTimeout(networking.idleTimeout)
.withMaxConnections(networking.maxConnections)
.cond(secure, _.withSslContext(SSLContext.getDefault))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ object Run {
).value,
if (config.ssl.enable) config.ssl.port else config.port,
config.ssl.enable,
config.hsts,
config.networking,
config.monitoring.metrics
)
Expand Down
5 changes: 5 additions & 0 deletions core/src/test/resources/test-config-new-style.hocon
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ collector {
ssl {
enable = true
}

hsts {
enable = true
maxAge = 180 days
}
}
5 changes: 5 additions & 0 deletions core/src/test/resources/test-config-old-style.hocon
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ collector {
ssl {
enable = true
}

hsts {
enable = true
maxAge = 180 days
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import cats.effect.testing.specs2.CatsEffect
import com.snowplowanalytics.snowplow.collector.core.Config.Buffer
import io.circe.generic.semiauto._

import scala.concurrent.duration.DurationInt

class ConfigParserSpec extends Specification with CatsEffect {

"Loading the configuration" should {
Expand Down Expand Up @@ -53,6 +55,7 @@ class ConfigParserSpec extends Specification with CatsEffect {
paths = Map.empty[String, String],
streams = expectedStreams,
ssl = TestUtils.testConfig.ssl.copy(enable = true),
hsts = TestUtils.testConfig.hsts.copy(enable = true, 180.days),
license = Config.License(false)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package com.snowplowanalytics.snowplow.collector.core

import scala.collection.mutable.ListBuffer
import cats.data.NonEmptyList

import scala.collection.mutable.ListBuffer
import org.specs2.mutable.Specification

import cats.effect.IO
import cats.effect.unsafe.implicits.global

import org.http4s.implicits._
import org.http4s._
import org.http4s.headers._
import org.http4s.Status._

import fs2.{Stream, text}
import org.typelevel.ci.CIString

import scala.concurrent.duration.DurationInt

class RoutesSpec extends Specification {

Expand Down Expand Up @@ -67,12 +68,14 @@ class RoutesSpec extends Specification {
def createTestServices(
enabledDefaultRedirect: Boolean = true,
enableRootResponse: Boolean = false,
enableCrossdomainTracking: Boolean = false
enableCrossdomainTracking: Boolean = false,
enableHsts: Boolean = false
) = {
val service = new TestService()
val routes =
new Routes(enabledDefaultRedirect, enableRootResponse, enableCrossdomainTracking, service).value.orNotFound
(service, routes)
val routesWithHsts = HttpServer.hstsMiddleware(Config.HSTS(enableHsts, 180.days), routes)
(service, routesWithHsts)
}

"The collector route" should {
Expand All @@ -97,6 +100,30 @@ class RoutesSpec extends Specification {
test(uri"/p3/p4")
}

"respond with an HSTS header when HSTS is enabled" in {
val (_, routesHstsOn) = createTestServices(enableHsts = true)
val (_, routesHstsOff) = createTestServices(enableHsts = false)
def testHstsOn(uri: Uri) = {
val request = Request[IO](method = Method.GET, uri = uri)
val response = routesHstsOn.run(request).unsafeRunSync()
response.headers.get(CIString("Strict-Transport-Security")) shouldEqual
Some(
NonEmptyList.of(Header.Raw(CIString("Strict-Transport-Security"), "max-age=15552000; includeSubDomains"))
)
}
def testHstsOff(uri: Uri) = {
val request = Request[IO](method = Method.GET, uri = uri)
val response = routesHstsOff.run(request).unsafeRunSync()
response.headers.get(CIString("Strict-Transport-Security")) shouldEqual None
}
testHstsOn(uri"/i")
testHstsOn(uri"/health")
testHstsOn(uri"/give-me-404")
testHstsOff(uri"/i")
testHstsOff(uri"/health")
testHstsOff(uri"/give-me-404")
}

"respond to the post cookie route with the cookie response" in {
val (collectorService, routes) = createTestServices()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ object TestUtils {
false,
443
),
hsts = HSTS(
false,
365.days
),
networking = Networking(
1024,
610.seconds
Expand Down
6 changes: 6 additions & 0 deletions examples/config.kafka.extended.hocon
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ collector {
port = 443
}

# optional HSTS configuration
hsts {
enable = false
maxAge = 365 days
}

# The collector responds with a cookie to requests with a path that matches the 'vendor/version' protocol.
# The expected values are:
# - com.snowplowanalytics.snowplow/tp2 for Tracker Protocol 2
Expand Down
6 changes: 6 additions & 0 deletions examples/config.kinesis.extended.hocon
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ collector {
port = 443
}

# optional HSTS configuration
hsts {
enable = false
maxAge = 365 days
}

# The collector responds with a cookie to requests with a path that matches the 'vendor/version' protocol.
# The expected values are:
# - com.snowplowanalytics.snowplow/tp2 for Tracker Protocol 2
Expand Down
6 changes: 6 additions & 0 deletions examples/config.nsq.extended.hocon
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ collector {
port = 443
}

# optional HSTS configuration
hsts {
enable = false
maxAge = 365 days
}

# The collector responds with a cookie to requests with a path that matches the 'vendor/version' protocol.
# The expected values are:
# - com.snowplowanalytics.snowplow/tp2 for Tracker Protocol 2
Expand Down
6 changes: 6 additions & 0 deletions examples/config.pubsub.extended.hocon
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ collector {
port = 443
}

# optional HSTS configuration
hsts {
enable = false
maxAge = 365 days
}

# The collector responds with a cookie to requests with a path that matches the 'vendor/version' protocol.
# The expected values are:
# - com.snowplowanalytics.snowplow/tp2 for Tracker Protocol 2
Expand Down
6 changes: 6 additions & 0 deletions examples/config.sqs.extended.hocon
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ collector {
port = 443
}

# optional HSTS configuration
hsts {
enable = false
maxAge = 365 days
}

# The collector responds with a cookie to requests with a path that matches the 'vendor/version' protocol.
# The expected values are:
# - com.snowplowanalytics.snowplow/tp2 for Tracker Protocol 2
Expand Down
6 changes: 6 additions & 0 deletions examples/config.stdout.extended.hocon
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ collector {
port = 443
}

# optional HSTS configuration
hsts {
enable = false
maxAge = 365 days
}

# The collector responds with a cookie to requests with a path that matches the 'vendor/version' protocol.
# The expected values are:
# - com.snowplowanalytics.snowplow/tp2 for Tracker Protocol 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ object KafkaConfigSpec {
)
),
ssl = Config.SSL(enable = false, redirect = false, port = 443),
hsts = Config.HSTS(enable = false, maxAge = 365.days),
enableDefaultRedirect = false,
redirectDomains = Set.empty,
preTerminationPeriod = 10.seconds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ object KinesisConfigSpec {
)
),
ssl = Config.SSL(enable = false, redirect = false, port = 443),
hsts = Config.HSTS(enable = false, maxAge = 365.days),
enableDefaultRedirect = false,
redirectDomains = Set.empty,
preTerminationPeriod = 10.seconds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ object NsqConfigSpec {
)
),
ssl = Config.SSL(enable = false, redirect = false, port = 443),
hsts = Config.HSTS(enable = false, maxAge = 365.days),
enableDefaultRedirect = false,
redirectDomains = Set.empty,
preTerminationPeriod = 10.seconds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ object ConfigSpec {
)
),
ssl = Config.SSL(enable = false, redirect = false, port = 443),
hsts = Config.HSTS(enable = false, maxAge = 365.days),
enableDefaultRedirect = false,
redirectDomains = Set.empty,
preTerminationPeriod = 10.seconds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ object SqsConfigSpec {
)
),
ssl = Config.SSL(enable = false, redirect = false, port = 443),
hsts = Config.HSTS(enable = false, maxAge = 365.days),
enableDefaultRedirect = false,
redirectDomains = Set.empty,
preTerminationPeriod = 10.seconds,
Expand Down
Loading