-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
112 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package sttp.model.headers | ||
|
||
import sttp.model.internal.Validate | ||
|
||
// Forwarded: by=<identifier>;for=<identifier>;host=<host>;proto=<http|https> | ||
case class Forwarded(by: Option[String], `for`: Option[String], host: Option[String], proto: Option[String]) { | ||
|
||
/** Serialize a single [[Forwarded]] header to a string | ||
* | ||
* @see | ||
* [[Forwarded#toString]] for a multi-header variant | ||
*/ | ||
override def toString: String = { | ||
val sb = new java.lang.StringBuilder() | ||
by.foreach(v => sb.append("by=").append(v).append(";")) | ||
`for`.foreach(v => sb.append("for=").append(v).append(";")) | ||
host.foreach(v => sb.append("host=").append(v).append(";")) | ||
proto.foreach(v => sb.append("proto=").append(v).append(";")) | ||
sb.toString | ||
} | ||
} | ||
|
||
object Forwarded { | ||
|
||
/** Parses a list of Forwarded headers. Each header can contain multiple Forwarded values. | ||
* | ||
* @see | ||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded | ||
*/ | ||
def parse(headerValues: List[String]): Either[String, List[Forwarded]] = { | ||
Validate.sequence(headerValues.map(parse)).map(_.flatten) | ||
} | ||
|
||
/** Parses a single Forwarded header, which can contain multiple Forwarded values. | ||
* | ||
* @see | ||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded | ||
*/ | ||
def parse(headerValue: String): Either[String, List[Forwarded]] = { | ||
def parseSingle(headerValue: String): Either[String, Forwarded] = { | ||
val parts = headerValue.split(";").map(_.trim).toList | ||
val kvPairs = parts.map { part => | ||
part.split("=").map(_.trim).toList match { | ||
case key :: value :: Nil => Right(key -> value) | ||
case _ => Left(s"Invalid part: $part") | ||
} | ||
} | ||
|
||
val pairs = kvPairs.collect { case Right(pair) => pair } | ||
if (pairs.size == kvPairs.size) { | ||
val by = pairs.collectFirst { case ("by", v) => v } | ||
val `for` = pairs.collectFirst { case ("for", v) => v } | ||
val host = pairs.collectFirst { case ("host", v) => v } | ||
val proto = pairs.collectFirst { case ("proto", v) => v } | ||
Right(Forwarded(by, `for`, host, proto)) | ||
} else | ||
Left(kvPairs.collect { case Left(error) => error }.mkString(", ")) | ||
} | ||
|
||
Validate.sequence(headerValue.split(",").map(_.trim).toList.map(parseSingle)) | ||
} | ||
|
||
/** Serialize a list of [[Forwarded]] headers to a single string. Each header will be separated by a comma. */ | ||
def toString(headers: List[Forwarded]): String = headers.map(_.toString).mkString(", ") | ||
} |
47 changes: 47 additions & 0 deletions
47
core/src/test/scala/sttp/model/headers/ForwardedTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package sttp.model.headers | ||
|
||
import org.scalatest.flatspec.AnyFlatSpec | ||
import org.scalatest.matchers.should.Matchers | ||
|
||
class ForwardedTest extends AnyFlatSpec with Matchers { | ||
|
||
it should "parse a single header correctly" in { | ||
val actual = Forwarded.parse("by=1.2.3.4;for=4.3.2.1;host=example.com;proto=http") | ||
actual shouldBe Right(List(Forwarded(Some("1.2.3.4"), Some("4.3.2.1"), Some("example.com"), Some("http")))) | ||
} | ||
|
||
it should "parse multiple headers correctly, when given as separate headers" in { | ||
val actual = Forwarded.parse(List("by=1.2.3.4;for=4.3.2.1", "host=example.com;proto=https")) | ||
actual shouldBe Right( | ||
List( | ||
Forwarded(Some("1.2.3.4"), Some("4.3.2.1"), None, None), | ||
Forwarded(None, None, Some("example.com"), Some("https")) | ||
) | ||
) | ||
} | ||
|
||
it should "parse multiple headers correctly, when given as a single header" in { | ||
val actual = Forwarded.parse(List("by=1.2.3.4;for=4.3.2.1, host=example.com;proto=https")) | ||
actual shouldBe Right( | ||
List( | ||
Forwarded(Some("1.2.3.4"), Some("4.3.2.1"), None, None), | ||
Forwarded(None, None, Some("example.com"), Some("https")) | ||
) | ||
) | ||
} | ||
|
||
it should "handle missing fields" in { | ||
val actual = Forwarded.parse("by=1.2.3.4;host=example.com") | ||
actual shouldBe Right(List(Forwarded(Some("1.2.3.4"), None, Some("example.com"), None))) | ||
} | ||
|
||
it should "return an error for invalid headers" in { | ||
val actual = Forwarded.parse("by=1.2.3.4;invalid") | ||
actual shouldBe Left("Invalid part: invalid") | ||
} | ||
|
||
it should "return an error for invalid key-value pairs" in { | ||
val actual = Forwarded.parse("by=1.2.3.4;for") | ||
actual shouldBe Left("Invalid part: for") | ||
} | ||
} |