mirror of
https://github.com/codex-storage/nim-websock.git
synced 2025-02-28 17:50:31 +00:00
WIP: Implement websocket TLS. (#7)
* Update http to use chronos http. * Implement TLS in websocket. * Add webscoket TLS test. * Minor nit. * Add TLS test file. * Update http to use chronos http. (#6) * Update http to use chronos http. * Add stream.nim file. * Address comments. * Fix CI failure. * Minor change. * Address comments. * Fix windows CI failing test. * minor cleanup * spacess * more idiomatic connect * use stew/base10 Co-authored-by: Dmitriy Ryajov <dryajov@gmail.com> * Implement TLS in websocket. * Minor nit. * merge master * wip * Update http to use chronos http. (#6) * Update http to use chronos http. * Add stream.nim file. * Address comments. * Fix CI failure. * Minor change. * Address comments. * Fix windows CI failing test. * minor cleanup * spacess * more idiomatic connect * use stew/base10 Co-authored-by: Dmitriy Ryajov <dryajov@gmail.com> * Update http to use chronos http. * Implement TLS in websocket. * Minor nit. * Update http to use chronos http. (#6) * Update http to use chronos http. * Add stream.nim file. * Address comments. * Fix CI failure. * Minor change. * Address comments. * Fix windows CI failing test. * minor cleanup * spacess * more idiomatic connect * use stew/base10 Co-authored-by: Dmitriy Ryajov <dryajov@gmail.com> * Implement TLS in websocket. * Minor nit. * add testing keys * wip * fix test * wip * remove eth dep and add skipdirs * fix package structure * fix deps * check nim version * Fix CI failure. * Don't call `ws.stream.closeWait()` * always close both ends to complete the sequence * misc * don't fail on close * Fix windows CI. * fix linux x86 builds * use consistent connect pattern * move keys to better place * return dumbResponse * small cleanup Co-authored-by: Dmitriy Ryajov <dryajov@gmail.com>
This commit is contained in:
parent
e4f00698ea
commit
6b76bd8261
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -76,21 +76,21 @@ jobs:
|
|||||||
if: runner.os == 'Linux' && matrix.target.cpu == 'i386'
|
if: runner.os == 'Linux' && matrix.target.cpu == 'i386'
|
||||||
run: |
|
run: |
|
||||||
sudo dpkg --add-architecture i386
|
sudo dpkg --add-architecture i386
|
||||||
sudo rm /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
|
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo DEBIAN_FRONTEND='noninteractive' apt-get install \
|
sudo DEBIAN_FRONTEND='noninteractive' apt-get install \
|
||||||
--no-install-recommends -yq gcc-multilib g++-multilib
|
--no-install-recommends -yq gcc-multilib g++-multilib \
|
||||||
|
libssl-dev:i386
|
||||||
mkdir -p external/bin
|
mkdir -p external/bin
|
||||||
cat << EOF > external/bin/gcc
|
cat << EOF > external/bin/gcc
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
exec $(which gcc) -m32 -mno-adx "\$@"
|
exec $(which gcc) -m32 "\$@"
|
||||||
EOF
|
EOF
|
||||||
cat << EOF > external/bin/g++
|
cat << EOF > external/bin/g++
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
exec $(which g++) -m32 -mno-adx "\$@"
|
exec $(which g++) -m32 "\$@"
|
||||||
EOF
|
EOF
|
||||||
chmod 755 external/bin/gcc external/bin/g++
|
chmod 755 external/bin/gcc external/bin/g++
|
||||||
echo "${{ github.workspace }}/external/bin" >> $GITHUB_PATH
|
echo '${{ github.workspace }}/external/bin' >> $GITHUB_PATH
|
||||||
|
|
||||||
- name: Install build dependencies (Windows)
|
- name: Install build dependencies (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import ../src/ws, nativesockets, chronos,chronicles, stew/byteutils
|
import pkg/[
|
||||||
|
chronos,
|
||||||
|
chronicles,
|
||||||
|
stew/byteutils]
|
||||||
|
|
||||||
|
import ../ws/ws
|
||||||
|
|
||||||
proc main() {.async.} =
|
proc main() {.async.} =
|
||||||
let ws = await WebSocket.connect(
|
let ws = await WebSocket.connect(
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
chronicles,
|
chronicles,
|
||||||
httputils,
|
httputils,
|
||||||
stew/byteutils]
|
stew/byteutils]
|
||||||
import ../src/ws
|
|
||||||
|
import ../ws/ws
|
||||||
|
|
||||||
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
if r.isOk():
|
if r.isOk():
|
||||||
@ -28,7 +29,7 @@ proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
|||||||
debug "Client Response: ", size = recvData.len
|
debug "Client Response: ", size = recvData.len
|
||||||
await ws.send(recvData)
|
await ws.send(recvData)
|
||||||
# await ws.close()
|
# await ws.close()
|
||||||
|
|
||||||
except WebSocketError as exc:
|
except WebSocketError as exc:
|
||||||
error "WebSocket error:", exception = exc.msg
|
error "WebSocket error:", exception = exc.msg
|
||||||
discard await request.respond(Http200, "Hello World")
|
discard await request.respond(Http200, "Hello World")
|
||||||
@ -41,8 +42,8 @@ when isMainModule:
|
|||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(
|
||||||
address, process,
|
address, process,
|
||||||
socketFlags = socketFlags)
|
socketFlags = socketFlags)
|
||||||
|
|
||||||
let server = res.get()
|
let server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
info "Server listening at ", data = address
|
info "Server listening at ", data = address
|
||||||
waitFor server.join()
|
waitFor server.join()
|
||||||
|
34
examples/tlsclient.nim
Normal file
34
examples/tlsclient.nim
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import pkg/[chronos,
|
||||||
|
chronos/streams/tlsstream,
|
||||||
|
chronicles,
|
||||||
|
stew/byteutils]
|
||||||
|
|
||||||
|
import ../ws/ws
|
||||||
|
|
||||||
|
proc main() {.async.} =
|
||||||
|
let ws = await WebSocket.tlsConnect(
|
||||||
|
"127.0.0.1",
|
||||||
|
Port(8888),
|
||||||
|
path = "/wss",
|
||||||
|
protocols = @["myfancyprotocol"],
|
||||||
|
flags = {NoVerifyHost,NoVerifyServerName})
|
||||||
|
debug "Websocket client: ", State = ws.readyState
|
||||||
|
|
||||||
|
let reqData = "Hello Server"
|
||||||
|
try:
|
||||||
|
echo "sending client "
|
||||||
|
await ws.send(reqData)
|
||||||
|
let buff = await ws.recv()
|
||||||
|
if buff.len <= 0:
|
||||||
|
break
|
||||||
|
let dataStr = string.fromBytes(buff)
|
||||||
|
debug "Server:", data = dataStr
|
||||||
|
|
||||||
|
assert dataStr == reqData
|
||||||
|
return # bail out
|
||||||
|
except WebSocketError as exc:
|
||||||
|
error "WebSocket error:", exception = exc.msg
|
||||||
|
|
||||||
|
# close the websocket
|
||||||
|
await ws.close()
|
||||||
|
waitFor(main())
|
57
examples/tlsserver.nim
Normal file
57
examples/tlsserver.nim
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import pkg/[chronos,
|
||||||
|
chronos/apps/http/shttpserver,
|
||||||
|
chronicles,
|
||||||
|
httputils,
|
||||||
|
stew/byteutils]
|
||||||
|
|
||||||
|
import ../ws/ws
|
||||||
|
import ../tests/keys
|
||||||
|
|
||||||
|
let secureKey = TLSPrivateKey.init(SecureKey)
|
||||||
|
let secureCert = TLSCertificate.init(SecureCert)
|
||||||
|
|
||||||
|
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
|
if r.isOk():
|
||||||
|
let request = r.get()
|
||||||
|
|
||||||
|
debug "Handling request:", uri = request.uri.path
|
||||||
|
if request.uri.path == "/wss":
|
||||||
|
debug "Initiating web socket connection."
|
||||||
|
try:
|
||||||
|
var ws = await createServer(request, "myfancyprotocol")
|
||||||
|
if ws.readyState != Open:
|
||||||
|
error "Failed to open websocket connection."
|
||||||
|
return
|
||||||
|
debug "Websocket handshake completed."
|
||||||
|
# Only reads header for data frame.
|
||||||
|
echo "receiving server "
|
||||||
|
let recvData = await ws.recv()
|
||||||
|
if recvData.len <= 0:
|
||||||
|
debug "Empty messages"
|
||||||
|
break
|
||||||
|
|
||||||
|
if ws.readyState == ReadyState.Closed:
|
||||||
|
return
|
||||||
|
debug "Response: ", data = string.fromBytes(recvData)
|
||||||
|
await ws.send(recvData)
|
||||||
|
except WebSocketError:
|
||||||
|
error "WebSocket error:", exception = getCurrentExceptionMsg()
|
||||||
|
discard await request.respond(Http200, "Hello World")
|
||||||
|
else:
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
let address = initTAddress("127.0.0.1:8888")
|
||||||
|
let serverFlags = {Secure, NotifyDisconnect}
|
||||||
|
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||||
|
let res = SecureHttpServerRef.new(
|
||||||
|
address, process,
|
||||||
|
serverFlags = serverFlags,
|
||||||
|
socketFlags = socketFlags,
|
||||||
|
tlsPrivateKey = secureKey,
|
||||||
|
tlsCertificate = secureCert)
|
||||||
|
|
||||||
|
let server = res.get()
|
||||||
|
server.start()
|
||||||
|
info "Server listening at ", data = address
|
||||||
|
waitFor server.join()
|
55
tests/keys.nim
Normal file
55
tests/keys.nim
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const
|
||||||
|
SecureKey* = """
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCdNv0SX02aeZ4/
|
||||||
|
Yc+p/Kwd5UVOHlpmK7/TVC/kcjFbdoUuKNn8pnX/fyhgSKpUYut+te7YRiZhqlaL
|
||||||
|
EZKjfy8GBZwXZnJCevFkTvGTTebXXExLIsLGfJqKeLAdFCQkX8wV3jV1DT5JLV+D
|
||||||
|
5+HWaiiBr38gsl4ZbfyedTF40JvzokCmcdlx9bpzX1j/b84L/zSwUyyEcgp5G28F
|
||||||
|
Jh5TnxAeDHJpOVjr8XMb/xoNqiDF6NwF96hvOZC14mZ1TxxW5bUzXprsy0l52pmh
|
||||||
|
dN3Crz11+t2h519hRKHxT6/l5pTx/+dApXiP6hMV04CQJNnas3NyRxTDR9dNel+3
|
||||||
|
+wD7/PRTAgMBAAECggEBAJuXPEbegxMKog7gYoE9S6oaqchySc0sJyCjBPL2ANsg
|
||||||
|
JRZV38cnh0hhNDh2MfxqGd7Bd6wbYQjvZ88iiRm+WW+ARcby4MnimtxHNNYwFvG0
|
||||||
|
qt0BffqqftfkMYfV0x8coAJUdFtvy+DoQstsxhlJ3uTaJtrZLD/GlmjMWzXSX0Vy
|
||||||
|
FXiLDO7/LoSjsjaf4e4aLofIyLJS3H1T+5cr/d2mdpRzkeWkxShODsK4cRLOlZ5I
|
||||||
|
pz4Wm2770DTbiYph8ixl/CnmYn6T7V0F5VYujALknipUBeQY4e/A9vrQ/pvqJV+W
|
||||||
|
JjFUne6Rxg/lJjh8vNJp2bK1ZbzpwmZLaZIoEz8t/qECgYEAzvCCA48uQPaurSQ3
|
||||||
|
cvHDhcVwYmEaH8MW8aIW/5l8XJK60GsUHPFhEsfD/ObI5PJJ9aOqgabpRHkvD4ZY
|
||||||
|
a8QJBxCy6UeogUeKvGks8VQ34SZXLimmgrL9Mlljv0v9PloEkVYbztYyX4GVO0ov
|
||||||
|
3oH+hKO+/MclzNDyeXZx3Vv4K+UCgYEAwnyb7tqp7fRqm/8EymIZV5pa0p6h609p
|
||||||
|
EhCBi9ii6d/ewEjsBhs7bPDBO4PO9ylvOvryYZH1hVbQja2anOCBjO8dAHRHWM86
|
||||||
|
964TFriywBQkYxp6dsB8nUjLBDza2xAM3m+OGi9/ATuhEAe5sXp/fZL3tkfSaOXI
|
||||||
|
A7Gzro+kS9cCgYEAtKScSfEeBlWQa9H2mV9UN5z/mtF61YkeqTW+b8cTGVh4vWEL
|
||||||
|
wKww+gzqGAV6Duk2CLijKeSDMmO64gl7fC83VjSMiTklbhz+jbQeKFhFI0Sty71N
|
||||||
|
/j+y6NXBTgdOfLRl0lzhj2/JrzdWBtie6tR9UloCaXSKmb04PTFY+kvDWsUCgYBR
|
||||||
|
krJUnKJpi/qrM2tu93Zpp/QwIxkG+We4i/PKFDNApQVo4S0d4o4qQ1DJBZ/pSxe8
|
||||||
|
RUUkZ3PzWVZgFlCjPAcadbBUYHEMbt7sw7Z98ToIFmqspo53AIVD8yQzwtKIz1KW
|
||||||
|
eXPAx+sdOUV008ivCBIxOVNswPMfzED4S7Bxpw3iQQKBgGJhct2nBsgu0l2/wzh9
|
||||||
|
tpKbalW1RllgptNQzjuBEZMTvPF0L+7BE09/exKtt4N9s3yAzi8o6Qo7RHX5djVc
|
||||||
|
SNgafV4jj7jt2Ilh6KOy9dshtLoEkS1NmiqfVe2go2auXZdyGm+I2yzKWdKGDO0J
|
||||||
|
diTtYf1sA0PgNXdSyDC03TZl
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
"""
|
||||||
|
|
||||||
|
SecureCert* = """
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDazCCAlOgAwIBAgIUe9fr78Dz9PedQ5Sq0uluMWQhX9wwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwRTELMAkGA1UEBhMCSU4xEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||||
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTAzMTcwOTMzMzZaFw0zMTAz
|
||||||
|
MTUwOTMzMzZaMEUxCzAJBgNVBAYTAklOMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||||
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4IBDwAwggEKAoIBAQCdNv0SX02aeZ4/Yc+p/Kwd5UVOHlpmK7/TVC/kcjFb
|
||||||
|
doUuKNn8pnX/fyhgSKpUYut+te7YRiZhqlaLEZKjfy8GBZwXZnJCevFkTvGTTebX
|
||||||
|
XExLIsLGfJqKeLAdFCQkX8wV3jV1DT5JLV+D5+HWaiiBr38gsl4ZbfyedTF40Jvz
|
||||||
|
okCmcdlx9bpzX1j/b84L/zSwUyyEcgp5G28FJh5TnxAeDHJpOVjr8XMb/xoNqiDF
|
||||||
|
6NwF96hvOZC14mZ1TxxW5bUzXprsy0l52pmhdN3Crz11+t2h519hRKHxT6/l5pTx
|
||||||
|
/+dApXiP6hMV04CQJNnas3NyRxTDR9dNel+3+wD7/PRTAgMBAAGjUzBRMB0GA1Ud
|
||||||
|
DgQWBBRkSY1AkGUpVNxG5fYocfgFODtQmTAfBgNVHSMEGDAWgBRkSY1AkGUpVNxG
|
||||||
|
5fYocfgFODtQmTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBt
|
||||||
|
D71VH7F8GOQXITFXCrHwEq1Fx3ScuSnL04NJrXw/e9huzLVQOchAYp/EIn4x2utN
|
||||||
|
S31dt94wvi/IysOVbR1LatYNF5kKgGj2Wc6DH0PswBMk8R1G8QMeCz+hCjf1VDHe
|
||||||
|
AAW1x2q20rJAvUrT6cRBQqeiMzQj0OaJbvfnd2hu0/d0DFkcuGVgBa2zlbG5rbdU
|
||||||
|
Jnq7MQfSaZHd0uBgiKkS+Zw6XaYfWfByCAGSnUqRdOChiJ2stFVLvu+9oQ+PJjJt
|
||||||
|
Er1u9bKTUyeuYpqXr2BP9dqphwu8R4NFVUg6DIRpMFMsybaL7KAd4hD22RXCvc0m
|
||||||
|
uLu7KODi+eW62MHqs4N2
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
"""
|
@ -1,2 +1,3 @@
|
|||||||
import ./testframes
|
import ./testframes
|
||||||
import ./testwebsockets
|
import ./testwebsockets
|
||||||
|
import ./testtlswebsockets
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
include ../src/ws
|
include ../ws/ws
|
||||||
include ../src/random
|
include ../ws/random
|
||||||
|
|
||||||
# TODO: Fix Test.
|
# TODO: Fix Test.
|
||||||
|
|
||||||
|
225
tests/testtlswebsockets.nim
Normal file
225
tests/testtlswebsockets.nim
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import std/strutils, httputils
|
||||||
|
|
||||||
|
import pkg/[asynctest,
|
||||||
|
chronos,
|
||||||
|
chronos/apps/http/shttpserver,
|
||||||
|
stew/byteutils]
|
||||||
|
|
||||||
|
import ../ws/[ws, stream],
|
||||||
|
../examples/tlsserver
|
||||||
|
|
||||||
|
import ./keys
|
||||||
|
|
||||||
|
var server: SecureHttpServerRef
|
||||||
|
let address = initTAddress("127.0.0.1:8888")
|
||||||
|
let serverFlags = {HttpServerFlags.Secure, HttpServerFlags.NotifyDisconnect}
|
||||||
|
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||||
|
let clientFlags = {NoVerifyHost, NoVerifyServerName}
|
||||||
|
|
||||||
|
let secureKey = TLSPrivateKey.init(SecureKey)
|
||||||
|
let secureCert = TLSCertificate.init(SecureCert)
|
||||||
|
|
||||||
|
suite "Test websocket TLS handshake":
|
||||||
|
teardown:
|
||||||
|
await server.closeWait()
|
||||||
|
|
||||||
|
test "Test for websocket TLS incorrect protocol":
|
||||||
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
|
if r.isErr():
|
||||||
|
return
|
||||||
|
|
||||||
|
let request = r.get()
|
||||||
|
check request.uri.path == "/wss"
|
||||||
|
expect WSProtoMismatchError:
|
||||||
|
var ws = await createServer(request, "proto")
|
||||||
|
check ws.readyState == ReadyState.Closed
|
||||||
|
|
||||||
|
return await request.respond(Http200, "Connection established")
|
||||||
|
|
||||||
|
let res = SecureHttpServerRef.new(
|
||||||
|
address, cb,
|
||||||
|
serverFlags = serverFlags,
|
||||||
|
socketFlags = socketFlags,
|
||||||
|
tlsPrivateKey = secureKey,
|
||||||
|
tlsCertificate = secureCert)
|
||||||
|
|
||||||
|
server = res.get()
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
expect WSFailedUpgradeError:
|
||||||
|
discard await WebSocket.tlsConnect(
|
||||||
|
"127.0.0.1",
|
||||||
|
Port(8888),
|
||||||
|
path = "/wss",
|
||||||
|
protocols = @["wrongproto"],
|
||||||
|
clientFlags)
|
||||||
|
|
||||||
|
test "Test for websocket TLS incorrect version":
|
||||||
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
|
if r.isErr():
|
||||||
|
return
|
||||||
|
|
||||||
|
let request = r.get()
|
||||||
|
check request.uri.path == "/wss"
|
||||||
|
expect WSVersionError:
|
||||||
|
var ws = await createServer(request, "proto")
|
||||||
|
check ws.readyState == ReadyState.Closed
|
||||||
|
|
||||||
|
return await request.respond(Http200, "Connection established")
|
||||||
|
|
||||||
|
let res = SecureHttpServerRef.new(
|
||||||
|
address, cb,
|
||||||
|
serverFlags = serverFlags,
|
||||||
|
socketFlags = socketFlags,
|
||||||
|
tlsPrivateKey = secureKey,
|
||||||
|
tlsCertificate = secureCert)
|
||||||
|
|
||||||
|
server = res.get()
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
expect WSFailedUpgradeError:
|
||||||
|
discard await WebSocket.tlsConnect(
|
||||||
|
"127.0.0.1",
|
||||||
|
Port(8888),
|
||||||
|
path = "/wss",
|
||||||
|
protocols = @["wrongproto"],
|
||||||
|
clientFlags,
|
||||||
|
version = 14)
|
||||||
|
|
||||||
|
test "Test for websocket TLS client headers":
|
||||||
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
|
check r.isOk()
|
||||||
|
let request = r.get()
|
||||||
|
check request.uri.path == "/wss"
|
||||||
|
check request.headers.getString("Connection").toUpperAscii() == "Upgrade".toUpperAscii()
|
||||||
|
check request.headers.getString("Upgrade").toUpperAscii() == "websocket".toUpperAscii()
|
||||||
|
check request.headers.getString("Cache-Control").toUpperAscii() == "no-cache".toUpperAscii()
|
||||||
|
check request.headers.getString("Sec-WebSocket-Version") == $WSDefaultVersion
|
||||||
|
|
||||||
|
check request.headers.contains("Sec-WebSocket-Key")
|
||||||
|
|
||||||
|
discard await request.respond( Http200,"Connection established")
|
||||||
|
|
||||||
|
let res = SecureHttpServerRef.new(
|
||||||
|
address, cb,
|
||||||
|
serverFlags = serverFlags,
|
||||||
|
socketFlags = socketFlags,
|
||||||
|
tlsPrivateKey = secureKey,
|
||||||
|
tlsCertificate = secureCert)
|
||||||
|
|
||||||
|
server = res.get()
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
expect WSFailedUpgradeError:
|
||||||
|
discard await WebSocket.tlsConnect(
|
||||||
|
"127.0.0.1",
|
||||||
|
Port(8888),
|
||||||
|
path = "/wss",
|
||||||
|
protocols = @["proto"],
|
||||||
|
clientFlags)
|
||||||
|
|
||||||
|
suite "Test websocket TLS transmission":
|
||||||
|
teardown:
|
||||||
|
await server.closeWait()
|
||||||
|
|
||||||
|
test "Server - test reading simple frame":
|
||||||
|
let testString = "Hello!"
|
||||||
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
|
if r.isErr():
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
|
let request = r.get()
|
||||||
|
check request.uri.path == "/wss"
|
||||||
|
let ws = await createServer(request, "proto")
|
||||||
|
let servRes = await ws.recv()
|
||||||
|
check string.fromBytes(servRes) == testString
|
||||||
|
await ws.close()
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
|
let res = SecureHttpServerRef.new(
|
||||||
|
address, cb,
|
||||||
|
serverFlags = serverFlags,
|
||||||
|
socketFlags = socketFlags,
|
||||||
|
tlsPrivateKey = secureKey,
|
||||||
|
tlsCertificate = secureCert)
|
||||||
|
|
||||||
|
server = res.get()
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
let wsClient = await WebSocket.tlsConnect(
|
||||||
|
"127.0.0.1",
|
||||||
|
Port(8888),
|
||||||
|
path = "/wss",
|
||||||
|
protocols = @["proto"],
|
||||||
|
clientFlags)
|
||||||
|
|
||||||
|
await wsClient.send(testString)
|
||||||
|
await wsClient.close()
|
||||||
|
|
||||||
|
test "Client - test reading simple frame":
|
||||||
|
let testString = "Hello!"
|
||||||
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
|
if r.isErr():
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
|
let request = r.get()
|
||||||
|
check request.uri.path == "/wss"
|
||||||
|
let ws = await createServer(request, "proto")
|
||||||
|
let servRes = await ws.recv()
|
||||||
|
check string.fromBytes(servRes) == testString
|
||||||
|
await ws.close()
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
|
let res = SecureHttpServerRef.new(
|
||||||
|
address, cb,
|
||||||
|
serverFlags = serverFlags,
|
||||||
|
socketFlags = socketFlags,
|
||||||
|
tlsPrivateKey = secureKey,
|
||||||
|
tlsCertificate = secureCert)
|
||||||
|
|
||||||
|
server = res.get()
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
let wsClient = await WebSocket.tlsConnect(
|
||||||
|
"127.0.0.1",
|
||||||
|
Port(8888),
|
||||||
|
path = "/wss",
|
||||||
|
protocols = @["proto"],
|
||||||
|
clientFlags)
|
||||||
|
|
||||||
|
await wsClient.send(testString)
|
||||||
|
await wsClient.close()
|
||||||
|
|
||||||
|
test "Client - test reading simple frame":
|
||||||
|
let testString = "Hello!"
|
||||||
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
|
if r.isErr():
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
|
let request = r.get()
|
||||||
|
check request.uri.path == "/wss"
|
||||||
|
let ws = await createServer(request, "proto")
|
||||||
|
await ws.send(testString)
|
||||||
|
await ws.close()
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
|
let res = SecureHttpServerRef.new(
|
||||||
|
address, cb,
|
||||||
|
serverFlags = serverFlags,
|
||||||
|
socketFlags = socketFlags,
|
||||||
|
tlsPrivateKey = secureKey,
|
||||||
|
tlsCertificate = secureCert)
|
||||||
|
|
||||||
|
server = res.get()
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
let wsClient = await WebSocket.tlsConnect(
|
||||||
|
"127.0.0.1",
|
||||||
|
Port(8888),
|
||||||
|
path = "/wss",
|
||||||
|
protocols = @["proto"],
|
||||||
|
clientFlags)
|
||||||
|
|
||||||
|
var clientRes = await wsClient.recv()
|
||||||
|
check string.fromBytes(clientRes) == testString
|
||||||
|
await wsClient.close()
|
@ -4,26 +4,29 @@ import pkg/[asynctest,
|
|||||||
chronos,
|
chronos,
|
||||||
chronos/apps/http/httpserver,
|
chronos/apps/http/httpserver,
|
||||||
stew/byteutils]
|
stew/byteutils]
|
||||||
import ../src/ws, ../src/stream
|
|
||||||
|
import ../ws/[ws, stream]
|
||||||
|
|
||||||
var server: HttpServerRef
|
var server: HttpServerRef
|
||||||
let address = initTAddress("127.0.0.1:8888")
|
let address = initTAddress("127.0.0.1:8888")
|
||||||
|
|
||||||
suite "Test handshake":
|
suite "Test handshake":
|
||||||
teardown:
|
teardown:
|
||||||
|
await server.stop()
|
||||||
await server.closeWait()
|
await server.closeWait()
|
||||||
|
|
||||||
test "Test for incorrect protocol":
|
test "Test for incorrect protocol":
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
expect WSProtoMismatchError:
|
expect WSProtoMismatchError:
|
||||||
var ws = await createServer(request, "proto")
|
var ws = await createServer(request, "proto")
|
||||||
check ws.readyState == ReadyState.Closed
|
check ws.readyState == ReadyState.Closed
|
||||||
|
|
||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(address, cb)
|
||||||
address, cb)
|
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -36,15 +39,16 @@ suite "Test handshake":
|
|||||||
|
|
||||||
test "Test for incorrect version":
|
test "Test for incorrect version":
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
expect WSVersionError:
|
expect WSVersionError:
|
||||||
var ws = await createServer(request, "proto")
|
var ws = await createServer(request, "proto")
|
||||||
check ws.readyState == ReadyState.Closed
|
check ws.readyState == ReadyState.Closed
|
||||||
|
|
||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(address, cb)
|
||||||
address, cb)
|
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -58,7 +62,9 @@ suite "Test handshake":
|
|||||||
|
|
||||||
test "Test for client headers":
|
test "Test for client headers":
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
check request.headers.getString("Connection").toUpperAscii() == "Upgrade".toUpperAscii()
|
check request.headers.getString("Connection").toUpperAscii() == "Upgrade".toUpperAscii()
|
||||||
@ -68,8 +74,7 @@ suite "Test handshake":
|
|||||||
|
|
||||||
check request.headers.contains("Sec-WebSocket-Key")
|
check request.headers.contains("Sec-WebSocket-Key")
|
||||||
|
|
||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(address, cb)
|
||||||
address, cb)
|
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -80,23 +85,49 @@ suite "Test handshake":
|
|||||||
path = "/ws",
|
path = "/ws",
|
||||||
protocols = @["proto"])
|
protocols = @["proto"])
|
||||||
|
|
||||||
|
test "Test for incorrect scheme":
|
||||||
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
|
if r.isErr():
|
||||||
|
return
|
||||||
|
|
||||||
|
let request = r.get()
|
||||||
|
check request.uri.path == "/ws"
|
||||||
|
|
||||||
|
expect WSProtoMismatchError:
|
||||||
|
var ws = await createServer(request, "proto")
|
||||||
|
check ws.readyState == ReadyState.Closed
|
||||||
|
|
||||||
|
return await request.respond(Http200, "Connection established")
|
||||||
|
|
||||||
|
let res = HttpServerRef.new(address, cb)
|
||||||
|
server = res.get()
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
let uri = "wx://127.0.0.1:8888/ws"
|
||||||
|
expect WSWrongUriSchemeError:
|
||||||
|
discard await WebSocket.connect(
|
||||||
|
parseUri(uri),
|
||||||
|
protocols = @["proto"])
|
||||||
|
|
||||||
suite "Test transmission":
|
suite "Test transmission":
|
||||||
teardown:
|
teardown:
|
||||||
await server.closeWait()
|
await server.closeWait()
|
||||||
|
|
||||||
test "Server - test reading simple frame":
|
test "Server - test reading simple frame":
|
||||||
let testString = "Hello!"
|
let testString = "Hello!"
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
let ws = await createServer(request, "proto")
|
let ws = await createServer(request, "proto")
|
||||||
let servRes = await ws.recv()
|
let servRes = await ws.recv()
|
||||||
|
|
||||||
check string.fromBytes(servRes) == testString
|
check string.fromBytes(servRes) == testString
|
||||||
await ws.stream.closeWait()
|
await ws.close()
|
||||||
|
|
||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(address, cb)
|
||||||
address, cb)
|
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -106,18 +137,21 @@ suite "Test transmission":
|
|||||||
path = "/ws",
|
path = "/ws",
|
||||||
protocols = @["proto"])
|
protocols = @["proto"])
|
||||||
await wsClient.send(testString)
|
await wsClient.send(testString)
|
||||||
|
await wsClient.close()
|
||||||
|
|
||||||
test "Client - test reading simple frame":
|
test "Client - test reading simple frame":
|
||||||
let testString = "Hello!"
|
let testString = "Hello!"
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
let ws = await createServer(request, "proto")
|
let ws = await createServer(request, "proto")
|
||||||
await ws.send(testString)
|
await ws.send(testString)
|
||||||
await ws.stream.closeWait()
|
await ws.close()
|
||||||
let res = HttpServerRef.new(
|
|
||||||
address, cb)
|
let res = HttpServerRef.new(address, cb)
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -128,6 +162,7 @@ suite "Test transmission":
|
|||||||
protocols = @["proto"])
|
protocols = @["proto"])
|
||||||
|
|
||||||
var clientRes = await wsClient.recv()
|
var clientRes = await wsClient.recv()
|
||||||
|
await wsClient.close()
|
||||||
check string.fromBytes(clientRes) == testString
|
check string.fromBytes(clientRes) == testString
|
||||||
|
|
||||||
suite "Test ping-pong":
|
suite "Test ping-pong":
|
||||||
@ -137,7 +172,9 @@ suite "Test ping-pong":
|
|||||||
test "Server - test ping-pong control messages":
|
test "Server - test ping-pong control messages":
|
||||||
var ping, pong = false
|
var ping, pong = false
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
let ws = await createServer(
|
let ws = await createServer(
|
||||||
@ -146,11 +183,11 @@ suite "Test ping-pong":
|
|||||||
onPong = proc() =
|
onPong = proc() =
|
||||||
pong = true
|
pong = true
|
||||||
)
|
)
|
||||||
|
|
||||||
await ws.ping()
|
await ws.ping()
|
||||||
await ws.close()
|
await ws.close()
|
||||||
|
|
||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(address, cb)
|
||||||
address, cb)
|
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -171,7 +208,9 @@ suite "Test ping-pong":
|
|||||||
test "Client - test ping-pong control messages":
|
test "Client - test ping-pong control messages":
|
||||||
var ping, pong = false
|
var ping, pong = false
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
let ws = await createServer(
|
let ws = await createServer(
|
||||||
@ -182,8 +221,9 @@ suite "Test ping-pong":
|
|||||||
)
|
)
|
||||||
|
|
||||||
discard await ws.recv()
|
discard await ws.recv()
|
||||||
let res = HttpServerRef.new(
|
await ws.close()
|
||||||
address, cb)
|
|
||||||
|
let res = HttpServerRef.new(address, cb)
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -208,13 +248,14 @@ suite "Test framing":
|
|||||||
|
|
||||||
test "should split message into frames":
|
test "should split message into frames":
|
||||||
let testString = "1234567890"
|
let testString = "1234567890"
|
||||||
var done = newFuture[void]()
|
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef]{.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef]{.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
let ws = await createServer(request, "proto")
|
|
||||||
|
|
||||||
|
let ws = await createServer(request, "proto")
|
||||||
let frame1 = await ws.readFrame()
|
let frame1 = await ws.readFrame()
|
||||||
check not isNil(frame1)
|
check not isNil(frame1)
|
||||||
var data1 = newSeq[byte](frame1.remainder().int)
|
var data1 = newSeq[byte](frame1.remainder().int)
|
||||||
@ -227,11 +268,10 @@ suite "Test framing":
|
|||||||
let read2 = await ws.stream.reader.readOnce(addr data2[0], data2.len)
|
let read2 = await ws.stream.reader.readOnce(addr data2[0], data2.len)
|
||||||
check read2 == 5
|
check read2 == 5
|
||||||
|
|
||||||
await ws.stream.closeWait()
|
await ws.close()
|
||||||
done.complete()
|
return dumbResponse()
|
||||||
|
|
||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(address, cb)
|
||||||
address, cb)
|
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -243,20 +283,22 @@ suite "Test framing":
|
|||||||
frameSize = 5)
|
frameSize = 5)
|
||||||
|
|
||||||
await wsClient.send(testString)
|
await wsClient.send(testString)
|
||||||
await done
|
await wsClient.close()
|
||||||
|
|
||||||
test "should fail to read past max message size":
|
test "should fail to read past max message size":
|
||||||
let testString = "1234567890"
|
let testString = "1234567890"
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async, gcsafe.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async, gcsafe.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
let ws = await createServer(request, "proto")
|
let ws = await createServer(request, "proto")
|
||||||
await ws.send(testString)
|
await ws.send(testString)
|
||||||
await ws.stream.closeWait()
|
await ws.close()
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(address, cb)
|
||||||
address, cb)
|
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -269,19 +311,24 @@ suite "Test framing":
|
|||||||
expect WSMaxMessageSizeError:
|
expect WSMaxMessageSizeError:
|
||||||
discard await wsClient.recv(5)
|
discard await wsClient.recv(5)
|
||||||
|
|
||||||
|
await wsClient.close()
|
||||||
|
|
||||||
suite "Test Closing":
|
suite "Test Closing":
|
||||||
teardown:
|
teardown:
|
||||||
await server.closeWait()
|
await server.closeWait()
|
||||||
|
|
||||||
test "Server closing":
|
test "Server closing":
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
let ws = await createServer(request, "proto")
|
let ws = await createServer(request, "proto")
|
||||||
await ws.close()
|
await ws.close()
|
||||||
let res = HttpServerRef.new(
|
return dumbResponse()
|
||||||
address, cb)
|
|
||||||
|
let res = HttpServerRef.new(address, cb)
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -296,12 +343,18 @@ suite "Test Closing":
|
|||||||
|
|
||||||
test "Server closing with status":
|
test "Server closing with status":
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
proc closeServer(status: Status, reason: string): CloseResult {.gcsafe.} =
|
proc closeServer(status: Status, reason: string): CloseResult
|
||||||
check status == Status.TooLarge
|
{.gcsafe, raises: [Defect].} =
|
||||||
check reason == "Message too big!"
|
try:
|
||||||
|
check status == Status.TooLarge
|
||||||
|
check reason == "Message too big!"
|
||||||
|
except Exception as exc:
|
||||||
|
raise newException(Defect, exc.msg)
|
||||||
|
|
||||||
return (Status.Fulfilled, "")
|
return (Status.Fulfilled, "")
|
||||||
|
|
||||||
@ -311,15 +364,19 @@ suite "Test Closing":
|
|||||||
onClose = closeServer)
|
onClose = closeServer)
|
||||||
|
|
||||||
await ws.close()
|
await ws.close()
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(address, cb)
|
||||||
address, cb)
|
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
proc clientClose(status: Status, reason: string): CloseResult {.gcsafe.} =
|
proc clientClose(status: Status, reason: string): CloseResult
|
||||||
check status == Status.Fulfilled
|
{.gcsafe, raises: [Defect].} =
|
||||||
return (Status.TooLarge, "Message too big!")
|
try:
|
||||||
|
check status == Status.Fulfilled
|
||||||
|
return (Status.TooLarge, "Message too big!")
|
||||||
|
except Exception as exc:
|
||||||
|
raise newException(Defect, exc.msg)
|
||||||
|
|
||||||
let wsClient = await WebSocket.connect(
|
let wsClient = await WebSocket.connect(
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
@ -333,14 +390,17 @@ suite "Test Closing":
|
|||||||
|
|
||||||
test "Client closing":
|
test "Client closing":
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
let ws = await createServer(request, "proto")
|
let ws = await createServer(request, "proto")
|
||||||
discard await ws.recv()
|
discard await ws.recv()
|
||||||
|
await ws.close()
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(address, cb)
|
||||||
address, cb)
|
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
@ -353,28 +413,39 @@ suite "Test Closing":
|
|||||||
|
|
||||||
test "Client closing with status":
|
test "Client closing with status":
|
||||||
proc cb(r: RequestFence): Future[HttpResponseRef] {.async, gcsafe.} =
|
proc cb(r: RequestFence): Future[HttpResponseRef] {.async, gcsafe.} =
|
||||||
check r.isOk()
|
if r.isErr():
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
let request = r.get()
|
let request = r.get()
|
||||||
check request.uri.path == "/ws"
|
check request.uri.path == "/ws"
|
||||||
proc closeServer(status: Status, reason: string): CloseResult {.gcsafe.} =
|
proc closeServer(status: Status, reason: string): CloseResult
|
||||||
check status == Status.Fulfilled
|
{.gcsafe, raises: [Defect].} =
|
||||||
return (Status.TooLarge, "Message too big!")
|
try:
|
||||||
|
check status == Status.Fulfilled
|
||||||
|
return (Status.TooLarge, "Message too big!")
|
||||||
|
except Exception as exc:
|
||||||
|
raise newException(Defect, exc.msg)
|
||||||
|
|
||||||
let ws = await createServer(
|
let ws = await createServer(
|
||||||
request,
|
request,
|
||||||
"proto",
|
"proto",
|
||||||
onClose = closeServer)
|
onClose = closeServer)
|
||||||
discard await ws.recv()
|
discard await ws.recv()
|
||||||
|
await ws.close()
|
||||||
|
return dumbResponse()
|
||||||
|
|
||||||
let res = HttpServerRef.new(
|
let res = HttpServerRef.new(address, cb)
|
||||||
address, cb)
|
|
||||||
server = res.get()
|
server = res.get()
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
proc clientClose(status: Status, reason: string): CloseResult {.gcsafe.} =
|
proc clientClose(status: Status, reason: string): CloseResult
|
||||||
check status == Status.TooLarge
|
{.gcsafe, raises: [Defect].} =
|
||||||
check reason == "Message too big!"
|
try:
|
||||||
return (Status.Fulfilled, "")
|
check status == Status.TooLarge
|
||||||
|
check reason == "Message too big!"
|
||||||
|
return (Status.Fulfilled, "")
|
||||||
|
except Exception as exc:
|
||||||
|
raise newException(Defect, exc.msg)
|
||||||
|
|
||||||
let wsClient = await WebSocket.connect(
|
let wsClient = await WebSocket.connect(
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
|
10
ws.nimble
10
ws.nimble
@ -3,16 +3,20 @@ version = "0.1.0"
|
|||||||
author = "Status Research & Development GmbH"
|
author = "Status Research & Development GmbH"
|
||||||
description = "WS protocol implementation"
|
description = "WS protocol implementation"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
skipDirs = @["examples", "test"]
|
||||||
|
|
||||||
requires "nim == 1.2.6"
|
requires "nim >= 1.2.6"
|
||||||
requires "chronos >= 2.5.2"
|
requires "chronos >= 2.5.2"
|
||||||
requires "httputils >= 0.2.0"
|
requires "httputils >= 0.2.0"
|
||||||
requires "chronicles >= 0.10.0"
|
requires "chronicles >= 0.10.0"
|
||||||
requires "urlly >= 0.2.0"
|
|
||||||
requires "stew >= 0.1.0"
|
requires "stew >= 0.1.0"
|
||||||
requires "eth"
|
|
||||||
requires "asynctest >= 0.2.0 & < 0.3.0"
|
requires "asynctest >= 0.2.0 & < 0.3.0"
|
||||||
requires "nimcrypto"
|
requires "nimcrypto"
|
||||||
|
requires "bearssl"
|
||||||
|
|
||||||
task test, "run tests":
|
task test, "run tests":
|
||||||
exec "nim c -r --opt:speed -d:debug --verbosity:0 --hints:off ./tests/testall.nim"
|
exec "nim c -r --opt:speed -d:debug --verbosity:0 --hints:off ./tests/testall.nim"
|
||||||
|
rmFile "./tests/testall"
|
||||||
|
rmFile "./tests/testwebsockets"
|
||||||
|
rmFile "./tests/testframes"
|
||||||
|
rmFile "./tests/testtlswebsockets"
|
||||||
|
@ -1,8 +1,23 @@
|
|||||||
import bearssl
|
import bearssl
|
||||||
|
export bearssl
|
||||||
|
|
||||||
## Random helpers: similar as in stdlib, but with BrHmacDrbgContext rng
|
## Random helpers: similar as in stdlib, but with BrHmacDrbgContext rng
|
||||||
const randMax = 18_446_744_073_709_551_615'u64
|
const randMax = 18_446_744_073_709_551_615'u64
|
||||||
|
|
||||||
|
proc newRng*(): ref BrHmacDrbgContext =
|
||||||
|
# You should only create one instance of the RNG per application / library
|
||||||
|
# Ref is used so that it can be shared between components
|
||||||
|
# TODO consider moving to bearssl
|
||||||
|
var seeder = brPrngSeederSystem(nil)
|
||||||
|
if seeder == nil:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
var rng = (ref BrHmacDrbgContext)()
|
||||||
|
brHmacDrbgInit(addr rng[], addr sha256Vtable, nil, 0)
|
||||||
|
if seeder(addr rng.vtable) == 0:
|
||||||
|
return nil
|
||||||
|
rng
|
||||||
|
|
||||||
proc rand*(rng: var BrHmacDrbgContext, max: Natural): int =
|
proc rand*(rng: var BrHmacDrbgContext, max: Natural): int =
|
||||||
if max == 0: return 0
|
if max == 0: return 0
|
||||||
var x: uint64
|
var x: uint64
|
@ -6,9 +6,9 @@
|
|||||||
import strutils
|
import strutils
|
||||||
|
|
||||||
const
|
const
|
||||||
HttpHeadersTimeout = timer.seconds(120) # timeout for receiving headers (120 sec)
|
|
||||||
HeaderSep = @[byte('\c'), byte('\L'), byte('\c'), byte('\L')]
|
HeaderSep = @[byte('\c'), byte('\L'), byte('\c'), byte('\L')]
|
||||||
MaxHttpHeadersSize = 8192 # maximum size of HTTP headers in octets
|
HttpHeadersTimeout = timer.seconds(120) # timeout for receiving headers (120 sec)
|
||||||
|
MaxHttpHeadersSize = 8192 # maximum size of HTTP headers in octets
|
||||||
|
|
||||||
proc readHeaders*(rstream: AsyncStreamReader): Future[seq[byte]] {.async.} =
|
proc readHeaders*(rstream: AsyncStreamReader): Future[seq[byte]] {.async.} =
|
||||||
var buffer = newSeq[byte](MaxHttpHeadersSize)
|
var buffer = newSeq[byte](MaxHttpHeadersSize)
|
||||||
@ -42,12 +42,12 @@ proc readHeaders*(rstream: AsyncStreamReader): Future[seq[byte]] {.async.} =
|
|||||||
|
|
||||||
if error:
|
if error:
|
||||||
buffer.setLen(0)
|
buffer.setLen(0)
|
||||||
|
|
||||||
return buffer
|
return buffer
|
||||||
|
|
||||||
proc closeWait*(wsStream : AsyncStream): Future[void] {.async.} =
|
proc closeWait*(wsStream : AsyncStream) {.async.} =
|
||||||
if not wsStream.writer.tsource.closed():
|
await allFutures(
|
||||||
await wsStream.writer.tsource.closeWait()
|
wsStream.writer.tsource.closeWait(),
|
||||||
if not wsStream.reader.tsource.closed():
|
wsStream.reader.tsource.closeWait())
|
||||||
await wsStream.reader.tsource.closeWait()
|
|
||||||
|
|
||||||
# TODO: Implement stream read and write wrapper.
|
# TODO: Implement stream read and write wrapper.
|
@ -1,3 +1,5 @@
|
|||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import std/[tables,
|
import std/[tables,
|
||||||
strutils,
|
strutils,
|
||||||
uri,
|
uri,
|
||||||
@ -7,13 +9,13 @@ import pkg/[chronos,
|
|||||||
chronos/apps/http/httptable,
|
chronos/apps/http/httptable,
|
||||||
chronos/apps/http/httpserver,
|
chronos/apps/http/httpserver,
|
||||||
chronos/streams/asyncstream,
|
chronos/streams/asyncstream,
|
||||||
|
chronos/streams/tlsstream,
|
||||||
chronicles,
|
chronicles,
|
||||||
httputils,
|
httputils,
|
||||||
stew/byteutils,
|
stew/byteutils,
|
||||||
stew/endians2,
|
stew/endians2,
|
||||||
stew/base64,
|
stew/base64,
|
||||||
stew/base10,
|
stew/base10,
|
||||||
eth/keys,
|
|
||||||
nimcrypto/sha]
|
nimcrypto/sha]
|
||||||
|
|
||||||
import ./random, ./stream
|
import ./random, ./stream
|
||||||
@ -125,14 +127,14 @@ type
|
|||||||
length: uint64 ## Message size.
|
length: uint64 ## Message size.
|
||||||
consumed: uint64 ## how much has been consumed from the frame
|
consumed: uint64 ## how much has been consumed from the frame
|
||||||
|
|
||||||
ControlCb* = proc() {.gcsafe.}
|
ControlCb* = proc() {.gcsafe, raises: [Defect].}
|
||||||
|
|
||||||
CloseResult* = tuple
|
CloseResult* = tuple
|
||||||
code: Status
|
code: Status
|
||||||
reason: string
|
reason: string
|
||||||
|
|
||||||
CloseCb* = proc(code: Status, reason: string):
|
CloseCb* = proc(code: Status, reason: string):
|
||||||
CloseResult {.gcsafe.}
|
CloseResult {.gcsafe, raises: [Defect].}
|
||||||
|
|
||||||
WebSocket* = ref object
|
WebSocket* = ref object
|
||||||
stream*: AsyncStream
|
stream*: AsyncStream
|
||||||
@ -208,9 +210,11 @@ proc handshake*(
|
|||||||
|
|
||||||
let cKey = ws.key & WSGuid
|
let cKey = ws.key & WSGuid
|
||||||
let acceptKey = Base64Pad.encode(sha1.digest(cKey.toOpenArray(0, cKey.high)).data)
|
let acceptKey = Base64Pad.encode(sha1.digest(cKey.toOpenArray(0, cKey.high)).data)
|
||||||
|
var headerData = [
|
||||||
|
("Connection", "Upgrade"),
|
||||||
|
("Upgrade", "webSocket" ),
|
||||||
|
("Sec-WebSocket-Accept", acceptKey)]
|
||||||
|
|
||||||
var headerData = [("Connection", "Upgrade"),("Upgrade", "webSocket" ),
|
|
||||||
("Sec-WebSocket-Accept", acceptKey)]
|
|
||||||
var headers = HttpTable.init(headerData)
|
var headers = HttpTable.init(headerData)
|
||||||
if ws.protocol != "":
|
if ws.protocol != "":
|
||||||
headers.add("Sec-WebSocket-Protocol", ws.protocol)
|
headers.add("Sec-WebSocket-Protocol", ws.protocol)
|
||||||
@ -404,7 +408,7 @@ proc handleControl*(ws: WebSocket, frame: Frame) {.async.} =
|
|||||||
|
|
||||||
if frame.length > 125:
|
if frame.length > 125:
|
||||||
raise newException(WSPayloadTooLarge,
|
raise newException(WSPayloadTooLarge,
|
||||||
"Control message payload is freater than 125 bytes!")
|
"Control message payload is greater than 125 bytes!")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Process control frame payload.
|
# Process control frame payload.
|
||||||
@ -439,7 +443,7 @@ proc readFrame*(ws: WebSocket): Future[Frame] {.async.} =
|
|||||||
##
|
##
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while ws.readyState != ReadyState.Closed: # read until a data frame arrives
|
while ws.readyState != ReadyState.Closed:
|
||||||
# Grab the header.
|
# Grab the header.
|
||||||
var header = newSeq[byte](2)
|
var header = newSeq[byte](2)
|
||||||
await ws.stream.reader.readExactly(addr header[0], 2)
|
await ws.stream.reader.readExactly(addr header[0], 2)
|
||||||
@ -525,7 +529,7 @@ proc recv*(
|
|||||||
size: int): Future[int] {.async.} =
|
size: int): Future[int] {.async.} =
|
||||||
## Attempts to read up to `size` bytes
|
## Attempts to read up to `size` bytes
|
||||||
##
|
##
|
||||||
## Will read as many frames as necesary
|
## Will read as many frames as necessary
|
||||||
## to fill the buffer until either
|
## to fill the buffer until either
|
||||||
## the message ends (frame.fin) or
|
## the message ends (frame.fin) or
|
||||||
## the buffer is full. If no data is on
|
## the buffer is full. If no data is on
|
||||||
@ -643,7 +647,8 @@ proc close*(
|
|||||||
proc initiateHandshake(
|
proc initiateHandshake(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
address: TransportAddress,
|
address: TransportAddress,
|
||||||
headers: HttpTable): Future[AsyncStream] {.async.} =
|
headers: HttpTable,
|
||||||
|
flags: set[TLSFlags] = {}): Future[AsyncStream] {.async.} =
|
||||||
## Initiate handshake with server
|
## Initiate handshake with server
|
||||||
|
|
||||||
var transp: StreamTransport
|
var transp: StreamTransport
|
||||||
@ -654,11 +659,27 @@ proc initiateHandshake(
|
|||||||
TransportError,
|
TransportError,
|
||||||
"Cannot connect to " & $transp.remoteAddress() & " Error: " & exc.msg)
|
"Cannot connect to " & $transp.remoteAddress() & " Error: " & exc.msg)
|
||||||
|
|
||||||
|
let requestHeader = "GET " & uri.path & " HTTP/1.1" & CRLF & $headers
|
||||||
let reader = newAsyncStreamReader(transp)
|
let reader = newAsyncStreamReader(transp)
|
||||||
let writer = newAsyncStreamWriter(transp)
|
let writer = newAsyncStreamWriter(transp)
|
||||||
let requestHeader = "GET " & uri.path & " HTTP/1.1" & CRLF & $headers
|
var stream: AsyncStream
|
||||||
await writer.write(requestHeader)
|
|
||||||
let res = await reader.readHeaders()
|
var res: seq[byte]
|
||||||
|
if uri.scheme == "https":
|
||||||
|
let tlsstream = newTLSClientAsyncStream(reader, writer, "", flags = flags)
|
||||||
|
stream = AsyncStream(
|
||||||
|
reader: tlsstream.reader,
|
||||||
|
writer: tlsstream.writer)
|
||||||
|
|
||||||
|
await tlsstream.writer.write(requestHeader)
|
||||||
|
res = await tlsstream.reader.readHeaders()
|
||||||
|
else:
|
||||||
|
stream = AsyncStream(
|
||||||
|
reader: reader,
|
||||||
|
writer: writer)
|
||||||
|
await stream.writer.write(requestHeader)
|
||||||
|
res = await stream.reader.readHeaders()
|
||||||
|
|
||||||
if res.len == 0:
|
if res.len == 0:
|
||||||
raise newException(ValueError, "Empty response from server")
|
raise newException(ValueError, "Empty response from server")
|
||||||
|
|
||||||
@ -674,14 +695,13 @@ proc initiateHandshake(
|
|||||||
" Header reason: " & resHeader.reason() &
|
" Header reason: " & resHeader.reason() &
|
||||||
" Address: " & $transp.remoteAddress())
|
" Address: " & $transp.remoteAddress())
|
||||||
|
|
||||||
return AsyncStream(
|
return stream
|
||||||
reader: reader,
|
|
||||||
writer: writer)
|
|
||||||
|
|
||||||
proc connect*(
|
proc connect*(
|
||||||
_: type WebSocket,
|
_: type WebSocket,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
protocols: seq[string] = @[],
|
protocols: seq[string] = @[],
|
||||||
|
flags: set[TLSFlags] = {},
|
||||||
version = WSDefaultVersion,
|
version = WSDefaultVersion,
|
||||||
frameSize = WSDefaultFrameSize,
|
frameSize = WSDefaultFrameSize,
|
||||||
onPing: ControlCb = nil,
|
onPing: ControlCb = nil,
|
||||||
@ -695,8 +715,10 @@ proc connect*(
|
|||||||
case uri.scheme
|
case uri.scheme
|
||||||
of "ws":
|
of "ws":
|
||||||
uri.scheme = "http"
|
uri.scheme = "http"
|
||||||
|
of "wss":
|
||||||
|
uri.scheme = "https"
|
||||||
else:
|
else:
|
||||||
raise newException(WSWrongUriSchemeError, "uri scheme has to be 'ws'")
|
raise newException(WSWrongUriSchemeError, "uri scheme has to be 'ws' or 'wss'")
|
||||||
|
|
||||||
var headerData = [
|
var headerData = [
|
||||||
("Connection", "Upgrade"),
|
("Connection", "Upgrade"),
|
||||||
@ -711,7 +733,7 @@ proc connect*(
|
|||||||
headers.add("Sec-WebSocket-Protocol", protocols.join(", "))
|
headers.add("Sec-WebSocket-Protocol", protocols.join(", "))
|
||||||
|
|
||||||
let address = initTAddress(uri.hostname & ":" & uri.port)
|
let address = initTAddress(uri.hostname & ":" & uri.port)
|
||||||
let stream = await initiateHandshake(uri, address, headers)
|
let stream = await initiateHandshake(uri, address, headers, flags)
|
||||||
|
|
||||||
# Client data should be masked.
|
# Client data should be masked.
|
||||||
return WebSocket(
|
return WebSocket(
|
||||||
@ -748,6 +770,36 @@ proc connect*(
|
|||||||
return await WebSocket.connect(
|
return await WebSocket.connect(
|
||||||
parseUri(uri),
|
parseUri(uri),
|
||||||
protocols,
|
protocols,
|
||||||
|
{},
|
||||||
|
version,
|
||||||
|
frameSize,
|
||||||
|
onPing,
|
||||||
|
onPong,
|
||||||
|
onClose)
|
||||||
|
|
||||||
|
proc tlsConnect*(
|
||||||
|
_: type WebSocket,
|
||||||
|
host: string,
|
||||||
|
port: Port,
|
||||||
|
path: string,
|
||||||
|
protocols: seq[string] = @[],
|
||||||
|
flags: set[TLSFlags] = {},
|
||||||
|
version = WSDefaultVersion,
|
||||||
|
frameSize = WSDefaultFrameSize,
|
||||||
|
onPing: ControlCb = nil,
|
||||||
|
onPong: ControlCb = nil,
|
||||||
|
onClose: CloseCb = nil): Future[WebSocket] {.async.} =
|
||||||
|
|
||||||
|
var uri = "wss://" & host & ":" & $port
|
||||||
|
if path.startsWith("/"):
|
||||||
|
uri.add path
|
||||||
|
else:
|
||||||
|
uri.add "/" & path
|
||||||
|
|
||||||
|
return await WebSocket.connect(
|
||||||
|
parseUri(uri),
|
||||||
|
protocols,
|
||||||
|
flags,
|
||||||
version,
|
version,
|
||||||
frameSize,
|
frameSize,
|
||||||
onPing,
|
onPing,
|
Loading…
x
Reference in New Issue
Block a user