WebSocket: Nim parsing library

The WebSocket protocol establishes a two-way communication channel via TCP. Messages are made up of one or more dataframes, and are delineated by frames with the fin bit set.

KS implementation details

License: CC0-1.0

References

This page hosts a formal specification of WebSocket using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Nim source code to parse WebSocket

websocket.nim

import kaitai_struct_nim_runtime
import options

type
  Websocket* = ref object of KaitaiStruct
    `initialFrame`*: Websocket_InitialFrame
    `trailingFrames`*: seq[Websocket_Dataframe]
    `parent`*: KaitaiStruct
  Websocket_Opcode* = enum
    continuation = 0
    text = 1
    binary = 2
    reserved_3 = 3
    reserved_4 = 4
    reserved_5 = 5
    reserved_6 = 6
    reserved_7 = 7
    close = 8
    ping = 9
    pong = 10
    reserved_control_b = 11
    reserved_control_c = 12
    reserved_control_d = 13
    reserved_control_e = 14
    reserved_control_f = 15
  Websocket_FrameHeader* = ref object of KaitaiStruct
    `finished`*: bool
    `reserved`*: uint64
    `opcode`*: Websocket_Opcode
    `isMasked`*: bool
    `lenPayloadPrimary`*: uint64
    `lenPayloadExtended1`*: uint16
    `lenPayloadExtended2`*: uint32
    `maskKey`*: uint32
    `parent`*: KaitaiStruct
    `lenPayloadInst`: int
    `lenPayloadInstFlag`: bool
  Websocket_InitialFrame* = ref object of KaitaiStruct
    `header`*: Websocket_FrameHeader
    `payloadBytes`*: seq[byte]
    `payloadText`*: string
    `parent`*: Websocket
  Websocket_Dataframe* = ref object of KaitaiStruct
    `header`*: Websocket_FrameHeader
    `payloadBytes`*: seq[byte]
    `payloadText`*: string
    `parent`*: Websocket

proc read*(_: typedesc[Websocket], io: KaitaiStream, root: KaitaiStruct, parent: KaitaiStruct): Websocket
proc read*(_: typedesc[Websocket_FrameHeader], io: KaitaiStream, root: KaitaiStruct, parent: KaitaiStruct): Websocket_FrameHeader
proc read*(_: typedesc[Websocket_InitialFrame], io: KaitaiStream, root: KaitaiStruct, parent: Websocket): Websocket_InitialFrame
proc read*(_: typedesc[Websocket_Dataframe], io: KaitaiStream, root: KaitaiStruct, parent: Websocket): Websocket_Dataframe

proc lenPayload*(this: Websocket_FrameHeader): int


##[
The WebSocket protocol establishes a two-way communication channel via TCP.
Messages are made up of one or more dataframes, and are delineated by
frames with the `fin` bit set.

]##
proc read*(_: typedesc[Websocket], io: KaitaiStream, root: KaitaiStruct, parent: KaitaiStruct): Websocket =
  template this: untyped = result
  this = new(Websocket)
  let root = if root == nil: cast[Websocket](this) else: cast[Websocket](root)
  this.io = io
  this.root = root
  this.parent = parent

  let initialFrameExpr = Websocket_InitialFrame.read(this.io, this.root, this)
  this.initialFrame = initialFrameExpr
  if this.initialFrame.header.finished != true:
    block:
      var i: int
      while true:
        let it = Websocket_Dataframe.read(this.io, this.root, this)
        this.trailingFrames.add(it)
        if it.header.finished:
          break
        inc i

proc fromFile*(_: typedesc[Websocket], filename: string): Websocket =
  Websocket.read(newKaitaiFileStream(filename), nil, nil)

