diff --git a/core/src/main/scala/org/http4s/ProductIdOrComment.scala b/core/src/main/scala/org/http4s/ProductIdOrComment.scala index 726b4ccea87..04d8e505e71 100644 --- a/core/src/main/scala/org/http4s/ProductIdOrComment.scala +++ b/core/src/main/scala/org/http4s/ProductIdOrComment.scala @@ -23,10 +23,15 @@ import org.http4s.util.Writer sealed trait ProductIdOrComment extends Renderable object ProductIdOrComment { - private[http4s] val serverAgentParser: P[(ProductId, List[ProductIdOrComment])] = { + @deprecated("Use serverAgentParser(Int) instead", "0.22.15") + private[http4s] val serverAgentParser: P[(ProductId, List[ProductIdOrComment])] = + serverAgentParser(Rfc7230.CommentDefaultMaxDepth) + + private[http4s] def serverAgentParser(maxDepth: Int): P[(ProductId, List[ProductIdOrComment])] = { val rws = P.charIn(' ', ' ').rep.void - ProductId.parser ~ (rws *> (ProductId.parser.orElse(ProductComment.parser))).rep0 + ProductId.parser ~ (rws *> (ProductId.parser.orElse(ProductComment.parser(maxDepth)))).rep0 } + } final case class ProductId(value: String, version: Option[String] = None) @@ -55,6 +60,10 @@ final case class ProductComment(value: String) extends ProductIdOrComment { } object ProductComment { - private[http4s] val parser = Rfc7230.comment.map(ProductComment.apply) + private[http4s] def parser(maxDepth: Int): P[ProductComment] = + Rfc7230.comment(maxDepth).map(ProductComment.apply) + @deprecated("Use parser(Int) instead", "0.22.15") + private[http4s] val parser: P[ProductComment] = + parser(Rfc7230.CommentDefaultMaxDepth) } diff --git a/core/src/main/scala/org/http4s/headers/HeaderCompanion.scala b/core/src/main/scala/org/http4s/headers/HeaderCompanion.scala index b5d388ff2d2..47bf283ab9c 100644 --- a/core/src/main/scala/org/http4s/headers/HeaderCompanion.scala +++ b/core/src/main/scala/org/http4s/headers/HeaderCompanion.scala @@ -29,7 +29,8 @@ private[headers] abstract class HeaderCompanion[A](_name: String) { implicit val headerInstance: Header[A, _ <: Header.Type] - private val invalidHeader = s"Invalid $name header" + private val invalidHeader = s"Invalid ${_name} header" + def parse(s: String): ParseResult[A] = ParseResult.fromParser(parser, invalidHeader)(s) diff --git a/core/src/main/scala/org/http4s/headers/Server.scala b/core/src/main/scala/org/http4s/headers/Server.scala index 5a8f22ee278..eeac0751066 100644 --- a/core/src/main/scala/org/http4s/headers/Server.scala +++ b/core/src/main/scala/org/http4s/headers/Server.scala @@ -17,10 +17,14 @@ package org.http4s package headers +import cats.parse.Parser +import org.http4s.internal.parsing.Rfc7230 import org.http4s.util.Renderable import org.http4s.util.Writer import org.typelevel.ci.CIString +import scala.annotation.nowarn + object Server extends HeaderCompanion[Server]("Server") { override val name: CIString = super.name @@ -28,26 +32,43 @@ object Server extends HeaderCompanion[Server]("Server") { def apply(id: ProductId, tail: ProductIdOrComment*): Server = apply(id, tail.toList) - private[http4s] val parser = - ProductIdOrComment.serverAgentParser.map { + @nowarn("cat=deprecation") + @deprecated("Use parse(Int) instead", "0.22.15") + override def parse(s: String): ParseResult[`Server`] = + parse(Rfc7230.CommentDefaultMaxDepth)(s) + + def parse(maxDepth: Int)(s: String): ParseResult[`Server`] = + parsePartiallyApplied(maxDepth)(s) + + private def parsePartiallyApplied(maxDepth: Int): String => ParseResult[`Server`] = + ParseResult.fromParser(parser(maxDepth), "Invalid Server header") + + @deprecated("Use parser(Int) instead", "0.22.15") + private[http4s] val parser: Parser[Server] = + parser(Rfc7230.CommentDefaultMaxDepth) + + private[http4s] def parser(maxDepth: Int): Parser[Server] = + ProductIdOrComment.serverAgentParser(maxDepth).map { case (product: ProductId, tokens: List[ProductIdOrComment]) => Server(product, tokens) } implicit val headerInstance: Header[Server, Header.Single] = - createRendered { h => - new Renderable { - def render(writer: Writer): writer.type = { - writer << h.product - h.rest.foreach { - case p: ProductId => writer << ' ' << p - case ProductComment(c) => writer << ' ' << '(' << c << ')' + Header.createRendered( + name, + h => + new Renderable { + def render(writer: Writer): writer.type = { + writer << h.product + h.rest.foreach { + case p: ProductId => writer << ' ' << p + case ProductComment(c) => writer << ' ' << '(' << c << ')' + } + writer } - writer - } - } - } - + }, + parsePartiallyApplied(100), + ) } /** Server header diff --git a/core/src/main/scala/org/http4s/headers/User-Agent.scala b/core/src/main/scala/org/http4s/headers/User-Agent.scala index 7d945111a67..fc6aafd7096 100644 --- a/core/src/main/scala/org/http4s/headers/User-Agent.scala +++ b/core/src/main/scala/org/http4s/headers/User-Agent.scala @@ -18,6 +18,7 @@ package org.http4s package headers import org.http4s.Header +import org.http4s.internal.parsing.Rfc7230 import org.http4s.util.Renderable import org.http4s.util.Renderer import org.http4s.util.Writer @@ -30,15 +31,29 @@ object `User-Agent` { val name = ci"User-Agent" + @deprecated("Use parse(Int)(String) instead", "0.22.15") def parse(s: String): ParseResult[`User-Agent`] = - ParseResult.fromParser(parser, "Invalid User-Agent header")(s) + parse(Rfc7230.CommentDefaultMaxDepth)(s) + def parse(maxDepth: Int)(s: String): ParseResult[`User-Agent`] = + parsePartiallyApplied(maxDepth)(s) + + private def parsePartiallyApplied(maxDepth: Int): String => ParseResult[`User-Agent`] = + ParseResult.fromParser(parser(maxDepth), "Invalid User-Agent header") + + @deprecated("Use parser(Int) instead", "0.22.15") private[http4s] val parser = ProductIdOrComment.serverAgentParser.map { case (product: ProductId, tokens: List[ProductIdOrComment]) => `User-Agent`(product, tokens) } + private[http4s] def parser(maxDepth: Int) = + ProductIdOrComment.serverAgentParser(maxDepth).map { + case (product: ProductId, tokens: List[ProductIdOrComment]) => + `User-Agent`(product, tokens) + } + implicit val headerInstance: Header[`User-Agent`, Header.Single] = Header.createRendered( name, @@ -54,7 +69,7 @@ object `User-Agent` { } }, - parse, + parsePartiallyApplied(100), ) implicit def convert(implicit diff --git a/core/src/main/scala/org/http4s/internal/parsing/Rfc7230.scala b/core/src/main/scala/org/http4s/internal/parsing/Rfc7230.scala index 0d26aa9e3c2..ed040624bd5 100644 --- a/core/src/main/scala/org/http4s/internal/parsing/Rfc7230.scala +++ b/core/src/main/scala/org/http4s/internal/parsing/Rfc7230.scala @@ -80,10 +80,23 @@ private[http4s] object Rfc7230 { .orElse(obsText) /* "(" *( ctext / quoted-pair / comment ) ")" */ - val comment: Parser[String] = Parser.recursive[String] { (comment: Parser[String]) => - between(char('('), cText.orElse(quotedPair).orElse(comment).rep0.string, char(')')) + def comment(maxDepth: Int): Parser[String] = { + def go(n: Int): Parser[String] = + between( + char('('), + if (n <= 0) Parser.failWith("exceeded maximum comment depth") + else cText.orElse(quotedPair).orElse(go(n - 1)).rep0.string, + char(')'), + ) + go(maxDepth) } + final val CommentDefaultMaxDepth = 100 + + @deprecated("Use comment(Int) instead", "0.22.15") + private[http4s] val comment: Parser[String] = + comment(CommentDefaultMaxDepth) + def headerRep[A](element: Parser[A]): Parser0[List[A]] = headerRep1(element).?.map(_.fold(List.empty[A])(_.toList)) diff --git a/docs/changelog.md b/docs/changelog.md index 6eb54ec1ba0..9e801b14c99 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,29 @@ Maintenance branches are merged before each new release. This change log is ordered chronologically, so each release contains all changes described below it. +# v0.22.15 (2023-01-04) + +## What's Changed +### http4s-core +* Fixes [CVE-2023-22465](https://github.com/http4s/http4s/security/advisories/GHSA-54w6-vxfh-fw7f) + +### Behind the scenes +* Set LoggingHandler in NettyTestServer to the default DEBUG level by @RafalSumislawski in https://github.com/http4s/http4s/pull/6497 + +**Full Changelog**: https://github.com/http4s/http4s/compare/v0.22.14...v0.22.15 + +# v0.21.34 (2023-01-04) + +## What's changed + +### http4s-core +* Fixes [CVE-2023-22465](https://github.com/http4s/http4s/security/advisories/GHSA-54w6-vxfh-fw7f) + +## Behind the scenes +* Don't publish website from 0.21 by @armanbilge in https://github.com/http4s/http4s/pull/6151 + +**Full Changelog**: https://github.com/http4s/http4s/compare/v0.21.33...v0.21.34 + # v0.22.14 (2022-06-21) This release is binary compatible with 0.22.x series. Routine maintenance has stopped on 0.22.x, but we'll continue to entertain patches from the community. All users are encouraged to upgrade to 0.23 (the latest stable series, on Cats-Effect 3). diff --git a/flake.lock b/flake.lock index 136beca3e27..477ad440dd6 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,16 @@ { "nodes": { "devshell": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, "locked": { - "lastModified": 1640301433, - "narHash": "sha256-eplae8ZNiEmxbOgwUn9IihaJfEUxoUilkwciRPGskYE=", + "lastModified": 1671489820, + "narHash": "sha256-qoei5HDJ8psd1YUPD7DhbHdhLIT9L2nadscp4Qk37uk=", "owner": "numtide", "repo": "devshell", - "rev": "f87fb932740abe1c1b46f6edd8a36ade17881666", + "rev": "5aa3a8039c68b4bf869327446590f4cdf90bb634", "type": "github" }, "original": { @@ -15,29 +19,28 @@ "type": "github" } }, - "flake-compat": { - "flake": false, + "flake-utils": { "locked": { - "lastModified": 1641205782, - "narHash": "sha256-4jY7RCWUoZ9cKD8co0/4tFARpWB+57+r1bLLvXNJliY=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "b7547d3eed6f32d06102ead8991ec52ab0a4f1a7", + "lastModified": 1642700792, + "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", "type": "github" }, "original": { - "owner": "edolstra", - "repo": "flake-compat", + "owner": "numtide", + "repo": "flake-utils", "type": "github" } }, - "flake-utils": { + "flake-utils_2": { "locked": { - "lastModified": 1638122382, - "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "owner": "numtide", "repo": "flake-utils", - "rev": "74f7e4319258e287b0f9cb95426c9853b282730b", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "type": "github" }, "original": { @@ -48,11 +51,27 @@ }, "nixpkgs": { "locked": { - "lastModified": 1641463613, - "narHash": "sha256-xZN9igqdZvnhSeTx8OlGby/OaYQHwgXVrBrPW0gLEh8=", + "lastModified": 1643381941, + "narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1672428209, + "narHash": "sha256-eejhqkDz2cb2vc5VeaWphJz8UXNuoNoM8/Op8eWv2tQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "77fda7f672726e1a95c8cd200f27bccfc86c870b", + "rev": "293a28df6d7ff3dec1e61e37cc4ee6e6c0fb0847", "type": "github" }, "original": { @@ -78,16 +97,15 @@ "typelevel-nix": { "inputs": { "devshell": "devshell", - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1642102721, - "narHash": "sha256-+idWVqOlnIziKDKAd4tP5XBYgAPt7HXknRPR4t0kP/c=", + "lastModified": 1672639231, + "narHash": "sha256-n2S6kRPr7IE9BQSqK1KL9HxVNxL16I4js/GW0ZAa73w=", "owner": "rossabaker", "repo": "typelevel-nix", - "rev": "f0712172fcb658e2032deb771aec58e3c9f8a13c", + "rev": "27b58dea50fcd1d185df9ea3024bb2e914dce0b4", "type": "github" }, "original": { diff --git a/tests/src/test/scala/org/http4s/parser/SimpleHeadersSpec.scala b/tests/src/test/scala/org/http4s/parser/SimpleHeadersSpec.scala index 1b15f55c223..6d5330eafd0 100644 --- a/tests/src/test/scala/org/http4s/parser/SimpleHeadersSpec.scala +++ b/tests/src/test/scala/org/http4s/parser/SimpleHeadersSpec.scala @@ -180,18 +180,18 @@ class SimpleHeadersSpec extends Http4sSuite { val header = `User-Agent`(ProductId("foo", Some("bar")), List(ProductId("foo"))) assertEquals(header.value, "foo/bar foo") - assertEquals(`User-Agent`.parse(header.value), Right(header)) + assertEquals(`User-Agent`.parse(100)(header.value), Right(header)) val header2 = `User-Agent`(ProductId("foo"), List(ProductId("bar", Some("biz")), ProductComment("blah"))) assertEquals(header2.value, "foo bar/biz (blah)") - assertEquals(`User-Agent`.parse(header2.value), Right(header2)) + assertEquals(`User-Agent`.parse(188)(header2.value), Right(header2)) val headerstr = "Mozilla/5.0 (Android; Mobile; rv:30.0) Gecko/30.0 Firefox/30.0" val headerraw = Header.Raw(`User-Agent`.name, headerstr) - val parsed = `User-Agent`.parse(headerraw.value) + val parsed = `User-Agent`.parse(100)(headerraw.value) assertEquals( parsed, Right( @@ -212,16 +212,16 @@ class SimpleHeadersSpec extends Http4sSuite { val header = Server(ProductId("foo", Some("bar")), List(ProductComment("foo"))) assertEquals(header.value, "foo/bar (foo)") - assertEquals(Server.parse(header.toRaw1.value), Right(header)) + assertEquals(Server.parse(100)(header.toRaw1.value), Right(header)) val header2 = Server(ProductId("foo"), List(ProductId("bar", Some("biz")), ProductComment("blah"))) assertEquals(header2.value, "foo bar/biz (blah)") - assertEquals(Server.parse(header2.toRaw1.value), Right(header2)) + assertEquals(Server.parse(100)(header2.toRaw1.value), Right(header2)) val headerstr = "nginx/1.14.0 (Ubuntu)" assertEquals( - Server.parse(headerstr), + Server.parse(100)(headerstr), Right( Server( ProductId("nginx", Some("1.14.0")), @@ -234,7 +234,7 @@ class SimpleHeadersSpec extends Http4sSuite { val headerstr2 = "CERN/3.0 libwww/2.17" assertEquals( - Server.parse(headerstr2), + Server.parse(100)(headerstr2), Right( Server( ProductId("CERN", Some("3.0")),