websocket
nimony/lib/std/websocket.nim
const WsGuid: stringThe GUID of RFC 6455 ยง4.2.2, appended to the key to make the accept.
const WsVersion: stringThe only version this module speaks. RFC 6455 has never had another.
const DefaultMaxMessage: int64The largest message accepted, in bytes. A message is assembled before it is handed out, so this is also the most memory one peer can make the other spend on a message.
const DefaultHandshakeMs: int64How long the client gives the server to answer the handshake.
const DefaultCloseMs: int64How long a side waits for the peer's half of the closing handshake.
const wscNormal: int64The connection did what it was for.
const wscGoingAway: int64One side is going away: a server shutting down, a page unloading.
const wscProtocolError: int64The peer broke the protocol.
const wscUnsupported: int64A message of a type this endpoint does not take.
const wscNoStatus: int64Never sent: what a peer sees when no code was sent at all.
const wscAbnormal: int64Never sent: what a peer sees when the connection went away without a closing handshake.
const wscInvalidPayload: int64A text message that was not UTF-8.
const wscPolicy: int64The message was understood and refused.
const wscTooBig: int64A frame or message was larger than the endpoint accepts.
const wscExtension: int64The client wanted an extension the server did not agree to.
const wscInternalError: int64The endpoint hit a condition of its own that it cannot describe.
const wscTlsError: int64Never sent: the TLS handshake failed, which is a local fact.
type MsgKind = enum mkText = (0, "mkText") mkBinary = (1, "mkBinary") mkPing = (2, "mkPing") mkPong = (3, "mkPong") mkClose = (4, "mkClose")
func dollar`.MsgKind(e: MsgKind): stringtype WsOpcode = HoleyEnumfunc dollar`.WsOpcode(e: WsOpcode): stringtype WsFrame = object fin: bool rsv: int64 opcode: WsOpcode masked: bool key: array[0..3, char] len: int64
func dollar`.WsRole(e: WsRole): stringtype WebSocketReader = object r: Reader role: WsRole maxMessage: int64 idleMs: int64 msgOp: int64 msgBuf: string closeSeen: bool closeCode: int64 failCode: int64 err: ErrorCode
type WebSocketWriter = object w: Writer role: WsRole closeSent: bool closeCode: int64
type WebSocket = object reader: WebSocketReader writer: WebSocketWriter sub: string err: ErrorCode aborted: bool
proc applyMask(data: var openArray[char]; key: array[0..3, char]; offset: int64)XORs
datawith the maskkey, startingoffsetbytes into the payload.Masking is its own inverse, so this both masks and unmasks: a sender applies it before the bytes go out, a receiver after they arrive. The offset is what makes a payload that arrives in pieces come out right โ the key restarts at the front of the frame, not at the front of the read.
proc parseFrameHeader(bytes: openArray[char]; f: var WsFrame): int64How many bytes the frame header at the front of
bytestakes,ParseIncompletewhile it has not all arrived,ParseBadwhen the header breaks the protocol.The length is not checked against a limit here: a peer announcing a length is not the same as a peer sending one, and the caller decides how large a frame it will wait for.
proc frameHeader(opcode: WsOpcode; payloadLen: int64; masked: bool; key: array[0..3, char]; fin: bool): stringThe header of a frame whose payload is
payloadLenbytes.fillHeaderwith a string to put it in: for a caller that needs the bytes on their own, not as part of a frame.proc isSendableCloseCode(code: int64): boolWhether
codemay go on the wire.1005 and 1006 are what a peer sees when no code was sent, and 1015 is a local fact about TLS; sending any of them would be a lie about what happened. 1004 was never defined.
The rest of 1000 to 2999 belongs to the protocol and is not assigned: a peer that sends such a code is not speaking RFC 6455 (ยง7.4.2).
3000 to 3999 are for libraries and frameworks to use, 4000 to 4999 for private use; both are legal on the wire.
proc buffered(rd: WebSocketReader): int64Protocol bytes the reader already has and has not consumed. Non-zero means a
recvwill not wait for the peer, which is what an echo server needs to know before it decides to flush: while frames are still buffered, the replies can wait and go out together.proc deadline(rd: WebSocketReader): DeadlineThe standing deadline of the receive side; the writer shares it.
proc renew(rd: var WebSocketReader; dl: Deadline)Replaces the standing deadline of the connection. Raises
BadOperationfor a closed or default-constructed reader.proc close(rd: var WebSocketReader)Drops the receive side without reading anything else.
proc upgradeWebSocket(c: var HttpConnection; protocols: openArray[string]; maxMessage: int64; idleMs: int64): WebSocketThe WebSocket behind the request
c.The request has to be a WebSocket request โ
GET, HTTP/1.1, anUpgrade: websocket, aConnectionthat saysUpgrade, the version 13 and a key โ and the answer is the 101 that goes out before this returns. A request that is not one gets 400 (405 for the wrong method, 426 with the version this server speaks for a version it does not), and this raises: there is no falling back to HTTP, because a client that asked for a WebSocket and got a page has been told something false.protocolsare the subprotocols this server speaks, in the order it prefers them; the one the client asked for that this server has is named in the 101.maxMessageis the largest message the connection accepts, in bytes.idleMsis how longrecvwaits for the next message, counted afresh for every call; 0 means no limit, which is what a WebSocket usually wants โ a connection with nothing to say is idle, not stuck.The
Originof the request is not this module's business. A server that cares about where a connection comes from โ the cross-site WebSocket hijacking of 2010 was about a server that did not โ reads the header itself and answers before the upgrade, because the answer is then an ordinary HTTP answer:if not allowed(c.requestHeader("Origin")): c.respond(403, "this origin is not welcome\n") return var ws = upgradeWebSocket(c)
The connection is move-only and has to be closed: leaving scope ends it.
proc connectWebSocket(url: string; headers: HttpHeaders; protocols: openArray[string]; maxMessage: int64; timeoutMs: int64; idleMs: int64; tls: ref TlsEngine.Obj): WebSocketA WebSocket to
url, which isws://host:port/pathโ orwss://for TLS.headersare extra request headers, in their wire format, and are put on the handshake unchanged;protocolsare offered asSec-WebSocket-Protocol, in this order, and the one the server picks issubprotocol.timeoutMsbounds the whole handshake โ connect, TLS and the answer โ andidleMsis how longrecvwaits for the next message, counted afresh for every call, 0 for no limit.A
wss://URL needs a TLS engine:std/tls'snewTlsEngine()is the one to pass, and this module does not import it, so that a server that only listens pays for none of it. Awss://URL without an engine, or an engine on aws://URL, is aBadOperation: one would speak plaintext to a TLS port, the other run a handshake the URL never asked for.The answer is checked the way RFC 6455 ยง4.1 says a client has to: the status is 101,
UpgradeandConnectionsay what they should, andSec-WebSocket-Acceptis the accept of the key this call made up. Anything else is aProtocolError(orBadProtocolfor a status that is not 101) โ a server that does not upgrade did not, and pretending otherwise turns a protocol mismatch into a stream of nonsense frames.proc reader(ws: var WebSocket): var WebSocketReaderThe receiving half of the connection. The pair's
recvis the usual entry point: it answers the pings and closes this half reports.proc writer(ws: var WebSocket): var WebSocketWriterThe sending half of the connection. The pair's
sendis the usual entry point: it is the only place that knows the close handshake.proc closeCode(ws: WebSocket): int64The close code of the connection: the one the peer sent, the one that was sent, or 0 while the connection is still open.
proc closeCode(rd: WebSocketReader): int64The close code the peer sent, or 0 while no close has been seen.
proc subprotocol(ws: WebSocket): stringThe subprotocol the handshake agreed on;
""when there is none.proc lastError(ws: WebSocket): ErrorCodeThe error that ended the connection, if one did.
proc lastError(rd: WebSocketReader): ErrorCodeThe error that ended the receive side, if one did.
proc abort(wr: var WebSocketWriter)Drops the sending side without a close frame.
proc abort(ws: var WebSocket)Drops the connection without a closing handshake.
The peer sees the connection go away without a goodbye, which is what an error looks like from its side. Use it when the connection is not worth a clean end โ and see the note at the end of this module about the trap it is there to avoid.
proc send(wr: var WebSocketWriter; kind: MsgKind; data: openArray[char]; flush: bool)Sends a message of
kindwith the bytesdata.Text has to be text: a
mkTextwhose bytes are not UTF-8 raises rather than goes out, because the peer would have to fail the connection over it. A ping or a pong carries at most 125 bytes. Nothing may follow a close frame, so after one this raisesBadOperation.With
flush = falsethe frame is only put in the connection's buffer and goes out with the nextflush, or with the next frame that asks for one, or when the buffer fills. That is what makes a fan-out cheap: a task with several frames for one connection writes them all with one syscall โ and the wait for that syscall is the backpressure that lets the next batch exist. A writer that stops writing has toflush;closedoes, and so does the pair'srecvbefore it parks.proc flush(wr: var WebSocketWriter)Hands everything sent with
flush = falseto the kernel.A flush of an empty buffer is free โ it does not reach the descriptor โ so this is safe to call whenever a batch of frames is finished.
proc close(wr: var WebSocketWriter; code: int64; reason: string)Sends the close frame and nothing else. Waiting for the peer's half is the pair's job; this is for a caller that only drives the writer.
proc recv(rd: var WebSocketReader; msg: var string): MsgKindThe next message of the receive side, in
msg.A text or binary message is its payload. A ping, a pong and a close are handed over as
mkPing,mkPongandmkClose: answering a ping and echoing a close need the writer, so that is the pair's business. A text message is checked as a whole; UTF-8 may be split across fragments.The fragments of a message are put back together here. A protocol error raises
ProtocolErrorwithfailCodenaming the close code to send; the caller of a bare reader has to send it (the pair does).With an
idleMs, each call gets that long: it replaces whatever deadline the connection had.proc recv(ws: var WebSocket; msg: var string): MsgKindThe next message of the connection, in
msg.A text or binary message is its payload. A ping is answered with a pong and not handed over โ the peer asked whether the connection is alive, and the answer is not the application's business. A pong and a close are handed over: the first is the answer to a ping of this side's, the second ends the conversation.
proc send(ws: var WebSocket; kind: MsgKind; data: openArray[char]; flush: bool)Sends a message of
kindwith the bytesdata; seeWebSocketWriter.send.With
flush = falsethe frames are only buffered; seeWebSocketWriter.send, andflushbelow.proc flush(ws: var WebSocket)Hands everything sent with
flush = falseto the peer; seeWebSocketWriter.flush.proc close(ws: var WebSocket; code: int64; reason: string)Ends the connection the way the protocol says: the close frame goes out, and then this side waits for the peer's close.
The wait is the connection's
idleMs, or five seconds when it has none. A peer that does not answer is a timeout and raises like every other timeout in this API.proc =destroy(ws: WebSocket)proc =wasMoved(ws: var WebSocket).nodestroy: the halves have been handed to the destination, so clearing this one must not run their destructors on them. A moved-from connection has no reader, writer or subprotocol of its own.