proc read*(_: typedesc[Websocket_FrameHeader], io: KaitaiStream, root: KaitaiStruct, parent: KaitaiStruct): Websocket_FrameHeader =
  template this: untyped = result
  this = new(Websocket_FrameHeader)
  let root = if root == nil: cast[Websocket](this) else: cast[Websocket](root)
  this.io = io
  this.root = root
  this.parent = parent

  let finishedExpr = this.io.readBitsIntBe(1) != 0
  this.finished = finishedExpr
  let reservedExpr = this.io.readBitsIntBe(3)
  this.reserved = reservedExpr
  let opcodeExpr = Websocket_Opcode(this.io.readBitsIntBe(4))
  this.opcode = opcodeExpr
  let isMaskedExpr = this.io.readBitsIntBe(1) != 0
  this.isMasked = isMaskedExpr
  let lenPayloadPrimaryExpr = this.io.readBitsIntBe(7)
  this.lenPayloadPrimary = lenPayloadPrimaryExpr
  alignToByte(this.io)
  if this.lenPayloadPrimary == 126:
    let lenPayloadExtended1Expr = this.io.readU2be()
    this.lenPayloadExtended1 = lenPayloadExtended1Expr
  if this.lenPayloadPrimary == 127:
    let lenPayloadExtended2Expr = this.io.readU4be()
    this.lenPayloadExtended2 = lenPayloadExtended2Expr
  if this.isMasked:
    let maskKeyExpr = this.io.readU4be()
    this.maskKey = maskKeyExpr

proc lenPayload(this: Websocket_FrameHeader): int = 
  if this.lenPayloadInstFlag:
    return this.lenPayloadInst
  let lenPayloadInstExpr = int((if this.lenPayloadPrimary <= 125: this.lenPayloadPrimary else: (if this.lenPayloadPrimary == 126: this.lenPayloadExtended1 else: this.lenPayloadExtended2)))
  this.lenPayloadInst = lenPayloadInstExpr
  this.lenPayloadInstFlag = true
  return this.lenPayloadInst

proc fromFile*(_: typedesc[Websocket_FrameHeader], filename: string): Websocket_FrameHeader =
  Websocket_FrameHeader.read(newKaitaiFileStream(filename), nil, nil)

proc read*(_: typedesc[Websocket_InitialFrame], io: KaitaiStream, root: KaitaiStruct, parent: Websocket): Websocket_InitialFrame =
  template this: untyped = result
  this = new(Websocket_InitialFrame)
  let root = if root == nil: cast[Websocket](this) else: cast[Websocket](root)
  this.io = io
  this.root = root
  this.parent = parent

  let headerExpr = Websocket_FrameHeader.read(this.io, this.root, this)
  this.header = headerExpr
  if this.header.opcode != websocket.text:
    let payloadBytesExpr = this.io.readBytes(int(this.header.lenPayload))
    this.payloadBytes = payloadBytesExpr
  if this.header.opcode == websocket.text:
    let payloadTextExpr = encode(this.io.readBytes(int(this.header.lenPayload)), "UTF-8")
    this.payloadText = payloadTextExpr

proc fromFile*(_: typedesc[Websocket_InitialFrame], filename: string): Websocket_InitialFrame =
  Websocket_InitialFrame.read(newKaitaiFileStream(filename), nil, nil)

proc read*(_: typedesc[Websocket_Dataframe], io: KaitaiStream, root: KaitaiStruct, parent: Websocket): Websocket_Dataframe =
  template this: untyped = result
  this = new(Websocket_Dataframe)
  let root = if root == nil: cast[Websocket](this) else: cast[Websocket](root)
  this.io = io
  this.root = root
  this.parent = parent

  let headerExpr = Websocket_FrameHeader.read(this.io, this.root, this)
  this.header = headerExpr
  if Websocket(this.root).initialFrame.header.opcode != websocket.text:
    let payloadBytesExpr = this.io.readBytes(int(this.header.lenPayload))
    this.payloadBytes = payloadBytesExpr
  if Websocket(this.root).initialFrame.header.opcode == websocket.text:
    let payloadTextExpr = encode(this.io.readBytes(int(this.header.lenPayload)), "UTF-8")
    this.payloadText = payloadTextExpr

proc fromFile*(_: typedesc[Websocket_Dataframe], filename: string): Websocket_Dataframe =
  Websocket_Dataframe.read(newKaitaiFileStream(filename), nil, nil)