glyde/websocket/handshake
The RFC 6455 opening handshake, as pure Gleam over BitArray. Client side
only, and it hashes nothing: SHA-1 of the key arrives as a digest.
Sec-WebSocket-Accept is the only evidence the peer understood a WebSocket
request rather than replaying a cached 101, so check verifies it exactly
and refuses anything the server offers that we did not ask for.
Types
The Sec-WebSocket-Accept we expect back. Kept apart from Key so that
passing one where the other goes does not compile: both are short base64,
and swapped they would fail every handshake with nothing to point at.
pub opaque type Accept
Why the bytes are not a response head. Not a Failure: that one is a head
that read fine and does not complete the handshake.
pub type Bad {
HeadNotText
StatusNotHttp(line: String)
HeaderWithoutColon(line: String)
}
Constructors
-
HeadNotText -
StatusNotHttp(line: String) -
HeaderWithoutColon(line: String)
Why a response is not the upgrade we asked for.
pub type Failure {
NotSwitching(status: Int)
MissingHeader(name: String)
WrongHeader(name: String, value: String)
RepeatedHeader(name: String)
UnwantedHeader(name: String, value: String)
}
Constructors
-
NotSwitching(status: Int)Anything other than 101. Discord answers a rate limited upgrade with 429.
-
MissingHeader(name: String) -
WrongHeader(name: String, value: String) -
RepeatedHeader(name: String)The same header twice. A response that answers one of these questions two ways is refused rather than read as whichever copy came first.
-
UnwantedHeader(name: String, value: String)The server agreed to something we never offered. RFC 6455 says to fail.
One header the caller adds to the opening GET. Only header builds one, so
a name that would forge a second header cannot reach this list.
pub opaque type Header
Why a header was refused.
pub type HeaderProblem {
Owned(name: String)
NotAToken(name: String)
Splittable
}
Constructors
-
Owned(name: String)One of the seven the handshake speaks for itself: the five it writes, and the two only a server is allowed to answer with.
-
NotAToken(name: String)Not an RFC 7230 token. A name with a colon in it is the one that matters: the server splits a line on its first colon, so
host: xas a name would arrive as ahostheader however the value is written. -
SplittableA carriage return or a newline in the value, which would end the header and start one the caller did not write.
The Sec-WebSocket-Key we sent. Only key builds one, so the nonce rule is
checked in one place and nothing else can go out as a key.
pub opaque type Key
pub type Next {
Head(response: Response, rest: BitArray)
Partial
Malformed(reason: Bad)
TooLong(bytes: Int)
}
Constructors
-
Head(response: Response, rest: BitArray)restis already frames: a server may send its first in the same packet as the 101. -
PartialThe head is not finished. Read more bytes and
feedthem. -
Malformed(reason: Bad) -
TooLong(bytes: Int)max_byteswent by with no blank line ending the head. Our bound, not the RFC’s.
The bytes of a head as they arrive, and the cap on them. max_bytes lives
in here so it cannot drift between calls, and so that a server which never
sends the blank line is this module’s problem rather than every caller’s.
pub type Reader {
Reader(buffer: BitArray, max_bytes: Int)
}
Constructors
-
Reader(buffer: BitArray, max_bytes: Int)
Values
pub fn accept(digest: BitArray) -> Accept
The Sec-WebSocket-Accept the server must send back, given the SHA-1 of
challenge(key).
pub fn challenge(key: Key) -> BitArray
The bytes whose SHA-1 the server is expected to have taken.
pub fn check(
response: Response,
accept accept: Accept,
) -> Result(Nil, Failure)
Decide whether a response completes the handshake. accept is what
accept/1 produced for the key we sent.
pub fn failure_to_string(failure: Failure) -> String
pub fn header(
name: String,
value: String,
) -> Result(Header, HeaderProblem)
Build a header for request. The name is trimmed and lowercased first.
pub fn key(nonce: BitArray) -> Result(Key, Int)
The Sec-WebSocket-Key for a nonce. RFC 6455 wants 16 random bytes, and
the caller draws them: this module has no random source. The Error hands
back the byte count it was given.
pub fn malformed_to_string(bad: Bad) -> String
pub fn new(max_bytes: Int) -> Reader
max_bytes bounds the head. It also bounds the rescan next does, which
costs work in the square of this number.