303 lines
8.8 KiB
Nim
303 lines
8.8 KiB
Nim
# json-rpc
|
|
# Copyright (c) 2023 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.
|
|
|
|
import
|
|
std/importutils,
|
|
unittest2,
|
|
chronicles,
|
|
websock/websock,
|
|
../json_rpc/rpcclient,
|
|
../json_rpc/rpcserver
|
|
|
|
createRpcSigsFromNim(RpcClient):
|
|
proc get_Banana(id: int): int
|
|
|
|
proc installHandlers(s: RpcServer) =
|
|
s.rpc("get_Banana") do(id: int) -> JsonString:
|
|
if id == 99:
|
|
return "123".JsonString
|
|
elif id == 100:
|
|
return "\"stop\"".JsonString
|
|
else:
|
|
return "\"error\"".JsonString
|
|
|
|
type
|
|
Shadow = ref object
|
|
something: int
|
|
|
|
proc setupClientHook(client: RpcClient): Shadow =
|
|
var shadow = Shadow(something: 0)
|
|
client.onProcessMessage = proc(client: RpcClient, line: string):
|
|
Result[bool, string] {.gcsafe, raises: [].} =
|
|
|
|
try:
|
|
let m = JrpcConv.decode(line, JsonNode)
|
|
if m["result"].kind == JString:
|
|
if m["result"].str == "stop":
|
|
shadow.something = 123
|
|
return ok(false)
|
|
else:
|
|
shadow.something = 77
|
|
return err("not stop")
|
|
|
|
return ok(true)
|
|
except CatchableError as exc:
|
|
return err(exc.msg)
|
|
shadow
|
|
|
|
suite "test client features":
|
|
var server = newRpcHttpServer(["127.0.0.1:0"])
|
|
server.installHandlers()
|
|
var client = newRpcHttpClient()
|
|
let shadow = client.setupClientHook()
|
|
|
|
server.start()
|
|
waitFor client.connect("http://" & $server.localAddress()[0])
|
|
|
|
test "client onProcessMessage hook":
|
|
let res = waitFor client.get_Banana(99)
|
|
check res == 123
|
|
check shadow.something == 0
|
|
|
|
expect JsonRpcError:
|
|
let res2 = waitFor client.get_Banana(123)
|
|
check res2 == 0
|
|
check shadow.something == 77
|
|
|
|
expect InvalidResponse:
|
|
let res2 = waitFor client.get_Banana(100)
|
|
check res2 == 0
|
|
check shadow.something == 123
|
|
|
|
waitFor server.stop()
|
|
waitFor server.closeWait()
|
|
|
|
|
|
type
|
|
TestSocketServer = ref object of RpcSocketServer
|
|
getData: proc(): string {.gcsafe, raises: [].}
|
|
|
|
proc processClient(server: StreamServer, transport: StreamTransport) {.async: (raises: []), gcsafe.} =
|
|
## Process transport data to the RPC server
|
|
try:
|
|
var rpc = getUserData[TestSocketServer](server)
|
|
while true:
|
|
var
|
|
value = await transport.readLine(router.defaultMaxRequestLength)
|
|
if value == "":
|
|
await transport.closeWait()
|
|
break
|
|
|
|
let res = rpc.getData()
|
|
discard await transport.write(res & "\r\n")
|
|
except TransportError as ex:
|
|
error "Transport closed during processing client", msg=ex.msg
|
|
except CatchableError as ex:
|
|
error "Error occured during processing client", msg=ex.msg
|
|
|
|
proc addStreamServer(server: TestSocketServer, address: TransportAddress) =
|
|
privateAccess(RpcSocketServer)
|
|
try:
|
|
info "Starting JSON-RPC socket server", address = $address
|
|
var transportServer = createStreamServer(address, processClient, {ReuseAddr}, udata = server)
|
|
server.servers.add(transportServer)
|
|
except CatchableError as exc:
|
|
error "Failed to create server", address = $address, message = exc.msg
|
|
|
|
if len(server.servers) == 0:
|
|
# Server was not bound, critical error.
|
|
raise newException(RpcBindError, "Unable to create server!")
|
|
|
|
proc new(T: type TestSocketServer, getData: proc(): string {.gcsafe, raises: [].}): T =
|
|
T(
|
|
router: RpcRouter.init(),
|
|
getData: getData,
|
|
)
|
|
|
|
|
|
suite "test rpc socket client":
|
|
let server = TestSocketServer.new(proc(): string {.gcsafe, raises: [].} =
|
|
return """{"jsonrpc":"2.0","result":10}"""
|
|
)
|
|
let serverAddress = initTAddress("127.0.0.1:0")
|
|
server.addStreamServer(serverAddress)
|
|
|
|
var client = newRpcSocketClient()
|
|
server.start()
|
|
waitFor client.connect(server.localAddress()[0])
|
|
|
|
test "missing id in server response":
|
|
expect JsonRpcError:
|
|
let res = waitFor client.get_Banana(11)
|
|
discard res
|
|
|
|
server.stop()
|
|
waitFor server.closeWait()
|
|
|
|
|
|
type
|
|
TestHttpServer = ref object of RpcHttpServer
|
|
getData: proc(): string {.gcsafe, raises: [].}
|
|
|
|
proc processClientRpc(rpcServer: TestHttpServer): HttpProcessCallback2 =
|
|
return proc (req: RequestFence): Future[HttpResponseRef]
|
|
{.async: (raises: [CancelledError]).} =
|
|
if not req.isOk():
|
|
return defaultResponse()
|
|
|
|
let
|
|
request = req.get()
|
|
headers = HttpTable.init([("Content-Type",
|
|
"application/json; charset=utf-8")])
|
|
try:
|
|
let data = rpcServer.getData()
|
|
let res = await request.respond(Http200, data, headers)
|
|
trace "JSON-RPC result has been sent"
|
|
return res
|
|
except CancelledError as exc:
|
|
raise exc
|
|
except CatchableError as exc:
|
|
debug "Internal error while processing JSON-RPC call"
|
|
return defaultResponse(exc)
|
|
|
|
proc addHttpServer(
|
|
rpcServer: TestHttpServer,
|
|
address: TransportAddress,
|
|
socketFlags: set[ServerFlags] = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr},
|
|
serverUri = Uri(),
|
|
serverIdent = "",
|
|
maxConnections: int = -1,
|
|
bufferSize: int = 4096,
|
|
backlogSize: int = 100,
|
|
httpHeadersTimeout = 10.seconds,
|
|
maxHeadersSize: int = 8192,
|
|
maxRequestBodySize: int = 1_048_576) =
|
|
let server = HttpServerRef.new(
|
|
address,
|
|
processClientRpc(rpcServer),
|
|
{},
|
|
socketFlags,
|
|
serverUri, "nim-json-rpc", maxConnections, backlogSize,
|
|
bufferSize, httpHeadersTimeout, maxHeadersSize, maxRequestBodySize
|
|
).valueOr:
|
|
error "Failed to create server", address = $address,
|
|
message = error
|
|
raise newException(RpcBindError, "Unable to create server: " & $error)
|
|
info "Starting JSON-RPC HTTP server", url = "http://" & $address
|
|
|
|
privateAccess(RpcHttpServer)
|
|
rpcServer.httpServers.add server
|
|
|
|
proc new(T: type TestHttpServer, getData: proc(): string {.gcsafe, raises: [].}): T =
|
|
T(
|
|
router: RpcRouter.init(),
|
|
maxChunkSize: 8192,
|
|
getData: getData,
|
|
)
|
|
|
|
suite "test rpc http client":
|
|
let server = TestHttpServer.new(proc(): string {.gcsafe, raises: [].} =
|
|
return """{"jsonrpc":"2.0","result":10}"""
|
|
)
|
|
let serverAddress = initTAddress("127.0.0.1:0")
|
|
server.addHttpServer(serverAddress)
|
|
|
|
var client = newRpcHttpClient()
|
|
server.start()
|
|
waitFor client.connect("http://" & $server.localAddress()[0])
|
|
|
|
test "missing id in server response":
|
|
expect JsonRpcError:
|
|
let res = waitFor client.get_Banana(11)
|
|
discard res
|
|
|
|
waitFor server.stop()
|
|
waitFor server.closeWait()
|
|
|
|
|
|
type
|
|
TestWsServer = ref object of RpcWebSocketServer
|
|
getData: proc(): string {.gcsafe, raises: [].}
|
|
|
|
proc handleRequest(rpc: TestWsServer, request: websock.HttpRequest)
|
|
{.async: (raises: [CancelledError]).} =
|
|
try:
|
|
let server = rpc.wsserver
|
|
let ws = await server.handleRequest(request)
|
|
if ws.readyState != ReadyState.Open:
|
|
error "Failed to open websocket connection"
|
|
return
|
|
|
|
trace "Websocket handshake completed"
|
|
while ws.readyState != ReadyState.Closed:
|
|
let recvData = await ws.recvMsg()
|
|
trace "Client message: ", size = recvData.len, binary = ws.binary
|
|
|
|
if ws.readyState == ReadyState.Closed:
|
|
# if session already terminated by peer,
|
|
# no need to send response
|
|
break
|
|
|
|
if recvData.len == 0:
|
|
await ws.close(
|
|
reason = "cannot process zero length message"
|
|
)
|
|
break
|
|
|
|
let data = rpc.getData()
|
|
|
|
trace "RPC result has been sent", address = $request.uri
|
|
await ws.send(data)
|
|
|
|
except WebSocketError as exc:
|
|
error "WebSocket error:", exception = exc.msg
|
|
|
|
except CancelledError as exc:
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
error "Something error", msg=exc.msg
|
|
|
|
proc newWsServer(address: TransportAddress, getData: proc(): string {.gcsafe, raises: [].}): TestWsServer =
|
|
|
|
let flags = {ServerFlags.TcpNoDelay,ServerFlags.ReuseAddr}
|
|
var server = new(TestWsServer)
|
|
proc processCallback(request: websock.HttpRequest): Future[void] =
|
|
handleRequest(server, request)
|
|
|
|
privateAccess(RpcWebSocketServer)
|
|
|
|
server.getData = getData
|
|
server.wsserver = WSServer.new(rng = HmacDrbgContext.new())
|
|
server.server = websock.HttpServer.create(
|
|
address,
|
|
processCallback,
|
|
flags
|
|
)
|
|
|
|
server
|
|
|
|
suite "test ws http client":
|
|
let serverAddress = initTAddress("127.0.0.1:0")
|
|
let server = newWsServer(serverAddress, proc(): string {.gcsafe, raises: [].} =
|
|
return """{"jsonrpc":"2.0","result":10}"""
|
|
)
|
|
|
|
var client = newRpcWebSocketClient()
|
|
server.start()
|
|
waitFor client.connect("ws://" & $server.localAddress())
|
|
|
|
test "missing id in server response":
|
|
expect JsonRpcError:
|
|
let res = waitFor client.get_Banana(11)
|
|
discard res
|
|
|
|
server.stop()
|
|
waitFor server.closeWait()
|