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

Add natchez-http4s-mtl: server middleware with Local[F, Span[F]] semantics #71

Merged
merged 7 commits into from
Oct 28, 2024
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p modules/http4s/.jvm/target modules/http4s/.js/target modules/http4s/.native/target project/target
run: mkdir -p modules/mtl/.native/target modules/http4s/.jvm/target modules/http4s/.js/target modules/core/.native/target modules/core/.js/target modules/core/.jvm/target modules/http4s/.native/target modules/mtl/.js/target modules/mtl/.jvm/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar modules/http4s/.jvm/target modules/http4s/.js/target modules/http4s/.native/target project/target
run: tar cf targets.tar modules/mtl/.native/target modules/http4s/.jvm/target modules/http4s/.js/target modules/core/.native/target modules/core/.js/target modules/core/.jvm/target modules/http4s/.native/target modules/mtl/.js/target modules/mtl/.jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
4 changes: 4 additions & 0 deletions .jvmopts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

-Xms1G
-Xmx4G
-XX:+UseG1GC
16 changes: 16 additions & 0 deletions .mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ pull_request_rules:
- status-success=Build and Test (ubuntu-latest, 3, temurin@17, rootNative)
actions:
merge: {}
- name: Label core PRs
conditions:
- files~=^modules/core/
actions:
label:
add:
- core
remove: []
- name: Label docs PRs
conditions:
- files~=^modules/docs/
Expand All @@ -47,3 +55,11 @@ pull_request_rules:
add:
- http4s
remove: []
- name: Label mtl PRs
conditions:
- files~=^modules/mtl/
actions:
label:
add:
- mtl
remove: []
40 changes: 37 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ val scala3Version = "3.3.4"
val slf4jVersion = "2.0.16"
val munitCEVersion = "2.0.0"
val scalacheckEffectVersion = "2.0.0-M2"
val catsMtlVersion = "1.4.0"

ThisBuild / organization := "org.tpolecat"
ThisBuild / tlSonatypeUseLegacyHost := false
ThisBuild / sonatypeCredentialHost := xerial.sbt.Sonatype.sonatypeLegacy
ThisBuild / licenses := Seq(("MIT", url("http://opensource.org/licenses/MIT")))
ThisBuild / homepage := Some(url("https://github.com/tpolecat/natchez-http4s"))
ThisBuild / developers := List(
Expand Down Expand Up @@ -46,11 +47,27 @@ ThisBuild / scalaVersion := scala213Version
ThisBuild / crossScalaVersions := Seq(scala212Version, scala213Version, scala3Version)

lazy val root = tlCrossRootProject.aggregate(
core,
http4s,
mtl,
examples,
docs
)

lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("modules/core"))
.settings(commonSettings)
.settings(
name := "natchez-http4s-core",
description := "Natchez middleware for http4s.",
libraryDependencies ++= Seq(
"org.tpolecat" %%% "natchez-core" % natchezVersion,
"org.http4s" %%% "http4s-core" % http4sVersion,
),
tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.6.1").toMap
)

lazy val http4s = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("modules/http4s"))
Expand All @@ -59,13 +76,30 @@ lazy val http4s = crossProject(JSPlatform, JVMPlatform, NativePlatform)
name := "natchez-http4s",
description := "Natchez middleware for http4s.",
libraryDependencies ++= Seq(
"org.tpolecat" %%% "natchez-core" % natchezVersion,
"org.http4s" %%% "http4s-core" % http4sVersion,
"org.http4s" %%% "http4s-client" % http4sVersion,
"org.http4s" %%% "http4s-server" % http4sVersion,
"org.tpolecat" %%% "natchez-testkit" % natchezVersion % Test,
)
)
.dependsOn(core)

lazy val mtl = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("modules/mtl"))
.settings(commonSettings)
.settings(
name := "natchez-http4s-mtl",
description := "Natchez middleware for http4s with cats-mtl Local[F, Span[F]] semantics.",
libraryDependencies ++= Seq(
"org.tpolecat" %%% "natchez-mtl" % natchezVersion,
"org.http4s" %%% "http4s-core" % http4sVersion,
"org.http4s" %%% "http4s-server" % http4sVersion,
"org.typelevel" %%% "cats-mtl" % catsMtlVersion,
"org.tpolecat" %%% "natchez-testkit" % natchezVersion % Test,
),
tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.6.1").toMap
)
.dependsOn(core, http4s % "test->test")

