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: x as a name would arrive as a host header however the value is written.

  • Splittable

    A 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)

    rest is already frames: a server may send its first in the same packet as the 101.

  • Partial

    The head is not finished. Read more bytes and feed them.

  • Malformed(reason: Bad)
  • TooLong(bytes: Int)

    max_bytes went 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)

The head only. A 101 has no body.

pub type Response {
  Response(status: Int, headers: List(#(String, String)))
}

Constructors

  • Response(status: Int, headers: List(#(String, String)))

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 feed(reader: Reader, bytes: BitArray) -> Reader
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.

pub fn next(reader: Reader) -> Next

Read the response head, if all of it has arrived. Header names come back lowercased and trimmed; values keep their case, since accept is base64.

pub fn request(
  host host: String,
  port port: Int,
  path path: String,
  key key: Key,
  headers headers: List(Header),
) -> BitArray

The bytes of the opening GET. path is sent as given, query string and all.

Search Document