Skip to content

Commit

Permalink
Add an option to send HSTS header (fix #408)
Browse files Browse the repository at this point in the history
  • Loading branch information
stanch authored and peel committed Jan 24, 2024
1 parent 6d0abf2 commit b08005a
Show file tree
Hide file tree
Showing 20 changed files with 116 additions and 10 deletions.
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

0 comments on commit b08005a

Please sign in to comment.