From 1693b5c0224098133dec628c850f0146b0a80f34 Mon Sep 17 00:00:00 2001 From: Volodymyr Melnychuk Date: Wed, 8 May 2019 16:10:48 +0300 Subject: [PATCH] initial commit --- README.md | 1 + news.nimble | 12 ++ src/news.nim | 352 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 365 insertions(+) create mode 100644 README.md create mode 100644 news.nimble create mode 100644 src/news.nim diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ + diff --git a/news.nimble b/news.nimble new file mode 100644 index 0000000..4e8d60d --- /dev/null +++ b/news.nimble @@ -0,0 +1,12 @@ +# Package + +version = "0.1" +author = "Andre von Houck, Volodymyr Melnychuk" +description = "Simple WebSocket library for nim." +license = "MIT" +srcDir = "src" + + +# Dependencies + +requires "nim >= 0.19.0" diff --git a/src/news.nim b/src/news.nim new file mode 100644 index 0000000..3c32515 --- /dev/null +++ b/src/news.nim @@ -0,0 +1,352 @@ +import httpcore, asynchttpserver, asyncdispatch, nativesockets, asyncnet, + strutils, streams, random, securehash, base64, uri, strformat + +type + ReadyState* = enum + Connecting = 0 # The connection is not yet open. + Open = 1 # The connection is open and ready to communicate. + Closing = 2 # The connection is in the process of closing. + Closed = 3 # The connection is closed or couldn't be opened. + + WebSocket* = ref object + req*: Request + version*: int + key*: string + protocol*: string + readyState*: ReadyState + maskFrames*: bool + + WebSocketError* = object of Exception + WebSocketClosedError* = object of WebSocketError + +template `[]`(value: uint8, index: int): bool = + ## get bits from uint8, uint8[2] gets 2nd bit + (value and (1 shl (7 - index))) != 0 + + +proc nibbleFromChar(c: char): int = + ## converts hex chars like `0` to 0 and `F` to 15 + case c: + of '0'..'9': (ord(c) - ord('0')) + of 'a'..'f': (ord(c) - ord('a') + 10) + of 'A'..'F': (ord(c) - ord('A') + 10) + else: 255 + + +proc nibbleToChar(value: int): char = + ## converts number like 0 to `0` and 15 to `fg` + case value: + of 0..9: char(value + ord('0')) + else: char(value + ord('a') - 10) + + +proc decodeBase16*(str: string): string = + ## base16 decode a 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])) + + +proc encodeBase16*(str: string): string = + ## base61 encode a string + result = newString(str.len * 2) + for i, c in str: + result[i * 2] = nibbleToChar(ord(c) shr 4) + result[i * 2 + 1] = nibbleToChar(ord(c) and 0x0f) + + +proc genMaskKey*(): array[4, char] = + ## Generates a random key of 4 random chars + [char(rand(255)), char(rand(255)), char(rand(255)), char(rand(255))] + +proc newWebSocket*(req: Request): Future[WebSocket] {.async.} = + ## Creates a new socket from a request + 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") + let 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 not ws.protocol.len == 0: + responce.add("Sec-WebSocket-Protocol: " & ws.protocol & "\c\L") + responce.add "\c\L" + + await ws.req.client.send(responce) + ws.readyState = Open + return ws + + +proc newWebSocket*(url: string): Future[WebSocket] {.async.} = + ## Creates a client + var ws = WebSocket() + ws.req = Request() + ws.req.client = newAsyncSocket() + + + let uri = parseUri(url) + var port = Port(9001) + if uri.scheme != "ws": + raise newException(WebSocketError, &"Scheme {uri.scheme} not supported yet.") + else: + port = Port(80) + if uri.port.len > 0: + port = Port(parseInt(uri.port)) + + await ws.req.client.connect(uri.hostname, port) + await ws.req.client.send &"""GET {url} HTTP/1.1 +Host: {uri.hostname}:{$port} +Connection: Upgrade +Upgrade: websocket +Sec-WebSocket-Version: 13 +Sec-WebSocket-Key: JCSoP2Cyk0cHZkKAit5DjA== +Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits + +""" + var output = "" + while not output.endsWith("\c\L\c\L"): + output.add await ws.req.client.recv(1) + + ws.readyState = Open + return ws + + +type + Opcode* = enum + ## 4 bits. Defines the interpretation of the "Payload data". + Cont = 0x0 ## denotes a continuation frame + Text = 0x1 ## denotes a text frame + Binary = 0x2 ## denotes a binary frame + # 3-7 are reserved for further non-control frames + Close = 0x8 ## denotes a connection close + Ping = 0x9 ## denotes a ping + Pong = 0xa ## denotes a pong + # B-F are reserved for further control frames + + #[ + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-------+-+-------------+-------------------------------+ + |F|R|R|R| opcode|M| Payload len | Extended payload length | + |I|S|S|S| (4) |A| (7) | (16/64) | + |N|V|V|V| |S| | (if payload len==126/127) | + | |1|2|3| |K| | | + +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + | Extended payload length continued, if payload len == 127 | + + - - - - - - - - - - - - - - - +-------------------------------+ + | |Masking-key, if MASK set to 1 | + +-------------------------------+-------------------------------+ + | Masking-key (continued) | Payload Data | + +-------------------------------- - - - - - - - - - - - - - - - + + : Payload Data continued ... : + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + | Payload Data continued ... | + +---------------------------------------------------------------+ + ]# + Frame* = tuple + fin: bool ## Indicates that this is the final fragment in a message. + rsv1: bool ## MUST be 0 unless negotiated that defines meanings + rsv2: bool + rsv3: bool + opcode: Opcode ## Defines the interpretation of the "Payload data". + mask: bool ## Defines whether the "Payload data" is masked. + data: string ## Payload data + + +proc encodeFrame*(f: Frame): string = + ## Encodes a frame into a string buffer + ## See https://tools.ietf.org/html/rfc6455#section-5.2 + + var ret = newStringStream() + + var b0 = (f.opcode.uint8 and 0x0f) # 0th byte: opcodes and flags + if f.fin: + b0 = b0 or 128u8 + + ret.write(b0) + + # Payload length can be 7 bits, 7+16 bits, or 7+64 bits + + var b1 = 0u8 # 1st byte: playload len start and mask bit + + if f.data.len <= 125: + b1 = f.data.len.uint8 + elif f.data.len > 125 and f.data.len <= 0xffff: + b1 = 126u8 + else: + b1 = 127u8 + + let b1unmasked = b1 + if f.mask: + b1 = b1 or (1 shl 7) + + ret.write(uint8 b1) + + # Only need more bytes if data len is 7+16 bits, or 7+64 bits + if f.data.len > 125 and f.data.len <= 0xffff: + # data len is 7+16 bits + ret.write(htons(f.data.len.uint16)) + elif f.data.len > 0xffff: + # data len is 7+64 bits + var len = f.data.len + ret.write char((len shr 56) and 255) + ret.write char((len shr 48) and 255) + ret.write char((len shr 40) and 255) + ret.write char((len shr 32) and 255) + ret.write char((len shr 24) and 255) + ret.write char((len shr 16) and 255) + ret.write char((len shr 8) and 255) + ret.write char(len and 255) + + var data = f.data + + if f.mask: + # if we need to maks it generate random mask key and mask the data + let maskKey = genMaskKey() + for i in 0..