Skip to content

Commit

Permalink
Allow to configure HSTS maxAge
Browse files Browse the repository at this point in the history
  • Loading branch information
stanch committed Jan 16, 2024
1 parent dad7cc3 commit fd503bf
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 42 deletions.
5 changes: 4 additions & 1 deletion src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"type": "fixed"
"size": 4
}
"sendHstsHeader": false
"hsts": {
"enable": false
"maxAge": "365 days"
}
}

"database" {
Expand Down
17 changes: 11 additions & 6 deletions src/main/scala/com/snowplowanalytics/iglu/server/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,18 @@ package com.snowplowanalytics.iglu.server

import java.nio.file.Path
import java.util.UUID

import cats.implicits._

import com.monovore.decline._

import io.circe.{Encoder, Json, JsonObject}
import io.circe.syntax._
import io.circe.generic.semiauto._

import pureconfig._
import pureconfig.generic.ProductHint
import pureconfig.generic.semiauto._
import pureconfig.module.http4s._
import migrations.MigrateFrom

import scala.concurrent.duration.FiniteDuration

import generated.BuildInfo.version

/**
Expand Down Expand Up @@ -222,12 +217,20 @@ object Config {
idleTimeout: Option[FiniteDuration],
maxConnections: Option[Int],
threadPool: ThreadPool,
sendHstsHeader: Boolean
hsts: Config.Hsts
)

implicit val httpConfigCirceEncoder: Encoder[Http] =
deriveEncoder[Http]

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

implicit val hstsConfigCirceEncoder: Encoder[Hsts] =
deriveEncoder[Hsts]

implicit val pureWebhookReader: ConfigReader[Webhook] = ConfigReader.fromCursor { cur =>
for {
objCur <- cur.asObjectCursor
Expand Down Expand Up @@ -263,6 +266,8 @@ object Config {

implicit val pureHttpReader: ConfigReader[Http] = deriveReader[Http]

implicit val pureHstsReader: ConfigReader[Hsts] = deriveReader[Hsts]

implicit val pureWebhooksReader: ConfigReader[List[Webhook]] = ConfigReader.fromCursor { cur =>
for {
objCur <- cur.asObjectCursor
Expand Down
23 changes: 9 additions & 14 deletions src/main/scala/com/snowplowanalytics/iglu/server/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,37 @@ package com.snowplowanalytics.iglu.server

import java.util.concurrent.{ExecutorService, Executors}
import java.util.UUID

import scala.concurrent.duration._
import scala.concurrent.ExecutionContext

import cats.data.Kleisli
import cats.effect.{Blocker, ContextShift, ExitCase, ExitCode, IO, Resource, Sync, Timer}
import cats.effect.concurrent.Ref

import io.circe.syntax._

import org.typelevel.log4cats.slf4j.Slf4jLogger

import fs2.Stream
import fs2.concurrent.SignallingRef

import org.http4s.{Headers, HttpApp, HttpRoutes, MediaType, Method, Request, Response, Status}
import org.http4s.headers.`Content-Type`
import org.http4s.headers.{`Content-Type`, `Strict-Transport-Security`}
import org.http4s.client.blaze.BlazeClientBuilder
import org.http4s.server.Router
import org.http4s.server.blaze.BlazeServerBuilder
import org.http4s.server.middleware.{AutoSlash, CORS, HSTS, Logger}
import org.http4s.syntax.string._
import org.http4s.server.{defaults => Http4sDefaults}
import org.http4s.util.{CaseInsensitiveString => CIString}

import org.http4s.rho.{AuthedContext, RhoMiddleware}
import org.http4s.rho.bits.PathAST.{PathMatch, TypedPath}
import org.http4s.rho.swagger.syntax.{io => ioSwagger}
import org.http4s.rho.swagger.models.{ApiKeyAuthDefinition, In, Info, SecurityRequirement}
import org.http4s.rho.swagger.SwaggerMetadata

import doobie.implicits._
import doobie.util.transactor.Transactor

import com.snowplowanalytics.iglu.server.migrations.{Bootstrap, MigrateFrom}
import com.snowplowanalytics.iglu.server.codecs.Swagger
import com.snowplowanalytics.iglu.server.middleware.{BadRequestHandler, CachingMiddleware}
import com.snowplowanalytics.iglu.server.model.{IgluResponse, Permission}
import com.snowplowanalytics.iglu.server.storage.Storage
import com.snowplowanalytics.iglu.server.service._

import generated.BuildInfo.version

object Server {
Expand Down Expand Up @@ -107,14 +97,19 @@ object Server {
swaggerConfig: Config.Swagger,
blocker: Blocker,
isHealthy: IO[Boolean],
sendHstsHeader: Boolean
hsts: Config.Hsts
)(implicit cs: ContextShift[IO]): HttpApp[IO] = {
val serverRoutes =
httpRoutes(storage, superKey, debug, patchesAllowed, webhook, cache, swaggerConfig, blocker, isHealthy)
val server = Kleisli[IO, Request[IO], Response[IO]](req => Router(serverRoutes: _*).run(req).getOrElse(NotFound))
if (sendHstsHeader) HSTS(server) else server
hstsMiddleware(hsts)(server)
}

def hstsMiddleware(hsts: Config.Hsts): HttpApp[IO] => HttpApp[IO] =
if (hsts.enable)
HSTS(_, `Strict-Transport-Security`.unsafeFromDuration(hsts.maxAge))
else identity

def httpRoutes(
storage: Storage[IO],
superKey: Option[UUID],
Expand Down Expand Up @@ -194,7 +189,7 @@ object Server {
config.swagger,
blocker,
isHealthy,
config.repoServer.sendHstsHeader
config.repoServer.hsts
)
)
.withIdleTimeout(config.repoServer.idleTimeout.getOrElse(Http4sDefaults.IdleTimeout))
Expand Down
5 changes: 4 additions & 1 deletion src/test/resources/valid-dummy-config.conf
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ repoServer {
type = "fixed"
size = 2
}
sendHstsHeader = true
hsts {
enable = true
maxAge = "365 days"
}
}

# 'postgres' contains configuration options for the postgre instance the server is using
Expand Down
15 changes: 10 additions & 5 deletions src/test/scala/com/snowplowanalytics/iglu/server/ConfigSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class ConfigSpec extends org.specs2.Specification {
parse minimal config with dummy DB from file $e5
"""

val noHsts = Config.Hsts(enable = false, maxAge = 365.days)

def e1 = {
val input = "--config foo.hocon"
val expected = Config.ServerCommand.Run(Some(Paths.get("foo.hocon")))
Expand Down Expand Up @@ -71,7 +73,7 @@ class ConfigSpec extends org.specs2.Specification {
pool,
false
),
Config.Http("0.0.0.0", 8080, Some(10.seconds), None, Config.ThreadPool.Global, sendHstsHeader = false),
Config.Http("0.0.0.0", 8080, Some(10.seconds), None, Config.ThreadPool.Global, noHsts),
true,
true,
List(
Expand Down Expand Up @@ -99,7 +101,7 @@ class ConfigSpec extends org.specs2.Specification {
val expected =
Config(
Config.StorageConfig.Dummy,
Config.Http("0.0.0.0", 8080, None, None, Config.ThreadPool.Fixed(2), sendHstsHeader = true),
Config.Http("0.0.0.0", 8080, None, None, Config.ThreadPool.Fixed(2), Config.Hsts(true, 365.days)),
true,
false,
Nil,
Expand Down Expand Up @@ -127,7 +129,7 @@ class ConfigSpec extends org.specs2.Specification {
Config.StorageConfig.ConnectionPool.NoPool(Config.ThreadPool.Fixed(2)),
true
),
Config.Http("0.0.0.0", 8080, None, None, Config.ThreadPool.Global, sendHstsHeader = false),
Config.Http("0.0.0.0", 8080, None, None, Config.ThreadPool.Global, noHsts),
true,
true,
List(
Expand Down Expand Up @@ -168,7 +170,10 @@ class ConfigSpec extends org.specs2.Specification {
"idleTimeout": null,
"maxConnections": null,
"threadPool": "global",
"sendHstsHeader": false
"hsts": {
"enable": false,
"maxAge": "365 days"
}
},
"debug" : true,
"patchesAllowed" : true,
Expand Down Expand Up @@ -223,7 +228,7 @@ class ConfigSpec extends org.specs2.Specification {
pool,
true
),
Config.Http("0.0.0.0", 8080, None, None, Config.ThreadPool.Fixed(4), sendHstsHeader = false),
Config.Http("0.0.0.0", 8080, None, None, Config.ThreadPool.Fixed(4), noHsts),
false,
false,
Nil,
Expand Down
36 changes: 21 additions & 15 deletions src/test/scala/com/snowplowanalytics/iglu/server/ServerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -161,21 +161,24 @@ class ServerSpec extends Specification {

val expected = Some(
List(
`Strict-Transport-Security`.unsafeFromLong(365 * 24 * 3600, includeSubDomains = true),
`Strict-Transport-Security`.unsafeFromLong(365 * 24 * 3600, includeSubDomains = true),
`Strict-Transport-Security`.unsafeFromLong(365 * 24 * 3600, includeSubDomains = true),
`Strict-Transport-Security`.unsafeFromLong(365 * 24 * 3600, includeSubDomains = true)
`Strict-Transport-Security`.unsafeFromLong(180 * 24 * 3600, includeSubDomains = true),
`Strict-Transport-Security`.unsafeFromLong(180 * 24 * 3600, includeSubDomains = true),
`Strict-Transport-Security`.unsafeFromLong(180 * 24 * 3600, includeSubDomains = true),
`Strict-Transport-Security`.unsafeFromLong(180 * 24 * 3600, includeSubDomains = true)
)
)

def action(hsts: Boolean) =
def action(hsts: Config.Hsts) =
for {
responses <- ServerSpec.executeRequests(reqs, hsts)
results = responses.traverse(res => res.headers.get(`Strict-Transport-Security`))
} yield results

val on = execute(action(hsts = true), hsts = true) must beEqualTo(expected)
val off = execute(action(hsts = false), hsts = false) must beEqualTo(None)
val hstsOn = Config.Hsts(enable = true, 180.days)
val hstsOff = Config.Hsts(enable = false, 365.days)

val on = execute(action(hstsOn), hstsOn) must beEqualTo(expected)
val off = execute(action(hstsOff), hstsOff) must beEqualTo(None)

on.and(off)
}
Expand All @@ -190,7 +193,7 @@ object ServerSpec {
.StorageConfig
.ConnectionPool
.Hikari(None, None, None, None, Config.ThreadPool.Cached, Config.ThreadPool.Cached)
def httpConfig(hsts: Boolean) = Config.Http("0.0.0.0", 8080, None, None, Config.ThreadPool.Cached, hsts)
def httpConfig(hsts: Config.Hsts) = Config.Http("0.0.0.0", 8080, None, None, Config.ThreadPool.Cached, hsts)
val storageConfig =
Config
.StorageConfig
Expand All @@ -205,30 +208,33 @@ object ServerSpec {
dbPoolConfig,
true
)
def config(hsts: Boolean) =
def config(hsts: Config.Hsts) =
Config(storageConfig, httpConfig(hsts), false, true, Nil, Config.Swagger(""), None, 10.seconds, false)

private def runServer(hsts: Boolean) = Server.buildServer(config(hsts), IO.pure(true)).flatMap(_.resource)
private val client = BlazeClientBuilder[IO](global).resource
private def env(hsts: Boolean) = client <* runServer(hsts)
private def runServer(hsts: Config.Hsts) = Server.buildServer(config(hsts), IO.pure(true)).flatMap(_.resource)
private val client = BlazeClientBuilder[IO](global).resource
private def env(hsts: Config.Hsts) = client <* runServer(hsts)

/** Execute requests against fresh server (only one execution per test is allowed) */
def executeRequests(requests: List[Request[IO]], hsts: Boolean = false): IO[List[Response[IO]]] = {
def executeRequests(
requests: List[Request[IO]],
hsts: Config.Hsts = Config.Hsts(false, 365.days)
): IO[List[Response[IO]]] = {
val r = requests.map { r =>
if (r.uri.host.isDefined) r
else r.withUri(uri"http://localhost:8080/api/schemas".addPath(r.uri.path))
}
env(hsts).use(client => r.traverse(client.run(_).use(IO.pure)))
}

def specification(hsts: Boolean) =
def specification(hsts: Config.Hsts) =
Resource.make {
Storage.initialize[IO](storageConfig).use(s => s.asInstanceOf[Postgres[IO]].drop) *>
Server.setup(ServerSpec.config(hsts), None).void *>
Storage.initialize[IO](storageConfig).use(_.addPermission(InMemory.DummySuperKey, Permission.Super))
}(_ => Storage.initialize[IO](storageConfig).use(s => s.asInstanceOf[Postgres[IO]].drop))

def execute[A](action: IO[A], hsts: Boolean = false): A =
def execute[A](action: IO[A], hsts: Config.Hsts = Config.Hsts(false, 365.days)): A =
specification(hsts).use(_ => action).unsafeRunSync()

case class TestResponse[E](status: Int, body: E)
Expand Down

0 comments on commit fd503bf

Please sign in to comment.