Permalink
Browse files

! http: add HttpOrigin and use it for Access-Control-Allow-Origin and…

… Origin headers, fixes #579
  • Loading branch information...
jrudolph committed Oct 10, 2013
1 parent af837e5 commit 015f3c65c01d93bb3f5960fe6e5e7b279c68ef69
@@ -38,7 +38,7 @@ private object RenderSupport {
}
implicit object ChunkedMessageEndRenderer extends Renderer[ChunkedMessageEnd] {
- implicit val trailerRenderer = Renderer.seqRenderer[Renderable, HttpHeader](CrLf)
+ implicit val trailerRenderer = Renderer.genericSeqRenderer[Renderable, HttpHeader](CrLf, Rendering.Empty)
def render[R <: Rendering](r: R, part: ChunkedMessageEnd): r.type = {
r ~~ '0'
if (!part.extension.isEmpty) r ~~ ';' ~~ part.extension
@@ -47,4 +47,4 @@ private object RenderSupport {
r ~~ CrLf
}
}
-}
+}
@@ -102,8 +102,8 @@ object HttpHeaders {
}
object `Access-Control-Allow-Origin` extends ModeledCompanion
- case class `Access-Control-Allow-Origin`(origin: Uri) extends ModeledHeader {
- def renderValue[R <: Rendering](r: R): r.type = r ~~ origin
+ case class `Access-Control-Allow-Origin`(allowedOrigins: AllowedOrigins) extends ModeledHeader {
+ def renderValue[R <: Rendering](r: R): r.type = r ~~ allowedOrigins
protected def companion = `Access-Control-Allow-Origin`
}
@@ -230,7 +230,8 @@ object HttpHeaders {
object Cookie extends ModeledCompanion {
def apply(first: HttpCookie, more: HttpCookie*): `Cookie` = apply(first +: more)
- implicit val cookiesRenderer = Renderer.seqRenderer[String, HttpCookie](separator = "; ") // cache
+ implicit val cookiesRenderer: Renderer[Seq[HttpCookie]] =
+ Renderer.seqRenderer(separator = "; ") // cache
}
case class Cookie(cookies: Seq[HttpCookie]) extends ModeledHeader {
import Cookie.cookiesRenderer
@@ -279,8 +280,8 @@ object HttpHeaders {
}
object Origin extends ModeledCompanion
- case class Origin(origin: Uri) extends ModeledHeader {
- def renderValue[R <: Rendering](r: R): r.type = r ~~ origin
+ case class Origin(originList: Seq[HttpOrigin]) extends ModeledHeader {
+ def renderValue[R <: Rendering](r: R): r.type = r ~~ originList
protected def companion = Origin
}
@@ -315,10 +316,8 @@ object HttpHeaders {
object Server extends ModeledCompanion {
def apply(products: String): Server = apply(ProductVersion.parseMultiple(products))
def apply(first: ProductVersion, more: ProductVersion*): Server = apply(first +: more)
- implicit val productsRenderer = Renderer.seqRenderer[Char, ProductVersion](separator = ' ') // cache
}
case class Server(products: Seq[ProductVersion])(implicit ev: ProtectedHeaderCreation.Enabled) extends ModeledHeader {
- import Server.productsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ products
protected def companion = Server
}
@@ -350,10 +349,8 @@ object HttpHeaders {
object `User-Agent` extends ModeledCompanion {
def apply(products: String): `User-Agent` = apply(ProductVersion.parseMultiple(products))
def apply(first: ProductVersion, more: ProductVersion*): `User-Agent` = apply(first +: more)
- implicit val productsRenderer = Renderer.seqRenderer[Char, ProductVersion](separator = ' ') // cache
}
case class `User-Agent`(products: Seq[ProductVersion])(implicit ev: ProtectedHeaderCreation.Enabled) extends ModeledHeader {
- import `User-Agent`.productsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ products
protected def companion = `User-Agent`
}
@@ -394,4 +391,4 @@ object HttpHeaders {
val lowercaseName = name.toLowerCase
def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value
}
-}
+}
@@ -0,0 +1,30 @@
+package spray.http
+
+import spray.http.HttpHeaders.Host
+import spray.http.parser.UriParser
+import spray.util._
+
+case class HttpOrigin(scheme: String, host: Host) extends ToStringRenderable {
+ def render[R <: Rendering](r: R): r.type = {
+ r ~~ scheme ~~ "://" ~~ host.host
+ if (host.port != 0) r ~~ ":" ~~ host.port
+ else r
+ }
+}
+object HttpOrigin {
+ implicit def apply(str: String): HttpOrigin = {
+ val parser = new UriParser(str, UTF8, Uri.ParsingMode.Relaxed)
+ parser.parseOrigin()
+ }
+
+ implicit val originListRenderer: Renderer[Seq[HttpOrigin]] =
+ Renderer.seqRenderer(" ", "null")
+}
+
+sealed trait AllowedOrigins extends ToStringRenderable
+case object AllOrigins extends AllowedOrigins {
+ def render[R <: Rendering](r: R): r.type = r ~~ '*'
+}
+case class SomeOrigins(originList: Seq[HttpOrigin]) extends AllowedOrigins {
+ def render[R <: Rendering](r: R): r.type = r ~~ originList
+}
@@ -31,9 +31,11 @@ case class ProductVersion(product: String = "", version: String = "", comment: S
}
object ProductVersion {
+ implicit val productsRenderer: Renderer[Seq[ProductVersion]] = Renderer.seqRenderer[ProductVersion](separator = " ")
+
def parseMultiple(string: String): Seq[ProductVersion] =
parser.HttpParser.parse(HttpParser.ProductVersionComments, string) match {
case Right(x) x
case Left(info) throw new IllegalArgumentException(s"'$string' is not a legal sequence of ProductVersions: ${info.formatPretty}")
}
-}
+}
@@ -81,8 +81,9 @@ object Renderer {
if (value.isEmpty) sRenderer.render(r, defaultValue) else tRenderer.render(r, value.get)
}
- def defaultSeqRenderer[T: Renderer] = seqRenderer[Renderable, T](Rendering.`, `)
- def seqRenderer[S, T](separator: S)(implicit sRenderer: Renderer[S], tRenderer: Renderer[T]) =
+ def defaultSeqRenderer[T: Renderer] = genericSeqRenderer[Renderable, T](Rendering.`, `, Rendering.Empty)
+ def seqRenderer[T: Renderer](separator: String = ", ", empty: String = "") = genericSeqRenderer[String, T](separator, empty)
+ def genericSeqRenderer[S, T](separator: S, empty: S)(implicit sRenderer: Renderer[S], tRenderer: Renderer[T]) =
new Renderer[Seq[T]] {
def render[R <: Rendering](r: R, value: Seq[T]): r.type = {
@tailrec def recI(values: IndexedSeq[T], ix: Int = 0): r.type =
@@ -100,6 +101,7 @@ object Renderer {
} else r
value match {
+ case Nil r ~~ empty
case x: IndexedSeq[T] recI(x)
case x: LinearSeq[T] recL(x)
case x sys.error("Unsupported Seq type: " + x)
@@ -186,6 +188,9 @@ object Rendering {
val `\"` = CharPredicate('\\', '"')
case object `, ` extends SingletonValueRenderable // default separator
+ case object Empty extends Renderable {
+ def render[R <: Rendering](r: R): r.type = r
+ }
case object CrLf extends Renderable {
def render[R <: Rendering](r: R): r.type = r ~~ '\r' ~~ '\n'
@@ -45,4 +45,13 @@ private[parser] trait AdditionalRules {
def AuthParam = rule {
Token ~ "=" ~ (Token | QuotedString) ~~> ((_, _))
}
-}
+
+ def originListOrNull: Rule1[Seq[HttpOrigin]] = rule {
+ "null" ~ push(Nil: Seq[HttpOrigin]) |
+ oneOrMore(origin)
+ }
+
+ def origin: Rule1[HttpOrigin] = rule {
+ oneOrMore(!LWS ~ ANY) ~> (HttpOrigin(_)) // offload to URL parser
+ }
+}
@@ -48,7 +48,8 @@ private[parser] trait CORSHeaders {
}
def `*Access-Control-Allow-Origin` = rule {
- oneOrMore(Text) ~> (`Access-Control-Allow-Origin`(_)) ~ EOI
+ ("*" ~ push(AllOrigins) | originListOrNull ~~> SomeOrigins.apply) ~ EOI ~~>
+ (`Access-Control-Allow-Origin`(_))
}
def `*Access-Control-Expose-Headers` = rule {
@@ -65,7 +66,7 @@ private[parser] trait CORSHeaders {
}
def `*Origin` = rule {
- oneOrMore(Text) ~> { uri Origin(Uri.parseAbsolute(uri)) } ~ EOI
+ originListOrNull ~ EOI ~~> (Origin(_))
}
def HttpMethodDef = rule {
@@ -77,4 +78,4 @@ private[parser] trait CORSHeaders {
}
}
-}
+}
@@ -67,6 +67,11 @@ private[http] class UriParser(input: ParserInput, charset: Charset, mode: Uri.Pa
resolve(_scheme, _userinfo, _host, _port, _path, _query, _fragment, base)
}
+ def parseOrigin(): HttpOrigin = {
+ complete("origin", origin)
+ HttpOrigin(_scheme, HttpHeaders.Host(_host.address, _port))
+ }
+
def URI =
scheme && ch(':') && `hier-part` && {
val mark = cursor
@@ -76,6 +81,8 @@ private[http] class UriParser(input: ParserInput, charset: Charset, mode: Uri.Pa
ch('#') && fragment || reset(mark)
}
+ def origin = scheme && ch(':') && ch('/') && ch('/') && hostAndPort
+
def `hier-part` = {
val mark = cursor
(ch('/') && ch('/') && authority && `path-abempty`
@@ -126,10 +133,7 @@ private[http] class UriParser(input: ParserInput, charset: Charset, mode: Uri.Pa
def authority = {
val mark = cursor
- (userinfo || reset(mark)) && host && {
- val mark = cursor
- ch(':') && port || reset(mark)
- }
+ (userinfo || reset(mark)) && hostAndPort
}
def userinfo = {
@@ -144,6 +148,12 @@ private[http] class UriParser(input: ParserInput, charset: Charset, mode: Uri.Pa
}
}
+ def hostAndPort =
+ host && {
+ val mark = cursor
+ ch(':') && port || reset(mark)
+ }
+
def host = {
val mark = cursor
(`IP-literal` || reset(mark) && IPv4address) && {
@@ -514,4 +524,4 @@ private[http] object UriParser {
def hexDigit(i: Int): Char = { val j = i & 0x0F; ('0' + j + -7 * sex(0x7ffffff6 + j)).toChar }
def hexValue(c: Char): Int = (c & 0x1f) + ((c >> 6) * 0x19) - 0x10
-}
+}
@@ -59,8 +59,9 @@ class HttpHeaderSpec extends Specification {
}
"Access-Control-Allow-Origin" in {
- "Access-Control-Allow-Origin: *" =!= `Access-Control-Allow-Origin`("*")
- "Access-Control-Allow-Origin: http://spray.io" =!= `Access-Control-Allow-Origin`("http://spray.io")
+ "Access-Control-Allow-Origin: *" =!= `Access-Control-Allow-Origin`(AllOrigins)
+ "Access-Control-Allow-Origin: null" =!= `Access-Control-Allow-Origin`(SomeOrigins(Nil))
+ "Access-Control-Allow-Origin: http://spray.io" =!= `Access-Control-Allow-Origin`(SomeOrigins(Seq("http://spray.io")))
}
"Access-Control-Expose-Headers" in {
@@ -220,7 +221,7 @@ class HttpHeaderSpec extends Specification {
}
"Origin" in {
- "Origin: http://spray.io" =!= Origin(Uri("http://spray.io"))
+ "Origin: http://spray.io" =!= Origin(Seq("http://spray.io"))
}
"Proxy-Authenticate" in {

0 comments on commit 015f3c6

Please sign in to comment.