Skip to content

Commit

Permalink
Httpbeast Support (#16)
Browse files Browse the repository at this point in the history
* Make masking work like the spec.
* Add jester sender
* Add HTTP Beast support.
* Fix tests.
* v0.4.0
  • Loading branch information
treeform authored Dec 4, 2019
1 parent b1bfb01 commit 116964a
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 54 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
echo "export PATH=~/.nimble/bin:$PATH" >> ~/.profile
choosenim stable
nim c src/ws.nim
nim c -r tests/test.nim
nim c tests/chat.nim
nim c tests/chat.nim
nim c tests/echo.nim
Expand All @@ -23,8 +24,7 @@ jobs:
nim c tests/sender_ping.nim
nim c tests/sender_protocol.nim
nim c tests/sender_wss.nim
nim c tests/test.nim
nim c tests/welcome.nim
nim c tests/welcome_protocol.nim
nimble install -y jester
nim c tests/jester_test.nim
nim c tests/jester_test.nim
86 changes: 49 additions & 37 deletions src/ws.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ type
Closed = 3 # The connection is closed or couldn't be opened.

WebSocket* = ref object
req*: Request
tcpSocket*: AsyncSocket
version*: int
key*: string
protocol*: string
readyState*: ReadyState
masked*: bool # send masked packets

WebSocketError* = object of Exception

Expand All @@ -40,7 +41,7 @@ proc nibbleToChar(value: int): char =
else: char(value + ord('a') - 10)


proc decodeBase16(str: string): string =
proc decodeBase16*(str: string): string =
## Base16 decode a string.
result = newString(str.len div 2)
for i in 0 ..< result.len:
Expand All @@ -49,7 +50,7 @@ proc decodeBase16(str: string): string =
nibbleFromChar(str[2 * i + 1]))


proc encodeBase16(str: string): string =
proc encodeBase16*(str: string): string =
## Base61 encode a string.
result = newString(str.len * 2)
for i, c in str:
Expand All @@ -63,6 +64,29 @@ proc genMaskKey(): array[4, char] =
[r(), r(), r(), r()]


proc handshake*(ws: WebSocket, headers: HttpHeaders) {.async.} =
ws.version = parseInt(headers["Sec-WebSocket-Version"])
ws.key = headers["Sec-WebSocket-Key"].strip()
if headers.hasKey("Sec-WebSocket-Protocol"):
ws.protocol = headers["Sec-WebSocket-Protocol"].strip()

