-
Notifications
You must be signed in to change notification settings - Fork 787
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
10 changed files
with
1,082 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,203 @@ | ||
/* | ||
* Copyright 2013-2020 http4s.org | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.http4s.headers | ||
|
||
import java.net.{Inet4Address, Inet6Address} | ||
|
||
import cats.data.NonEmptyList | ||
import cats.syntax.either._ | ||
import org.http4s._ | ||
import org.http4s.util.{Renderable, Writer} | ||
|
||
object Forwarded | ||
extends HeaderKey.Internal[Forwarded] | ||
with HeaderKey.Recurring | ||
with ForwardedRenderers | ||
with parser.ForwardedModelParsing { | ||
|
||
final case class Node(nodeName: Node.Name, nodePort: Option[Node.Port] = None) | ||
|
||
protected[http4s] val NodeNameIpv4 = Node.Name.Ipv4 | ||
|
||
object Node { | ||
def apply(nodeName: Name, nodePort: Port): Node = apply(nodeName, Some(nodePort)) | ||
|
||
sealed trait Name { _: Product => } | ||
|
||
object Name { | ||
case class Ipv4(address: Uri.Ipv4Address) extends Name | ||
case class Ipv6(address: Uri.Ipv6Address) extends Name | ||
case object Unknown extends Name | ||
|
||
def apply(address: Uri.Ipv4Address): Name = Name.Ipv4(address) | ||
def apply(address: Inet4Address): Name = apply(Uri.Ipv4Address.fromInet4Address(address)) | ||
def apply(a: Byte, b: Byte, c: Byte, d: Byte): Name = apply(Uri.Ipv4Address(a, b, c, d)) | ||
|
||
def apply(address: Uri.Ipv6Address): Name = Name.Ipv6(address) | ||
def apply(address: Inet6Address): Name = apply(Uri.Ipv6Address.fromInet6Address(address)) | ||
def apply(a: Short, b: Short, c: Short, d: Short, e: Short, f: Short, g: Short, h: Short) | ||
: Name = apply(Uri.Ipv6Address(a, b, c, d, e, f, g, h)) | ||
} | ||
|
||
sealed trait Port { _: Product => } | ||
|
||
object Port { | ||
private[this] final case class C(value: Int) extends Port { | ||
override def productPrefix: String = "Port" | ||
} | ||
|
||
def fromInt(num: Int): ParseResult[Port] = | ||
checkPortNum(num).toLeft(C(num)) | ||
|
||
def unapply(port: Port): Option[Int] = | ||
PartialFunction.condOpt(port) { | ||
case C(num) => num | ||
} | ||
} | ||
|
||
sealed trait Obfuscated extends Name with Port { _: Product => | ||
|
||
/** | ||
* Obfuscated value must start with '_' (underscore) symbol. | ||
*/ | ||
def value: String | ||
} | ||
object Obfuscated { | ||
private[this] final case class C(value: String) extends Obfuscated { | ||
override def productPrefix: String = "Obfuscated" | ||
} | ||
|
||
def fromString(s: String): ParseResult[Obfuscated] = | ||
new ModelNodeObfuscatedParser(s).parse | ||
|
||
def unapply(o: Obfuscated): Option[String] = Some(o.value) | ||
|
||
// Referenced by model parsers. | ||
private[http4s] def apply(s: String): Obfuscated = C(s) | ||
} | ||
|
||
def fromString(s: String): ParseResult[Node] = new ModelNodeParser(s).parse | ||
} | ||
|
||
sealed trait Host { _: Product => | ||
def host: Uri.Host | ||
def port: Option[Int] | ||
} | ||
|
||
object Host { | ||
private[this] final case class C(host: Uri.Host, port: Option[Int]) extends Host { | ||
override def productPrefix: String = "Host" | ||
} | ||
|
||
def apply(uriHost: Uri.Host): Host = C(uriHost, None) | ||
|
||
def from(uriHost: Uri.Host, port: Int): ParseResult[Host] = | ||
checkPortNum(port).toLeft(C(uriHost, Some(port))) | ||
|
||
def from(uriHost: Uri.Host, port: Option[Int]): ParseResult[Host] = | ||
port.fold(apply(uriHost).asRight[ParseFailure])(from(uriHost, _)) | ||
|
||
def fromUri(uri: Uri): ParseResult[Host] = | ||
uri.host.toRight(Failures.missingHost(uri)).flatMap(from(_, uri.port)) | ||
|
||
def fromString(s: String): ParseResult[Host] = new ModelHostParser(s).parse | ||
|
||
def unapply(host: Host): Option[(Uri.Host, Option[Int])] = Some((host.host, host.port)) | ||
} | ||
|
||
type Proto = Uri.Scheme | ||
val Proto: Uri.Scheme.type = Uri.Scheme | ||
|
||
sealed trait Element extends Renderable { _: Product => | ||
def `by`: Option[Node] | ||
def `for`: Option[Node] | ||
def `host`: Option[Host] | ||
def `proto`: Option[Proto] | ||
|
||
def withBy(value: Node): Element | ||
def withFor(value: Node): Element | ||
def withHost(value: Host): Element | ||
def withProto(value: Proto): Element | ||
|
||
def withoutBy: Element | ||
def withoutFor: Element | ||
def withoutHost: Element | ||
def withoutProto: Element | ||
|
||
override def render(writer: Writer): writer.type = renderElement(writer, this) | ||
} | ||
|
||
/** | ||
* Enables the following construction syntax (while preserving type safety and consistency): | ||
* {{{ | ||
* Element | ||
* .withBy(<by-node>) | ||
* .withFor(<for-node>) | ||
* .withHost(<host>) | ||
* .withProto(<schema>)` | ||
* }}} | ||
*/ | ||
object Element { | ||
// Since at least one of the fields must be set to `Some`, | ||
// the `Element` trait implementation is hidden. | ||
private[this] final case class C( | ||
`by`: Option[Node] = None, | ||
`for`: Option[Node] = None, | ||
`host`: Option[Host] = None, | ||
`proto`: Option[Proto] = None) | ||
extends Element { | ||
|
||
def withBy(value: Node): Element = copy(`by` = Some(value)) | ||
def withFor(value: Node): Element = copy(`for` = Some(value)) | ||
def withHost(value: Host): Element = copy(`host` = Some(value)) | ||
def withProto(value: Proto): Element = copy(`proto` = Some(value)) | ||
|
||
def withoutBy: Element = copy(`by` = None) | ||
def withoutFor: Element = copy(`for` = None) | ||
def withoutHost: Element = copy(`host` = None) | ||
def withoutProto: Element = copy(`proto` = None) | ||
|
||
override def productPrefix: String = "Element" | ||
} | ||
|
||
def withBy(value: Node): Element = C(`by` = Some(value)) | ||
def withFor(value: Node): Element = C(`for` = Some(value)) | ||
def withHost(value: Host): Element = C(`host` = Some(value)) | ||
def withProto(value: Proto): Element = C(`proto` = Some(value)) | ||
|
||
def unapply(elem: Element): Option[(Option[Node], Option[Node], Option[Host], Option[Proto])] = | ||
Some((elem.`by`, elem.`for`, elem.`host`, elem.`proto`)) | ||
} | ||
|
||
final val PortMin = 0 | ||
final val PortMax = 65535 | ||
|
||
private def checkPortNum(portNum: Int): Option[ParseFailure] = | ||
if ((portNum >= PortMin) && (portNum <= PortMax)) | ||
None | ||
else | ||
Some(Failures.invalidPortNum(portNum)) | ||
|
||
private object Failures { | ||
def invalidPortNum(num: Int) = | ||
ParseFailure("invalid port number", s"port $num is not in range $PortMin..$PortMax") | ||
def missingHost(uri: Uri) = | ||
ParseFailure("missing host", s"no host defined in the URI '$uri'") | ||
} | ||
|
||
override def parse(s: String): ParseResult[Forwarded] = parser.HttpHeaderParser.FORWARDED(s) | ||
} | ||
|
||
final case class Forwarded(values: NonEmptyList[Forwarded.Element]) | ||
extends Header.RecurringRenderable { | ||
|
||
override type Value = Forwarded.Element | ||
override def key: Forwarded.type = Forwarded | ||
|
||
def apply(firstElem: Forwarded.Element, otherElems: Forwarded.Element*): Forwarded = | ||
Forwarded(NonEmptyList.of(firstElem, otherElems: _*)) | ||
} |
94 changes: 94 additions & 0 deletions
94
core/src/main/scala/org/http4s/headers/ForwardedRenderers.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,94 @@ | ||
/* | ||
* Copyright 2013-2020 http4s.org | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.http4s.headers | ||
|
||
import java.nio.charset.StandardCharsets | ||
|
||
import cats.Eval | ||
import cats.syntax.flatMap._ | ||
import org.http4s.Uri | ||
import org.http4s.parser.Rfc2616BasicRules | ||
import org.http4s.util.{Renderer, Writer} | ||
|
||
/** | ||
* Renderers for the [[Forwarded]] header models. | ||
*/ | ||
private[http4s] trait ForwardedRenderers { | ||
import Forwarded._ | ||
|
||
implicit val http4sForwardedNodeNameRenderer: Renderer[Node.Name] = | ||
new Renderer[Node.Name] { | ||
override def render(writer: Writer, nodeName: Node.Name): writer.type = | ||
nodeName match { | ||
case Node.Name.Ipv4(ipv4addr) => writer << ipv4addr | ||
case Node.Name.Ipv6(ipv6addr) => writer << '[' << ipv6addr << ']' | ||
case Node.Name.Unknown => writer << "unknown" | ||
case Node.Obfuscated(str) => writer << str | ||
} | ||
} | ||
|
||
implicit val http4sForwardedNodePortRenderer: Renderer[Node.Port] = new Renderer[Node.Port] { | ||
override def render(writer: Writer, nodePort: Node.Port): writer.type = | ||
nodePort match { | ||
case Node.Port(num) => writer << num | ||
case Node.Obfuscated(str) => writer << str | ||
} | ||
} | ||
|
||
implicit val http4sForwardedNodeRenderer: Renderer[Node] = new Renderer[Node] { | ||
override def render(writer: Writer, node: Node): writer.type = { | ||
writer << node.nodeName | ||
node.nodePort.fold[writer.type](writer)(writer << ':' << _) | ||
} | ||
} | ||
|
||
implicit val http4sForwardedHostRenderer: Renderer[Host] = new Renderer[Host] { | ||
// See in `Rfc3986Parser`: `RegName` -> `SubDelims` | ||
private val RegNameChars = Uri.Unreserved ++ "!$&'()*+,;=" | ||
|
||
override def render(writer: Writer, host: Host): writer.type = { | ||
host.host match { | ||
case Uri.RegName(name) => | ||
// TODO: A workaround for #1651, remove when the former issue gets fixed. | ||
writer << Uri.encode(name.value, StandardCharsets.ISO_8859_1, toSkip = RegNameChars) | ||
case other => | ||
writer << other | ||
} | ||
host.port.fold[writer.type](writer)(writer << ':' << _) | ||
} | ||
} | ||
|
||
protected def renderElement(writer: Writer, elem: Element): writer.type = { | ||
|
||
def renderParamEval[A: Renderer](name: String, maybeValue: Option[A]) = | ||
maybeValue.map { value => | ||
// Do not write it immediately since we're going to interleave existing parameters with ';' | ||
Eval.always[writer.type] { // NOTE: not clear why the explicit type is necessary here | ||
writer << name << '=' | ||
|
||
val rendered = Renderer.renderString(value) | ||
// TODO: Rfc2616BasicRules.isToken should be used instead, but as for now it works not as expected. | ||
// See: https://gitter.im/http4s/http4s-dev?at=5f55d832ec534f584fea2572 | ||
if (Rfc2616BasicRules.token(rendered).getOrElse(null) == rendered) | ||
writer << rendered | ||
else | ||
writer <<# rendered // quote non-token values | ||
} | ||
}.toList | ||
|
||
{ | ||
import elem._ | ||
renderParamEval("by", `by`) ::: | ||
renderParamEval("for", `for`) ::: | ||
renderParamEval("host", `host`) ::: | ||
renderParamEval("proto", `proto`) | ||
}.reduceLeft { (leftParamEval, rightParamEval) => | ||
// Interleave every couple of parameters with ';' | ||
leftParamEval >> Eval.always(writer << ';') >> rightParamEval | ||
}.value // all actual rendering happens here | ||
} | ||
} |
Oops, something went wrong.