2021-06-27 06:35:36 +00:00
|
|
|
## nim-websock
|
2021-05-25 00:47:27 +00:00
|
|
|
## Copyright (c) 2021 Status Research & Development GmbH
|
|
|
|
## Licensed under either of
|
|
|
|
## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
|
|
|
## * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
|
|
|
## at your option.
|
|
|
|
## This file may not be copied, modified, or distributed except according to
|
|
|
|
## those terms.
|
|
|
|
|
2021-04-13 22:05:58 +00:00
|
|
|
{.push raises: [Defect].}
|
|
|
|
|
2021-03-18 15:30:21 +00:00
|
|
|
import std/[tables,
|
|
|
|
strutils,
|
2021-06-01 02:39:14 +00:00
|
|
|
strformat,
|
2021-05-25 14:02:32 +00:00
|
|
|
sequtils,
|
2021-03-18 15:30:21 +00:00
|
|
|
uri,
|
|
|
|
parseutils]
|
|
|
|
|
|
|
|
import pkg/[chronos,
|
2021-04-05 21:01:10 +00:00
|
|
|
chronos/apps/http/httptable,
|
|
|
|
chronos/streams/asyncstream,
|
2021-04-13 22:05:58 +00:00
|
|
|
chronos/streams/tlsstream,
|
2021-03-18 15:30:21 +00:00
|
|
|
chronicles,
|
|
|
|
httputils,
|
|
|
|
stew/byteutils,
|
|
|
|
stew/base64,
|
2021-04-05 21:01:10 +00:00
|
|
|
stew/base10,
|
|
|
|
nimcrypto/sha]
|
2021-03-18 15:30:21 +00:00
|
|
|
|
2021-06-15 14:27:56 +00:00
|
|
|
import ./utils, ./frame, ./session, /types, ./http, ./extensions/extutils
|
2021-03-11 03:34:14 +00:00
|
|
|
|
2021-06-01 02:39:14 +00:00
|
|
|
export utils, session, frame, types, http
|
2021-03-11 03:34:14 +00:00
|
|
|
|
2021-06-11 20:04:09 +00:00
|
|
|
logScope:
|
2021-07-14 17:26:46 +00:00
|
|
|
topics = "websock ws-server"
|
2021-06-11 20:04:09 +00:00
|
|
|
|
2021-03-11 03:34:14 +00:00
|
|
|
type
|
2021-05-25 14:02:32 +00:00
|
|
|
WSServer* = ref object of WebSocket
|
|
|
|
protocols: seq[string]
|
2021-06-01 20:24:00 +00:00
|
|
|
factories: seq[ExtFactory]
|
2021-05-25 14:02:32 +00:00
|
|
|
|
2021-06-01 02:39:14 +00:00
|
|
|
func toException(e: string): ref WebSocketError =
|
|
|
|
(ref WebSocketError)(msg: e)
|
2021-04-05 21:01:10 +00:00
|
|
|
|
2021-06-01 02:39:14 +00:00
|
|
|
func toException(e: cstring): ref WebSocketError =
|
|
|
|
(ref WebSocketError)(msg: $e)
|
2021-04-05 21:01:10 +00:00
|
|
|
|
2021-06-15 14:27:56 +00:00
|
|
|
func contains(extensions: openArray[Ext], extName: string): bool =
|
|
|
|
for ext in extensions:
|
|
|
|
if ext.name == extName:
|
|
|
|
return true
|
|
|
|
|
|
|
|
proc getFactory(factories: openArray[ExtFactory], extName: string): ExtFactoryProc =
|
|
|
|
for n in factories:
|
|
|
|
if n.name == extName:
|
|
|
|
return n.factory
|
|
|
|
|
|
|
|
proc selectExt(isServer: bool,
|
|
|
|
extensions: var seq[Ext],
|
|
|
|
factories: openArray[ExtFactory],
|
|
|
|
exts: openArray[string]): string {.raises: [Defect, WSExtError].} =
|
|
|
|
|
|
|
|
var extList: seq[AppExt]
|
|
|
|
var response = ""
|
|
|
|
for ext in exts:
|
|
|
|
# each of "Sec-WebSocket-Extensions" can have multiple
|
|
|
|
# extensions or fallback extension
|
|
|
|
if not parseExt(ext, extList):
|
|
|
|
raise newException(WSExtError, "extension syntax error: " & ext)
|
|
|
|
|
|
|
|
for i, ext in extList:
|
|
|
|
if extensions.contains(ext.name):
|
|
|
|
# don't accept this fallback if prev ext
|
|
|
|
# configuration already accepted
|
|
|
|
trace "extension fallback not accepted", ext=ext.name
|
|
|
|
continue
|
|
|
|
|
|
|
|
# now look for right factory
|
|
|
|
let factory = factories.getFactory(ext.name)
|
|
|
|
if factory.isNil:
|
|
|
|
# no factory? it's ok, just skip it
|
|
|
|
trace "no extension factory", ext=ext.name
|
|
|
|
continue
|
|
|
|
|
|
|
|
let extRes = factory(isServer, ext.params)
|
|
|
|
if extRes.isErr:
|
|
|
|
# cannot create extension because of
|
|
|
|
# wrong/incompatible params? skip or fallback
|
|
|
|
trace "skip extension", ext=ext.name, msg=extRes.error
|
|
|
|
continue
|
|
|
|
|
|
|
|
let ext = extRes.get()
|
|
|
|
doAssert(not ext.isNil)
|
|
|
|
if i > 0:
|
|
|
|
# add separator if more than one exts
|
|
|
|
response.add ", "
|
|
|
|
response.add ext.toHttpOptions
|
|
|
|
|
|
|
|
# finally, accept the extension
|
|
|
|
trace "extension accepted", ext=ext.name
|
|
|
|
extensions.add ext
|
|
|
|
|
|
|
|
# HTTP response for "Sec-WebSocket-Extensions"
|
|
|
|
response
|
|
|
|
|
2021-03-18 15:30:21 +00:00
|
|
|
proc connect*(
|
2021-04-05 21:01:10 +00:00
|
|
|
_: type WebSocket,
|
2021-03-18 15:30:21 +00:00
|
|
|
uri: Uri,
|
|
|
|
protocols: seq[string] = @[],
|
2021-06-15 14:27:56 +00:00
|
|
|
factories: seq[ExtFactory] = @[],
|
2021-04-13 22:05:58 +00:00
|
|
|
flags: set[TLSFlags] = {},
|
2021-03-18 15:30:21 +00:00
|
|
|
version = WSDefaultVersion,
|
|
|
|
frameSize = WSDefaultFrameSize,
|
|
|
|
onPing: ControlCb = nil,
|
|
|
|
onPong: ControlCb = nil,
|
2021-05-25 14:02:32 +00:00
|
|
|
onClose: CloseCb = nil,
|
|
|
|
rng: Rng = nil): Future[WSSession] {.async.} =
|
2021-03-18 15:30:21 +00:00
|
|
|
## create a new websockets client
|
|
|
|
##
|
|
|
|
|
2021-06-01 02:39:14 +00:00
|
|
|
var rng = if isNil(rng): newRng() else: rng
|
2021-06-12 00:54:38 +00:00
|
|
|
var key = Base64Pad.encode(genWebSecKey(rng))
|
2021-03-11 03:34:14 +00:00
|
|
|
var uri = uri
|
2021-06-01 02:39:14 +00:00
|
|
|
let client = case uri.scheme:
|
|
|
|
of "wss":
|
|
|
|
uri.scheme = "https"
|
|
|
|
await TlsHttpClient.connect(uri.hostname, uri.port.parseInt(), tlsFlags = flags)
|
|
|
|
of "ws":
|
|
|
|
uri.scheme = "http"
|
|
|
|
await HttpClient.connect(uri.hostname, uri.port.parseInt())
|
|
|
|
else:
|
|
|
|
raise newException(WSWrongUriSchemeError,
|
|
|
|
"uri scheme has to be 'ws' or 'wss'")
|
2021-03-11 03:34:14 +00:00
|
|
|
|
2021-06-01 02:39:14 +00:00
|
|
|
let headerData = [
|
2021-04-05 21:01:10 +00:00
|
|
|
("Connection", "Upgrade"),
|
|
|
|
("Upgrade", "websocket"),
|
|
|
|
("Cache-Control", "no-cache"),
|
|
|
|
("Sec-WebSocket-Version", $version),
|
2021-06-12 00:54:38 +00:00
|
|
|
("Sec-WebSocket-Key", key),
|
|
|
|
("Host", uri.hostname & ":" & uri.port)]
|
2021-03-18 15:30:21 +00:00
|
|
|
|
2021-04-05 21:01:10 +00:00
|
|
|
var headers = HttpTable.init(headerData)
|
2021-06-01 02:39:14 +00:00
|
|
|
if protocols.len > 0:
|
2021-04-05 21:01:10 +00:00
|
|
|
headers.add("Sec-WebSocket-Protocol", protocols.join(", "))
|
2021-03-18 15:30:21 +00:00
|
|
|
|
2021-06-15 14:27:56 +00:00
|
|
|
var extOffer = ""
|
|
|
|
for i, f in factories:
|
|
|
|
if i > 0:
|
|
|
|
extOffer.add ", "
|
|
|
|
extOffer.add f.clientOffer
|
|
|
|
|
|
|
|
if extOffer.len > 0:
|
|
|
|
headers.add("Sec-WebSocket-Extensions", extOffer)
|
|
|
|
|
2021-06-01 02:39:14 +00:00
|
|
|
let response = try:
|
|
|
|
await client.request(uri, headers = headers)
|
|
|
|
except CatchableError as exc:
|
2021-06-11 20:04:09 +00:00
|
|
|
trace "Websocket failed during handshake", exc = exc.msg
|
2021-06-01 02:39:14 +00:00
|
|
|
await client.close()
|
|
|
|
raise exc
|
|
|
|
|
|
|
|
if response.code != Http101.toInt():
|
|
|
|
raise newException(WSFailedUpgradeError,
|
|
|
|
&"Server did not reply with a websocket upgrade: " &
|
|
|
|
&"Header code: {response.code} Header reason: {response.reason} " &
|
|
|
|
&"Address: {client.address}")
|
|
|
|
|
|
|
|
let proto = response.headers.getString("Sec-WebSocket-Protocol")
|
|
|
|
if proto.len > 0 and protocols.len > 0:
|
|
|
|
if proto notin protocols:
|
|
|
|
raise newException(WSFailedUpgradeError,
|
|
|
|
&"Invalid protocol returned {proto}!")
|
2021-03-11 03:34:14 +00:00
|
|
|
|
2021-06-15 14:27:56 +00:00
|
|
|
var extensions: seq[Ext]
|
|
|
|
let exts = response.headers.getList("Sec-WebSocket-Extensions")
|
|
|
|
discard selectExt(false, extensions, factories, exts)
|
|
|
|
|
2021-03-11 03:34:14 +00:00
|
|
|
# Client data should be masked.
|
2021-06-15 14:27:56 +00:00
|
|
|
let session = WSSession(
|
2021-06-01 02:39:14 +00:00
|
|
|
stream: client.stream,
|
2021-04-14 11:07:38 +00:00
|
|
|
readyState: ReadyState.Open,
|
2021-03-18 15:30:21 +00:00
|
|
|
masked: true,
|
2021-06-15 14:27:56 +00:00
|
|
|
extensions: system.move(extensions),
|
2021-06-01 02:39:14 +00:00
|
|
|
rng: rng,
|
2021-03-18 15:30:21 +00:00
|
|
|
frameSize: frameSize,
|
|
|
|
onPing: onPing,
|
|
|
|
onPong: onPong,
|
|
|
|
onClose: onClose)
|
|
|
|
|
2021-06-15 14:27:56 +00:00
|
|
|
for ext in session.extensions:
|
|
|
|
ext.session = session
|
|
|
|
|
|
|
|
return session
|
|
|
|
|
2021-03-18 15:30:21 +00:00
|
|
|
proc connect*(
|
2021-04-05 21:01:10 +00:00
|
|
|
_: type WebSocket,
|
2021-06-01 02:39:14 +00:00
|
|
|
address: TransportAddress,
|
2021-03-18 15:30:21 +00:00
|
|
|
path: string,
|
|
|
|
protocols: seq[string] = @[],
|
2021-06-15 14:27:56 +00:00
|
|
|
factories: seq[ExtFactory] = @[],
|
2021-06-01 02:39:14 +00:00
|
|
|
secure = false,
|
|
|
|
flags: set[TLSFlags] = {},
|
2021-03-18 15:30:21 +00:00
|
|
|
version = WSDefaultVersion,
|
|
|
|
frameSize = WSDefaultFrameSize,
|
|
|
|
onPing: ControlCb = nil,
|
|
|
|
onPong: ControlCb = nil,
|
2021-06-01 02:39:14 +00:00
|
|
|
onClose: CloseCb = nil,
|
|
|
|
rng: Rng = nil): Future[WSSession] {.async.} =
|
2021-03-18 15:30:21 +00:00
|
|
|
## Create a new websockets client
|
|
|
|
## using a string path
|
|
|
|
##
|
2021-03-11 03:34:14 +00:00
|
|
|
|
2021-06-01 02:39:14 +00:00
|
|
|
var uri = if secure:
|
|
|
|
&"wss://"
|
|
|
|
else:
|
|
|
|
&"ws://"
|
|
|
|
|
|
|
|
uri &= address.host & ":" & $address.port
|
2021-03-11 03:34:14 +00:00
|
|
|
if path.startsWith("/"):
|
|
|
|
uri.add path
|
|
|
|
else:
|
2021-06-01 02:39:14 +00:00
|
|
|
uri.add &"/{path}"
|
2021-03-18 15:30:21 +00:00
|
|
|
|
2021-04-05 21:01:10 +00:00
|
|
|
return await WebSocket.connect(
|
2021-06-01 02:39:14 +00:00
|
|
|
uri = parseUri(uri),
|
|
|
|
protocols = protocols,
|
2021-06-15 14:27:56 +00:00
|
|
|
factories = factories,
|
2021-06-01 02:39:14 +00:00
|
|
|
flags = flags,
|
|
|
|
version = version,
|
|
|
|
frameSize = frameSize,
|
|
|
|
onPing = onPing,
|
|
|
|
onPong = onPong,
|
|
|
|
onClose = onClose)
|
|
|
|
|
|
|
|
proc connect*(
|
2021-04-13 22:05:58 +00:00
|
|
|
_: type WebSocket,
|
|
|
|
host: string,
|
|
|
|
port: Port,
|
|
|
|
path: string,
|
|
|
|
protocols: seq[string] = @[],
|
2021-06-15 14:27:56 +00:00
|
|
|
factories: seq[ExtFactory] = @[],
|
2021-06-01 02:39:14 +00:00
|
|
|
secure = false,
|
2021-04-13 22:05:58 +00:00
|
|
|
flags: set[TLSFlags] = {},
|
|
|
|
version = WSDefaultVersion,
|
|
|
|
frameSize = WSDefaultFrameSize,
|
|
|
|
onPing: ControlCb = nil,
|
|
|
|
onPong: ControlCb = nil,
|
2021-05-25 14:02:32 +00:00
|
|
|
onClose: CloseCb = nil,
|
|
|
|
rng: Rng = nil): Future[WSSession] {.async.} =
|
2021-04-13 22:05:58 +00:00
|
|
|
|
|
|
|
return await WebSocket.connect(
|
2021-06-01 02:39:14 +00:00
|
|
|
address = initTAddress(host, port),
|
|
|
|
path = path,
|
|
|
|
protocols = protocols,
|
2021-06-15 14:27:56 +00:00
|
|
|
factories = factories,
|
2021-06-12 00:54:38 +00:00
|
|
|
secure = secure,
|
2021-06-01 02:39:14 +00:00
|
|
|
flags = flags,
|
|
|
|
version = version,
|
|
|
|
frameSize = frameSize,
|
|
|
|
onPing = onPing,
|
|
|
|
onPong = onPong,
|
|
|
|
onClose = onClose,
|
|
|
|
rng = rng)
|
2021-05-25 14:02:32 +00:00
|
|
|
|
|
|
|
proc handleRequest*(
|
|
|
|
ws: WSServer,
|
2021-06-01 02:39:14 +00:00
|
|
|
request: HttpRequest,
|
|
|
|
version: uint = WSDefaultVersion): Future[WSSession]
|
|
|
|
{.
|
|
|
|
async,
|
|
|
|
raises: [
|
|
|
|
Defect,
|
|
|
|
WSHandshakeError,
|
|
|
|
WSProtoMismatchError]
|
|
|
|
.} =
|
2021-05-25 14:02:32 +00:00
|
|
|
## Creates a new socket from a request.
|
|
|
|
##
|
|
|
|
|
|
|
|
if not request.headers.contains("Sec-WebSocket-Version"):
|
|
|
|
raise newException(WSHandshakeError, "Missing version header")
|
|
|
|
|
2021-06-01 02:39:14 +00:00
|
|
|
ws.version = Base10.decode(
|
|
|
|
uint,
|
|
|
|
request.headers.getString("Sec-WebSocket-Version"))
|
|
|
|
.tryGet() # this method throws
|
|
|
|
|
|
|
|
if ws.version != version:
|
|
|
|
await request.stream.writer.sendError(Http426)
|
2021-06-11 20:04:09 +00:00
|
|
|
trace "Websocket version not supported", version = ws.version
|
2021-05-25 14:02:32 +00:00
|
|
|
|
2021-06-01 02:39:14 +00:00
|
|
|
raise newException(WSVersionError,
|
|
|
|
&"Websocket version not supported, Version: {version}")
|
|
|
|
|
|
|
|
ws.key = request.headers.getString("Sec-WebSocket-Key").strip()
|
|
|
|
let wantProtos = if request.headers.contains("Sec-WebSocket-Protocol"):
|
|
|
|
request.headers.getList("Sec-WebSocket-Protocol")
|
|
|
|
else:
|
|
|
|
@[""]
|
|
|
|
|
|
|
|
let protos = wantProtos.filterIt(
|
|
|
|
it in ws.protocols
|
|
|
|
)
|
|
|
|
|
|
|
|
let
|
|
|
|
cKey = ws.key & WSGuid
|
|
|
|
acceptKey = Base64Pad.encode(
|
|
|
|
sha1.digest(cKey.toOpenArray(0, cKey.high)).data)
|
|
|
|
|
|
|
|
var headers = HttpTable.init([
|
|
|
|
("Connection", "Upgrade"),
|
|
|
|
("Upgrade", "websocket"),
|
|
|
|
("Sec-WebSocket-Accept", acceptKey)])
|
|
|
|
|
|
|
|
let protocol = if protos.len > 0: protos[0] else: ""
|
|
|
|
if protocol.len > 0:
|
|
|
|
headers.add("Sec-WebSocket-Protocol", protocol) # send back the first matching proto
|
|
|
|
else:
|
2021-06-11 20:04:09 +00:00
|
|
|
trace "Didn't match any protocol", supported = ws.protocols, requested = wantProtos
|
2021-06-01 02:39:14 +00:00
|
|
|
|
2021-06-15 14:27:56 +00:00
|
|
|
# it is possible to have multiple "Sec-WebSocket-Extensions"
|
|
|
|
let exts = request.headers.getList("Sec-WebSocket-Extensions")
|
|
|
|
let extResp = selectExt(true, ws.extensions, ws.factories, exts)
|
|
|
|
if extResp.len > 0:
|
|
|
|
# send back any accepted extensions
|
|
|
|
headers.add("Sec-WebSocket-Extensions", extResp)
|
|
|
|
|
2021-06-01 02:39:14 +00:00
|
|
|
try:
|
|
|
|
await request.sendResponse(Http101, headers = headers)
|
|
|
|
except CancelledError as exc:
|
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
|
|
raise newException(WSHandshakeError,
|
|
|
|
"Failed to sent handshake response. Error: " & exc.msg)
|
|
|
|
|
2021-06-15 14:27:56 +00:00
|
|
|
let session = WSSession(
|
2021-06-01 02:39:14 +00:00
|
|
|
readyState: ReadyState.Open,
|
|
|
|
stream: request.stream,
|
|
|
|
proto: protocol,
|
2021-06-15 14:27:56 +00:00
|
|
|
extensions: system.move(ws.extensions),
|
2021-06-01 02:39:14 +00:00
|
|
|
masked: false,
|
|
|
|
rng: ws.rng,
|
|
|
|
frameSize: ws.frameSize,
|
|
|
|
onPing: ws.onPing,
|
|
|
|
onPong: ws.onPong,
|
|
|
|
onClose: ws.onClose)
|
2021-05-25 14:02:32 +00:00
|
|
|
|
2021-06-15 14:27:56 +00:00
|
|
|
for ext in session.extensions:
|
|
|
|
ext.session = session
|
|
|
|
|
|
|
|
return session
|
|
|
|
|
2021-05-25 14:02:32 +00:00
|
|
|
proc new*(
|
|
|
|
_: typedesc[WSServer],
|
|
|
|
protos: openArray[string] = [""],
|
2021-06-01 20:24:00 +00:00
|
|
|
factories: openArray[ExtFactory] = [],
|
2021-05-25 14:02:32 +00:00
|
|
|
frameSize = WSDefaultFrameSize,
|
|
|
|
onPing: ControlCb = nil,
|
|
|
|
onPong: ControlCb = nil,
|
|
|
|
onClose: CloseCb = nil,
|
|
|
|
rng: Rng = nil): WSServer =
|
|
|
|
|
|
|
|
return WSServer(
|
|
|
|
protocols: @protos,
|
|
|
|
masked: false,
|
|
|
|
rng: if isNil(rng): newRng() else: rng,
|
|
|
|
frameSize: frameSize,
|
2021-06-01 20:24:00 +00:00
|
|
|
factories: @factories,
|
2021-05-25 14:02:32 +00:00
|
|
|
onPing: onPing,
|
|
|
|
onPong: onPong,
|
|
|
|
onClose: onClose)
|