let
sh = secureHash(ws.key & "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
acceptKey = base64.encode(decodeBase16($sh))

var responce = "HTTP/1.1 101 Web Socket Protocol Handshake\c\L"
responce.add("Sec-WebSocket-Accept: " & acceptKey & "\c\L")
responce.add("Connection: Upgrade\c\L")
responce.add("Upgrade: webSocket\c\L")

if ws.protocol != "":
responce.add("Sec-WebSocket-Protocol: " & ws.protocol & "\c\L")
responce.add "\c\L"

await ws.tcpSocket.send(responce)
ws.readyState = Open


proc newWebSocket*(req: Request): Future[WebSocket] {.async.} =
## Creates a new socket from a request.
try:
Expand All @@ -71,27 +95,9 @@ proc newWebSocket*(req: Request): Future[WebSocket] {.async.} =
raise newException(WebSocketError, "Not a valid websocket handshake.")

var ws = WebSocket()
ws.req = req
ws.version = parseInt(req.headers["Sec-WebSocket-Version"])
ws.key = req.headers["Sec-WebSocket-Key"].strip()
if req.headers.hasKey("Sec-WebSocket-Protocol"):
ws.protocol = req.headers["Sec-WebSocket-Protocol"].strip()

let
sh = secureHash(ws.key & "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
acceptKey = base64.encode(decodeBase16($sh))

var responce = "HTTP/1.1 101 Web Socket Protocol Handshake\c\L"
responce.add("Sec-WebSocket-Accept: " & acceptKey & "\c\L")
responce.add("Connection: Upgrade\c\L")
responce.add("Upgrade: webSocket\c\L")

if ws.protocol != "":
responce.add("Sec-WebSocket-Protocol: " & ws.protocol & "\c\L")
responce.add "\c\L"

await ws.req.client.send(responce)
ws.readyState = Open
ws.masked = false
ws.tcpSocket = req.client
await ws.handshake(req.headers)
return ws

except ValueError, KeyError:
Expand All @@ -105,8 +111,8 @@ proc newWebSocket*(req: Request): Future[WebSocket] {.async.} =
proc newWebSocket*(url: string, protocol: string = ""): Future[WebSocket] {.async.} =
## Creates a new WebSocket connection, protocol is optinal, "" means no protocol.
var ws = WebSocket()
ws.req = Request()
ws.req.client = newAsyncSocket()
ws.masked = true
ws.tcpSocket = newAsyncSocket()
ws.protocol = protocol

var uri = parseUri(url)
Expand Down Expand Up @@ -138,7 +144,7 @@ proc newWebSocket*(url: string, protocol: string = ""): Future[WebSocket] {.asyn
if ws.protocol != "":
if ws.protocol != res.headers["Sec-WebSocket-Protocol"]:
raise newException(WebSocketError, "Protocols don't match")
ws.req.client = client.getSocket()
ws.tcpSocket = client.getSocket()

ws.readyState = Open
return ws
Expand Down Expand Up @@ -256,7 +262,7 @@ proc send*(ws: WebSocket, text: string, opcode = Opcode.Text):
rsv2: false,
rsv3: false,
opcode: opcode,
mask: false,
mask: ws.masked,
data: text
))
const maxSize = 1024*1024
Expand All @@ -265,7 +271,7 @@ proc send*(ws: WebSocket, text: string, opcode = Opcode.Text):
var i = 0
while i < frame.len:
let data = frame[i ..< min(frame.len, i + maxSize)]
await ws.req.client.send(data)
await ws.tcpSocket.send(data)
i += maxSize
await sleepAsync(1)
except Defect, IOError, OSError:
Expand All @@ -280,12 +286,12 @@ proc recvFrame(ws: WebSocket): Future[Frame] {.async.} =
## Gets a frame from the WebSocket.
## See https://tools.ietf.org/html/rfc6455#section-5.2

if cast[int](ws.req.client.getFd) == -1:
if cast[int](ws.tcpSocket.getFd) == -1:
ws.readyState = Closed
return result

# Grab the header.
let header = await ws.req.client.recv(2)
let header = await ws.tcpSocket.recv(2)

if header.len != 2:
ws.readyState = Closed
Expand All @@ -312,15 +318,15 @@ proc recvFrame(ws: WebSocket): Future[Frame] {.async.} =
let headerLen = uint(b1 and 0x7f)
if headerLen == 0x7e:
# Length must be 7+16 bits.
var lenstr = await ws.req.client.recv(2)
var lenstr = await ws.tcpSocket.recv(2)
if lenstr.len != 2:
raise newException(WebSocketError, "Socket closed")

finalLen = cast[ptr uint16](lenstr[0].addr)[].htons

elif headerLen == 0x7f:
# Length must be 7+64 bits.
var lenstr = await ws.req.client.recv(8)
var lenstr = await ws.tcpSocket.recv(8)
if lenstr.len != 8:
raise newException(WebSocketError, "Socket closed")
finalLen = cast[ptr uint32](lenstr[4].addr)[].htonl
Expand All @@ -331,15 +337,21 @@ proc recvFrame(ws: WebSocket): Future[Frame] {.async.} =

# Do we need to apply mask?
result.mask = (b1 and 0x80) == 0x80

if ws.masked == result.mask:
# Server sends unmasked but accepts only masked.
# Client sends masked but accepts only unmasked.
raise newException(WebSocketError, "Socket mask missmatch")

var maskKey = ""
if result.mask:
# Read the mask.
maskKey = await ws.req.client.recv(4)
maskKey = await ws.tcpSocket.recv(4)
if maskKey.len != 4:
raise newException(WebSocketError, "Socket closed")

# Read the data.
result.data = await ws.req.client.recv(int finalLen)
result.data = await ws.tcpSocket.recv(int finalLen)
if result.data.len != int finalLen:
raise newException(WebSocketError, "Socket closed")

Expand Down Expand Up @@ -422,7 +434,7 @@ proc hangup*(ws: WebSocket) =
## Closes the Socket without sending a close packet
ws.readyState = Closed
try:
ws.req.client.close()
ws.tcpSocket.close()
except:
raise newException(
WebSocketError,
Expand All @@ -435,6 +447,6 @@ proc close*(ws: WebSocket) =
ws.readyState = Closed
proc close() {.async.} =
await ws.send("", Close)
ws.req.client.close()
ws.tcpSocket.close()
asyncCheck close()

43 changes: 36 additions & 7 deletions src/ws/jester_extra.nim
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
import jester, ws, asyncdispatch, asynchttpserver, strutils, std/sha1, base64, nativesockets
import ws, asyncdispatch, asynchttpserver, strutils, std/sha1, base64, nativesockets
import jester, jester/private/utils

proc newWebSocket*(req: jester.Request): Future[WebSocket] {.async.} =
when useHttpBeast:
import httpbeast, options, asyncnet


proc newWebSocket*(req: httpbeast.Request): Future[WebSocket] {.async.} =
## Creates a new socket from an httpbeast request.
try:
let headers = req.headers.get

if not headers.hasKey("Sec-WebSocket-Version"):
req.send(Http404, "Not Found")
raise newException(WebSocketError, "Not a valid websocket handshake.")

var ws = WebSocket()
ws.masked = false

# Here is the magic:
req.forget() # Remove from HttpBeast event loop.
asyncdispatch.register(req.client.AsyncFD) # Add to async event loop.

ws.tcpSocket = newAsyncSocket(req.client.AsyncFD)
await ws.handshake(headers)
return ws

except ValueError, KeyError:
# Wrap all exceptions in a WebSocketError so its easy to catch
raise newException(
WebSocketError,
"Failed to create WebSocket from request: " & getCurrentExceptionMsg()
)


proc newWebSocket*(req: jester.Request): Future[WebSocket] {.async.} =
## Creates a new socket from a jester request.
when defined(useHttpBeast):
raise newException("Websockets dont supprot http beast.")
else:
let req: asynchttpserver.Request = cast[asynchttpserver.Request](req.getNativeReq())
return await ws.newWebSocket(req)
return await newWebSocket(req.getNativeReq())
14 changes: 9 additions & 5 deletions tests/jester_test.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import ws, ws/jester_extra

routes:
get "/ws":
var ws = await newWebSocket(request)
await ws.send("Welcome to simple echo server")
while ws.readyState == Open:
let packet = await ws.receiveStrPacket()
await ws.send(packet)
try:
var ws = await newWebSocket(request)
await ws.send("Welcome to simple echo server")
while ws.readyState == Open:
let packet = await ws.receiveStrPacket()
await ws.send(packet)
except WebSocketError:
echo "socket closed"
result[0] = TCActionRaw # tell jester we handled the request
12 changes: 12 additions & 0 deletions tests/sender_jester.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ws, asyncdispatch, asynchttpserver

proc main() {.async.} =
var ws = await newWebSocket("ws://0.0.0.0:5000/ws")
echo await ws.receiveStrPacket()
await ws.send("Hi, how are you?")
echo await ws.receiveStrPacket()
ws.close()

waitFor main()


5 changes: 3 additions & 2 deletions tests/test.nim
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ block: # 7+64 bits length
))[0..32] == "\129\127\0\0\0\0\0\1\169\"How are you this is the"

block: # masking
assert encodeFrame((
let data = encodeFrame((
fin: true,
rsv1: false,
rsv2: false,
rsv3: false,
opcode: Opcode.Text,
mask: true,
data: "hi there"
)) == "\129\136\13M\137/e$\169[e(\251J"
))
assert data == "\129\136\207\216\5e\167\177%\17\167\189w\0"
2 changes: 1 addition & 1 deletion ws.nimble
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Package

version = "0.3.3"
version = "0.4.0"
author = "Andre von Houck"
description = "Simple WebSocket library for nim."
license = "MIT"
Expand Down

0 comments on commit 116964a

Please sign in to comment.