From 28cc44c8defc0b248b3abbc8205759b69a98f7f6 Mon Sep 17 00:00:00 2001 From: hlaaftana <10591326+hlaaftana@users.noreply.github.com> Date: Sat, 2 May 2020 19:51:35 +0300 Subject: [PATCH] 0.5.0 (#68) * made Frame an object, the bools and the opcode are now bitfields * masking key can be an array with `-d:websocketStricterMaskingKey` * added error enum for server header verification * NEP-1 fixes * documentation improvements * undeprecate some procs * readme doc link now doesn't have version in the link to ease updating versions --- README.md | 2 +- examples/clientexample.nim | 2 +- examples/serverexample.nim | 6 +- websocket.nimble | 2 +- websocket/client.nim | 4 +- websocket/private/hex.nim | 10 ++- websocket/server.nim | 57 ++++++++++---- websocket/shared.nim | 149 ++++++++++++++++++++++--------------- 8 files changed, 143 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 2308c74..2eb71da 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,4 @@ and features are missing - namely: * A robust RNG for the client data masking -See [the API docs](http://niv.github.io/websocket.nim/docs/0.3.0/websocket.html) for usage instructions. +See [the API docs](http://niv.github.io/websocket.nim/docs/websocket.html) for usage instructions. diff --git a/examples/clientexample.nim b/examples/clientexample.nim index d5700e5..587ba3a 100644 --- a/examples/clientexample.nim +++ b/examples/clientexample.nim @@ -1,4 +1,4 @@ -import ../websocket, asyncnet, asyncdispatch +import ../websocket, asyncdispatch let ws = waitFor newAsyncWebsocketClient("localhost", Port(8080), path = "/", protocols = @["myfancyprotocol"]) diff --git a/examples/serverexample.nim b/examples/serverexample.nim index b043c3f..4c29341 100644 --- a/examples/serverexample.nim +++ b/examples/serverexample.nim @@ -7,7 +7,7 @@ proc cb(req: Request) {.async.} = if ws.isNil: echo "WS negotiation failed: ", error - await req.respond(Http400, "Websocket negotiation failed: " & error) + await req.respond(Http400, "Websocket negotiation failed: " & $error) req.client.close() return @@ -19,9 +19,9 @@ proc cb(req: Request) {.async.} = case opcode of Opcode.Text: - waitFor ws.sendText("thanks for the data!") + await ws.sendText("thanks for the data!") of Opcode.Binary: - waitFor ws.sendBinary(data) + await ws.sendBinary(data) of Opcode.Close: asyncCheck ws.close() let (closeCode, reason) = extractCloseData(data) diff --git a/websocket.nimble b/websocket.nimble index 41f5a16..0229c12 100644 --- a/websocket.nimble +++ b/websocket.nimble @@ -1,6 +1,6 @@ # Package -version = "0.4.2" +version = "0.5.0" author = "niv" description = "websockets for nim" license = "MIT" diff --git a/websocket/client.nim b/websocket/client.nim index f73383a..ff278cf 100644 --- a/websocket/client.nim +++ b/websocket/client.nim @@ -67,8 +67,8 @@ proc newAsyncWebsocketClient*(uri: Uri, client: AsyncHttpClient, of "wss": uri.scheme = "https" else: - raise newException(ProtocolError, - "uri scheme has to be 'ws' for plaintext or 'wss' for websocket over ssl.") + raise newException(ProtocolError, "uri scheme has to be " & + "'ws' for plaintext or 'wss' for websocket over ssl.") var headers = newHttpHeaders({ "Connection": "Upgrade", diff --git a/websocket/private/hex.nim b/websocket/private/hex.nim index 07906f1..f7fce2e 100644 --- a/websocket/private/hex.nim +++ b/websocket/private/hex.nim @@ -8,16 +8,20 @@ proc nibbleFromChar(c: char): int = proc decodeHex*(str: string): string = result = newString(str.len div 2) for i in 0.. 0: + if error != HeaderVerificationError.None: return (nil, error) await client.send(handshake) @@ -120,11 +145,11 @@ proc verifyWebsocketRequest*( sock: client, protocol: protocol ), - "" + None ) proc verifyWebsocketRequest*( req: asynchttpserver.Request, protocol = "" -): Future[tuple[ws: AsyncWebSocket, error: string]] = +): Future[tuple[ws: AsyncWebSocket, error: HeaderVerificationError]] = ## Convenience wrapper for AsyncHttpServer requests. return verifyWebsocketRequest(req.client, req.headers, protocol) diff --git a/websocket/shared.nim b/websocket/shared.nim index e6d7ceb..42366bd 100644 --- a/websocket/shared.nim +++ b/websocket/shared.nim @@ -1,46 +1,61 @@ import asyncdispatch, asyncnet, streams, nativesockets, strutils, times, oids, random, endians -when (NimMajor, NimMinor) >= (0, 19): +when declared(system.Defect): type ProtocolError* = object of Defect else: type ProtocolError* = object of Exception +when defined(websocketStricterMaskingKey): + type MaskingKey* = array[4, byte] +else: + type MaskingKey* = string + ## The type used as a masking key. This is `string` by default, use + ## ``-d:websocketStricterMaskingKey`` to switch to `array[4, char]`. + type Opcode* {.pure.} = enum Cont = 0x0 ## Continued Frame (when the previous was fin = 0) Text = 0x1 ## Text frames need to be valid UTF-8 Binary = 0x2 ## Binary frames can be anything. - Close = 0x8 ## Socket is being closed by the remote, or we intend to close it. + Close = 0x8 ## The remote is closing the socket or we intend to close it. Ping = 0x9 ## Ping - Pong = 0xa ## Pong. Needs to echo back the app data in ping. + Pong = 0xa + ## Pong. Needs to echo back the app data in ping. + ## This library handles these by default but you can use + ## ``-d:websocketIgnorePing`` to handle them yourself. - Frame* = tuple + Frame* = object ## A frame read off the netlayer. - fin: bool ## Last frame in current packet. - rsv1, rsv2, rsv3: bool ## Extension data: negotiated in http prequel, or 0. + fin {.bitsize: 1.}: bool ## Last frame in current packet. + rsv1 {.bitsize: 1.}, rsv2 {.bitsize: 1.}, rsv3 {.bitsize: 1.}: bool + ## Extension data: negotiated in http prequel, or 0. - masked: bool ## If the frame was received masked/is supposed to be masked. + masked {.bitsize: 1.}: bool + ## If the frame was received masked/is supposed to be masked. - # array[4, byte]? array[4, char]? - maskingKey: string ## The masking key if the frame is supposed to be masked. - ## If masked is false, this is an empty string. - ## Otherwise, length is 4. + opcode {.bitsize: 4.}: Opcode ## The opcode of this frame. - opcode: Opcode ## The opcode of this frame. + maskingKey: MaskingKey + ## The masking key if the frame is supposed to be masked. + ## If masked is false, this is an empty string. + ## Otherwise, length is 4. data: string ## App data SocketKind* {.pure.} = enum Client, Server - AsyncWebSocketObj = object of RootObj + AsyncWebSocketObj* = object sock*: AsyncSocket protocol*: string kind*: SocketKind AsyncWebSocket* = ref AsyncWebSocketObj + ## A small wrapper around asyncnet.AsyncSocket that keeps track of + ## whether the socket is a client or server and a string of the protocol + ## being used to communicate. const MaxHeaderSize = 14 @@ -66,24 +81,28 @@ proc htonll(x: uint64): uint64 = (x shl 40'u64 and 0xff000000000000'u64) or (x shl 56'u64) -proc mask*(data: var string, maskingKey: string) = +proc mask*(data: var string, maskingKey: MaskingKey) = for i in 0 ..< data.len: data[i] = (data[i].uint8 xor maskingKey[i mod 4].uint8).char -template unmask*(data: var string, maskingKey: string): auto = +template unmask*(data: var string, maskingKey: MaskingKey): auto = mask(data, maskingKey) -proc generateMaskingKey*: string = +proc generateMaskingKey*: MaskingKey = + ## Uses the `random` module of the standard library to generate + ## a random 4 character string. Used by default. Not secure. when defined(websocketUnmaskedByDefault): result = "" else: - template rnd: untyped = - when declared(random.rand): - rand(255).char + type Octet = typeof(result[0]) + template rnd: Octet = + Octet(when declared(random.rand): + rand(255) else: - random(256).char + random(256)) - result = newString(4) + when MaskingKey is string: + result = newString(4) result[0] = rnd result[1] = rnd result[2] = rnd @@ -130,18 +149,13 @@ proc makeFrame*(f: Frame): string = if data.len > 0: copyMem(addr result[headerLen], addr data[0], data.len) -proc makeFrame*(opcode: Opcode, data: string, maskingKey = generateMaskingKey()): string = +proc makeFrame*(opcode: Opcode, data: string, + maskingKey = generateMaskingKey()): string = ## A convenience shorthand. - result = makeFrame((fin: true, rsv1: false, rsv2: false, rsv3: false, + result = makeFrame(Frame(fin: true, rsv1: false, rsv2: false, rsv3: false, masked: maskingKey.len != 0, maskingKey: maskingKey, opcode: opcode, data: data)) -proc makeFrame*(opcode: Opcode, data: string, masked: bool): string {.deprecated.} = - ## A convenience shorthand. - ## **Deprecated since 0.3.2**: Frames should always be masked, either - ## call makeFrame(opcode, data) or call makeFrame(opcode, data, maskingKey). - result = makeFrame(opcode, data, if masked: generateMaskingKey() else: "") - proc raiseReadException(ws: AsyncSocket, kind: typedesc, msg: string) = raise newException(kind, msg) @@ -196,9 +210,14 @@ proc recvFrame*(ws: AsyncSocket): Future[Frame] {.async.} = f.masked = (b1 and 0x80) == 0x80 if f.masked: - f.maskingKey = await ws.recv(4, {}) + let keyData = await ws.recv(4, {}) + when MaskingKey is string: + f.maskingKey = keyData + else: + f.maskingKey = [keyData[0].byte, keyData[1].byte, + keyData[2].byte, keyData[3].byte] else: - f.maskingKey = "" + reset(f.maskingKey) f.data = await ws.recv(finalLen, {}) if f.data.len != finalLen: ws.raiseReadException(IOError, "socket closed") @@ -222,7 +241,9 @@ proc extractCloseData*(data: string): tuple[code: int, reason: string] = # key is the socket fd type PingRequest = Future[void] # tuple[data: string, fut: Future[void]] -when not defined(websocketIgnorePing): +const respondToPing = not defined(websocketIgnorePing) + +when respondToPing: import tables, options var reqPing {.threadvar.}: Option[Table[int, PingRequest]] @@ -234,14 +255,14 @@ proc readData*(ws: AsyncSocket): ## Note: You will still see control frames, but they are all handled for you ## (Ping/Pong, Cont, Close, and so on). ## - ## The only ones you need to care about are Opcode.Text and Opcode.Binary, the - ## so-called application frames. + ## The only ones you need to care about are Opcode.Text and Opcode.Binary, + ## the so-called application frames. ## ## Will raise IOError when the socket disconnects and ProtocolError on any ## websocket-related issues. var resultData = "" - when not defined(websocketIgnorePing): + when respondToPing: if reqPing.isNone: reqPing = some(initTable[int, PingRequest]()) @@ -254,11 +275,11 @@ proc readData*(ws: AsyncSocket): case f.opcode of Opcode.Ping: - when not defined(websocketIgnorePing): + when respondToPing: await ws.send(makeFrame(Opcode.Pong, f.data)) of Opcode.Pong: - when not defined(websocketIgnorePing): + when respondToPing: if pingTable.hasKey(ws.getFD().AsyncFD.int): pingTable[ws.getFD().AsyncFD.int].complete() of Opcode.Text, Opcode.Binary, Opcode.Close, Opcode.Cont: @@ -266,7 +287,7 @@ proc readData*(ws: AsyncSocket): if not f.fin: continue # handle case: ping never arrives and client closes the connection - when not defined(websocketIgnorePing): + when respondToPing: if f.opcode == Opcode.Close and pingTable.hasKey(ws.getFD().AsyncFD.int): let closeData = extractCloseData(resultData) let ex = newException(IOError, "socket closed while waiting for pong") @@ -280,37 +301,37 @@ proc readData*(ws: AsyncSocket): return (f.opcode, resultData) proc sendText*( - ws: AsyncSocket, p: string, + sock: AsyncSocket, p: string, maskingKey = generateMaskingKey() -): Future[void] {.async, deprecated: "Use the AsyncWebSocket variant instead".} = +): Future[void] {.async.} = ## Sends text data. Will only return after all data has been sent out. - await ws.send(makeFrame(Opcode.Text, p, maskingKey)) + await sock.send(makeFrame(Opcode.Text, p, maskingKey)) proc sendBinary*( - ws: AsyncSocket, p: string, maskingKey = generateMaskingKey() -): Future[void] {.async, deprecated: "Use the AsyncWebSocket variant instead".} = + sock: AsyncSocket, p: string, maskingKey = generateMaskingKey() +): Future[void] {.async.} = ## Sends binary data. Will only return after all data has been sent out. - await ws.send(makeFrame(Opcode.Binary, p, maskingKey)) + await sock.send(makeFrame(Opcode.Binary, p, maskingKey)) proc sendPing*( - ws: AsyncSocket, maskingKey = generateMaskingKey(), token: string = "" -): Future[void] {.async, deprecated: "Use the AsyncWebSocket variant instead".} = + sock: AsyncSocket, maskingKey = generateMaskingKey(), token: string = "" +): Future[void] {.async.} = ## Sends a WS ping message. ## Will generate a suitable token if you do not provide one. let pingId = if token == "": $genOid() else: token - await ws.send(makeFrame(Opcode.Ping, pingId, maskingKey)) + await sock.send(makeFrame(Opcode.Ping, pingId, maskingKey)) -proc sendChain*(ws: AsyncSocket, p: seq[string], opcode = Opcode.Text, maskingKeys: seq[string] = @[]): Future[void] {.async.} = +proc sendChain*(sock: AsyncSocket, p: seq[string], opcode = Opcode.Text, maskingKeys: seq[MaskingKey] = @[]): Future[void] {.async.} = ## Sends data over multiple frames. Will only return after all data has been sent out. for i, data in p: let maskKey = if i < maskingKeys.len: maskingKeys[i] else: generateMaskingKey() - let f: Frame = (fin: i == p.high, + let f = Frame(fin: i == p.high, rsv1: false, rsv2: false, rsv3: false, masked: maskKey.len != 0, maskingKey: maskKey, opcode: if i == 0: opcode else: Opcode.Cont, data: data) - await ws.send(makeFrame(f)) + await sock.send(makeFrame(f)) proc readData*(ws: AsyncWebSocket): Future[tuple[opcode: Opcode, data: string]] = ## Reads reassembled data off the websocket and give you joined frame data. @@ -329,27 +350,30 @@ proc readData*(ws: AsyncWebSocket): Future[tuple[opcode: Opcode, data: string]] proc sendText*(ws: AsyncWebSocket, p: string, maskingKey = generateMaskingKey()): Future[void] = ## Sends text data. Will only return after all data has been sent out. let maskingKey = - if ws.kind == Server: "" + if ws.kind == Server: default(MaskingKey) else: maskingKey - result = ws.sock.send(makeFrame(Opcode.Text, p, maskingKey)) + result = ws.sock.sendBinary(p, maskingKey) -proc sendBinary*(ws: AsyncWebSocket, p: string, maskingKey = generateMaskingKey()): Future[void] = +proc sendBinary*(ws: AsyncWebSocket, p: string, + maskingKey = generateMaskingKey()): Future[void] {.inline.} = ## Sends binary data. Will only return after all data has been sent out. let maskingKey = - if ws.kind == Server: "" + if ws.kind == Server: default(MaskingKey) else: maskingKey - result = ws.sock.send(makeFrame(Opcode.Binary, p, maskingKey)) + result = ws.sock.sendBinary(p, maskingKey) -proc sendPing*(ws: AsyncWebSocket, maskingKey = generateMaskingKey(), token: string = "") {.async.} = +proc sendPing*(ws: AsyncWebSocket, maskingKey = generateMaskingKey(), + token: string = ""): Future[void] {.inline.} = ## Sends a WS ping message. ## Will generate a suitable token if you do not provide one. + ## let maskingKey = - if ws.kind == Server: "" + if ws.kind == Server: default(MaskingKey) else: maskingKey - let pingId = if token == "": $genOid() else: token - await ws.sock.send(makeFrame(Opcode.Ping, pingId, maskingKey)) + result = ws.sock.sendPing(maskingKey, token) -proc closeWebsocket*(ws: AsyncSocket, code = 0, reason = ""): Future[void] {.async.} = +proc closeWebsocket*(ws: AsyncSocket, code = 0, + reason = ""): Future[void] {.async.} = ## Closes the socket. defer: @@ -369,11 +393,12 @@ proc closeWebsocket*(ws: AsyncSocket, code = 0, reason = ""): Future[void] {.asy 1000 ) -proc close*(ws: AsyncWebSocket, code = 0, reason = ""): Future[void] = +proc close*(ws: AsyncWebSocket, code = 0, + reason = ""): Future[void] {.inline.} = ## Closes the socket. result = ws.sock.closeWebsocket(code, reason) -when isMainModule: +when isMainModule and not defined(websocketStricterMaskingKey): block test1: let expected = "\129\139key1#\0\21]\4E.^\25\9\29" let got = makeFrame(Opcode.Text, "Hello World", "key1")