From 1a602249a0fd06c6e80afec695fcdbfa076f0f0a Mon Sep 17 00:00:00 2001 From: Artur Baruk Date: Wed, 8 Jan 2025 11:20:06 +0000 Subject: [PATCH 1/7] [MCC-1289760]: add mauth request sender for http4s --- build.sbt | 10 ++ .../mauth/SttpHttp4sMAuthRequestSender.scala | 13 +++ .../SttpHttp4sMAuthRequestSenderSpec.scala | 107 ++++++++++++++++++ project/BuildSettings.scala | 2 +- project/Dependencies.scala | 7 +- 5 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 modules/mauth-sender-sttp-http4s-http/src/main/scala/com/mdsol/mauth/SttpHttp4sMAuthRequestSender.scala create mode 100644 modules/mauth-sender-sttp-http4s-http/src/test/scala/com/mdsol/mauth/SttpHttp4sMAuthRequestSenderSpec.scala diff --git a/build.sbt b/build.sbt index 9df1892..4d32d81 100644 --- a/build.sbt +++ b/build.sbt @@ -143,6 +143,15 @@ lazy val `mauth-sender-sttp-akka-http` = scalaModuleProject("mauth-sender-sttp-a Dependencies.test(scalaMock, scalaTest, wiremock, sttpAkkaHttpBackend).map(withExclusions) ) +lazy val `mauth-sender-sttp-http4s-http` = scalaModuleProject("mauth-sender-sttp-http4s-http") + .dependsOn(`mauth-signer-sttp`, `mauth-test-utils` % "test") + .settings( + publishSettings, + libraryDependencies ++= + Dependencies.compile(catsEffect, scalaLibCompat, sttp, sttpFs2, scalaLogging).map(withExclusions) ++ + Dependencies.test(scalaMock, http4sEmberClient, scalaTest, wiremock, sttpHttp4sHttpBackend).map(withExclusions) + ) + lazy val `mauth-authenticator` = javaModuleProject("mauth-authenticator") .dependsOn(`mauth-common`) .settings( @@ -212,6 +221,7 @@ lazy val `mauth-jvm-clients` = (project in file(".")) `mauth-signer-sttp`, `mauth-signer-apachehttp`, `mauth-sender-sttp-akka-http`, + `mauth-sender-sttp-http4s-http`, `mauth-test-utils`, `mauth-authenticator-http4s` ) diff --git a/modules/mauth-sender-sttp-http4s-http/src/main/scala/com/mdsol/mauth/SttpHttp4sMAuthRequestSender.scala b/modules/mauth-sender-sttp-http4s-http/src/main/scala/com/mdsol/mauth/SttpHttp4sMAuthRequestSender.scala new file mode 100644 index 0000000..8866572 --- /dev/null +++ b/modules/mauth-sender-sttp-http4s-http/src/main/scala/com/mdsol/mauth/SttpHttp4sMAuthRequestSender.scala @@ -0,0 +1,13 @@ +package com.mdsol.mauth + +import cats.effect.IO +import sttp.client3.{Request, Response, SttpBackend} +import sttp.capabilities.fs2.Fs2Streams + +class SttpHttp4sMAuthRequestSender( + signer: MAuthSttpSigner, + sttpBackend: SttpBackend[IO, Fs2Streams[IO]] +) extends SttpMAuthRequestSender[IO] { + override def send[T](request: Request[T, Any]): IO[Response[T]] = + sttpBackend.send(signer.signSttpRequest(request)) +} diff --git a/modules/mauth-sender-sttp-http4s-http/src/test/scala/com/mdsol/mauth/SttpHttp4sMAuthRequestSenderSpec.scala b/modules/mauth-sender-sttp-http4s-http/src/test/scala/com/mdsol/mauth/SttpHttp4sMAuthRequestSenderSpec.scala new file mode 100644 index 0000000..e807256 --- /dev/null +++ b/modules/mauth-sender-sttp-http4s-http/src/test/scala/com/mdsol/mauth/SttpHttp4sMAuthRequestSenderSpec.scala @@ -0,0 +1,107 @@ +package com.mdsol.mauth + +import cats.effect.IO +import cats.effect.kernel.Resource +import cats.effect.unsafe.implicits.global +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock._ +import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig +import com.github.tomakehurst.wiremock.verification.LoggedRequest +import com.mdsol.mauth.test.utils.TestFixtures +import com.mdsol.mauth.test.utils.TestFixtures._ +import com.mdsol.mauth.util.EpochTimeProvider +import com.mdsol.mauth.util.MAuthKeysHelper.getPrivateKeyFromString +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.scalatest.Inside._ +import org.scalatest.matchers.should.Matchers._ +import org.scalatest.wordspec.AsyncWordSpec +import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll} +import sttp.client3.basicRequest +import sttp.client3.http4s.Http4sBackend +import sttp.model.{MediaType, Uri} + +import java.net.URI +import java.security.Security +import java.util.UUID +import scala.jdk.CollectionConverters._ + +class SttpHttp4sMAuthRequestSenderSpec extends AsyncWordSpec with BeforeAndAfter with BeforeAndAfterAll { + + Security.addProvider(new BouncyCastleProvider) + + val wiremockServer: WireMockServer = new WireMockServer(wireMockConfig.dynamicPort()) + + lazy val requestSender: Resource[IO, SttpHttp4sMAuthRequestSender] = for { + sttpBackend <- Http4sBackend.usingDefaultEmberClientBuilder[IO]() + } yield new SttpHttp4sMAuthRequestSender(v1v2Signer, sttpBackend) + + "correctly send auth signatures and content-type header" in { + val req = basicRequest + .get(Uri(new URI(s"${wiremockServer.baseUrl()}$REQUEST_NORMALIZE_PATH"))) + .body("") + .contentType(MediaType.ApplicationJson) + + requestSender + .use { case sttpSender => + sttpSender.send(req).map { _ => + inside(getRecordedRequests()) { case List(r) => + r.getHeader("content-type") shouldBe "application/json" + r.getHeader(TIME_HEADER_V1) shouldBe EPOCH_TIME + r.getHeader(AUTH_HEADER_V1) shouldBe s"MWS $APP_UUID_V2:$SIGNATURE_NORMALIZE_PATH_V1" + + r.getHeader(TIME_HEADER_V2) shouldBe EPOCH_TIME + r.getHeader(AUTH_HEADER_V2) shouldBe s"MWSV2 $APP_UUID_V2:$SIGNATURE_NORMALIZE_PATH_V2;" + } + } + } + .unsafeToFuture() + } + + "sends a default content type (text/plain UTF-8) when content type not specified" in { + val req = basicRequest.get(Uri(new URI(s"${wiremockServer.baseUrl()}/"))).body("hello") + + requestSender + .use { case sttpSender => + sttpSender.send(req).map { _ => + inside(getRecordedRequests()) { case List(r) => + r.getHeader("content-type") shouldBe "text/plain; charset=UTF-8" + } + } + } + .unsafeToFuture() + } + + private def getRecordedRequests(): List[LoggedRequest] = + wiremockServer.getAllServeEvents.asScala.toList.map(_.getRequest) + + lazy val v1v2Signer: MAuthSttpSigner = { + val epochTimeProvider: EpochTimeProvider = () => EPOCH_TIME.toLong + new MAuthSttpSignerImpl( + UUID.fromString(APP_UUID_V2), + getPrivateKeyFromString(TestFixtures.PRIVATE_KEY_2), + epochTimeProvider, + SignerConfiguration.ALL_SIGN_VERSIONS + ) + } + + before { + wiremockServer.stubFor( + get(anyUrl()) + .willReturn(aResponse().withStatus(200)) + ) + } + + after { + wiremockServer.resetAll() + } + + override protected def beforeAll(): Unit = { + super.beforeAll() + wiremockServer.start() + } + + override protected def afterAll(): Unit = { + super.afterAll() + wiremockServer.stop() + } +} diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index e4c9e1a..8ac617b 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -6,7 +6,7 @@ import sbt._ object BuildSettings { val env: util.Map[String, String] = System.getenv() val scala212 = "2.12.17" - val scala213 = "2.13.14" + val scala213 = "2.13.15" lazy val basicSettings = Seq( homepage := Some(new URI("https://github.com/mdsol/mauth-jvm-clients").toURL), diff --git a/project/Dependencies.scala b/project/Dependencies.scala index afbc424..03a9daf 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -7,8 +7,8 @@ object Dependencies extends DependencyUtils { val akka: String = "2.6.20" // Do not update beyond 2.6.* due to license changes val akkaHttp: String = "10.2.10" // Do not update beyond 10.2.* due to license changes val logback = "1.5.6" - val sttp = "3.9.7" - val http4s = "0.23.25" + val sttp = "3.10.2" + val http4s = "0.23.29" val enumeratum = "1.7.4" val log4cats = "2.7.0" val circe = "0.14.9" @@ -34,12 +34,15 @@ object Dependencies extends DependencyUtils { val catsEffect: ModuleID = "org.typelevel" %% "cats-effect" % "3.4.8" val sttp: ModuleID = "com.softwaremill.sttp.client3" %% "core" % Version.sttp val sttpAkkaHttpBackend: ModuleID = "com.softwaremill.sttp.client3" %% "akka-http-backend" % Version.sttp + val sttpHttp4sHttpBackend: ModuleID = "com.softwaremill.sttp.client3" %% "http4s-backend" % Version.sttp + val sttpFs2: ModuleID = "com.softwaremill.sttp.shared" %% "fs2" % "1.4.2" val scalaLibCompat: ModuleID = "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0" val caffeine: ModuleID = "com.github.ben-manes.caffeine" % "caffeine" % "3.1.8" val http4sDsl: ModuleID = "org.http4s" %% "http4s-dsl" % Version.http4s val http4sDsl022: ModuleID = "org.http4s" %% "http4s-dsl" % Version.http4s022 val http4sClient: ModuleID = "org.http4s" %% "http4s-client" % Version.http4s val http4sClient022: ModuleID = "org.http4s" %% "http4s-client" % Version.http4s022 + val http4sEmberClient: ModuleID = "org.http4s" %% "http4s-ember-client" % Version.http4s val enumeratum: ModuleID = "com.beachape" %% "enumeratum" % Version.enumeratum val log4cats: ModuleID = "org.typelevel" %% "log4cats-slf4j" % Version.log4cats From 923a2d317c05cd924438c5f55f90497a98a19463 Mon Sep 17 00:00:00 2001 From: Artur Baruk Date: Wed, 8 Jan 2025 11:24:56 +0000 Subject: [PATCH 2/7] [MCC-1289760]: lint formatting --- project/Dependencies.scala | 58 +++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 03a9daf..e83fa19 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -16,35 +16,35 @@ object Dependencies extends DependencyUtils { val http4s022 = "0.22.15" } - val akkaHttp: ModuleID = "com.typesafe.akka" %% "akka-http" % Version.akkaHttp - val akkaHttpCache: ModuleID = "com.typesafe.akka" %% "akka-http-caching" % Version.akkaHttp - val akkaStream: ModuleID = "com.typesafe.akka" %% "akka-stream" % Version.akka - val apacheHttpClient: ModuleID = "org.apache.httpcomponents" % "httpclient" % "4.5.14" - val bouncyCastlePkix: ModuleID = "org.bouncycastle" % "bcpkix-jdk18on" % "1.78.1" - val commonsCodec: ModuleID = "commons-codec" % "commons-codec" % "1.17.0" - val commonsLang3: ModuleID = "org.apache.commons" % "commons-lang3" % "3.14.0" - val guava: ModuleID = "com.google.guava" % "guava" % "31.1-jre" - val jacksonDataBind: ModuleID = "com.fasterxml.jackson.core" % "jackson-databind" % "2.17.0" - val logbackClassic: ModuleID = "ch.qos.logback" % "logback-classic" % Version.logback - val logbackCore: ModuleID = "ch.qos.logback" % "logback-core" % Version.logback - val slf4jApi: ModuleID = "org.slf4j" % "slf4j-api" % "2.0.12" - val typeSafeConfig: ModuleID = "com.typesafe" % "config" % "1.4.3" - val scalaCacheCore: ModuleID = "com.github.cb372" %% "scalacache-core" % "1.0.0-M6" - val scalaLogging: ModuleID = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5" - val catsEffect: ModuleID = "org.typelevel" %% "cats-effect" % "3.4.8" - val sttp: ModuleID = "com.softwaremill.sttp.client3" %% "core" % Version.sttp - val sttpAkkaHttpBackend: ModuleID = "com.softwaremill.sttp.client3" %% "akka-http-backend" % Version.sttp - val sttpHttp4sHttpBackend: ModuleID = "com.softwaremill.sttp.client3" %% "http4s-backend" % Version.sttp - val sttpFs2: ModuleID = "com.softwaremill.sttp.shared" %% "fs2" % "1.4.2" - val scalaLibCompat: ModuleID = "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0" - val caffeine: ModuleID = "com.github.ben-manes.caffeine" % "caffeine" % "3.1.8" - val http4sDsl: ModuleID = "org.http4s" %% "http4s-dsl" % Version.http4s - val http4sDsl022: ModuleID = "org.http4s" %% "http4s-dsl" % Version.http4s022 - val http4sClient: ModuleID = "org.http4s" %% "http4s-client" % Version.http4s - val http4sClient022: ModuleID = "org.http4s" %% "http4s-client" % Version.http4s022 - val http4sEmberClient: ModuleID = "org.http4s" %% "http4s-ember-client" % Version.http4s - val enumeratum: ModuleID = "com.beachape" %% "enumeratum" % Version.enumeratum - val log4cats: ModuleID = "org.typelevel" %% "log4cats-slf4j" % Version.log4cats + val akkaHttp: ModuleID = "com.typesafe.akka" %% "akka-http" % Version.akkaHttp + val akkaHttpCache: ModuleID = "com.typesafe.akka" %% "akka-http-caching" % Version.akkaHttp + val akkaStream: ModuleID = "com.typesafe.akka" %% "akka-stream" % Version.akka + val apacheHttpClient: ModuleID = "org.apache.httpcomponents" % "httpclient" % "4.5.14" + val bouncyCastlePkix: ModuleID = "org.bouncycastle" % "bcpkix-jdk18on" % "1.78.1" + val commonsCodec: ModuleID = "commons-codec" % "commons-codec" % "1.17.0" + val commonsLang3: ModuleID = "org.apache.commons" % "commons-lang3" % "3.14.0" + val guava: ModuleID = "com.google.guava" % "guava" % "31.1-jre" + val jacksonDataBind: ModuleID = "com.fasterxml.jackson.core" % "jackson-databind" % "2.17.0" + val logbackClassic: ModuleID = "ch.qos.logback" % "logback-classic" % Version.logback + val logbackCore: ModuleID = "ch.qos.logback" % "logback-core" % Version.logback + val slf4jApi: ModuleID = "org.slf4j" % "slf4j-api" % "2.0.12" + val typeSafeConfig: ModuleID = "com.typesafe" % "config" % "1.4.3" + val scalaCacheCore: ModuleID = "com.github.cb372" %% "scalacache-core" % "1.0.0-M6" + val scalaLogging: ModuleID = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5" + val catsEffect: ModuleID = "org.typelevel" %% "cats-effect" % "3.4.8" + val sttp: ModuleID = "com.softwaremill.sttp.client3" %% "core" % Version.sttp + val sttpAkkaHttpBackend: ModuleID = "com.softwaremill.sttp.client3" %% "akka-http-backend" % Version.sttp + val sttpHttp4sHttpBackend: ModuleID = "com.softwaremill.sttp.client3" %% "http4s-backend" % Version.sttp + val sttpFs2: ModuleID = "com.softwaremill.sttp.shared" %% "fs2" % "1.4.2" + val scalaLibCompat: ModuleID = "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0" + val caffeine: ModuleID = "com.github.ben-manes.caffeine" % "caffeine" % "3.1.8" + val http4sDsl: ModuleID = "org.http4s" %% "http4s-dsl" % Version.http4s + val http4sDsl022: ModuleID = "org.http4s" %% "http4s-dsl" % Version.http4s022 + val http4sClient: ModuleID = "org.http4s" %% "http4s-client" % Version.http4s + val http4sClient022: ModuleID = "org.http4s" %% "http4s-client" % Version.http4s022 + val http4sEmberClient: ModuleID = "org.http4s" %% "http4s-ember-client" % Version.http4s + val enumeratum: ModuleID = "com.beachape" %% "enumeratum" % Version.enumeratum + val log4cats: ModuleID = "org.typelevel" %% "log4cats-slf4j" % Version.log4cats lazy val circeBasic: Seq[ModuleID] = Seq( "io.circe" %% "circe-core" % Version.circe, From 0b5dcb72ac2d27213281d00d4479cd8e91136904 Mon Sep 17 00:00:00 2001 From: Artur Baruk Date: Wed, 8 Jan 2025 11:49:06 +0000 Subject: [PATCH 3/7] [MCC-1289760]: github actions workflow fix --- .github/workflows/ci.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 87ce28e..a522232 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,15 +14,18 @@ env: jobs: test_and_release: - runs-on: ubuntu-latest + runs-on: [self-hosted, ubuntu-latest] steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: coursier/cache-action@v6 - - uses: coursier/setup-action@v1 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - jvm: temurin:11 + java-version: '21' + distribution: 'temurin' + cache: 'sbt' - name: Lint run: sbt "scalafmtSbtCheck;scalafmtCheckAll" - name: Test From f3a76fc95328fe19beab3a10ad6f720d8f76e4e8 Mon Sep 17 00:00:00 2001 From: Artur Baruk Date: Wed, 8 Jan 2025 11:56:00 +0000 Subject: [PATCH 4/7] [MCC-1289760]: github actions workflow fix --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a522232..5d068f1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ env: jobs: test_and_release: - runs-on: [self-hosted, ubuntu-latest] + runs-on: [self-hosted, ubuntu-latest-highcpu] steps: - uses: actions/checkout@v4 with: From aafeb5ec66f191144564d8f6378009edef3249f5 Mon Sep 17 00:00:00 2001 From: Artur Baruk Date: Wed, 8 Jan 2025 12:02:04 +0000 Subject: [PATCH 5/7] [MCC-1289760]: github actions workflow fix --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5d068f1..82ad433 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ env: jobs: test_and_release: - runs-on: [self-hosted, ubuntu-latest-highcpu] + runs-on: [ubuntu-latest] steps: - uses: actions/checkout@v4 with: @@ -26,6 +26,7 @@ jobs: java-version: '21' distribution: 'temurin' cache: 'sbt' + - uses: sbt/setup-sbt@v1 - name: Lint run: sbt "scalafmtSbtCheck;scalafmtCheckAll" - name: Test From ad855dc066ee7627de9fbce22ece582521271d85 Mon Sep 17 00:00:00 2001 From: Artur Baruk Date: Wed, 8 Jan 2025 12:15:48 +0000 Subject: [PATCH 6/7] [MCC-1289760]: drop support for scala 2.12 --- build.sbt | 2 +- project/BuildSettings.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 4d32d81..cc769f0 100644 --- a/build.sbt +++ b/build.sbt @@ -23,7 +23,7 @@ def scalaModuleProject(modName: String): Project = { .settings( basicSettings, moduleName := modName, - crossScalaVersions := Seq(scala212, scala213) + crossScalaVersions := Seq(scala213) ) } diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index 8ac617b..b09577c 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -5,7 +5,7 @@ import sbt._ object BuildSettings { val env: util.Map[String, String] = System.getenv() - val scala212 = "2.12.17" +// val scala212 = "2.12.17" val scala213 = "2.13.15" lazy val basicSettings = Seq( From cddadca022e25661c329d4889ecb7ecb22fee0f9 Mon Sep 17 00:00:00 2001 From: Artur Baruk Date: Wed, 8 Jan 2025 12:21:48 +0000 Subject: [PATCH 7/7] [MCC-1289760]: re-add support for scala 2.12 --- build.sbt | 2 +- project/BuildSettings.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index cc769f0..4d32d81 100644 --- a/build.sbt +++ b/build.sbt @@ -23,7 +23,7 @@ def scalaModuleProject(modName: String): Project = { .settings( basicSettings, moduleName := modName, - crossScalaVersions := Seq(scala213) + crossScalaVersions := Seq(scala212, scala213) ) } diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index b09577c..a01c651 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -5,7 +5,7 @@ import sbt._ object BuildSettings { val env: util.Map[String, String] = System.getenv() -// val scala212 = "2.12.17" + val scala212 = "2.12.20" val scala213 = "2.13.15" lazy val basicSettings = Seq(