lazy val examples = project
.in(file("modules/examples"))
Expand Down
29 changes: 29 additions & 0 deletions modules/core/src/main/scala/natchez/http4s/DefaultValues.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2021 by Rob Norris
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package natchez
package http4s

import org.http4s.headers.*
import org.typelevel.ci.*

private[natchez] object DefaultValues {
private[natchez] val ExcludedHeaders: Set[CIString] = {
val payload = Set(
`Content-Length`.name,
ci"Content-Type",
`Content-Range`.name,
ci"Trailer",
`Transfer-Encoding`.name,
)

val security = Set(
Authorization.name,
Cookie.name,
`Set-Cookie`.name,
)

payload ++ security
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
package natchez.http4s.syntax

import cats.~>
import cats.data.{ Kleisli, OptionT }
import cats.data.{Kleisli, OptionT}
import cats.data.Kleisli.applyK
import cats.effect.MonadCancel
import natchez.{ EntryPoint, Kernel, Span }
import natchez.{EntryPoint, Kernel, Span}
import org.http4s.HttpRoutes
import cats.effect.Resource
import natchez.http4s.DefaultValues
import org.http4s.server.websocket.WebSocketBuilder2
import org.typelevel.ci.CIString

Expand Down Expand Up @@ -120,26 +121,7 @@ trait EntryPointOps[F[_]] { outer =>
}

object EntryPointOps {
val ExcludedHeaders: Set[CIString] = {
import org.http4s.headers._
import org.typelevel.ci._

val payload = Set(
`Content-Length`.name,
ci"Content-Type",
`Content-Range`.name,
ci"Trailer",
`Transfer-Encoding`.name,
)

val security = Set(
Authorization.name,
Cookie.name,
`Set-Cookie`.name,
)

payload ++ security
}
val ExcludedHeaders: Set[CIString] = DefaultValues.ExcludedHeaders
}

trait ToEntryPointOps {
Expand Down
21 changes: 14 additions & 7 deletions modules/http4s/src/test/scala/natchez/http4s/InMemory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ package http4s
import cats.data.Kleisli
import cats.effect.{IO, MonadCancelThrow}
import munit.CatsEffectSuite
import org.typelevel.ci.*

trait InMemorySuite extends CatsEffectSuite {
trait InMemorySuite
extends CatsEffectSuite
with Arbitraries {
type Lineage = InMemory.Lineage
val Lineage = InMemory.Lineage
val Lineage: InMemory.Lineage.type = InMemory.Lineage
type NatchezCommand = InMemory.NatchezCommand
val NatchezCommand = InMemory.NatchezCommand
val NatchezCommand: InMemory.NatchezCommand.type = InMemory.NatchezCommand

trait TraceTest {
def program[F[_]: MonadCancelThrow: Trace]: F[Unit]
def expectedHistory: List[(Lineage, NatchezCommand)]
}

def traceTest(name: String, tt: TraceTest) = {
def traceTest(name: String, tt: TraceTest): Unit = {
test(s"$name - Kleisli")(
testTraceKleisli(tt.program[Kleisli[IO, Span[IO], *]](implicitly, _), tt.expectedHistory)
)
Expand All @@ -30,7 +33,7 @@ trait InMemorySuite extends CatsEffectSuite {
def testTraceKleisli(
traceProgram: Trace[Kleisli[IO, Span[IO], *]] => Kleisli[IO, Span[IO], Unit],
expectedHistory: List[(Lineage, NatchezCommand)]
) = testTrace[Kleisli[IO, Span[IO], *]](
): IO[Unit] = testTrace[Kleisli[IO, Span[IO], *]](
traceProgram,
root => IO.pure(Trace[Kleisli[IO, Span[IO], *]] -> (k => k.run(root))),
expectedHistory
Expand All @@ -39,13 +42,13 @@ trait InMemorySuite extends CatsEffectSuite {
def testTraceIoLocal(
traceProgram: Trace[IO] => IO[Unit],
expectedHistory: List[(Lineage, NatchezCommand)]
) = testTrace[IO](traceProgram, Trace.ioTrace(_).map(_ -> identity), expectedHistory)
): IO[Unit] = testTrace[IO](traceProgram, Trace.ioTrace(_).map(_ -> identity), expectedHistory)

def testTrace[F[_]](
traceProgram: Trace[F] => F[Unit],
makeTraceAndResolver: Span[IO] => IO[(Trace[F], F[Unit] => IO[Unit])],
expectedHistory: List[(Lineage, NatchezCommand)]
) =
): IO[Unit] =
InMemory.EntryPoint.create[IO].flatMap { ep =>
val traced = ep.root("root").use { r =>
makeTraceAndResolver(r).flatMap { case (traceInstance, resolve) =>
Expand All @@ -56,4 +59,8 @@ trait InMemorySuite extends CatsEffectSuite {
assertEquals(history.toList, expectedHistory)
}
}

val CustomHeaderName = ci"X-Custom-Header"
val CorrelationIdName = ci"X-Correlation-Id"

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,84 +8,22 @@ import cats.Monad
import cats.data.{Chain, Kleisli}
import cats.effect.{IO, MonadCancelThrow, Resource}
import munit.ScalaCheckEffectSuite
import natchez.Span.Options.SpanCreationPolicy
import natchez.*
import natchez.Span.SpanKind
import natchez.TraceValue.StringValue
import natchez.http4s.syntax.entrypoint.*
import natchez.*
import org.http4s.*
import org.http4s.client.Client
import org.http4s.dsl.request.*
import org.http4s.headers.*
import org.http4s.syntax.literals.*
import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.effect.PropF
import org.scalacheck.{Arbitrary, Gen}
import org.typelevel.ci.*

class NatchezMiddlewareSuite
extends InMemorySuite
with ScalaCheckEffectSuite {

private val CustomHeaderName = ci"X-Custom-Header"
private val CorrelationIdName = ci"X-Correlation-Id"

private implicit val arbTraceValue: Arbitrary[TraceValue] = Arbitrary {
Gen.oneOf(
arbitrary[String].map(TraceValue.StringValue(_)),
arbitrary[Boolean].map(TraceValue.BooleanValue(_)),
arbitrary[Number].map(TraceValue.NumberValue(_)),
)
}

private implicit val arbAttribute: Arbitrary[(String, TraceValue)] = Arbitrary {
for {
key <- arbitrary[String]
value <- arbitrary[TraceValue]
} yield key -> value
}

private implicit val arbCIString: Arbitrary[CIString] = Arbitrary {
Gen.alphaLowerStr.map(CIString(_))
}

private implicit val arbKernel: Arbitrary[Kernel] = Arbitrary {
arbitrary[Map[CIString, String]].map(Kernel(_))
}

private implicit val arbSpanCreationPolicy: Arbitrary[SpanCreationPolicy] = Arbitrary {
Gen.oneOf(SpanCreationPolicy.Default, SpanCreationPolicy.Coalesce, SpanCreationPolicy.Suppress)
}

private implicit val arbSpanKind: Arbitrary[SpanKind] = Arbitrary {
Gen.oneOf(
SpanKind.Internal,
SpanKind.Client,
SpanKind.Server,
SpanKind.Producer,
SpanKind.Consumer,
)
}

private implicit val arbSpanOptions: Arbitrary[Span.Options] = Arbitrary {
for {
parentKernel <- arbitrary[Option[Kernel]]
spanCreationPolicy <- arbitrary[SpanCreationPolicy]
spanKind <- arbitrary[SpanKind]
links <- arbitrary[List[Kernel]].map(Chain.fromSeq)
} yield {
links.foldLeft {
parentKernel.foldLeft {
Span
.Options
.Defaults
.withSpanKind(spanKind)
.withSpanCreationPolicy(spanCreationPolicy)
}(_.withParentKernel(_))
}(_.withLink(_))
}
}

test("do not leak security and payload headers to the client request") {
val headers = Headers(
// security
Expand Down
Loading