+htc #16887 implement Websocket header parsing/rendering
This commit is contained in:
parent
050c0549f3
commit
2cf1c41eef
6 changed files with 233 additions and 1 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue