Skip to content

Commit

Permalink
0.5.0 (#68)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
metagn committed May 2, 2020
1 parent b246341 commit 28cc44c
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 89 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion examples/clientexample.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ../websocket, asyncnet, asyncdispatch
import ../websocket, asyncdispatch

let ws = waitFor newAsyncWebsocketClient("localhost", Port(8080),
path = "/", protocols = @["myfancyprotocol"])
Expand Down
6 changes: 3 additions & 3 deletions examples/serverexample.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion websocket.nimble
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Package

version = "0.4.2"
version = "0.5.0"
author = "niv"
description = "websockets for nim"
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions websocket/client.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 7 additions & 3 deletions websocket/private/hex.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@ proc nibbleFromChar(c: char): int =
proc decodeHex*(str: string): string =
result = newString(str.len div 2)
for i in 0..<result.len:
result[i] = chr((nibbleFromChar(str[2 * i]) shl 4) or nibbleFromChar(str[2 * i + 1]))
result[i] = chr((nibbleFromChar(str[2 * i]) shl 4) or
nibbleFromChar(str[2 * i + 1]))

proc nibbleToChar(nibble: int): char {.inline.} =
const byteMap = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
const byteMap = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'
]
return byteMap[nibble]

proc encodeHex*(str: string): string =
result = newString(str.len * 2)
for i in 0..<str.len:
let a = ord(str[i]) shr 4 and 0x0f
let a = ord(str[i]) shr 4
let b = ord(str[i]) and 0x0f
result[i * 2] = nibbleToChar(a)
result[i * 2 + 1] = nibbleToChar(b)
57 changes: 41 additions & 16 deletions websocket/server.nim
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
##
## 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
##
Expand All @@ -23,9 +23,9 @@
##
## 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)
Expand All @@ -48,33 +48,58 @@ import private/hex

import shared

type HeaderVerificationError* {.pure.} = enum
None
## No error.
UnsupportedVersion
## The Sec-Websocket-Version header gave an unsupported version.
## The only currently supported version is 13.
NoKey
## No Sec-Websocket-Key was provided.
ProtocolAdvertised
## A protocol was advertised but the server gave no protocol.
NoProtocolsSupported
## None of the advertised protocols match the server protocol.
NoProtocolAdvertised
## Server asked for a protocol but no protocol was advertised.

proc `$`*(error: HeaderVerificationError): string =
const errorTable: array[HeaderVerificationError, string] = [
"no error",
"the only supported sec-websocket-version is 13",
"no sec-websocket-key provided",
"server does not support protocol negotation",
"no advertised protocol supported",
"no protocol advertised"
]
result = errorTable[error]

proc makeHandshakeResponse*(key, protocol: string): string =
let sh = secureHash(key & "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
let acceptKey = decodeHex($sh).encode

result = "HTTP/1.1 101 Web Socket Protocol Handshake\c\L"
result.add("Sec-Websocket-Accept: " & acceptKey & "\c\L")
result.add("Connection: Upgrade\c\L")
result.add("Upgrade: websocket\c\L")
result.add("Connection: Upgrade\c\LUpgrade: websocket\c\L")
if protocol.len != 0:
result.add("Sec-Websocket-Protocol: " & protocol & "\c\L")
result.add "\c\L"

proc verifyHeaders(
headers: HttpHeaders, protocol: string
): tuple[handshake: string, error: string] =
): tuple[handshake: string, error: HeaderVerificationError] =
# if headers.hasKey("sec-websocket-extensions"):
# TODO: transparently support extensions

if headers.getOrDefault("sec-websocket-version") != "13":
return ("", "the only supported sec-websocket-version is 13")
return ("", UnsupportedVersion)

if not headers.hasKey("sec-websocket-key"):
return ("", "no sec-websocket-key provided")
return ("", NoKey)

if headers.hasKey("sec-websocket-protocol"):
if protocol.len == 0:
return ("", "server does not support protocol negotation")
return ("", ProtocolAdvertised)

block protocolCheck:
let prot = protocol.toLowerAscii()
Expand All @@ -83,15 +108,15 @@ proc verifyHeaders(
if prot == it.strip.toLowerAscii():
break protocolCheck

return ("", "no advertised protocol supported; server speaks `" & protocol & "`")
return ("", NoProtocolsSupported)
elif protocol.len != 0:
return ("", "no protocol advertised, but server demands `" & protocol & "`")
return ("", NoProtocolAdvertised)

return (makeHandshakeResponse(headers["sec-websocket-key"], protocol), "")
return (makeHandshakeResponse(headers["sec-websocket-key"], protocol), None)

proc verifyWebsocketRequest*(
client: AsyncSocket, headers: HttpHeaders, protocol = ""
): Future[tuple[ws: AsyncWebSocket, error: string]] {.async.} =
): Future[tuple[ws: AsyncWebSocket, error: HeaderVerificationError]] {.async.} =
## Verifies the request is a websocket request:
## * Supports protocol version 13 only
## * Does not support extensions (yet)
Expand All @@ -109,7 +134,7 @@ proc verifyWebsocketRequest*(
## After successful negotiation, you can immediately start sending/reading
## websocket frames.
let (handshake, error) = verifyHeaders(headers, protocol)
if error.len > 0:
if error != HeaderVerificationError.None:
return (nil, error)

await client.send(handshake)
Expand All @@ -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)
Loading

0 comments on commit 28cc44c

Please sign in to comment.