mirror of
https://github.com/status-im/news.git
synced 2025-02-19 23:28:07 +00:00
initial commit
This commit is contained in:
parent
a8f34be09e
commit
1693b5c022
12
news.nimble
Normal file
12
news.nimble
Normal file
@ -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"
|
352
src/news.nim
Normal file
352
src/news.nim
Normal file
@ -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..<data.len:
|
||||
data[i] = (data[i].uint8 xor maskKey[i mod 4].uint8).char
|
||||
# write mask key next
|
||||
ret.write(maskKey)
|
||||
|
||||
# write the data
|
||||
ret.write(data)
|
||||
ret.setPosition(0)
|
||||
return ret.readAll()
|
||||
|
||||
|
||||
proc send*(ws: WebSocket, text: string, opcode = Opcode.Text): Future[void] {.async.} =
|
||||
## write data to WebSocket
|
||||
var frame = encodeFrame((
|
||||
fin: true,
|
||||
rsv1: false,
|
||||
rsv2: false,
|
||||
rsv3: false,
|
||||
opcode: opcode,
|
||||
mask: ws.maskFrames,
|
||||
data: text
|
||||
))
|
||||
const maxSize = 1024*1024
|
||||
# send stuff in 1 megabyte chunks to prevent IOErrors
|
||||
# with really large packets
|
||||
var i = 0
|
||||
while i < frame.len:
|
||||
let data = frame[i ..< min(frame.len, i + maxSize)]
|
||||
await ws.req.client.send(data)
|
||||
i += maxSize
|
||||
await sleepAsync(1)
|
||||
|
||||
|
||||
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:
|
||||
ws.readyState = Closed
|
||||
return result
|
||||
|
||||
# grab the header
|
||||
let header = await ws.req.client.recv(2)
|
||||
|
||||
if header.len != 2:
|
||||
ws.readyState = Closed
|
||||
raise newException(WebSocketClosedError, "socket closed")
|
||||
|
||||
let b0 = header[0].uint8
|
||||
let b1 = header[1].uint8
|
||||
|
||||
# read the flags and fin from the header
|
||||
result.fin = b0[0]
|
||||
result.rsv1 = b0[1]
|
||||
result.rsv2 = b0[2]
|
||||
result.rsv3 = b0[3]
|
||||
result.opcode = (b0 and 0x0f).Opcode
|
||||
|
||||
# if any of the rsv are set close the socket
|
||||
if result.rsv1 or result.rsv2 or result.rsv3:
|
||||
ws.readyState = Closed
|
||||
raise newException(WebSocketError, "WebSocket Potocol missmatch")
|
||||
|
||||
# Payload length can be 7 bits, 7+16 bits, or 7+64 bits
|
||||
var finalLen: uint = 0
|
||||
|
||||
let headerLen = uint(b1 and 0x7f)
|
||||
if headerLen == 0x7e:
|
||||
# length must be 7+16 bits
|
||||
var lenstr = await ws.req.client.recv(2)
|
||||
if lenstr.len != 2:
|
||||
raise newException(WebSocketClosedError, "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)
|
||||
if lenstr.len != 8:
|
||||
raise newException(WebSocketClosedError, "Socket closed")
|
||||
finalLen = cast[ptr uint32](lenstr[4].addr)[].htonl
|
||||
|
||||
else:
|
||||
# length must be 7 bits
|
||||
finalLen = headerLen
|
||||
|
||||
# do we need to apply mask?
|
||||
result.mask = (b1 and 0x80) == 0x80
|
||||
var maskKey = ""
|
||||
if result.mask:
|
||||
# read mask
|
||||
maskKey = await ws.req.client.recv(4)
|
||||
if maskKey.len != 4:
|
||||
raise newException(WebSocketClosedError, "Socket closed")
|
||||
|
||||
# read the data
|
||||
result.data = await ws.req.client.recv(int finalLen)
|
||||
if result.data.len != int finalLen:
|
||||
raise newException(WebSocketClosedError, "Socket closed")
|
||||
|
||||
if result.mask:
|
||||
# apply mask if we need too
|
||||
for i in 0 ..< result.data.len:
|
||||
result.data[i] = (result.data[i].uint8 xor maskKey[i mod 4].uint8).char
|
||||
|
||||
proc sendPing*(ws: WebSocket): Future[void] {.async.} =
|
||||
await ws.send("", Opcode.Ping)
|
||||
|
||||
proc sendPong(ws: WebSocket): Future[void] {.async.} =
|
||||
await ws.send("", Opcode.Pong)
|
||||
|
||||
proc receivePacket*(ws: WebSocket): Future[string] {.async.} =
|
||||
## wait for a string packet to come
|
||||
var frame = await ws.recvFrame()
|
||||
if frame.opcode == Text or frame.opcode == Binary:
|
||||
result = frame.data
|
||||
# If there are more parits read and wait for them
|
||||
while frame.fin != true:
|
||||
frame = await ws.recvFrame()
|
||||
if frame.opcode != Cont:
|
||||
raise newException(WebSocketError, "Socket did not get continue frame")
|
||||
result.add frame.data
|
||||
return
|
||||
|
||||
if frame.opcode == Ping:
|
||||
await ws.sendPong()
|
||||
|
||||
elif frame.opcode == Pong:
|
||||
discard
|
||||
|
||||
elif frame.opcode == Close:
|
||||
raise newException(WebSocketClosedError, "Socket closed")
|
||||
|
||||
|
||||
proc close*(ws: WebSocket) =
|
||||
## close the socket
|
||||
ws.readyState = Closed
|
||||
ws.req.client.close()
|
Loading…
x
Reference in New Issue
Block a user