+htc #16887 implement Websocket header parsing/rendering

This commit is contained in:
Johannes Rudolph 2015-02-18 17:00:33 +01:00
parent 050c0549f3
commit 2cf1c41eef
6 changed files with 233 additions and 1 deletions

View file

@ -0,0 +1,24 @@
/*
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.model.headers
import akka.http.util.{ Rendering, ValueRenderable }
import scala.collection.immutable
/**
* A websocket extension as defined in http://tools.ietf.org/html/rfc6455#section-4.3
*/
final case class WebsocketExtension(name: String, params: immutable.Map[String, String] = Map.empty) extends ValueRenderable {
def render[R <: Rendering](r: R): r.type = {
r ~~ name
if (params.nonEmpty)
params.foreach {
case (k, "") r ~~ "; " ~~ k
case (k, v) r ~~ "; " ~~ k ~~ '=' ~~# v
}
r
}
}

View file

@ -7,7 +7,10 @@ package headers
import java.lang.Iterable
import java.net.InetSocketAddress
import java.security.MessageDigest
import java.util
import akka.parboiled2.util.Base64
import scala.annotation.tailrec
import scala.collection.immutable
import akka.http.util._
@ -563,6 +566,103 @@ final case class Referer(uri: Uri) extends japi.headers.Referer with ModeledHead
def getUri: akka.http.model.japi.Uri = uri.asJava
}
/**
* INTERNAL API
*/
// http://tools.ietf.org/html/rfc6455#section-4.3
private[http] object `Sec-WebSocket-Accept` extends ModeledCompanion {
// Defined at http://tools.ietf.org/html/rfc6455#section-4.2.2
val MagicGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
/** Generates the matching accept header for this key */
def forKey(key: `Sec-WebSocket-Key`): `Sec-WebSocket-Accept` = {
val sha1 = MessageDigest.getInstance("sha1")
val salted = key.key + MagicGuid
val hash = sha1.digest(salted.asciiBytes)
val acceptKey = Base64.rfc2045().encodeToString(hash, false)
`Sec-WebSocket-Accept`(acceptKey)
}
}
/**
* INTERNAL API
*/
private[http] final case class `Sec-WebSocket-Accept`(key: String) extends ModeledHeader {
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ key
protected def companion = `Sec-WebSocket-Accept`
}
/**
* INTERNAL API
*/
// http://tools.ietf.org/html/rfc6455#section-4.3
private[http] object `Sec-WebSocket-Extensions` extends ModeledCompanion {
implicit val extensionsRenderer = Renderer.defaultSeqRenderer[WebsocketExtension]
}
/**
* INTERNAL API
*/
private[http] final case class `Sec-WebSocket-Extensions`(extensions: immutable.Seq[WebsocketExtension]) extends ModeledHeader {
require(extensions.nonEmpty, "Sec-WebSocket-Extensions.extensions must not be empty")
import `Sec-WebSocket-Extensions`.extensionsRenderer
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ extensions
protected def companion = `Sec-WebSocket-Extensions`
}
// http://tools.ietf.org/html/rfc6455#section-4.3
/**
* INTERNAL API
*/
private[http] object `Sec-WebSocket-Key` extends ModeledCompanion
/**
* INTERNAL API
*/
private[http] final case class `Sec-WebSocket-Key`(key: String) extends ModeledHeader {
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ key
protected def companion = `Sec-WebSocket-Key`
}
// http://tools.ietf.org/html/rfc6455#section-4.3
/**
* INTERNAL API
*/
private[http] object `Sec-WebSocket-Protocol` extends ModeledCompanion {
implicit val protocolsRenderer = Renderer.defaultSeqRenderer[String]
}
/**
* INTERNAL API
*/
private[http] final case class `Sec-WebSocket-Protocol`(protocols: immutable.Seq[String]) extends ModeledHeader {
require(protocols.nonEmpty, "Sec-WebSocket-Protocol.protocols must not be empty")
import `Sec-WebSocket-Protocol`.protocolsRenderer
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ protocols
protected def companion = `Sec-WebSocket-Protocol`
}
// http://tools.ietf.org/html/rfc6455#section-4.3
/**
* INTERNAL API
*/
private[http] object `Sec-WebSocket-Version` extends ModeledCompanion {
implicit val versionsRenderer = Renderer.defaultSeqRenderer[Int]
}
/**
* INTERNAL API
*/
private[http] final case class `Sec-WebSocket-Version`(versions: immutable.Seq[Int]) extends ModeledHeader {
require(versions.nonEmpty, "Sec-WebSocket-Version.versions must not be empty")
require(versions.forall(v v >= 0 && v <= 255), s"Sec-WebSocket-Version.versions must be in the range 0 <= version <= 255 but were $versions")
import `Sec-WebSocket-Version`.versionsRenderer
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ versions
def hasVersion(versionNumber: Int): Boolean = versions.exists(_ == versionNumber)
protected def companion = `Sec-WebSocket-Version`
}
// http://tools.ietf.org/html/rfc7231#section-7.4.2
object Server extends ModeledCompanion {
def apply(products: String): Server = apply(ProductVersion.parseMultiple(products))

View file

@ -26,7 +26,8 @@ private[http] class HeaderParser(val input: ParserInput) extends Parser with Dyn
with IpAddressParsing
with LinkHeader
with SimpleHeaders
with StringBuilding {
with StringBuilding
with WebsocketHeaders {
import CharacterClasses._
// http://www.rfc-editor.org/errata_search.php?rfc=7230 errata id 4189
@ -111,6 +112,11 @@ private[http] object HeaderParser {
"range",
"referer",
"server",
"sec-websocket-accept",
"sec-websocket-extensions",
"sec-websocket-key",
"sec-websocket-protocol",
"sec-websocket-version",
"set-cookie",
"transfer-encoding",
"user-agent",

View file

@ -0,0 +1,61 @@
/*
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.model.parser
import akka.http.model.headers._
import akka.parboiled2._
// see grammar at http://tools.ietf.org/html/rfc6455#section-4.3
private[parser] trait WebsocketHeaders { this: Parser with CommonRules with CommonActions
import CharacterClasses._
import Base64Parsing.rfc2045Alphabet
def `sec-websocket-accept` = rule {
`base64-value-non-empty` ~ EOI ~> (`Sec-WebSocket-Accept`(_))
}
def `sec-websocket-extensions` = rule {
oneOrMore(extension).separatedBy(listSep) ~ EOI ~> (`Sec-WebSocket-Extensions`(_))
}
def `sec-websocket-key` = rule {
`base64-value-non-empty` ~ EOI ~> (`Sec-WebSocket-Key`(_))
}
def `sec-websocket-protocol` = rule {
oneOrMore(token).separatedBy(listSep) ~ EOI ~> (`Sec-WebSocket-Protocol`(_))
}
def `sec-websocket-version` = rule {
oneOrMore(version).separatedBy(listSep) ~ EOI ~> (`Sec-WebSocket-Version`(_))
}
private def `base64-value-non-empty` = rule {
capture(oneOrMore(`base64-data`) ~ optional(`base64-padding`) | `base64-padding`)
}
private def `base64-data` = rule { 4.times(`base64-character`) }
private def `base64-padding` = rule {
2.times(`base64-character`) ~ "==" |
3.times(`base64-character`) ~ "="
}
private def `base64-character` = rfc2045Alphabet
private def extension = rule {
`extension-token` ~ zeroOrMore(ws(";") ~ `extension-param`) ~>
((name, params) WebsocketExtension(name, Map(params: _*)))
}
private def `extension-token`: Rule1[String] = token
private def `extension-param`: Rule1[(String, String)] =
rule {
token ~ optional(ws("=") ~ word) ~> ((name: String, value: Option[String]) (name, value.getOrElse("")))
}
private def version = rule {
capture(
NZDIGIT ~ optional(DIGIT ~ optional(DIGIT)) |
DIGIT) ~> (_.toInt)
}
private def NZDIGIT = DIGIT19
}

View file

@ -84,6 +84,9 @@ private[http] object Renderer {
implicit object CharRenderer extends Renderer[Char] {
def render[R <: Rendering](r: R, value: Char): r.type = r ~~ value
}
implicit object IntRenderer extends Renderer[Int] {
def render[R <: Rendering](r: R, value: Int): r.type = r ~~ value
}
implicit object StringRenderer extends Renderer[String] {
def render[R <: Rendering](r: R, value: String): r.type = r ~~ value
}

View file

@ -369,6 +369,44 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
"Range: bytes=0-1, 2-3, -99" =!= Range(ByteRange(0, 1), ByteRange(2, 3), ByteRange.suffix(99))
}
"Sec-WebSocket-Accept" in {
"Sec-WebSocket-Accept: ZGgwOTM0Z2owcmViamRvcGcK" =!= `Sec-WebSocket-Accept`("ZGgwOTM0Z2owcmViamRvcGcK")
}
"Sec-WebSocket-Extensions" in {
"Sec-WebSocket-Extensions: abc" =!=
`Sec-WebSocket-Extensions`(Vector(WebsocketExtension("abc")))
"Sec-WebSocket-Extensions: abc, def" =!=
`Sec-WebSocket-Extensions`(Vector(WebsocketExtension("abc"), WebsocketExtension("def")))
"Sec-WebSocket-Extensions: abc; param=2; use_y, def" =!=
`Sec-WebSocket-Extensions`(Vector(WebsocketExtension("abc", Map("param" -> "2", "use_y" -> "")), WebsocketExtension("def")))
"Sec-WebSocket-Extensions: abc; param=\",xyz\", def" =!=
`Sec-WebSocket-Extensions`(Vector(WebsocketExtension("abc", Map("param" -> ",xyz")), WebsocketExtension("def")))
// real examples from https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19
"Sec-WebSocket-Extensions: permessage-deflate" =!=
`Sec-WebSocket-Extensions`(Vector(WebsocketExtension("permessage-deflate")))
"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits; server_max_window_bits=10" =!=
`Sec-WebSocket-Extensions`(Vector(WebsocketExtension("permessage-deflate", Map("client_max_window_bits" -> "", "server_max_window_bits" -> "10"))))
"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits; server_max_window_bits=10, permessage-deflate; client_max_window_bits" =!=
`Sec-WebSocket-Extensions`(Vector(
WebsocketExtension("permessage-deflate", Map("client_max_window_bits" -> "", "server_max_window_bits" -> "10")),
WebsocketExtension("permessage-deflate", Map("client_max_window_bits" -> ""))))
}
"Sec-WebSocket-Key" in {
"Sec-WebSocket-Key: c2Zxb3JpbmgyMzA5dGpoMDIzOWdlcm5vZ2luCg==" =!= `Sec-WebSocket-Key`("c2Zxb3JpbmgyMzA5dGpoMDIzOWdlcm5vZ2luCg==")
}
"Sec-WebSocket-Protocol" in {
"Sec-WebSocket-Protocol: chat" =!= `Sec-WebSocket-Protocol`(Vector("chat"))
"Sec-WebSocket-Protocol: chat, superchat" =!= `Sec-WebSocket-Protocol`(Vector("chat", "superchat"))
}
"Sec-WebSocket-Version" in {
"Sec-WebSocket-Version: 25" =!= `Sec-WebSocket-Version`(Vector(25))
"Sec-WebSocket-Version: 13, 8, 7" =!= `Sec-WebSocket-Version`(Vector(13, 8, 7))
"Sec-WebSocket-Version: 255" =!= `Sec-WebSocket-Version`(Vector(255))
"Sec-WebSocket-Version: 0" =!= `Sec-WebSocket-Version`(Vector(0))
}
"Set-Cookie" in {
"Set-Cookie: SID=\"31d4d96e407aad42\"" =!=
`Set-Cookie`(HttpCookie("SID", "31d4d96e407aad42")).renderedTo("SID=31d4d96e407aad42")