├── tests ├── nim.cfg ├── private │ ├── file_callsigs.nim │ ├── helpers.nim │ ├── stintjson.nim │ ├── ethcallsigs.nim │ ├── ethhexstrings.nim │ └── ethtypes.nim ├── all.nim ├── testhttp.nim ├── testproxy.nim ├── testhook.nim ├── testethcalls.nim ├── test_batch_call.nim ├── test_callsigs.nim ├── testhttps.nim ├── test_router_rpc.nim ├── testserverclient.nim ├── test_jrpc_sys.nim ├── test_client_hook.nim └── testrpcmacro.nim ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── config.nims ├── json_rpc ├── rpcclient.nim ├── rpcserver.nim ├── jsonmarshal.nim ├── private │ ├── utils.nim │ ├── shared_wrapper.nim │ ├── client_handler_wrapper.nim │ ├── server_handler_wrapper.nim │ └── jrpc_sys.nim ├── errors.nim ├── server.nim ├── rpcproxy.nim ├── clients │ ├── httpclient.nim │ ├── websocketclient.nim │ └── socketclient.nim ├── servers │ ├── socketserver.nim │ ├── websocketserver.nim │ └── httpserver.nim ├── router.nim └── client.nim ├── nim.cfg ├── json_rpc.nimble └── LICENSE /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | # Avoid some rare stack corruption while using exceptions with a SEH-enabled 2 | # toolchain: https://github.com/status-im/nimbus-eth2/issues/3121 3 | @if windows and not vcc: 4 | --define:nimRawSetjmp 5 | @end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all files without extensions (unix executable files) 2 | * 3 | !*.* 4 | !*/ 5 | 6 | # Ignore all binary outputs 7 | *.so 8 | *.dylib 9 | *.a 10 | *.la 11 | *.exe 12 | *.dll 13 | 14 | nimcache/ 15 | /build 16 | 17 | # Ignore editor settings 18 | .vscode 19 | 20 | 21 | nimble.develop 22 | nimble.paths 23 | 24 | /vendor 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | uses: status-im/nimbus-common-workflow/.github/workflows/common.yml@main 12 | with: 13 | nimble-version: 6fe9d817a70a14fa57022a9cca46eb443ee5a040 14 | test-command: | 15 | nimble setup -l 16 | nimble test 17 | -------------------------------------------------------------------------------- /tests/private/file_callsigs.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | proc shh_uninstallFilter(id: int): bool 11 | proc getFilter(id: Variant): string 12 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | # begin Nimble config (version 1) 11 | when fileExists("nimble.paths"): 12 | include "nimble.paths" 13 | # end Nimble config 14 | -------------------------------------------------------------------------------- /json_rpc/rpcclient.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import client 11 | import clients/[socketclient, httpclient, websocketclient] 12 | export client, socketclient, httpclient, websocketclient 13 | -------------------------------------------------------------------------------- /json_rpc/rpcserver.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import server 11 | import servers/[socketserver, httpserver, websocketserver] 12 | export server, socketserver, httpserver, websocketserver 13 | -------------------------------------------------------------------------------- /nim.cfg: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | # Avoid some rare stack corruption while using exceptions with a SEH-enabled 11 | # toolchain: https://github.com/status-im/nimbus-eth2/issues/3121 12 | @if windows and not vcc: 13 | --define:nimRawSetjmp 14 | @end 15 | -------------------------------------------------------------------------------- /tests/all.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {. warning[UnusedImport]:off .} 11 | 12 | import 13 | testrpcmacro, 14 | testethcalls, 15 | testhttp, 16 | testhttps, 17 | testserverclient, 18 | testproxy, 19 | testhook, 20 | test_jrpc_sys, 21 | test_router_rpc, 22 | test_callsigs, 23 | test_client_hook, 24 | test_batch_call 25 | -------------------------------------------------------------------------------- /json_rpc/jsonmarshal.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2023-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | json_serialization 14 | 15 | export 16 | json_serialization 17 | 18 | createJsonFlavor JrpcConv, 19 | automaticObjectSerialization = false, 20 | requireAllFields = false, 21 | omitOptionalFields = false, # Don't skip optional fields==none in Writer 22 | allowUnknownFields = true, 23 | skipNullFields = true, # Skip optional fields==null in Reader 24 | automaticPrimitivesSerialization = false 25 | 26 | # JrpcConv is a namespace/flavor for encoding and decoding 27 | # parameters and return value of a rpc method. 28 | 29 | when declared(json_serialization.automaticSerialization): 30 | # Nim 1.6 cannot use this new feature 31 | JrpcConv.automaticSerialization(JsonNode, true) 32 | -------------------------------------------------------------------------------- /tests/private/helpers.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.used.} 11 | 12 | import 13 | ../../json_rpc/router 14 | 15 | converter toStr*(value: distinct (string|JsonString)): string = string(value) 16 | 17 | template `==`*(a: JsonString, b: JsonNode): bool = 18 | parseJson(string a) == b 19 | 20 | template `==`*(a: JsonNode, b: JsonString): bool = 21 | a == parseJson(string b) 22 | 23 | when declared(json_serialization.automaticSerialization): 24 | # Nim 1.6 cannot use this new feature 25 | JrpcConv.automaticSerialization(int, true) 26 | JrpcConv.automaticSerialization(string, true) 27 | JrpcConv.automaticSerialization(array, true) 28 | JrpcConv.automaticSerialization(byte, true) 29 | JrpcConv.automaticSerialization(seq, true) 30 | JrpcConv.automaticSerialization(float, true) 31 | JrpcConv.automaticSerialization(JsonString, true) 32 | JrpcConv.automaticSerialization(bool, true) 33 | JrpcConv.automaticSerialization(int64, true) 34 | JrpcConv.automaticSerialization(ref, true) 35 | JrpcConv.automaticSerialization(enum, true) 36 | -------------------------------------------------------------------------------- /tests/private/stintjson.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/json, 3 | stint, 4 | ../../json_rpc/jsonmarshal 5 | 6 | {.push gcsafe, raises: [].} 7 | 8 | template stintStr(n: UInt256|Int256): JsonNode = 9 | var s = n.toHex 10 | if s.len mod 2 != 0: s = "0" & s 11 | s = "0x" & s 12 | %s 13 | 14 | proc `%`*(n: UInt256): JsonNode = n.stintStr 15 | 16 | proc `%`*(n: Int256): JsonNode = n.stintStr 17 | 18 | proc writeValue*(w: var JsonWriter[JrpcConv], val: UInt256) 19 | {.gcsafe, raises: [IOError].} = 20 | writeValue(w, val.stintStr) 21 | 22 | proc writeValue*(w: var JsonWriter[JrpcConv], val: ref UInt256) 23 | {.gcsafe, raises: [IOError].} = 24 | writeValue(w, val[].stintStr) 25 | 26 | proc readValue*(r: var JsonReader[JrpcConv], v: var UInt256) 27 | {.gcsafe, raises: [JsonReaderError].} = 28 | ## Allows UInt256 to be passed as a json string. 29 | ## Expects base 16 string, starting with "0x". 30 | try: 31 | let hexStr = r.readValue string 32 | if hexStr.len > 64 + 2: # including "0x" 33 | raise newException(ValueError, "Value for '" & $v.type & "' too long for UInt256: " & $hexStr.len) 34 | v = hexStr.parse(StUint[256], 16) # TODO: Handle errors 35 | except Exception as err: 36 | r.raiseUnexpectedValue("Error deserializing for '" & $v.type & "' stream: " & err.msg) 37 | 38 | proc readValue*(r: var JsonReader[JrpcConv], v: var ref UInt256) 39 | {.gcsafe, raises: [JsonReaderError].} = 40 | ## Allows ref UInt256 to be passed as a json string. 41 | ## Expects base 16 string, starting with "0x". 42 | readValue(r, v[]) 43 | 44 | {.pop.} 45 | -------------------------------------------------------------------------------- /json_rpc/private/utils.nim: -------------------------------------------------------------------------------- 1 | import chronos, ../errors 2 | 3 | from std/net import IPv6_any, IPv4_any 4 | 5 | template processResolvedAddresses(what: string) = 6 | if ips.len == 0: 7 | # Addresses could not be resolved, critical error. 8 | raise newException(RpcAddressUnresolvableError, "Unable to resolve " & what) 9 | 10 | var dualStack = Opt.none(Port) 11 | for ip in ips: 12 | # Only yield the "any" address once because we try to use dual stack where 13 | # available 14 | if ip.toIpAddress() == IPv6_any(): 15 | dualStack = Opt.some(ip.port) 16 | elif ip.toIpAddress() == IPv4_any() and dualStack == Opt.some(ip.port): 17 | continue 18 | yield ip 19 | 20 | iterator resolveIP*( 21 | addresses: openArray[string] 22 | ): TransportAddress {.raises: [JsonRpcError].} = 23 | var ips: seq[TransportAddress] 24 | # Resolve IPv6 first so that dual stack detection works as expected 25 | for address in addresses: 26 | try: 27 | for resolved in resolveTAddress(address, AddressFamily.IPv6): 28 | if resolved notin ips: 29 | ips.add resolved 30 | except TransportAddressError: 31 | discard 32 | 33 | for address in addresses: 34 | try: 35 | for resolved in resolveTAddress(address, AddressFamily.IPv4): 36 | if resolved notin ips: 37 | ips.add resolved 38 | except TransportAddressError: 39 | discard 40 | 41 | processResolvedAddresses($addresses) 42 | 43 | iterator resolveIP*( 44 | address: string, port: Port 45 | ): TransportAddress {.raises: [JsonRpcError].} = 46 | var ips: seq[TransportAddress] 47 | # Resolve IPv6 first so that dual stack detection works as expected 48 | try: 49 | for resolved in resolveTAddress(address, port, AddressFamily.IPv6): 50 | if resolved notin ips: 51 | ips.add resolved 52 | except TransportAddressError: 53 | discard 54 | 55 | try: 56 | for resolved in resolveTAddress(address, port, AddressFamily.IPv4): 57 | if resolved notin ips: 58 | ips.add resolved 59 | except TransportAddressError: 60 | discard 61 | 62 | processResolvedAddresses($address & ":" & $port) 63 | -------------------------------------------------------------------------------- /json_rpc/errors.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import results, json_serialization 13 | 14 | type 15 | JsonRpcError* = object of CatchableError 16 | ## Base type of all nim-json-rpc errors 17 | 18 | RpcTransportError* = object of JsonRpcError 19 | ## Raised when there is an issue with the underlying transport - the parent 20 | ## exception may be set to provide more information 21 | 22 | FailedHttpResponse* {.deprecated: "RpcTransportError".} = RpcTransportError 23 | ## Obsolete name for RpcTransportError 24 | 25 | ErrorResponse* = object of RpcTransportError 26 | status*: int 27 | ## Raised when the server responds with a HTTP-style error status code 28 | ## indicating that the call was not processed 29 | 30 | RpcPostError* = object of RpcTransportError 31 | ## raised when the underlying transport fails to send the request - the 32 | ## underlying client may or may not have received the request 33 | 34 | InvalidResponse* = object of JsonRpcError 35 | ## raised when the server response violates the JSON-RPC protocol 36 | payload*: seq[byte] 37 | 38 | RpcBindError* = object of JsonRpcError 39 | RpcAddressUnresolvableError* = object of JsonRpcError 40 | 41 | RequestDecodeError* = object of JsonRpcError 42 | ## raised when fail to decode RequestRx 43 | payload*: seq[byte] 44 | 45 | ApplicationError* = object of JsonRpcError 46 | ## Error to be raised by the application request handlers when the server 47 | ## needs to respond with a custom application error. The error code should 48 | ## be outside the range of -32768 to -32000. A custom JSON data object may 49 | ## be provided. 50 | code*: int 51 | data*: results.Opt[JsonString] 52 | 53 | InvalidRequest* {.deprecated: "ApplicationError".} = ApplicationError 54 | -------------------------------------------------------------------------------- /json_rpc.nimble: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | mode = ScriptMode.Verbose 11 | 12 | packageName = "json_rpc" 13 | version = "0.5.4" 14 | author = "Status Research & Development GmbH" 15 | description = "Ethereum remote procedure calls" 16 | license = "Apache License 2.0" 17 | skipDirs = @["tests"] 18 | 19 | ### Dependencies 20 | requires "nim >= 1.6.0", 21 | "stew", 22 | "nimcrypto", 23 | "stint", 24 | "chronos >= 4.0.3 & < 5.0.0", 25 | "httputils >= 0.3.0", 26 | "chronicles", 27 | "websock >= 0.2.1 & < 0.3.0", 28 | "serialization >= 0.4.4", 29 | "json_serialization >= 0.4.2", 30 | "unittest2" 31 | 32 | let nimc = getEnv("NIMC", "nim") # Which nim compiler to use 33 | let lang = getEnv("NIMLANG", "c") # Which backend (c/cpp/js) 34 | let flags = getEnv("NIMFLAGS", "") # Extra flags for the compiler 35 | let verbose = getEnv("V", "") notin ["", "0"] 36 | 37 | let cfg = 38 | " --styleCheck:usages --styleCheck:error" & 39 | (if verbose: "" else: " --verbosity:0") & 40 | " --skipUserCfg --outdir:build --nimcache:build/nimcache -f" & 41 | " --threads:on -d:chronicles_log_level=ERROR" 42 | 43 | proc build(args, path: string) = 44 | exec nimc & " " & lang & " " & cfg & " " & flags & " " & args & " " & path 45 | 46 | proc run(args, path: string) = 47 | build args & " --mm:refc -r", path 48 | if (NimMajor, NimMinor) > (1, 6): 49 | build args & " --mm:orc -r", path 50 | 51 | proc buildOnly(args, path: string) = 52 | build args & " --mm:refc", path 53 | if (NimMajor, NimMinor) > (1, 6): 54 | build args & " --mm:orc", path 55 | 56 | task test, "run tests": 57 | run "", "tests/all" 58 | 59 | when not defined(windows): 60 | # on windows, socker server build failed 61 | buildOnly "-d:chronicles_log_level=TRACE -d:\"chronicles_sinks=textlines[dynamic],json[dynamic]\"", "tests/all" 62 | -------------------------------------------------------------------------------- /json_rpc/private/shared_wrapper.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2024-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | std/[json, macros], 14 | ./jrpc_sys, 15 | ../jsonmarshal 16 | 17 | iterator paramsIter*(params: NimNode): tuple[name, ntype: NimNode] = 18 | ## Forward iterator of handler parameters 19 | for i in 1 ..< params.len: 20 | let arg = params[i] 21 | let argType = arg[^2] 22 | for j in 0 ..< arg.len-2: 23 | yield (arg[j], argType) 24 | 25 | func ensureReturnType*(params: NimNode): NimNode = 26 | let retType = ident"JsonNode" 27 | if params.isNil or params.kind == nnkEmpty or params.len == 0: 28 | return nnkFormalParams.newTree(retType) 29 | 30 | if params.len >= 1 and params[0].kind == nnkEmpty: 31 | params[0] = retType 32 | 33 | params 34 | 35 | template noWrap*(returnType: type): auto = 36 | ## Condition when return type should not be encoded 37 | ## to Json 38 | returnType is JsonString or returnType is void 39 | 40 | func paramsTx*(params: JsonNode): RequestParamsTx = 41 | if params.kind == JArray: 42 | var args: seq[JsonString] 43 | for x in params: 44 | args.add JrpcConv.encode(x).JsonString 45 | RequestParamsTx( 46 | kind: rpPositional, 47 | positional: system.move(args), 48 | ) 49 | elif params.kind == JObject: 50 | var args: seq[ParamDescNamed] 51 | for k, v in params: 52 | args.add ParamDescNamed( 53 | name: k, 54 | value: JrpcConv.encode(v).JsonString, 55 | ) 56 | RequestParamsTx( 57 | kind: rpNamed, 58 | named: system.move(args), 59 | ) 60 | else: 61 | RequestParamsTx( 62 | kind: rpPositional, 63 | positional: @[JrpcConv.encode(params).JsonString], 64 | ) 65 | 66 | func requestTx*(name: string, params: sink RequestParamsTx, id: int): RequestTx = 67 | RequestTx( 68 | id: Opt.some(RequestId(kind: riNumber, num: id)), 69 | `method`: name, 70 | params: params, 71 | ) 72 | -------------------------------------------------------------------------------- /tests/private/ethcallsigs.nim: -------------------------------------------------------------------------------- 1 | ## This module contains signatures for the Ethereum client RPCs. 2 | ## The signatures are not imported directly, but read and processed with parseStmt, 3 | ## then a procedure body is generated to marshal native Nim parameters to json and visa versa. 4 | import json, stint, ethtypes 5 | 6 | proc web3_clientVersion(): string 7 | proc web3_sha3(data: string): string 8 | proc net_version(): string 9 | proc net_peerCount(): int 10 | proc net_listening(): bool 11 | proc eth_protocolVersion(): string 12 | proc eth_syncing(): JsonNode 13 | proc eth_coinbase(): string 14 | proc eth_mining(): bool 15 | proc eth_hashrate(): int 16 | proc eth_gasPrice(): int64 17 | proc eth_accounts(): seq[array[20, byte]] 18 | proc eth_blockNumber(): int 19 | proc eth_getBalance(data: array[20, byte], quantityTag: string): int 20 | proc eth_getStorageAt(data: array[20, byte], quantity: int, quantityTag: string): seq[byte] 21 | proc eth_getTransactionCount(data: array[20, byte], quantityTag: string) 22 | proc eth_getBlockTransactionCountByHash(data: array[32, byte]) 23 | proc eth_getBlockTransactionCountByNumber(quantityTag: string) 24 | proc eth_getUncleCountByBlockHash(data: array[32, byte]) 25 | proc eth_getUncleCountByBlockNumber(quantityTag: string) 26 | proc eth_getCode(data: array[20, byte], quantityTag: string): seq[byte] 27 | proc eth_sign(data: array[20, byte], message: seq[byte]): seq[byte] 28 | proc eth_sendTransaction(obj: EthSend): UInt256 29 | proc eth_sendRawTransaction(data: string, quantityTag: int): UInt256 30 | proc eth_call(call: EthCall, quantityTag: string): UInt256 31 | proc eth_estimateGas(call: EthCall, quantityTag: string): UInt256 32 | proc eth_getBlockByHash(data: array[32, byte], fullTransactions: bool): BlockObject 33 | proc eth_getBlockByNumber(quantityTag: string, fullTransactions: bool): BlockObject 34 | proc eth_getTransactionByHash(data: UInt256): TransactionObject 35 | proc eth_getTransactionByBlockHashAndIndex(data: UInt256, quantity: int): TransactionObject 36 | proc eth_getTransactionByBlockNumberAndIndex(quantityTag: string, quantity: int): TransactionObject 37 | proc eth_getTransactionReceipt(data: UInt256): ReceiptObject 38 | proc eth_getUncleByBlockHashAndIndex(data: UInt256, quantity: int64): BlockObject 39 | proc eth_getUncleByBlockNumberAndIndex(quantityTag: string, quantity: int64): BlockObject 40 | proc eth_getCompilers(): seq[string] 41 | proc eth_compileLLL(): seq[byte] 42 | proc eth_compileSolidity(): seq[byte] 43 | proc eth_compileSerpent(): seq[byte] 44 | proc eth_newBlockFilter(): int 45 | proc eth_newPendingTransactionFilter(): int 46 | proc eth_uninstallFilter(filterId: int): bool 47 | proc eth_getWork(): seq[UInt256] 48 | proc eth_submitWork(nonce: int64, powHash: UInt256, mixDigest: UInt256): bool 49 | proc eth_submitHashrate(hashRate: UInt256, id: UInt256): bool 50 | proc shh_post(): string 51 | proc shh_newIdentity(): array[60, byte] 52 | proc shh_hasIdentity(identity: array[60, byte]): bool 53 | proc shh_newGroup(): array[60, byte] 54 | proc shh_uninstallFilter(id: int): bool 55 | -------------------------------------------------------------------------------- /tests/testhttp.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | unittest2, chronos/unittest2/asynctests, 12 | ../json_rpc/[rpcserver, rpcclient, jsonmarshal], 13 | ./private/helpers 14 | 15 | const TestsCount = 100 16 | const bigChunkSize = 4 * 8192 17 | 18 | suite "JSON-RPC/http": 19 | setup: 20 | var httpsrv = newRpcHttpServer(["127.0.0.1:0"]) 21 | # Create RPC on server 22 | httpsrv.rpc("myProc") do(input: string, data: array[0..3, int]): 23 | result = %("Hello " & input & " data: " & $data) 24 | httpsrv.rpc("noParamsProc") do(): 25 | result = %("Hello world") 26 | 27 | httpsrv.rpc("bigchunkMethod") do() -> seq[byte]: 28 | result = newSeq[byte](bigChunkSize) 29 | for i in 0.. void: 81 | notif = true 82 | 83 | var client = newRpcHttpClient() 84 | await client.connect("http://" & serverAddress) 85 | 86 | await client.notify("notif", RequestParamsTx()) 87 | await client.close() 88 | 89 | check: 90 | notif 91 | -------------------------------------------------------------------------------- /tests/testproxy.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | unittest2, chronicles, 12 | ../json_rpc/[rpcclient, rpcserver, rpcproxy] 13 | 14 | let srvAddress = initTAddress("127.0.0.1", Port(0)) 15 | let proxySrvAddress = "127.0.0.1:0" 16 | 17 | template registerMethods(srv: RpcServer, proxy: RpcProxy) = 18 | srv.rpc("myProc") do(input: string, data: array[0..3, int]): 19 | return %("Hello " & input & " data: " & $data) 20 | # Create RPC on proxy server 21 | proxy.registerProxyMethod("myProc") 22 | 23 | # Create standard handler on server 24 | proxy.rpc("myProc1") do(input: string, data: array[0..3, int]): 25 | return %("Hello " & input & " data: " & $data) 26 | 27 | suite "Proxy RPC through http": 28 | var srv = newRpcHttpServer([srvAddress]) 29 | var proxy = RpcProxy.new([proxySrvAddress], getHttpClientConfig("http://" & $srv.localAddress()[0])) 30 | var client = newRpcHttpClient() 31 | 32 | registerMethods(srv, proxy) 33 | 34 | srv.start() 35 | waitFor proxy.start() 36 | waitFor client.connect("http://" & $proxy.localAddress()[0]) 37 | 38 | test "Successful RPC call thorugh proxy": 39 | let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]]) 40 | check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" 41 | test "Successful RPC call no proxy": 42 | let r = waitFor client.call("myProc1", %[%"abc", %[1, 2, 3, 4]]) 43 | check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" 44 | test "Missing params": 45 | expect(CatchableError): 46 | discard waitFor client.call("myProc", %[%"abc"]) 47 | test "Method missing on server and proxy server": 48 | expect(CatchableError): 49 | discard waitFor client.call("missingMethod", %[%"abc"]) 50 | 51 | waitFor srv.stop() 52 | waitFor srv.closeWait() 53 | waitFor proxy.stop() 54 | waitFor proxy.closeWait() 55 | 56 | suite "Proxy RPC through websockets": 57 | var srv = newRpcWebSocketServer(srvAddress) 58 | var proxy = RpcProxy.new([proxySrvAddress], getWebSocketClientConfig("ws://" & $srv.localAddress())) 59 | var client = newRpcHttpClient() 60 | 61 | registerMethods(srv, proxy) 62 | 63 | srv.start() 64 | waitFor proxy.start() 65 | waitFor client.connect("http://" & $proxy.localAddress()[0]) 66 | 67 | test "Successful RPC call thorugh proxy": 68 | let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]]) 69 | check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" 70 | test "Successful RPC call no proxy": 71 | let r = waitFor client.call("myProc1", %[%"abc", %[1, 2, 3, 4]]) 72 | check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" 73 | test "Missing params": 74 | expect(CatchableError): 75 | discard waitFor client.call("myProc", %[%"abc"]) 76 | test "Method missing on server and proxy server": 77 | expect(CatchableError): 78 | discard waitFor client.call("missingMethod", %[%"abc"]) 79 | 80 | srv.stop() 81 | waitFor srv.closeWait() 82 | waitFor proxy.stop() 83 | waitFor proxy.closeWait() 84 | -------------------------------------------------------------------------------- /tests/testhook.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | unittest2, 12 | websock/websock, 13 | ../json_rpc/[rpcclient, rpcserver] 14 | 15 | const 16 | serverHost = "127.0.0.1" 17 | serverPort = 0 # let the OS choose the port 18 | serverAddress = serverHost & ":" & $serverPort 19 | 20 | proc setupServer*(srv: RpcServer) = 21 | srv.rpc("testHook") do(input: string): 22 | return %("Hello " & input) 23 | 24 | proc authHeaders(): seq[(string, string)] = 25 | @[("Auth-Token", "Good Token")] 26 | 27 | suite "HTTP server hook test": 28 | proc mockAuth(req: HttpRequestRef): Future[HttpResponseRef] {.async: (raises: [CatchableError]).} = 29 | if req.headers.getString("Auth-Token") == "Good Token": 30 | return HttpResponseRef(nil) 31 | 32 | return await req.respond(Http401, "Unauthorized access") 33 | 34 | let srv = newRpcHttpServer([serverAddress], @[HttpAuthHook(mockAuth)]) 35 | srv.setupServer() 36 | srv.start() 37 | 38 | test "no auth token": 39 | let client = newRpcHttpClient() 40 | waitFor client.connect("http://" & $srv.localAddress()[0]) 41 | expect ErrorResponse: 42 | let r = waitFor client.call("testHook", %[%"abc"]) 43 | discard r 44 | 45 | test "good auth token": 46 | let client = newRpcHttpClient(getHeaders = authHeaders) 47 | waitFor client.connect("http://" & $srv.localAddress()[0]) 48 | let r = waitFor client.call("testHook", %[%"abc"]) 49 | check r.string == "\"Hello abc\"" 50 | 51 | waitFor srv.closeWait() 52 | 53 | proc wsAuthHeaders(ctx: Hook, 54 | headers: var HttpTable): Result[void, string] 55 | {.gcsafe, raises: [].} = 56 | headers.add("Auth-Token", "Good Token") 57 | return ok() 58 | 59 | suite "Websocket server hook test": 60 | let hook = Hook(append: wsAuthHeaders) 61 | 62 | proc mockAuth(req: websock.HttpRequest): Future[bool] {.async: (raises: [CatchableError]).} = 63 | if not req.headers.contains("Auth-Token"): 64 | await req.sendResponse(code = Http403, data = "Missing Auth-Token") 65 | return false 66 | 67 | let token = req.headers.getString("Auth-Token") 68 | if token != "Good Token": 69 | await req.sendResponse(code = Http401, data = "Unauthorized access") 70 | return false 71 | 72 | return true 73 | 74 | let srv = newRpcWebSocketServer( 75 | serverHost, 76 | Port(serverPort), 77 | authHooks = @[WsAuthHook(mockAuth)] 78 | ) 79 | srv.setupServer() 80 | srv.start() 81 | let client = newRpcWebSocketClient() 82 | 83 | test "no auth token": 84 | try: 85 | waitFor client.connect("ws://" & $srv.localAddress()) 86 | check false 87 | except CatchableError as e: 88 | check e.msg == "Server did not reply with a websocket upgrade: Header code: 403 Header reason: Forbidden Address: " & $srv.localAddress() 89 | 90 | test "good auth token": 91 | waitFor client.connect("ws://" & $srv.localAddress(), hooks = @[hook]) 92 | let r = waitFor client.call("testHook", %[%"abc"]) 93 | check r.string == "\"Hello abc\"" 94 | 95 | srv.stop() 96 | waitFor srv.closeWait() 97 | -------------------------------------------------------------------------------- /json_rpc/server.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | std/[json, sequtils, sets], 14 | chronos, 15 | ./[client, errors, jsonmarshal, router], 16 | ./private/jrpc_sys, 17 | ./private/shared_wrapper 18 | 19 | export chronos, client, jsonmarshal, router, sets 20 | 21 | type 22 | RpcServer* = ref object of RootRef 23 | router*: RpcRouter 24 | 25 | # For servers that expose bidirectional connections, keep track of them 26 | connections*: HashSet[RpcConnection] 27 | 28 | # ------------------------------------------------------------------------------ 29 | # Constructors 30 | # ------------------------------------------------------------------------------ 31 | 32 | proc new*(T: type RpcServer): T = 33 | T(router: RpcRouter.init()) 34 | 35 | # ------------------------------------------------------------------------------ 36 | # Public functions 37 | # ------------------------------------------------------------------------------ 38 | 39 | template rpc*(server: RpcServer, path: string, body: untyped): untyped = 40 | server.router.rpc(path, body) 41 | 42 | template hasMethod*(server: RpcServer, methodName: string): bool = 43 | server.router.hasMethod(methodName) 44 | 45 | proc executeMethod*(server: RpcServer, 46 | methodName: string, 47 | params: RequestParamsTx): Future[JsonString] {.async: (raises: [CancelledError, JsonRpcError]).} = 48 | 49 | let 50 | req = requestTx(methodName, params, 0) 51 | reqData = JrpcSys.encode(req) 52 | respData = await server.router.route(reqData) 53 | 54 | processsSingleResponse(respData.toOpenArrayByte(0, respData.high()), 0) 55 | 56 | proc executeMethod*(server: RpcServer, 57 | methodName: string, 58 | args: JsonNode): Future[JsonString] {.async: (raises: [CancelledError, JsonRpcError], raw: true).} = 59 | 60 | let params = paramsTx(args) 61 | server.executeMethod(methodName, params) 62 | 63 | proc executeMethod*(server: RpcServer, 64 | methodName: string, 65 | args: JsonString): Future[JsonString] {.async: (raises: [CancelledError, JsonRpcError]).} = 66 | 67 | let params = try: 68 | let x = JrpcSys.decode(args.string, RequestParamsRx) 69 | x.toTx 70 | except SerializationError as exc: 71 | raise newException(JsonRpcError, exc.msg) 72 | 73 | await server.executeMethod(methodName, params) 74 | 75 | # Wrapper for message processing 76 | 77 | proc route*(server: RpcServer, line: string): Future[string] {.async: (raises: [], raw: true).} = 78 | server.router.route(line) 79 | proc route*(server: RpcServer, line: seq[byte]): Future[string] {.async: (raises: [], raw: true).} = 80 | server.router.route(line) 81 | 82 | proc notify*( 83 | server: RpcServer, name: string, params: RequestParamsTx 84 | ) {.async: (raises: [CancelledError]).} = 85 | let notifications = server.connections.mapIt(it.notify(name, params)) 86 | # Discard results, we don't care here .. 87 | await allFutures(notifications) 88 | 89 | # Server registration 90 | 91 | proc register*(server: RpcServer, name: string, rpc: RpcProc) = 92 | ## Add a name/code pair to the RPC server. 93 | server.router.register(name, rpc) 94 | 95 | proc unRegisterAll*(server: RpcServer) = 96 | # Remove all remote procedure calls from this server. 97 | server.router.clear 98 | 99 | {.pop.} 100 | -------------------------------------------------------------------------------- /tests/testethcalls.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | unittest2, tables, 12 | stint, chronicles, 13 | ../json_rpc/[rpcclient, rpcserver], 14 | ./private/helpers, 15 | ./private/ethtypes, 16 | ./private/ethprocs, 17 | ./private/stintjson 18 | 19 | from os import getCurrentDir, DirSep, AltSep 20 | from strutils import rsplit 21 | template sourceDir: string = currentSourcePath.rsplit({DirSep, AltSep}, 1)[0] 22 | 23 | var 24 | server = newRpcSocketServer("127.0.0.1", Port(0)) 25 | client = newRpcSocketClient() 26 | 27 | ## Generate Ethereum server RPCs 28 | server.addEthRpcs() 29 | 30 | ## Generate client convenience marshalling wrappers from forward declarations 31 | createRpcSigs(RpcClient, sourceDir & "/private/ethcallsigs.nim") 32 | 33 | func rpcDynamicName(name: string): string = 34 | "rpc." & name 35 | 36 | ## Create custom RPC with StUint input parameter 37 | server.rpc(rpcDynamicName "uint256Param") do(i: UInt256): 38 | let r = i + 1.stuint(256) 39 | return %r 40 | 41 | ## Create custom RPC with StUInt return parameter 42 | server.rpc(rpcDynamicName "testReturnUint256") do() -> UInt256: 43 | let r: UInt256 = "0x1234567890abcdef".parse(UInt256, 16) 44 | return r 45 | 46 | proc testLocalCalls(server: RpcServer): Future[seq[JsonString]] {.async.} = 47 | ## Call RPCs created with `rpc` locally. 48 | ## This simply demonstrates async calls of the procs generated by the `rpc` macro. 49 | let 50 | uint256Param = server.executeMethod("rpc.uint256Param", %[%"0x1234567890"]) 51 | returnUint256 = server.executeMethod("rpc.testReturnUint256", %[]) 52 | 53 | await noCancel(allFutures(uint256Param, returnUint256)) 54 | var pending: seq[JsonString] 55 | pending.add uint256Param.read() 56 | pending.add returnUint256.read() 57 | return pending 58 | 59 | proc testRemoteUInt256(client: RpcClient): Future[seq[JsonString]] {.async.} = 60 | ## Call function remotely on server, testing `stint` types 61 | let 62 | uint256Param = client.call("rpc.uint256Param", %[%"0x1234567890"]) 63 | returnUint256 = client.call("rpc.testReturnUint256", %[]) 64 | 65 | await noCancel(allFutures(uint256Param, returnUint256)) 66 | var pending: seq[JsonString] 67 | pending.add uint256Param.read() 68 | pending.add returnUint256.read() 69 | return pending 70 | 71 | proc testSigCalls(client: RpcClient): Future[seq[string]] {.async.} = 72 | ## Remote call using proc generated from signatures in `ethcallsigs.nim` 73 | let 74 | version = client.web3_clientVersion() 75 | sha3 = client.web3_sha3("0x68656c6c6f20776f726c64") 76 | 77 | await noCancel(allFutures(version, sha3)) 78 | var pending: seq[string] 79 | pending.add version.read() 80 | pending.add sha3.read() 81 | return pending 82 | 83 | server.start() 84 | waitFor client.connect(server.localAddress()[0]) 85 | 86 | 87 | suite "Local calls": 88 | let localResults = testLocalCalls(server).waitFor 89 | test "UInt256 param local": 90 | check localResults[0] == %"0x1234567891" 91 | test "Return UInt256 local": 92 | check localResults[1] == %"0x1234567890abcdef" 93 | 94 | suite "Remote calls": 95 | let remoteResults = testRemoteUInt256(client).waitFor 96 | test "UInt256 param": 97 | check remoteResults[0] == %"0x1234567891" 98 | test "Return UInt256": 99 | check remoteResults[1] == %"0x1234567890abcdef" 100 | 101 | suite "Generated from signatures": 102 | let sigResults = testSigCalls(client).waitFor 103 | test "Version": 104 | check sigResults[0] == "Nimbus-RPC-Test" 105 | test "SHA3": 106 | check sigResults[1] == "0x47173285A8D7341E5E972FC677286384F802F8EF42A5EC5F03BBFA254CB01FAD" 107 | 108 | server.stop() 109 | waitFor server.closeWait() 110 | -------------------------------------------------------------------------------- /tests/test_batch_call.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | unittest2, 12 | ../json_rpc/rpcclient, 13 | ../json_rpc/rpcserver 14 | 15 | createRpcSigsFromNim(RpcClient): 16 | proc get_banana(id: int): bool 17 | proc get_apple(id: string): string 18 | proc get_except(): string 19 | 20 | proc setupServer(server: RpcServer) = 21 | server.rpc("get_banana") do(id: int) -> bool: 22 | return id == 13 23 | 24 | server.rpc("get_apple") do(id: string) -> string: 25 | return "apple: " & id 26 | 27 | server.rpc("get_except") do() -> string: 28 | raise newException(ValueError, "get_except error") 29 | 30 | suite "Socket batch call": 31 | var srv = newRpcSocketServer(["127.0.0.1:0"]) 32 | var client = newRpcSocketClient() 33 | 34 | srv.setupServer() 35 | srv.start() 36 | waitFor client.connect(srv.localAddress()[0]) 37 | 38 | test "batch call basic": 39 | let batch = client.prepareBatch() 40 | 41 | batch.get_banana(11) 42 | batch.get_apple("green") 43 | batch.get_except() 44 | 45 | let res = waitFor batch.send() 46 | check res.isOk 47 | if res.isErr: 48 | debugEcho res.error 49 | break 50 | 51 | let r = res.get 52 | check r[0].error.isNone 53 | check r[0].result.string == "false" 54 | 55 | check r[1].error.isNone 56 | check r[1].result.string == "\"apple: green\"" 57 | 58 | check r[2].error.isSome 59 | check r[2].error.get == """{"code":-32000,"message":"`get_except` raised an exception","data":"get_except error"}""" 60 | check r[2].result.string.len == 0 61 | 62 | test "rpc call after batch call": 63 | let res = waitFor client.get_banana(13) 64 | check res == true 65 | 66 | srv.stop() 67 | waitFor srv.closeWait() 68 | 69 | suite "HTTP batch call": 70 | var srv = newRpcHttpServer(["127.0.0.1:0"]) 71 | var client = newRpcHttpClient() 72 | 73 | srv.setupServer() 74 | srv.start() 75 | waitFor client.connect("http://" & $srv.localAddress()[0]) 76 | 77 | test "batch call basic": 78 | let batch = client.prepareBatch() 79 | 80 | batch.get_banana(11) 81 | batch.get_apple("green") 82 | batch.get_except() 83 | 84 | let res = waitFor batch.send() 85 | check res.isOk 86 | if res.isErr: 87 | debugEcho res.error 88 | break 89 | 90 | let r = res.get 91 | check r[0].error.isNone 92 | check r[0].result.string == "false" 93 | 94 | check r[1].error.isNone 95 | check r[1].result.string == "\"apple: green\"" 96 | 97 | check r[2].error.isSome 98 | check r[2].error.get == """{"code":-32000,"message":"`get_except` raised an exception","data":"get_except error"}""" 99 | check r[2].result.string.len == 0 100 | 101 | test "rpc call after batch call": 102 | let res = waitFor client.get_banana(13) 103 | check res == true 104 | 105 | waitFor srv.stop() 106 | waitFor srv.closeWait() 107 | 108 | suite "Websocket batch call": 109 | var srv = newRpcWebSocketServer("127.0.0.1", Port(0)) 110 | var client = newRpcWebSocketClient() 111 | 112 | srv.setupServer() 113 | srv.start() 114 | waitFor client.connect("ws://" & $srv.localAddress()) 115 | 116 | test "batch call basic": 117 | let batch = client.prepareBatch() 118 | 119 | batch.get_banana(11) 120 | batch.get_apple("green") 121 | batch.get_except() 122 | 123 | let res = waitFor batch.send() 124 | check res.isOk 125 | if res.isErr: 126 | debugEcho res.error 127 | break 128 | 129 | let r = res.get 130 | check r[0].error.isNone 131 | check r[0].result.string == "false" 132 | 133 | check r[1].error.isNone 134 | check r[1].result.string == "\"apple: green\"" 135 | 136 | check r[2].error.isSome 137 | check r[2].error.get == """{"code":-32000,"message":"`get_except` raised an exception","data":"get_except error"}""" 138 | check r[2].result.string.len == 0 139 | 140 | test "rpc call after batch call": 141 | let res = waitFor client.get_banana(13) 142 | check res == true 143 | 144 | srv.stop() 145 | waitFor srv.closeWait() 146 | -------------------------------------------------------------------------------- /tests/test_callsigs.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2023-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | unittest2, 12 | ../json_rpc/rpcclient, 13 | ../json_rpc/rpcserver 14 | 15 | from os import getCurrentDir, DirSep, AltSep 16 | from strutils import rsplit 17 | template sourceDir: string = currentSourcePath.rsplit({DirSep, AltSep}, 1)[0] 18 | 19 | type 20 | Variant = int | bool | string 21 | RefObject = ref object 22 | name: string 23 | 24 | RefObject.useDefaultSerializationIn JrpcConv 25 | 26 | createRpcSigs(RpcClient, sourceDir & "/private/file_callsigs.nim") 27 | 28 | createSingleRpcSig(RpcClient, "bottle"): 29 | proc get_Bottle(id: int): bool 30 | 31 | createSingleRpcSig(RpcClient, "mouse"): 32 | proc getVariant(id: Variant): bool 33 | 34 | createRpcSigsFromNim(RpcClient): 35 | proc get_Banana(id: int): bool 36 | proc get_Combo(id, index: int, name: string): bool 37 | proc get_Name(id: int): string 38 | proc getJsonString(name: string): JsonString 39 | proc getVariant(id: Variant): bool 40 | proc getRefObject(shouldNull: bool): RefObject 41 | 42 | proc installHandlers(s: RpcServer) = 43 | s.rpc("shh_uninstallFilter") do(id: int) -> bool: 44 | if id == 123: 45 | return true 46 | else: 47 | return false 48 | 49 | s.rpc("get_Bottle") do(id: int) -> bool: 50 | if id == 456: 51 | return true 52 | else: 53 | return false 54 | 55 | s.rpc("get_Banana") do(id: int) -> bool: 56 | if id == 789: 57 | return true 58 | else: 59 | return false 60 | 61 | s.rpc("get_Combo") do(id, index: int, name: string) -> bool: 62 | if index == 77 and name == "banana": 63 | return true 64 | return false 65 | 66 | s.rpc("get_Name") do(id: int) -> string: 67 | if id == 99: 68 | return "king kong" 69 | return "godzilla" 70 | 71 | s.rpc("getJsonString") do(name: string) -> JsonString: 72 | if name == "me": 73 | return "true".JsonString 74 | return "123".JsonString 75 | 76 | s.rpc("getVariant") do(id: string) -> bool: 77 | if id == "33": 78 | return true 79 | return false 80 | 81 | s.rpc("getFilter") do(id: string) -> string: 82 | if id == "cow": 83 | return "moo" 84 | return "meow" 85 | 86 | s.rpc("getRefObject") do(shouldNull: bool) -> Refobject: 87 | if shouldNull: return nil 88 | return RefObject(name: "meow") 89 | 90 | suite "test callsigs": 91 | var server = newRpcSocketServer(["127.0.0.1:0"]) 92 | server.installHandlers() 93 | var client = newRpcSocketClient() 94 | 95 | server.start() 96 | waitFor client.connect(server.localAddress()[0]) 97 | 98 | test "callsigs from file": 99 | let res = waitFor client.shh_uninstallFilter(123) 100 | check res == true 101 | 102 | let res2 = waitFor client.getFilter("cow") 103 | check res2 == "moo" 104 | 105 | test "callsigs alias": 106 | let res = waitFor client.bottle(456) 107 | check res == true 108 | 109 | let res2 = waitFor client.mouse("33") 110 | check res2 == true 111 | 112 | let res3 = waitFor client.mouse("55") 113 | check res3 == false 114 | 115 | expect JsonRpcError: 116 | let res4 = waitFor client.mouse(33) 117 | check res4 == true 118 | 119 | test "callsigs from nim": 120 | let res = waitFor client.get_Banana(789) 121 | check res == true 122 | 123 | let res2 = waitFor client.get_Name(99) 124 | check res2 == "king kong" 125 | 126 | let res3 = waitFor client.get_Combo(0, 77, "banana") 127 | check res3 == true 128 | 129 | let res4 = waitFor client.getJsonString("me") 130 | check res4 == "true".JsonString 131 | 132 | let res5 = waitFor client.getVariant("33") 133 | check res5 == true 134 | 135 | let res6 = waitFor client.getVariant("55") 136 | check res6 == false 137 | 138 | expect JsonRpcError: 139 | let res4 = waitFor client.getVariant(33) 140 | check res4 == true 141 | 142 | test "Handle null return value correctly": 143 | let res = waitFor client.getRefObject(true) 144 | check res.isNil 145 | 146 | let res2 = waitFor client.getRefObject(false) 147 | check res2.isNil.not 148 | check res2.name == "meow" 149 | 150 | server.stop() 151 | waitFor server.closeWait() 152 | -------------------------------------------------------------------------------- /json_rpc/rpcproxy.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | pkg/websock/websock, 14 | ./servers/[httpserver], 15 | ./clients/[httpclient, websocketclient], 16 | ./private/jrpc_sys 17 | 18 | type 19 | ClientKind* = enum 20 | Http, 21 | WebSocket 22 | 23 | ClientConfig* = object 24 | case kind*: ClientKind 25 | of Http: 26 | httpUri*: string 27 | of WebSocket: 28 | wsUri*: string 29 | compression*: bool 30 | flags*: set[TLSFlags] 31 | 32 | RpcProxy* = ref object of RootRef 33 | rpcHttpServer*: RpcHttpServer 34 | case kind*: ClientKind 35 | of Http: 36 | httpUri*: string 37 | httpClient*: RpcHttpClient 38 | of WebSocket: 39 | wsUri*: string 40 | webSocketClient*: RpcWebSocketClient 41 | compression*: bool 42 | flags*: set[TLSFlags] 43 | 44 | # TODO Add validations that provided uri-s are correct https/wss uri and retrun 45 | # Result[string, ClientConfig] 46 | proc getHttpClientConfig*(uri: string): ClientConfig = 47 | ClientConfig(kind: Http, httpUri: uri) 48 | 49 | proc getWebSocketClientConfig*( 50 | uri: string, 51 | compression: bool = false, 52 | flags: set[TLSFlags] = { 53 | TLSFlags.NoVerifyHost, TLSFlags.NoVerifyServerName}): ClientConfig = 54 | ClientConfig(kind: WebSocket, wsUri: uri, compression: compression, flags: flags) 55 | 56 | proc proxyCall(client: RpcClient, name: string): RpcProc = 57 | return proc(params: RequestParamsRx): Future[JsonString] {.async: (raw: true).} = 58 | client.call(name, params.toTx) 59 | 60 | proc getClient*(proxy: RpcProxy): RpcClient = 61 | case proxy.kind 62 | of Http: 63 | proxy.httpClient 64 | of WebSocket: 65 | proxy.webSocketClient 66 | 67 | proc new*(T: type RpcProxy, server: RpcHttpServer, cfg: ClientConfig): T = 68 | case cfg.kind 69 | of Http: 70 | let client = newRpcHttpClient() 71 | return T(rpcHttpServer: server, kind: Http, httpUri: cfg.httpUri, httpClient: client) 72 | of WebSocket: 73 | let client = newRpcWebSocketClient() 74 | return T( 75 | rpcHttpServer: server, 76 | kind: WebSocket, 77 | wsUri: cfg.wsUri, 78 | webSocketClient: client, 79 | compression: cfg.compression, 80 | flags: cfg.flags 81 | ) 82 | 83 | proc new*( 84 | T: type RpcProxy, 85 | listenAddresses: openArray[TransportAddress], 86 | cfg: ClientConfig, 87 | authHooks: seq[HttpAuthHook] = @[] 88 | ): T {.raises: [CatchableError].} = 89 | RpcProxy.new(newRpcHttpServer(listenAddresses, RpcRouter.init(), authHooks), cfg) 90 | 91 | proc new*( 92 | T: type RpcProxy, 93 | listenAddresses: openArray[string], 94 | cfg: ClientConfig, 95 | authHooks: seq[HttpAuthHook] = @[]): T {.raises: [CatchableError].} = 96 | RpcProxy.new(newRpcHttpServer(listenAddresses, RpcRouter.init(), authHooks), cfg) 97 | 98 | proc connectToProxy(proxy: RpcProxy): Future[void] = 99 | case proxy.kind 100 | of Http: 101 | return proxy.httpClient.connect(proxy.httpUri) 102 | of WebSocket: 103 | return proxy.webSocketClient.connect( 104 | uri = proxy.wsUri, 105 | compression = proxy.compression, 106 | flags = proxy.flags) 107 | 108 | proc start*(proxy: RpcProxy) {.async.} = 109 | proxy.rpcHttpServer.start() 110 | await proxy.connectToProxy() 111 | 112 | template rpc*(server: RpcProxy, path: string, body: untyped): untyped = 113 | server.rpcHttpServer.rpc(path, body) 114 | 115 | proc registerProxyMethod*(proxy: var RpcProxy, methodName: string) = 116 | try: 117 | proxy.rpcHttpServer.register(methodName, proxyCall(proxy.getClient(), methodName)) 118 | except CatchableError as err: 119 | # Adding proc type to table gives invalid exception tracking, see Nim bug: https://github.com/nim-lang/Nim/issues/18376 120 | raiseAssert err.msg 121 | 122 | proc stop*(proxy: RpcProxy) {.async: (raises: []).} = 123 | await proxy.getClient().close() 124 | await proxy.rpcHttpServer.stop() 125 | 126 | proc closeWait*(proxy: RpcProxy) {.async: (raises: []).} = 127 | await proxy.rpcHttpServer.closeWait() 128 | 129 | func localAddress*(proxy: RpcProxy): seq[TransportAddress] = 130 | proxy.rpcHttpServer.localAddress() 131 | 132 | {.pop.} 133 | -------------------------------------------------------------------------------- /json_rpc/clients/httpclient.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | std/uri, 14 | chronos/apps/http/httpclient, 15 | httputils, 16 | ../[client, errors] 17 | 18 | export client, errors, HttpClientFlag, HttpClientFlags 19 | 20 | type 21 | HttpClientOptions* = object 22 | httpMethod: HttpMethod 23 | 24 | RpcHttpClient* = ref object of RpcClient 25 | httpSession: HttpSessionRef 26 | httpAddress: HttpAddress 27 | getHeaders: GetJsonRpcRequestHeaders 28 | 29 | proc new*( 30 | T: type RpcHttpClient, 31 | secure = false, 32 | getHeaders: GetJsonRpcRequestHeaders = nil, 33 | flags: HttpClientFlags = {}, 34 | maxMessageSize = defaultMaxMessageSize, 35 | ): T = 36 | var moreFlags: HttpClientFlags 37 | if secure: 38 | moreFlags.incl HttpClientFlag.NoVerifyHost 39 | moreFlags.incl HttpClientFlag.NoVerifyServerName 40 | 41 | T( 42 | maxMessageSize: maxMessageSize, 43 | httpSession: HttpSessionRef.new(flags = flags + moreFlags), 44 | getHeaders: getHeaders, 45 | ) 46 | 47 | method send( 48 | client: RpcHttpClient, reqData: seq[byte] 49 | ) {.async: (raises: [CancelledError, JsonRpcError]).} = 50 | doAssert client.httpSession != nil 51 | if client.httpAddress.addresses.len == 0: 52 | raise newException(RpcTransportError, "No remote addresses to connect to") 53 | 54 | var headers = 55 | if not isNil(client.getHeaders): 56 | client.getHeaders() 57 | else: 58 | @[] 59 | headers.add(("Content-Type", "application/json")) 60 | 61 | let 62 | req = HttpClientRequestRef.post( 63 | client.httpSession, client.httpAddress, body = reqData, headers = headers 64 | ) 65 | 66 | res = 67 | try: 68 | await req.send() 69 | except HttpError as exc: 70 | raise (ref RpcPostError)(msg: exc.msg, parent: exc) 71 | finally: 72 | await req.closeWait() 73 | 74 | try: 75 | if res.status < 200 or res.status >= 300: # res.status is not 2xx (success) 76 | raise (ref ErrorResponse)(status: res.status, msg: res.reason) 77 | except HttpError as exc: 78 | raise (ref RpcTransportError)(msg: exc.msg, parent: exc) 79 | finally: 80 | await res.closeWait() 81 | 82 | method request( 83 | client: RpcHttpClient, reqData: seq[byte] 84 | ): Future[seq[byte]] {.async: (raises: [CancelledError, JsonRpcError]).} = 85 | doAssert client.httpSession != nil 86 | if client.httpAddress.addresses.len == 0: 87 | raise newException(RpcTransportError, "No remote addresses to connect to") 88 | 89 | var headers = 90 | if not isNil(client.getHeaders): 91 | client.getHeaders() 92 | else: 93 | @[] 94 | headers.add(("Content-Type", "application/json")) 95 | 96 | let 97 | req = HttpClientRequestRef.post( 98 | client.httpSession, client.httpAddress, body = reqData, headers = headers 99 | ) 100 | 101 | res = 102 | try: 103 | await req.send() 104 | except HttpError as exc: 105 | raise (ref RpcPostError)(msg: exc.msg, parent: exc) 106 | finally: 107 | await req.closeWait() 108 | 109 | try: 110 | if res.status < 200 or res.status >= 300: # res.status is not 2xx (success) 111 | raise (ref ErrorResponse)(status: res.status, msg: res.reason) 112 | 113 | await res.getBodyBytes(client.maxMessageSize) 114 | except HttpError as exc: 115 | raise (ref RpcTransportError)(msg: exc.msg, parent: exc) 116 | finally: 117 | await res.closeWait() 118 | 119 | proc newRpcHttpClient*( 120 | maxBodySize = defaultMaxMessageSize, 121 | secure = false, 122 | getHeaders: GetJsonRpcRequestHeaders = nil, 123 | flags: HttpClientFlags = {}, 124 | ): RpcHttpClient = 125 | RpcHttpClient.new(secure, getHeaders, flags, maxBodySize) 126 | 127 | proc connect*( 128 | client: RpcHttpClient, url: string 129 | ) {.async: (raises: [CancelledError, JsonRpcError]).} = 130 | client.httpAddress = client.httpSession.getAddress(url).valueOr: 131 | raise newException(RpcAddressUnresolvableError, error) 132 | client.remote = client.httpAddress.id 133 | 134 | proc connect*( 135 | client: RpcHttpClient, address: string, port: Port, secure: bool 136 | ) {.async: (raises: [CancelledError, JsonRpcError]).} = 137 | let uri = Uri( 138 | scheme: if secure: "https" else: "http", 139 | hostname: address, 140 | port: $port) 141 | 142 | client.httpAddress = getAddress(client.httpSession, uri).valueOr: 143 | raise newException(RpcAddressUnresolvableError, error) 144 | client.remote = client.httpAddress.id 145 | 146 | method close*(client: RpcHttpClient) {.async: (raises: []).} = 147 | if not client.httpSession.isNil: 148 | await client.httpSession.closeWait() 149 | 150 | {.pop.} 151 | -------------------------------------------------------------------------------- /json_rpc/private/client_handler_wrapper.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2024-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | macros, 14 | ./shared_wrapper, 15 | ./jrpc_sys 16 | 17 | func createRpcProc(procName, parameters, callBody: NimNode): NimNode = 18 | # parameters come as a tree 19 | var paramList = newSeq[NimNode]() 20 | for p in parameters: paramList.add(p) 21 | 22 | # build proc 23 | result = newProc(procName, paramList, callBody) 24 | 25 | # export this proc 26 | result[0] = nnkPostfix.newTree(ident"*", newIdentNode($procName)) 27 | 28 | func createBatchCallProc(procName, parameters, callBody: NimNode): NimNode = 29 | # parameters come as a tree 30 | var paramList = newSeq[NimNode]() 31 | for p in parameters: paramList.add(p) 32 | 33 | # build proc 34 | result = newProc(procName, paramList, callBody) 35 | 36 | # export this proc 37 | result[0] = nnkPostfix.newTree(ident"*", newIdentNode($procName)) 38 | 39 | func setupConversion(reqParams, params: NimNode): NimNode = 40 | # populate json params 41 | # even rpcs with no parameters have an empty json array node sent 42 | 43 | params.expectKind nnkFormalParams 44 | result = newStmtList() 45 | result.add quote do: 46 | var `reqParams` = RequestParamsTx(kind: rpPositional) 47 | 48 | for parName, parType in paramsIter(params): 49 | result.add quote do: 50 | `reqParams`.positional.add encode(JrpcConv, `parName`).JsonString 51 | 52 | template maybeUnwrapClientResult*(client, meth, reqParams, returnType): auto = 53 | ## Don't decode e.g. JsonString, return as is 54 | when noWrap(returnType): 55 | client.call(meth, reqParams) 56 | else: 57 | proc complete(f: auto): Future[returnType] {.async.} = 58 | let res = await f 59 | decode(JrpcConv, res.string, returnType) 60 | let fut = client.call(meth, reqParams) 61 | complete(fut) 62 | 63 | func createRpcFromSig*(clientType, rpcDecl: NimNode, alias = NimNode(nil)): NimNode = 64 | ## This procedure will generate something like this: 65 | ## - Currently it always send positional parameters to the server 66 | ## 67 | ## proc rpcApi(client: RpcClient; paramA: TypeA; paramB: TypeB): Future[RetType] = 68 | ## {.gcsafe.}: 69 | ## var reqParams = RequestParamsTx(kind: rpPositional) 70 | ## reqParams.positional.add encode(JrpcConv, paramA).JsonString 71 | ## reqParams.positional.add encode(JrpcConv, paramB).JsonString 72 | ## let res = await client.call("rpcApi", reqParams) 73 | ## result = decode(JrpcConv, res.string, typeof RetType) 74 | ## 75 | ## 2nd version to handle batch request after calling client.prepareBatch() 76 | ## proc rpcApi(batch: RpcBatchCallRef; paramA: TypeA; paramB: TypeB) = 77 | ## var reqParams = RequestParamsTx(kind: rpPositional) 78 | ## reqParams.positional.add encode(JrpcConv, paramA).JsonString 79 | ## reqParams.positional.add encode(JrpcConv, paramB).JsonString 80 | ## batch.batch.add RpcBatchItem(meth: "rpcApi", params: reqParams) 81 | 82 | # Each input parameter in the rpc signature is converted 83 | # to json using JrpcConv.encode. 84 | # Return types are then converted back to native Nim types. 85 | 86 | let 87 | params = rpcDecl.findChild(it.kind == nnkFormalParams).ensureReturnType 88 | procName = if alias.isNil: rpcDecl.name else: alias 89 | pathStr = $rpcDecl.name 90 | returnType = params[0] 91 | reqParams = ident "reqParams" 92 | setup = setupConversion(reqParams, params) 93 | clientIdent = ident"client" 94 | batchParams = params.copy 95 | batchIdent = ident "batch" 96 | 97 | # insert rpc client as first parameter 98 | params.insert(1, nnkIdentDefs.newTree( 99 | clientIdent, 100 | ident($clientType), 101 | newEmptyNode() 102 | )) 103 | 104 | # convert return type to Future 105 | params[0] = nnkBracketExpr.newTree(ident"Future", returnType) 106 | 107 | # perform rpc call 108 | let callBody = quote do: 109 | # populate request params 110 | `setup` 111 | maybeUnwrapClientResult(`clientIdent`, `pathStr`, `reqParams`, `returnType`) 112 | 113 | # insert RpcBatchCallRef as first parameter 114 | batchParams.insert(1, nnkIdentDefs.newTree( 115 | batchIdent, 116 | ident "RpcBatchCallRef", 117 | newEmptyNode() 118 | )) 119 | 120 | # remove return type 121 | batchParams[0] = newEmptyNode() 122 | 123 | let batchCallBody = quote do: 124 | `setup` 125 | `batchIdent`.batch.add RpcBatchItem( 126 | meth: `pathStr`, 127 | params: `reqParams` 128 | ) 129 | 130 | # create rpc proc 131 | result = newStmtList() 132 | result.add createRpcProc(procName, params, callBody) 133 | result.add createBatchCallProc(procName, batchParams, batchCallBody) 134 | 135 | when defined(nimDumpRpcs): 136 | debugEcho pathStr, ":\n", result.repr 137 | 138 | func processRpcSigs*(clientType, parsedCode: NimNode): NimNode = 139 | result = newStmtList() 140 | 141 | for line in parsedCode: 142 | if line.kind == nnkProcDef: 143 | var procDef = createRpcFromSig(clientType, line) 144 | result.add(procDef) 145 | 146 | func cresteSignaturesFromString*(clientType: NimNode, sigStrings: string): NimNode = 147 | try: 148 | result = processRpcSigs(clientType, sigStrings.parseStmt()) 149 | except ValueError as exc: 150 | doAssert(false, exc.msg) 151 | 152 | {.pop.} 153 | -------------------------------------------------------------------------------- /tests/testhttps.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import unittest2, chronos/unittest2/asynctests 11 | import ../json_rpc/[rpcserver, rpcclient] 12 | import chronos/[streams/tlsstream, apps/http/httpcommon] 13 | 14 | const TestsCount = 100 15 | 16 | # To create self-signed certificate and key you can use openssl 17 | # openssl req -new -x509 -sha256 -newkey rsa:2048 -nodes \ 18 | # -keyout example-com.key.pem -days 3650 -out example-com.cert.pem 19 | const HttpsSelfSignedRsaKey = """ 20 | -----BEGIN PRIVATE KEY----- 21 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCn7tXGLKMIMzOG 22 | tVzUixax1/ftlSLcpEAkZMORuiCCnYjtIJhGZdzRFZC8fBlfAJZpLIAOfX2L2f1J 23 | ZuwpwDkOIvNqKMBrl5Mvkl5azPT0rtnjuwrcqN5NFtbmZPKFYvbjex2aXGqjl5MW 24 | nQIs/ZA++DVEXmaN9oDxcZsvRMDKfrGQf9iLeoVL47Gx9KpqNqD/JLIn4LpieumV 25 | yYidm6ukTOqHRvrWm36y6VvKW4TE97THacULmkeahtTf8zDJbbh4EO+gifgwgJ2W 26 | BUS0+5hMcWu8111mXmanlOVlcoW8fH8RmPjL1eK1Z3j3SVHEf7oWZtIVW5gGA0jQ 27 | nfA4K51RAgMBAAECggEANZ7/R13tWKrwouy6DWuz/WlWUtgx333atUQvZhKmWs5u 28 | cDjeJmxUC7b1FhoSB9GqNT7uTLIpKkSaqZthgRtNnIPwcU890Zz+dEwqMJgNByvl 29 | it+oYjjRco/+YmaNQaYN6yjelPE5Y678WlYb4b29Fz4t0/zIhj/VgEKkKH2tiXpS 30 | TIicoM7pSOscEUfaW3yp5bS5QwNU6/AaF1wws0feBACd19ZkcdPvr52jopbhxlXw 31 | h3XTV/vXIJd5zWGp0h/Jbd4xcD4MVo2GjfkeORKY6SjDaNzt8OGtePcKnnbUVu8b 32 | 2XlDxukhDQXqJ3g0sHz47mhvo4JeIM+FgymRm+3QmQKBgQDTawrEA3Zy9WvucaC7 33 | Zah02oE9nuvpF12lZ7WJh7+tZ/1ss+Fm7YspEKaUiEk7nn1CAVFtem4X4YCXTBiC 34 | Oqq/o+ipv1yTur0ae6m4pwLm5wcMWBh3H5zjfQTfrClNN8yjWv8u3/sq8KesHPnT 35 | R92/sMAptAChPgTzQphWbxFiYwKBgQDLWFaBqXfZYVnTyUvKX8GorS6jGWc6Eh4l 36 | lAFA+2EBWDICrUxsDPoZjEXrWCixdqLhyehaI3KEFIx2bcPv6X2c7yx3IG5lA/Gx 37 | TZiKlY74c6jOTstkdLW9RJbg1VUHUVZMf/Owt802YmEfUI5S5v7jFmKW6VG+io+K 38 | +5KYeHD1uwKBgQDMf53KPA82422jFwYCPjLT1QduM2q97HwIomhWv5gIg63+l4BP 39 | rzYMYq6+vZUYthUy41OAMgyLzPQ1ZMXQMi83b7R9fTxvKRIBq9xfYCzObGnE5vHD 40 | SDDZWvR75muM5Yxr9nkfPkgVIPMO6Hg+hiVYZf96V0LEtNjU9HWmJYkLQQKBgQCQ 41 | ULGUdGHKtXy7AjH3/t3CiKaAupa4cANVSCVbqQy/l4hmvfdu+AbH+vXkgTzgNgKD 42 | nHh7AI1Vj//gTSayLlQn/Nbh9PJkXtg5rYiFUn+VdQBo6yMOuIYDPZqXFtCx0Nge 43 | kvCwisHpxwiG4PUhgS+Em259DDonsM8PJFx2OYRx4QKBgEQpGhg71Oi9MhPJshN7 44 | dYTowaMS5eLTk2264ARaY+hAIV7fgvUa+5bgTVaWL+Cfs33hi4sMRqlEwsmfds2T 45 | cnQiJ4cU20Euldfwa5FLnk6LaWdOyzYt/ICBJnKFRwfCUbS4Bu5rtMEM+3t0wxnJ 46 | IgaD04WhoL9EX0Qo3DC1+0kG 47 | -----END PRIVATE KEY----- 48 | """ 49 | 50 | # This SSL certificate will expire 13 October 2030. 51 | const HttpsSelfSignedRsaCert = """ 52 | -----BEGIN CERTIFICATE----- 53 | MIIDnzCCAoegAwIBAgIUUdcusjDd3XQi3FPM8urdFG3qI+8wDQYJKoZIhvcNAQEL 54 | BQAwXzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 55 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEYMBYGA1UEAwwPMTI3LjAuMC4xOjQz 56 | ODA4MB4XDTIwMTAxMjIxNDUwMVoXDTMwMTAxMDIxNDUwMVowXzELMAkGA1UEBhMC 57 | QVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdp 58 | dHMgUHR5IEx0ZDEYMBYGA1UEAwwPMTI3LjAuMC4xOjQzODA4MIIBIjANBgkqhkiG 59 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+7VxiyjCDMzhrVc1IsWsdf37ZUi3KRAJGTD 60 | kboggp2I7SCYRmXc0RWQvHwZXwCWaSyADn19i9n9SWbsKcA5DiLzaijAa5eTL5Je 61 | Wsz09K7Z47sK3KjeTRbW5mTyhWL243sdmlxqo5eTFp0CLP2QPvg1RF5mjfaA8XGb 62 | L0TAyn6xkH/Yi3qFS+OxsfSqajag/ySyJ+C6YnrplcmInZurpEzqh0b61pt+sulb 63 | yluExPe0x2nFC5pHmobU3/MwyW24eBDvoIn4MICdlgVEtPuYTHFrvNddZl5mp5Tl 64 | ZXKFvHx/EZj4y9XitWd490lRxH+6FmbSFVuYBgNI0J3wOCudUQIDAQABo1MwUTAd 65 | BgNVHQ4EFgQUBKha84woY5WkFxKw7qx1cONg1H8wHwYDVR0jBBgwFoAUBKha84wo 66 | Y5WkFxKw7qx1cONg1H8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC 67 | AQEAHZMYt9Ry+Xj3vTbzpGFQzYQVTJlfJWSN6eWNOivRFQE5io9kOBEe5noa8aLo 68 | dLkw6ztxRP2QRJmlhGCO9/HwS17ckrkgZp3EC2LFnzxcBmoZu+owfxOT1KqpO52O 69 | IKOl8eVohi1pEicE4dtTJVcpI7VCMovnXUhzx1Ci4Vibns4a6H+BQa19a1JSpifN 70 | tO8U5jkjJ8Jprs/VPFhJj2O3di53oDHaYSE5eOrm2ZO14KFHSk9cGcOGmcYkUv8B 71 | nV5vnGadH5Lvfxb/BCpuONabeRdOxMt9u9yQ89vNpxFtRdZDCpGKZBCfmUP+5m3m 72 | N8r5CwGcIX/XPC3lKazzbZ8baA== 73 | -----END CERTIFICATE----- 74 | """ 75 | 76 | let secureKey = TLSPrivateKey.init(HttpsSelfSignedRsaKey) 77 | let secureCert = TLSCertificate.init(HttpsSelfSignedRsaCert) 78 | 79 | suite "JSON-RPC/https": 80 | setup: 81 | var secureHttpSrv = RpcHttpServer.new() 82 | 83 | secureHttpSrv.addSecureHttpServer("127.0.0.1:0", secureKey, secureCert) 84 | 85 | # Create RPC on server 86 | secureHttpSrv.rpc("myProc") do(input: string, data: array[0..3, int]): 87 | result = %("Hello " & input & " data: " & $data) 88 | secureHttpSrv.rpc("noParamsProc") do(): 89 | result = %("Hello world") 90 | 91 | secureHttpSrv.start() 92 | let serverAddress = $secureHttpSrv.localAddress()[0] 93 | teardown: 94 | waitFor secureHttpSrv.stop() 95 | waitFor secureHttpSrv.closeWait() 96 | 97 | asyncTest "Simple RPC call": 98 | var client = newRpcHttpClient(secure=true) 99 | await client.connect("https://" & serverAddress) 100 | var r = await client.call("noParamsProc", %[]) 101 | check r.string == "\"Hello world\"" 102 | 103 | asyncTest "Continuous RPC calls (" & $TestsCount & " messages)": 104 | var client = newRpcHttpClient(secure=true) 105 | for i in 0.. 2: true 25 | else: false 26 | 27 | template hasHexHeader*(value: HexDataStr|HexQuantityStr): bool = 28 | value.string.hasHexHeader 29 | 30 | func isHexChar*(c: char): bool = 31 | if c notin {'0'..'9'} and 32 | c notin {'a'..'f'} and 33 | c notin {'A'..'F'}: false 34 | else: true 35 | 36 | proc validate*(value: HexQuantityStr): bool = 37 | template strVal: untyped = value.string 38 | if not value.hasHexHeader: 39 | return false 40 | # No leading zeros 41 | if strVal[2] == '0': return false 42 | for i in 2.. string: 22 | var res = "A: " & $A 23 | res.add ", B: " & $B.get(99) 24 | res.add ", C: " & C 25 | res.add ", D: " & $D.get(77) 26 | res.add ", E: " & E.get("none") 27 | return res 28 | 29 | server.rpc("results_opt") do(A: int, B: Opt[int], C: string, D: Opt[int], E: Opt[string]) -> string: 30 | var res = "A: " & $A 31 | res.add ", B: " & $B.get(99) 32 | res.add ", C: " & C 33 | res.add ", D: " & $D.get(77) 34 | res.add ", E: " & E.get("none") 35 | return res 36 | 37 | server.rpc("mixed_opt") do(A: int, B: Opt[int], C: string, D: Option[int], E: Opt[string]) -> string: 38 | var res = "A: " & $A 39 | res.add ", B: " & $B.get(99) 40 | res.add ", C: " & C 41 | res.add ", D: " & $D.get(77) 42 | res.add ", E: " & E.get("none") 43 | return res 44 | 45 | server.rpc("alias_opt") do(A: int, B: OptAlias[int], C: string, D: Option[int], E: OptAlias[string]) -> string: 46 | var res = "A: " & $A 47 | res.add ", B: " & $B.get(99) 48 | res.add ", C: " & C 49 | res.add ", D: " & $D.get(77) 50 | res.add ", E: " & E.get("none") 51 | return res 52 | 53 | server.rpc("noParams") do() -> int: 54 | return 123 55 | 56 | server.rpc("emptyParams"): 57 | return %777 58 | 59 | server.rpc("comboParams") do(a, b, c: int) -> int: 60 | return a+b+c 61 | 62 | server.rpc("returnJsonString") do(a, b, c: int) -> JsonString: 63 | return JsonString($(a+b+c)) 64 | 65 | func req(meth: string, params: string): string = 66 | """{"jsonrpc":"2.0", "method": """ & 67 | "\"" & meth & "\", \"params\": " & params & """, "id":0}""" 68 | 69 | template test_optional(meth: static[string]) = 70 | test meth & " B E, positional": 71 | let n = req(meth, "[44, null, \"apple\", 33]") 72 | let res = waitFor server.route(n) 73 | check res == """{"jsonrpc":"2.0","result":"A: 44, B: 99, C: apple, D: 33, E: none","id":0}""" 74 | 75 | test meth & " B D E, positional": 76 | let n = req(meth, "[44, null, \"apple\"]") 77 | let res = waitFor server.route(n) 78 | check res == """{"jsonrpc":"2.0","result":"A: 44, B: 99, C: apple, D: 77, E: none","id":0}""" 79 | 80 | test meth & " D E, positional": 81 | let n = req(meth, "[44, 567, \"apple\"]") 82 | let res = waitFor server.route(n) 83 | check res == """{"jsonrpc":"2.0","result":"A: 44, B: 567, C: apple, D: 77, E: none","id":0}""" 84 | 85 | test meth & " D wrong E, positional": 86 | let n = req(meth, "[44, 567, \"apple\", \"banana\"]") 87 | let res = waitFor server.route(n) 88 | when meth == "std_option": 89 | check res == """{"jsonrpc":"2.0","error":{"code":-32000,"message":"`std_option` raised an exception","data":"Parameter [D] of type 'Option[system.int]' could not be decoded: number expected"},"id":0}""" 90 | elif meth == "results_opt": 91 | check res == """{"jsonrpc":"2.0","error":{"code":-32000,"message":"`results_opt` raised an exception","data":"Parameter [D] of type 'Opt[system.int]' could not be decoded: number expected"},"id":0}""" 92 | elif meth == "mixed_opt": 93 | check res == """{"jsonrpc":"2.0","error":{"code":-32000,"message":"`mixed_opt` raised an exception","data":"Parameter [D] of type 'Option[system.int]' could not be decoded: number expected"},"id":0}""" 94 | else: 95 | check res == """{"jsonrpc":"2.0","error":{"code":-32000,"message":"`alias_opt` raised an exception","data":"Parameter [D] of type 'Option[system.int]' could not be decoded: number expected"},"id":0}""" 96 | 97 | test meth & " D extra, positional": 98 | let n = req(meth, "[44, 567, \"apple\", 999, \"banana\", true]") 99 | let res = waitFor server.route(n) 100 | check res == """{"jsonrpc":"2.0","result":"A: 44, B: 567, C: apple, D: 999, E: banana","id":0}""" 101 | 102 | test meth & " B D E, named": 103 | let n = req(meth, """{"A": 33, "C":"banana" }""") 104 | let res = waitFor server.route(n) 105 | check res == """{"jsonrpc":"2.0","result":"A: 33, B: 99, C: banana, D: 77, E: none","id":0}""" 106 | 107 | test meth & " B E, D front, named": 108 | let n = req(meth, """{"D": 8887, "A": 33, "C":"banana" }""") 109 | let res = waitFor server.route(n) 110 | check res == """{"jsonrpc":"2.0","result":"A: 33, B: 99, C: banana, D: 8887, E: none","id":0}""" 111 | 112 | test meth & " B E, D front, extra X, named": 113 | let n = req(meth, """{"D": 8887, "X": false , "A": 33, "C":"banana"}""") 114 | let res = waitFor server.route(n) 115 | check res == """{"jsonrpc":"2.0","result":"A: 33, B: 99, C: banana, D: 8887, E: none","id":0}""" 116 | 117 | suite "rpc router": 118 | test "no params": 119 | let n = req("noParams", "[]") 120 | let res = waitFor server.route(n) 121 | check res == """{"jsonrpc":"2.0","result":123,"id":0}""" 122 | 123 | test "no params with params": 124 | let n = req("noParams", "[123]") 125 | let res = waitFor server.route(n) 126 | check res == """{"jsonrpc":"2.0","error":{"code":-32000,"message":"`noParams` raised an exception","data":"Expected 0 JSON parameter(s) but got 1"},"id":0}""" 127 | 128 | test_optional("std_option") 129 | test_optional("results_opt") 130 | test_optional("mixed_opt") 131 | test_optional("alias_opt") 132 | 133 | test "empty params": 134 | let n = req("emptyParams", "[]") 135 | let res = waitFor server.route(n) 136 | check res == """{"jsonrpc":"2.0","result":777,"id":0}""" 137 | 138 | test "combo params": 139 | let n = req("comboParams", "[6,7,8]") 140 | let res = waitFor server.route(n) 141 | check res == """{"jsonrpc":"2.0","result":21,"id":0}""" 142 | 143 | test "return json string": 144 | let n = req("returnJsonString", "[6,7,8]") 145 | let res = waitFor server.route(n) 146 | check res == """{"jsonrpc":"2.0","result":21,"id":0}""" 147 | -------------------------------------------------------------------------------- /tests/testserverclient.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | chronos/unittest2/asynctests, 12 | ../json_rpc/[rpcclient, rpcserver], 13 | ./private/helpers 14 | 15 | # Create RPC on server 16 | proc setupServer*(srv: RpcServer) = 17 | srv.rpc("myProc") do(input: string, data: array[0..3, int]): 18 | return %("Hello " & input & " data: " & $data) 19 | 20 | srv.rpc("myError") do(input: string, data: array[0..3, int]): 21 | raise (ref ValueError)(msg: "someMessage") 22 | 23 | srv.rpc("invalidRequest") do(): 24 | raise (ref InvalidRequest)(code: -32001, msg: "Unknown payload") 25 | 26 | template callTests(client: untyped) = 27 | test "Successful RPC call": 28 | let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]]) 29 | check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" 30 | 31 | test "Missing params": 32 | expect(CatchableError): 33 | discard waitFor client.call("myProc", %[%"abc"]) 34 | 35 | test "Error RPC call": 36 | expect(CatchableError): # The error type wont be translated 37 | discard waitFor client.call("myError", %[%"abc", %[1, 2, 3, 4]]) 38 | 39 | test "Invalid request exception": 40 | try: 41 | discard waitFor client.call("invalidRequest", %[]) 42 | check false 43 | except CatchableError as e: 44 | check e.msg == """{"code":-32001,"message":"Unknown payload"}""" 45 | 46 | suite "Socket Server/Client RPC/newLine": 47 | setup: 48 | const framing = Framing.newLine() 49 | var srv = newRpcSocketServer(["127.0.0.1:0"], framing = framing) 50 | var client = newRpcSocketClient(framing = framing) 51 | 52 | srv.setupServer() 53 | srv.start() 54 | waitFor client.connect(srv.localAddress()[0]) 55 | 56 | teardown: 57 | waitFor client.close() 58 | srv.stop() 59 | waitFor srv.closeWait() 60 | 61 | callTests(client) 62 | 63 | suite "Socket Server/Client RPC/httpHeader": 64 | setup: 65 | const framing = Framing.httpHeader() 66 | var srv = newRpcSocketServer(["127.0.0.1:0"], framing = framing) 67 | var client = newRpcSocketClient(framing = framing) 68 | 69 | srv.setupServer() 70 | srv.start() 71 | waitFor client.connect(srv.localAddress()[0]) 72 | 73 | teardown: 74 | waitFor client.close() 75 | srv.stop() 76 | waitFor srv.closeWait() 77 | 78 | callTests(client) 79 | 80 | suite "Socket Server/Client RPC/lengthHeaderBE32": 81 | setup: 82 | const framing = Framing.lengthHeaderBE32() 83 | var srv = newRpcSocketServer(["127.0.0.1:0"], framing = framing) 84 | var client = newRpcSocketClient(framing = framing) 85 | 86 | srv.setupServer() 87 | srv.start() 88 | waitFor client.connect(srv.localAddress()[0]) 89 | 90 | teardown: 91 | waitFor client.close() 92 | srv.stop() 93 | waitFor srv.closeWait() 94 | 95 | callTests(client) 96 | 97 | suite "Websocket Server/Client RPC": 98 | setup: 99 | var srv = newRpcWebSocketServer("127.0.0.1", Port(0)) 100 | var client = newRpcWebSocketClient() 101 | 102 | srv.setupServer() 103 | srv.start() 104 | waitFor client.connect("ws://" & $srv.localAddress()) 105 | 106 | callTests(client) 107 | 108 | teardown: 109 | waitFor client.close() 110 | srv.stop() 111 | waitFor srv.closeWait() 112 | 113 | suite "Websocket Server/Client RPC with Compression": 114 | setup: 115 | var srv = newRpcWebSocketServer("127.0.0.1", Port(0), 116 | compression = true) 117 | var client = newRpcWebSocketClient() 118 | 119 | srv.setupServer() 120 | srv.start() 121 | waitFor client.connect("ws://" & $srv.localAddress(), 122 | compression = true) 123 | 124 | teardown: 125 | waitFor client.close() 126 | srv.stop() 127 | waitFor srv.closeWait() 128 | 129 | callTests(client) 130 | 131 | suite "Custom processClient": 132 | test "Should be able to use custom processClient": 133 | var wasCalled: bool = false 134 | 135 | proc processClientHook(server: StreamServer, transport: StreamTransport) {.async: (raises: []).} = 136 | wasCalled = true 137 | 138 | var srv = newRpcSocketServer(processClientHook) 139 | srv.addStreamServer("localhost", Port(8888)) 140 | var client = newRpcSocketClient() 141 | srv.setupServer() 142 | srv.start() 143 | waitFor client.connect(srv.localAddress()[0]) 144 | asyncCheck client.call("", %[]) 145 | srv.stop() 146 | waitFor srv.closeWait() 147 | check wasCalled 148 | 149 | template notifyTest(router, client: untyped) = 150 | asyncTest "notifications": 151 | var 152 | notified = newAsyncEvent() 153 | notified2 = newAsyncEvent() 154 | 155 | router[].rpc("some_notify") do() -> void: 156 | notified.fire() 157 | router[].rpc("some_notify2") do() -> void: 158 | notified2.fire() 159 | 160 | await srv.notify("some_notify", default(RequestParamsTx)) 161 | await srv.notify("doesnt_exist", default(RequestParamsTx)) 162 | await srv.notify("some_notify2", default(RequestParamsTx)) 163 | 164 | check: 165 | await notified.wait().withTimeout(1.seconds) 166 | await notified2.wait().withTimeout(1.seconds) 167 | 168 | suite "Socket Bidirectional": 169 | setup: 170 | var router = new RpcRouter 171 | 172 | var srv = newRpcSocketServer(["127.0.0.1:0"]) 173 | var client = newRpcSocketClient(router = router) 174 | 175 | srv.start() 176 | 177 | waitFor client.connect(srv.localAddress()[0]) 178 | 179 | teardown: 180 | waitFor client.close() 181 | 182 | srv.stop() 183 | waitFor srv.closeWait() 184 | 185 | notifyTest(router, client) 186 | 187 | suite "Websocket Bidirectional": 188 | setup: 189 | var router = new RpcRouter 190 | 191 | var srv = newRpcWebSocketServer("127.0.0.1", Port(0)) 192 | var client = newRpcWebSocketClient(router = router) 193 | 194 | srv.start() 195 | 196 | waitFor client.connect("ws://" & $srv.localAddress()) 197 | 198 | teardown: 199 | waitFor client.close() 200 | 201 | srv.stop() 202 | waitFor srv.closeWait() 203 | 204 | notifyTest(router, client) 205 | -------------------------------------------------------------------------------- /json_rpc/clients/websocketclient.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | std/uri, 14 | chronicles, 15 | websock/[websock, extensions/compression/deflate], 16 | chronos/apps/http/httptable, 17 | ../[client, errors, router], 18 | ../private/jrpc_sys 19 | 20 | export client, errors 21 | 22 | type 23 | RpcWebSocketClient* = ref object of RpcConnection 24 | transport*: WSSession 25 | uri*: Uri 26 | loop*: Future[void] 27 | getHeaders*: GetJsonRpcRequestHeaders 28 | 29 | proc new*( 30 | T: type RpcWebSocketClient, 31 | getHeaders: GetJsonRpcRequestHeaders = nil, 32 | maxMessageSize = defaultMaxMessageSize, 33 | router = default(ref RpcRouter), 34 | ): T = 35 | let router = 36 | if router != nil: 37 | proc(request: RequestBatchRx): Future[seq[byte]] {.async: (raises: [], raw: true).} = 38 | router[].route(request) 39 | else: 40 | nil 41 | 42 | T(getHeaders: getHeaders, maxMessageSize: maxMessageSize, router: router) 43 | 44 | proc newRpcWebSocketClient*( 45 | getHeaders: GetJsonRpcRequestHeaders = nil, 46 | maxMessageSize = defaultMaxMessageSize, 47 | router = default(ref RpcRouter), 48 | ): RpcWebSocketClient = 49 | ## Creates a new client instance. 50 | RpcWebSocketClient.new(getHeaders, maxMessageSize, router) 51 | 52 | method send*( 53 | client: RpcWebSocketClient, reqData: seq[byte] 54 | ) {.async: (raises: [CancelledError, JsonRpcError]).} = 55 | if client.transport.isNil: 56 | raise newException( 57 | RpcTransportError, "Transport is not initialised (missing a call to connect?)" 58 | ) 59 | try: 60 | await client.transport.send(reqData, Opcode.Binary) 61 | except CancelledError as exc: 62 | raise exc 63 | except CatchableError as exc: 64 | raise (ref RpcPostError)(msg: exc.msg, parent: exc) 65 | 66 | method request*( 67 | client: RpcWebSocketClient, reqData: seq[byte] 68 | ): Future[seq[byte]] {.async: (raises: [CancelledError, JsonRpcError]).} = 69 | ## Remotely calls the specified RPC method. 70 | let transport = client.transport 71 | if transport.isNil: 72 | raise newException( 73 | RpcTransportError, "Transport is not initialised (missing a call to connect?)" 74 | ) 75 | 76 | client.withPendingFut(fut): 77 | try: 78 | await transport.send(reqData, Opcode.Binary) 79 | except CatchableError as exc: 80 | # If there's an error sending, the "next messages" facility will be 81 | # broken since we don't know if the server observed the message or not - 82 | # the same goes for cancellation during write 83 | try: 84 | await noCancel transport.close() 85 | except CatchableError as exc: 86 | # TODO https://github.com/status-im/nim-websock/pull/178 87 | raiseAssert exc.msg 88 | raise (ref RpcPostError)(msg: exc.msg, parent: exc) 89 | 90 | await fut 91 | 92 | proc processMessages*(client: RpcWebSocketClient) {.async: (raises: []).} = 93 | # Provide backwards compat with consumers that don't set a max message size 94 | # for example by constructing RpcWebSocketHandler without going through init 95 | let maxMessageSize = 96 | if client.maxMessageSize == 0: defaultMaxMessageSize else: client.maxMessageSize 97 | 98 | var lastError: ref JsonRpcError 99 | while client.transport.readyState != ReadyState.Closed: 100 | try: 101 | let data = await client.transport.recvMsg(maxMessageSize) 102 | 103 | let fallback = client.callOnProcessMessage(data).valueOr: 104 | lastError = (ref RequestDecodeError)(msg: error, payload: data) 105 | break 106 | 107 | if not fallback: 108 | continue 109 | 110 | let resp = await client.processMessage(data) 111 | 112 | if resp.len > 0: 113 | await client.transport.send(resp, Opcode.Binary) 114 | except CatchableError as exc: 115 | lastError = (ref RpcTransportError)(msg: exc.msg, parent: exc) 116 | break 117 | 118 | if lastError == nil: 119 | lastError = (ref RpcTransportError)(msg: "Connection closed") 120 | 121 | client.clearPending(lastError) 122 | 123 | try: 124 | await noCancel client.transport.close() 125 | client.transport = nil 126 | except CatchableError as exc: 127 | # TODO https://github.com/status-im/nim-websock/pull/178 128 | raiseAssert exc.msg 129 | 130 | if not client.onDisconnect.isNil: 131 | client.onDisconnect() 132 | 133 | proc addExtraHeaders( 134 | headers: var HttpTable, 135 | client: RpcWebSocketClient, 136 | extraHeaders: HttpTable) = 137 | # Apply client instance overrides 138 | if client.getHeaders != nil: 139 | for header in client.getHeaders(): 140 | headers.add(header[0], header[1]) 141 | 142 | # Apply call specific overrides 143 | for header in extraHeaders.stringItems: 144 | headers.add(header.key, header.value) 145 | 146 | # Apply default origin 147 | discard headers.hasKeyOrPut("Origin", "http://localhost") 148 | 149 | proc connect*( 150 | client: RpcWebSocketClient, 151 | uri: string, 152 | extraHeaders: HttpTable = default(HttpTable), 153 | compression = false, 154 | hooks: seq[Hook] = @[], 155 | flags: set[TLSFlags] = {}) {.async: (raises: [CancelledError, JsonRpcError]).} = 156 | proc headersHook(ctx: Hook, headers: var HttpTable): Result[void, string] = 157 | headers.addExtraHeaders(client, extraHeaders) 158 | ok() 159 | var ext: seq[ExtFactory] = if compression: @[deflateFactory()] 160 | else: @[] 161 | let uri = parseUri(uri) 162 | let ws = try: 163 | await WebSocket.connect( 164 | uri=uri, 165 | factories=ext, 166 | hooks=hooks & Hook(append: headersHook), 167 | flags=flags) 168 | except CancelledError as exc: 169 | raise exc 170 | except CatchableError as exc: 171 | # TODO https://github.com/status-im/nim-websock/pull/178 172 | raise (ref RpcTransportError)(msg: exc.msg, parent: exc) 173 | 174 | client.transport = ws 175 | client.uri = uri 176 | client.remote = uri.hostname & ":" & uri.port 177 | client.loop = processMessages(client) 178 | 179 | method close*(client: RpcWebSocketClient) {.async: (raises: [], raw: true).} = 180 | client.loop.cancelAndWait() 181 | -------------------------------------------------------------------------------- /json_rpc/router.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | std/[macros, sequtils, tables, json], 14 | stew/byteutils, 15 | chronicles, 16 | chronos, 17 | ./private/[jrpc_sys, server_handler_wrapper], 18 | ./[errors, jsonmarshal] 19 | 20 | export chronos, jsonmarshal, json 21 | 22 | logScope: 23 | topics = "jsonrpc router" 24 | 25 | type 26 | RpcProc* = proc(params: RequestParamsRx): Future[JsonString] {.async.} 27 | ## Procedure signature accepted as an RPC call by server - if the function 28 | ## has no return value, return `JsonString("null")` 29 | 30 | RpcRouter* = object 31 | procs*: Table[string, RpcProc] 32 | 33 | const 34 | # https://www.jsonrpc.org/specification#error_object 35 | JSON_PARSE_ERROR* = -32700 36 | INVALID_REQUEST* = -32600 37 | METHOD_NOT_FOUND* = -32601 38 | INVALID_PARAMS* = -32602 39 | INTERNAL_ERROR* = -32603 40 | SERVER_ERROR* = -32000 41 | JSON_ENCODE_ERROR* = -32001 42 | 43 | # ------------------------------------------------------------------------------ 44 | # Private helpers 45 | # ------------------------------------------------------------------------------ 46 | 47 | func methodNotFound(msg: sink string): ResponseError = 48 | ResponseError(code: METHOD_NOT_FOUND, message: msg) 49 | 50 | func serverError(msg: sink string, data: sink JsonString): ResponseError = 51 | ResponseError(code: SERVER_ERROR, message: msg, data: Opt.some(data)) 52 | 53 | func applicationError( 54 | code: int, msg: sink string, data: sink Opt[JsonString] 55 | ): ResponseError = 56 | ResponseError(code: code, message: msg, data: data) 57 | 58 | proc respResult(req: RequestRx2, res: sink JsonString): ResponseTx = 59 | if req.id.isSome(): 60 | ResponseTx( 61 | kind: rkResult, 62 | result: res, 63 | id: req.id.expect("just checked"), 64 | ) 65 | else: 66 | default(ResponseTx) 67 | 68 | proc respError*(req: RequestRx2, error: sink ResponseError): ResponseTx = 69 | if req.id.isSome(): 70 | ResponseTx( 71 | kind: rkError, 72 | error: error, 73 | id: req.id.expect("just checked"), 74 | ) 75 | else: 76 | default(ResponseTx) 77 | 78 | proc lookup(router: RpcRouter, req: RequestRx2): Opt[RpcProc] = 79 | let rpcProc = router.procs.getOrDefault(req.meth) 80 | 81 | if rpcProc.isNil: 82 | Opt.none(RpcProc) 83 | else: 84 | ok(rpcProc) 85 | 86 | proc wrapError*(code: int, msg: string): seq[byte] = 87 | JrpcSys.withWriter(writer): 88 | writer.writeValue( 89 | ResponseTx(kind: rkError, error: ResponseError(code: code, message: msg)) 90 | ) 91 | 92 | # ------------------------------------------------------------------------------ 93 | # Public functions 94 | # ------------------------------------------------------------------------------ 95 | 96 | proc init*(T: type RpcRouter): T = discard 97 | 98 | proc register*(router: var RpcRouter, path: string, call: RpcProc) = 99 | router.procs[path] = call 100 | 101 | proc clear*(router: var RpcRouter) = 102 | router.procs.clear 103 | 104 | proc hasMethod*(router: RpcRouter, methodName: string): bool = 105 | router.procs.hasKey(methodName) 106 | 107 | proc route*(router: RpcRouter, req: RequestRx2): 108 | Future[ResponseTx] {.async: (raises: []).} = 109 | let rpcProc = router.lookup(req).valueOr: 110 | debug "Request for non-registered method", id = req.id, methodName = req.meth 111 | return 112 | req.respError(methodNotFound("'" & req.meth & "' is not a registered RPC method")) 113 | 114 | try: 115 | debug "Processing JSON-RPC request", id = req.id, methodName = req.meth 116 | let res = await rpcProc(req.params) 117 | debug "Processed JSON-RPC request", id = req.id, methodName = req.meth, len = string(res).len 118 | req.respResult(res) 119 | except ApplicationError as err: 120 | debug "Error occurred within RPC", methodName = req.meth, err = err.msg, code = err.code 121 | req.respError(applicationError(err.code, err.msg, err.data)) 122 | except CatchableError as err: 123 | debug "Error occurred within RPC", methodName = req.meth, err = err.msg 124 | 125 | # Note: Errors that are not specifically raised as `ApplicationError`s will 126 | # be returned as custom server errors. 127 | req.respError( 128 | serverError( 129 | "`" & req.meth & "` raised an exception", escapeJson(err.msg).JsonString 130 | ) 131 | ) 132 | 133 | proc route*(router: RpcRouter, request: RequestBatchRx): 134 | Future[seq[byte]] {.async: (raises: []).} = 135 | ## Route to RPC from string data. Data is expected to be able to be 136 | ## converted to Json. 137 | ## Returns string of Json from RPC result/error node 138 | 139 | case request.kind 140 | of rbkSingle: 141 | let response = await router.route(request.single) 142 | if request.single.id.isSome: 143 | JrpcSys.withWriter(writer): 144 | writer.writeValue(response) 145 | else: 146 | default(seq[byte]) 147 | of rbkMany: 148 | # check raising type to ensure `value` below is safe to use 149 | let resFut: seq[Future[ResponseTx].Raising([])] = 150 | request.many.mapIt(router.route(it)) 151 | 152 | await noCancel(allFutures(resFut)) 153 | 154 | var resps = newSeqOfCap[ResponseTx](resFut.len) 155 | for i, fut in resFut: 156 | if request.many[i].id.isSome(): 157 | resps.add fut.value() 158 | 159 | if resps.len > 0: 160 | JrpcSys.withWriter(writer): 161 | writer.writeArray: 162 | for f in resFut: 163 | writer.writeValue(f.value()) 164 | else: 165 | default(seq[byte]) 166 | 167 | proc route*( 168 | router: RpcRouter, data: string | seq[byte] 169 | ): Future[string] {.async: (raises: []).} = 170 | ## Route to RPC from string data. Data is expected to be able to be 171 | ## converted to Json. 172 | ## Returns string of Json from RPC result/error node 173 | let request = 174 | try: 175 | JrpcSys.decode(data, RequestBatchRx) 176 | except IncompleteObjectError as err: 177 | return string.fromBytes(wrapError(INVALID_REQUEST, err.msg)) 178 | except SerializationError as err: 179 | return string.fromBytes(wrapError(JSON_PARSE_ERROR, err.msg)) 180 | 181 | string.fromBytes(await router.route(request)) 182 | 183 | macro rpc*(server: RpcRouter, path: static[string], body: untyped): untyped = 184 | ## Define a remote procedure call. 185 | ## Input and return parameters are defined using the ``do`` notation. 186 | ## For example: 187 | ## .. code-block:: nim 188 | ## myServer.rpc("path") do(param1: int, param2: float) -> string: 189 | ## result = $param1 & " " & $param2 190 | ## ``` 191 | ## Input parameters are automatically marshalled from json to Nim types, 192 | ## and output parameters are automatically marshalled to json for transport. 193 | let 194 | params = body.findChild(it.kind == nnkFormalParams) 195 | procBody = if body.kind == nnkStmtList: body else: body.body 196 | procWrapper = genSym(nskProc, $path & "_rpcWrapper") 197 | 198 | result = wrapServerHandler($path, params, procBody, procWrapper) 199 | 200 | result.add quote do: 201 | `server`.register(`path`, `procWrapper`) 202 | 203 | when defined(nimDumpRpcs): 204 | echo "\n", path, ": ", result.repr 205 | 206 | {.pop.} 207 | -------------------------------------------------------------------------------- /json_rpc/servers/websocketserver.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | chronicles, 14 | chronos, 15 | websock/[websock, types], 16 | websock/extensions/compression/deflate, 17 | json_serialization/std/net as jsnet, 18 | ../[errors, server], 19 | ../private/jrpc_sys, 20 | ../clients/websocketclient 21 | 22 | export errors, server, jsnet 23 | 24 | logScope: 25 | topics = "jsonrpc server ws" 26 | 27 | type 28 | # WsAuthHook: handle CORS, JWT auth, etc. in HTTP header 29 | # before actual request processed 30 | # return value: 31 | # - true: auth success, continue execution 32 | # - false: could not authenticate, stop execution 33 | # and return the response 34 | WsAuthHook* = 35 | proc(request: HttpRequest): Future[bool] {.async: (raises: [CatchableError]).} 36 | 37 | # This inheritance arrangement is useful for 38 | # e.g. combo HTTP server 39 | RpcWebSocketHandler* = ref object of RpcServer 40 | wsserver*: WSServer 41 | maxMessageSize*: int 42 | 43 | RpcWebSocketServer* = ref object of RpcWebSocketHandler 44 | server: StreamServer 45 | authHooks: seq[WsAuthHook] 46 | 47 | proc serveHTTP*(rpc: RpcWebSocketHandler, request: HttpRequest) 48 | {.async: (raises: [CancelledError]).} = 49 | let ws = try: 50 | let server = rpc.wsserver 51 | let ws = await server.handleRequest(request) 52 | if ws.readyState != ReadyState.Open: 53 | error "Failed to open websocket connection", address = $request.uri 54 | return 55 | ws 56 | except WebSocketError as exc: 57 | error "WebSocket error:", address = $request.uri, msg = exc.msg 58 | return 59 | except CancelledError as exc: 60 | raise exc 61 | except CatchableError as exc: 62 | debug "Internal error while processing JSON-RPC call", msg = exc.msg 63 | return 64 | 65 | trace "Websocket handshake completed" 66 | let c = RpcWebSocketClient( 67 | transport: ws, 68 | remote: $request.uri, 69 | maxMessageSize: rpc.maxMessageSize, 70 | router: proc( 71 | request: RequestBatchRx 72 | ): Future[seq[byte]] {.async: (raises: [], raw: true).} = 73 | rpc.router.route(request), 74 | ) 75 | rpc.connections.incl(c) 76 | 77 | await c.processMessages() 78 | 79 | rpc.connections.excl(c) 80 | 81 | proc handleRequest( 82 | rpc: RpcWebSocketServer, request: HttpRequest 83 | ) {.async: (raises: [CancelledError]).} = 84 | trace "Handling request:", uri = $request.uri 85 | 86 | # if hook result is false, 87 | # it means we should return immediately 88 | try: 89 | for hook in rpc.authHooks: 90 | let res = await hook(request) 91 | if not res: 92 | return 93 | except CancelledError as exc: 94 | raise exc 95 | except CatchableError as exc: 96 | debug "Internal error while processing JSON-RPC hook", msg=exc.msg 97 | try: 98 | await request.sendResponse(Http503, 99 | data = "", 100 | content = "Internal error, processing JSON-RPC hook: " & exc.msg) 101 | return 102 | except CatchableError as exc: 103 | debug "Something error", msg=exc.msg 104 | return 105 | 106 | await rpc.serveHTTP(request) 107 | 108 | proc initWebsocket(rpc: RpcWebSocketServer, compression: bool, 109 | authHooks: seq[WsAuthHook], 110 | rng: ref HmacDrbgContext) = 111 | if compression: 112 | let deflateFactory = deflateFactory() 113 | rpc.wsserver = WSServer.new(factories = [deflateFactory], rng = rng) 114 | else: 115 | rpc.wsserver = WSServer.new(rng = rng) 116 | rpc.authHooks = authHooks 117 | 118 | proc newRpcWebSocketServer*( 119 | address: TransportAddress, 120 | compression: bool = false, 121 | flags: set[ServerFlags] = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}, 122 | authHooks: seq[WsAuthHook] = @[], 123 | rng = HmacDrbgContext.new(), 124 | maxMessageSize = defaultMaxMessageSize, 125 | ): RpcWebSocketServer {.raises: [JsonRpcError].} = 126 | var server = RpcWebSocketServer(maxMessageSize: maxMessageSize) 127 | 128 | proc processCallback(request: HttpRequest): Future[void] = 129 | handleRequest(server, request) 130 | 131 | server.initWebsocket(compression, authHooks, rng) 132 | try: 133 | server.server = HttpServer.create( 134 | address, 135 | processCallback, 136 | flags 137 | ) 138 | except CatchableError as exc: 139 | raise (ref RpcBindError)(msg: "Unable to create server: " & exc.msg, parent: exc) 140 | 141 | server 142 | 143 | proc newRpcWebSocketServer*( 144 | host: string, 145 | port: Port, 146 | compression: bool = false, 147 | flags: set[ServerFlags] = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}, 148 | authHooks: seq[WsAuthHook] = @[], 149 | rng = HmacDrbgContext.new(), 150 | maxMessageSize = defaultMaxMessageSize, 151 | ): RpcWebSocketServer {.raises: [JsonRpcError].} = 152 | try: 153 | newRpcWebSocketServer( 154 | initTAddress(host, port), 155 | compression, 156 | flags, 157 | authHooks, 158 | rng, 159 | maxMessageSize, 160 | ) 161 | except TransportError as exc: 162 | raise (ref RpcBindError)(msg: "Unable to create server: " & exc.msg, parent: exc) 163 | 164 | proc newRpcWebSocketServer*( 165 | address: TransportAddress, 166 | tlsPrivateKey: TLSPrivateKey, 167 | tlsCertificate: TLSCertificate, 168 | compression: bool = false, 169 | flags: set[ServerFlags] = {ServerFlags.TcpNoDelay, 170 | ServerFlags.ReuseAddr}, 171 | tlsFlags: set[TLSFlags] = {}, 172 | tlsMinVersion = TLSVersion.TLS12, 173 | tlsMaxVersion = TLSVersion.TLS12, 174 | authHooks: seq[WsAuthHook] = @[], 175 | rng = HmacDrbgContext.new(), 176 | maxMessageSize = defaultMaxMessageSize, 177 | ): RpcWebSocketServer {.raises: [JsonRpcError].} = 178 | 179 | var server = RpcWebSocketServer(maxMessageSize: maxMessageSize) 180 | proc processCallback(request: HttpRequest): Future[void] = 181 | handleRequest(server, request) 182 | 183 | server.initWebsocket(compression, authHooks, rng) 184 | try: 185 | server.server = TlsHttpServer.create( 186 | address, 187 | tlsPrivateKey, 188 | tlsCertificate, 189 | processCallback, 190 | flags, 191 | tlsFlags, 192 | tlsMinVersion, 193 | tlsMaxVersion 194 | ) 195 | except CatchableError as exc: 196 | raise (ref RpcBindError)(msg: "Unable to create server: " & exc.msg, parent: exc) 197 | 198 | server 199 | 200 | proc newRpcWebSocketServer*( 201 | host: string, 202 | port: Port, 203 | tlsPrivateKey: TLSPrivateKey, 204 | tlsCertificate: TLSCertificate, 205 | compression: bool = false, 206 | flags: set[ServerFlags] = {ServerFlags.TcpNoDelay, 207 | ServerFlags.ReuseAddr}, 208 | tlsFlags: set[TLSFlags] = {}, 209 | tlsMinVersion = TLSVersion.TLS12, 210 | tlsMaxVersion = TLSVersion.TLS12, 211 | authHooks: seq[WsAuthHook] = @[], 212 | rng = HmacDrbgContext.new()): RpcWebSocketServer {.raises: [JsonRpcError].} = 213 | 214 | try: 215 | newRpcWebSocketServer( 216 | initTAddress(host, port), 217 | tlsPrivateKey, 218 | tlsCertificate, 219 | compression, 220 | flags, 221 | tlsFlags, 222 | tlsMinVersion, 223 | tlsMaxVersion, 224 | authHooks, 225 | rng 226 | ) 227 | except TransportError as exc: 228 | raise (ref RpcBindError)(msg: "Unable to create server: " & exc.msg, parent: exc) 229 | 230 | proc start*(server: RpcWebSocketServer) {.raises: [JsonRpcError].} = 231 | ## Start the RPC server. 232 | try: 233 | info "Starting JSON-RPC WebSocket server", address = server.server.local 234 | server.server.start() 235 | except TransportOsError as exc: 236 | raise (ref RpcBindError)(msg: "Unable to start server: " & exc.msg, parent: exc) 237 | 238 | proc stop*(server: RpcWebSocketServer) = 239 | ## Stop the RPC server. 240 | try: 241 | server.server.stop() 242 | notice "Stopped JSON-RPC WebSocket server", address = server.server.local 243 | except TransportOsError as exc: 244 | warn "Could not stop JSON-RPC WebSocket server", err = exc.msg 245 | 246 | proc close*(server: RpcWebSocketServer) = 247 | ## Cleanup resources of RPC server. 248 | server.server.close() 249 | 250 | proc closeWait*(server: RpcWebSocketServer) {.async: (raises: []).} = 251 | ## Cleanup resources of RPC server. 252 | await server.server.closeWait() 253 | 254 | proc localAddress*(server: RpcWebSocketServer): TransportAddress = 255 | server.server.localAddress() 256 | -------------------------------------------------------------------------------- /tests/test_jrpc_sys.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2023-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | 8 | import unittest2, ../json_rpc/private/jrpc_sys 9 | 10 | suite "jrpc_sys serialization": 11 | test "request: id": 12 | const cases = [ 13 | ( 14 | """{"jsonrpc":"2.0","method":"none"}""", 15 | RequestTx(`method`: "none", id: Opt.none(RequestId)), 16 | ), 17 | ( 18 | """{"jsonrpc":"2.0","method":"null","id":null}""", 19 | RequestTx(`method`: "null", id: Opt.some(RequestId(kind: riNull))), 20 | ), 21 | ( 22 | """{"jsonrpc":"2.0","method":"num","id":42}""", 23 | RequestTx(`method`: "num", id: Opt.some(RequestId(kind: riNumber, num: 42))), 24 | ), 25 | ( 26 | """{"jsonrpc":"2.0","method":"str","id":"str"}""", 27 | RequestTx(`method`: "str", id: Opt.some(RequestId(kind: riString, str: "str"))), 28 | ), 29 | ] 30 | 31 | for (expected, tx) in cases: 32 | let 33 | encoded = JrpcSys.encode(tx) 34 | rx = JrpcSys.decode(expected, RequestRx2) 35 | checkpoint(expected) 36 | checkpoint(encoded) 37 | checkpoint($rx) 38 | check: 39 | encoded == expected 40 | tx.id == rx.id 41 | 42 | test "request: parameters": 43 | const cases = [ 44 | ( 45 | """{"jsonrpc":"2.0","method":"empty_positional"}""", 46 | RequestTx( 47 | `method`: "empty_positional", 48 | params: RequestParamsTx(kind: rpPositional, positional: @[]), 49 | ), 50 | ), 51 | ( 52 | """{"jsonrpc":"2.0","method":"int_positional","params":[123,true,"hello"],"id":123}""", 53 | RequestTx( 54 | `method`: "int_positional", 55 | id: Opt.some(RequestId(kind: riNumber, num: 123)), 56 | params: RequestParamsTx( 57 | kind: rpPositional, 58 | positional: 59 | @[JsonString("123"), JsonString("true"), JsonString("\"hello\"")], 60 | ), 61 | ), 62 | ), 63 | ( 64 | """{"jsonrpc":"2.0","method":"string_named","params":{"banana":true,"apple":123},"id":"word"}""", 65 | RequestTx( 66 | `method`: "string_named", 67 | id: Opt.some(RequestId(kind: riString, str: "word")), 68 | params: RequestParamsTx( 69 | kind: rpNamed, 70 | named: 71 | @[ 72 | ParamDescNamed(name: "banana", value: JsonString("true")), 73 | ParamDescNamed(name: "apple", value: JsonString("123")), 74 | ], 75 | ), 76 | ), 77 | ), 78 | ] 79 | for (expected, tx) in cases: 80 | let 81 | encoded = JrpcSys.encode(tx) 82 | rx = JrpcSys.decode(encoded, RequestRx2) 83 | checkpoint(expected) 84 | checkpoint(encoded) 85 | checkpoint($rx) 86 | check: 87 | encoded == expected 88 | tx.params.kind == rx.params.kind 89 | if tx.params.kind == rpPositional: 90 | let 91 | tpos = tx.params.positional 92 | rpos = rx.params.positional 93 | check: 94 | tpos.len == rpos.len 95 | for i in 0 ..< tpos.len: 96 | check tpos[i] == rpos[i].param 97 | elif tx.params.kind == rpNamed: 98 | let 99 | tnamed = tx.params.named 100 | rnamed = rx.params.named 101 | check: 102 | tnamed.len == rnamed.len 103 | for i in 0 ..< tnamed.len: 104 | check: 105 | tnamed[i].name == rnamed[i].name 106 | tnamed[i].value == rnamed[i].value 107 | 108 | test "response: result and error encodings": 109 | const cases = [ 110 | ( 111 | """{"jsonrpc":"2.0","result":true,"id":null}""", 112 | ResponseTx(kind: rkResult, result: JsonString("true")), 113 | ), 114 | ( 115 | """{"jsonrpc":"2.0","error":{"code":999,"message":"fatal"},"id":null}""", 116 | ResponseTx(kind: rkError, error: ResponseError(code: 999, message: "fatal")), 117 | ), 118 | ] 119 | for (expected, tx) in cases: 120 | let 121 | encoded = JrpcSys.encode(tx) 122 | rx = JrpcSys.decode(encoded, ResponseRx2) 123 | checkpoint(expected) 124 | checkpoint(encoded) 125 | checkpoint($rx) 126 | check: 127 | encoded == expected 128 | if tx.kind == rkResult: 129 | check: 130 | rx.kind == ResponseKind.rkResult 131 | rx.id == tx.id 132 | else: 133 | check: 134 | rx.kind == ResponseKind.rkError 135 | rx.id == tx.id 136 | rx.error.code == tx.error.code 137 | rx.error.message == tx.error.message 138 | 139 | test "batch requests: single and many encodings": 140 | const cases = [ 141 | ( 142 | """{"jsonrpc":"2.0","method":"a"}""", 143 | RequestBatchRx(kind: rbkSingle, single: RequestRx2(`method`: "a")), 144 | ), 145 | ( 146 | """[{"jsonrpc":"2.0","method":"a"},{"jsonrpc":"2.0","method":"b"}]""", 147 | RequestBatchRx( 148 | kind: rbkMany, many: @[RequestRx2(`method`: "a"), RequestRx2(`method`: "b")] 149 | ), 150 | ), 151 | ] 152 | for (expected, tx) in cases: 153 | let rx = JrpcSys.decode(expected, RequestBatchRx) 154 | checkpoint(expected) 155 | checkpoint($rx) 156 | if tx.kind == rbkSingle: 157 | check: 158 | rx.kind == rbkSingle 159 | rx.single.`method` == tx.single.`method` 160 | else: 161 | check: 162 | rx.kind == rbkMany 163 | rx.many.len == tx.many.len 164 | 165 | test "batch responses: single and many encodings": 166 | const cases = [ 167 | ( 168 | """{"jsonrpc":"2.0","result":null,"id":null}""", 169 | ResponseBatchRx( 170 | kind: rbkSingle, 171 | single: ResponseRx2(kind: rkResult, result: JsonString("null")), 172 | ), 173 | ), 174 | ( 175 | """[{"jsonrpc":"2.0","result":null,"id":null},{"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":null}]""", 176 | ResponseBatchRx( 177 | kind: rbkMany, 178 | many: 179 | @[ 180 | ResponseRx2(kind: rkResult, result: JsonString("null")), 181 | ResponseRx2( 182 | kind: rkError, 183 | error: ResponseError(code: -32601, message: "Method not found"), 184 | ), 185 | ], 186 | ), 187 | ), 188 | ] 189 | for (expected, tx) in cases: 190 | let rx = JrpcSys.decode(expected, ResponseBatchRx) 191 | checkpoint(expected) 192 | checkpoint($rx) 193 | if tx.kind == rbkSingle: 194 | check: 195 | rx.kind == rbkSingle 196 | else: 197 | check: 198 | rx.kind == rbkMany 199 | rx.many.len == tx.many.len 200 | 201 | test "malformed JSON and top-level incorrect types are rejected": 202 | expect UnexpectedValueError: 203 | discard JrpcSys.decode("{ this is not valid json }", RequestRx2) 204 | expect UnexpectedValueError: 205 | discard JrpcSys.decode("123", RequestRx2) 206 | expect UnexpectedValueError: 207 | discard JrpcSys.decode("\"just a string\"", RequestRx2) 208 | 209 | test "invalid constructs: empty batch and mixed-type batch entries rejected": 210 | expect UnexpectedValueError: 211 | discard JrpcSys.decode("[]", RequestBatchRx) 212 | expect UnexpectedValueError: 213 | discard JrpcSys.decode("[]", ResponseBatchRx) 214 | 215 | let mixed = 216 | """[{"jsonrpc":"2.0","method":"foo","params":[]},42,{"jsonrpc":"2.0","method":"notify_no_id","params":["a"]}]""" 217 | expect UnexpectedValueError: 218 | discard JrpcSys.decode(mixed, RequestBatchRx) 219 | 220 | test "invalid id types rejected": 221 | expect UnexpectedValueError: 222 | discard JrpcSys.decode("""{"jsonrpc":"2.0","id":{},"method":"m"}""", RequestRx2) 223 | expect UnexpectedValueError: 224 | discard 225 | JrpcSys.decode("""{"jsonrpc":"2.0","id":[1,2],"method":"m"}""", RequestRx2) 226 | 227 | test "error response preserves standard fields and encoder correctness": 228 | const cases = [ 229 | ( 230 | """{"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":null}""", 231 | ResponseTx( 232 | kind: rkError, error: ResponseError(code: -32601, message: "Method not found") 233 | ), 234 | ) 235 | ] 236 | for (expected, tx) in cases: 237 | let 238 | encoded = JrpcSys.encode(tx) 239 | rx = JrpcSys.decode(encoded, ResponseRx2) 240 | check: 241 | encoded == expected 242 | rx.kind == ResponseKind.rkError 243 | rx.error.code == tx.error.code 244 | rx.error.message == tx.error.message 245 | -------------------------------------------------------------------------------- /tests/private/ethtypes.nim: -------------------------------------------------------------------------------- 1 | import stint 2 | 3 | type 4 | SyncObject* = object 5 | startingBlock*: int 6 | currentBlock*: int 7 | highestBlock*: int 8 | 9 | EthSend* = object 10 | source*: array[20, byte] # the address the transaction is send from. 11 | to*: array[20, byte] # (optional when creating new contract) the address the transaction is directed to. 12 | gas*: int # (optional, default: 90000) integer of the gas provided for the transaction execution. It will return unused gas. 13 | gasPrice*: int # (optional, default: To-Be-Determined) integer of the gasPrice used for each paid gas. 14 | value*: int # (optional) integer of the value sent with this transaction. 15 | data*: int # the compiled code of a contract OR the hash of the invoked method signature and encoded parameters. For details see Ethereum Contract ABI. 16 | nonce*: int # (optional) integer of a nonce. This allows to overwrite your own pending transactions that use the same nonce 17 | 18 | EthCall* = object 19 | source*: array[20, byte] # (optional) The address the transaction is send from. 20 | to*: array[20, byte] # The address the transaction is directed to. 21 | gas*: int # (optional) Integer of the gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions. 22 | gasPrice*: int # (optional) Integer of the gasPrice used for each paid gas. 23 | value*: int # (optional) Integer of the value sent with this transaction. 24 | data*: int # (optional) Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI. 25 | 26 | ## A block object, or null when no block was found 27 | BlockObject* = ref object 28 | number*: int # the block number. null when its pending block. 29 | hash*: UInt256 # hash of the block. null when its pending block. 30 | parentHash*: UInt256 # hash of the parent block. 31 | nonce*: int64 # hash of the generated proof-of-work. null when its pending block. 32 | sha3Uncles*: UInt256 # SHA3 of the uncles data in the block. 33 | logsBloom*: array[256, byte] # the bloom filter for the logs of the block. null when its pending block. 34 | transactionsRoot*: UInt256 # the root of the transaction trie of the block. 35 | stateRoot*: UInt256 # the root of the final state trie of the block. 36 | receiptsRoot*: UInt256 # the root of the receipts trie of the block. 37 | miner*: array[20, byte] # the address of the beneficiary to whom the mining rewards were given. 38 | difficulty*: int # integer of the difficulty for this block. 39 | totalDifficulty*: int # integer of the total difficulty of the chain until this block. 40 | extraData*: string # the "extra data" field of this block. 41 | size*: int # integer the size of this block in bytes. 42 | gasLimit*: int # the maximum gas allowed in this block. 43 | gasUsed*: int # the total used gas by all transactions in this block. 44 | timestamp*: int # the unix timestamp for when the block was collated. 45 | transactions*: seq[UInt256] # list of transaction objects, or 32 Bytes transaction hashes depending on the last given parameter. 46 | uncles*: seq[UInt256] # list of uncle hashes. 47 | 48 | TransactionObject* = object # A transaction object, or null when no transaction was found: 49 | hash*: UInt256 # hash of the transaction. 50 | nonce*: int64 # TODO: Is int? the number of transactions made by the sender prior to this one. 51 | blockHash*: UInt256 # hash of the block where this transaction was in. null when its pending. 52 | blockNumber*: int64 # block number where this transaction was in. null when its pending. 53 | transactionIndex*: int64 # integer of the transactions index position in the block. null when its pending. 54 | source*: array[20, byte] # address of the sender. 55 | to*: array[20, byte] # address of the receiver. null when its a contract creation transaction. 56 | value*: int64 # value transferred in Wei. 57 | gasPrice*: int64 # gas price provided by the sender in Wei. 58 | gas*: int64 # gas provided by the sender. 59 | input*: seq[byte] # the data send along with the transaction. 60 | 61 | ReceiptKind* = enum rkRoot, rkStatus 62 | ReceiptObject* = object 63 | # A transaction receipt object, or null when no receipt was found: 64 | transactionHash*: UInt256 # hash of the transaction. 65 | transactionIndex*: int # integer of the transactions index position in the block. 66 | blockHash*: UInt256 # hash of the block where this transaction was in. 67 | blockNumber*: int # block number where this transaction was in. 68 | cumulativeGasUsed*: int # the total amount of gas used when this transaction was executed in the block. 69 | gasUsed*: int # the amount of gas used by this specific transaction alone. 70 | contractAddress*: array[20, byte] # the contract address created, if the transaction was a contract creation, otherwise null. 71 | logs*: seq[string] # TODO: See Wiki for details. list of log objects, which this transaction generated. 72 | logsBloom*: array[256, byte] # bloom filter for light clients to quickly retrieve related logs. 73 | # TODO: 74 | #case kind*: ReceiptKind 75 | #of rkRoot: root*: UInt256 # post-transaction stateroot (pre Byzantium). 76 | #of rkStatus: status*: int # 1 = success, 0 = failure. 77 | 78 | FilterDataKind* = enum fkItem, fkList 79 | FilterData* = object 80 | # Difficult to process variant objects in input data, as kind is immutable. 81 | # TODO: This might need more work to handle "or" options 82 | kind*: FilterDataKind 83 | items*: seq[FilterData] 84 | item*: UInt256 85 | # TODO: I don't think this will work as input, need only one value that is either UInt256 or seq[UInt256] 86 | 87 | FilterOptions* = object 88 | fromBlock*: string # (optional, default: "latest") integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions. 89 | toBlock*: string # (optional, default: "latest") integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions. 90 | address*: seq[array[20, byte]] # (optional) contract address or a list of addresses from which logs should originate. 91 | topics*: seq[FilterData] # (optional) list of DATA topics. Topics are order-dependent. Each topic can also be a list of DATA with "or" options. 92 | 93 | LogObject* = object 94 | removed*: bool # true when the log was removed, due to a chain reorganization. false if its a valid log. 95 | logIndex*: int # integer of the log index position in the block. null when its pending log. 96 | transactionIndex*: ref int # integer of the transactions index position log was created from. null when its pending log. 97 | transactionHash*: UInt256 # hash of the transactions this log was created from. null when its pending log. 98 | blockHash*: ref UInt256 # hash of the block where this log was in. null when its pending. null when its pending log. 99 | blockNumber*: ref int64 # the block number where this log was in. null when its pending. null when its pending log. 100 | address*: array[20, byte] # address from which this log originated. 101 | data*: seq[UInt256] # contains one or more 32 Bytes non-indexed arguments of the log. 102 | topics*: array[4, UInt256] # array of 0 to 4 32 Bytes DATA of indexed log arguments. 103 | # (In solidity: The first topic is the hash of the signature of the event. 104 | # (e.g. Deposit(address,bytes32,uint256)), except you declared the event with the anonymous specifier.) 105 | 106 | WhisperPost* = object 107 | # The whisper post object: 108 | source*: array[60, byte] # (optional) the identity of the sender. 109 | to*: array[60, byte] # (optional) the identity of the receiver. When present whisper will encrypt the message so that only the receiver can decrypt it. 110 | topics*: seq[UInt256] # TODO: Correct type? list of DATA topics, for the receiver to identify messages. 111 | payload*: UInt256 # TODO: Correct type - maybe string? the payload of the message. 112 | priority*: int # integer of the priority in a rang from ... (?). 113 | ttl*: int # integer of the time to live in seconds. 114 | 115 | WhisperMessage* = object 116 | # (?) are from the RPC Wiki, indicating uncertainty in type format. 117 | hash*: UInt256 # (?) the hash of the message. 118 | source*: array[60, byte] # the sender of the message, if a sender was specified. 119 | to*: array[60, byte] # the receiver of the message, if a receiver was specified. 120 | expiry*: int # integer of the time in seconds when this message should expire (?). 121 | ttl*: int # integer of the time the message should float in the system in seconds (?). 122 | sent*: int # integer of the unix timestamp when the message was sent. 123 | topics*: seq[UInt256] # list of DATA topics the message contained. 124 | payload*: string # TODO: Correct type? the payload of the message. 125 | workProved*: int # integer of the work this message required before it was send (?). 126 | -------------------------------------------------------------------------------- /json_rpc/clients/socketclient.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | stew/[arrayops, byteutils, endians2], 14 | chronicles, 15 | ../[client, errors, router], 16 | ../private/jrpc_sys, 17 | httputils 18 | 19 | export client, errors 20 | 21 | when not declared(newSeqUninit): # nim 2.2+ 22 | template newSeqUninit[T: byte](len: int): seq[byte] = 23 | newSeqUninitialized[byte](len) 24 | 25 | type 26 | RpcSocketClient* = ref object of RpcConnection 27 | ## StreamTransport-based bidirectional connection with pluggable framing 28 | ## options for delineating messages. 29 | transport*: StreamTransport 30 | address*: TransportAddress 31 | loop*: Future[void] 32 | framing*: Framing 33 | 34 | Framing* = object 35 | recvMsg: proc(transport: StreamTransport, limit: int): Future[seq[byte]] {. 36 | async: (raises: [CancelledError, TransportError]), nimcall 37 | .} 38 | sendMsg: proc(transport: StreamTransport, sendMsg: seq[byte]) {. 39 | async: (raises: [CancelledError, TransportError]), nimcall 40 | .} 41 | 42 | proc recvMsgNewLine( 43 | transport: StreamTransport, maxMessageSize: int 44 | ): Future[seq[byte]] {.async: (raises: [CancelledError, TransportError]).} = 45 | let data = await transport.readLine(maxMessageSize, sep = "\r\n") 46 | toBytes(data) 47 | 48 | proc sendMsgNewLine( 49 | transport: StreamTransport, msg: seq[byte] 50 | ) {.async: (raises: [CancelledError, TransportError]).} = 51 | discard await transport.write(msg & toBytes("\r\n")) 52 | 53 | proc newLine*( 54 | T: type Framing 55 | ): T {.deprecated: "Prefer lengthHeaderBE32 or httpHeader in in new applications".} = 56 | ## A framing that suffixes messages with "\r\n". This framing is supported 57 | ## only for historical purposes and may be removed in a future version. 58 | ## 59 | ## The framing can only be used with payloads that do not contain newlines and 60 | ## message length is checked only after that many bytes have been transmitted. 61 | T(recvMsg: recvMsgNewLine, sendMsg: sendMsgNewLine) 62 | 63 | proc recvMsgHttpHeader( 64 | transport: StreamTransport, maxMessageSize: int 65 | ): Future[seq[byte]] {.async: (raises: [CancelledError, TransportError]).} = 66 | var buf {.noinit.}: array[1024, byte] 67 | let 68 | bytes = await transport.readUntil(addr buf[0], buf.len, toBytes("\r\n\r\n")) 69 | headers = parseHeaders(buf.toOpenArray(0, bytes - 1), true) 70 | 71 | let len = headers.contentLength() 72 | if len <= 0 or len > maxMessageSize: 73 | return 74 | 75 | result = newSeqUninit[byte](len) 76 | await transport.readExactly(addr result[0], result.len) 77 | 78 | proc sendMsgHttpHeader( 79 | transport: StreamTransport, msg: seq[byte] 80 | ) {.async: (raises: [CancelledError, TransportError]).} = 81 | discard await transport.write("Content-Length: " & $msg.len & "\r\n\r\n") 82 | discard await transport.write(msg) 83 | 84 | proc httpHeader*(T: type Framing): T = 85 | ## Framing using a HTTP-like `Content-Length: \r\n` header followed by 86 | ## an empty line ("\r\n") followed by a the message itself. 87 | ## 88 | ## This encoding is compatible with the default encoding used by StreamJsonRPC 89 | ## and https://www.npmjs.com/package/vscode-jsonrpc. 90 | ## 91 | ## For a higher-performance option, use `Framing.lengthHeaderBE32`. 92 | 93 | T(recvMsg: recvMsgHttpHeader, sendMsg: sendMsgHttpHeader) 94 | 95 | proc recvMsgLengthHeaderBE32( 96 | transport: StreamTransport, maxMessageSize: int 97 | ): Future[seq[byte]] {.async: (raises: [CancelledError, TransportError]).} = 98 | var 99 | pos: int 100 | lenBE32: array[4, byte] 101 | payload: seq[byte] 102 | error: ref TransportError 103 | 104 | proc predicate(data: openArray[byte]): tuple[consumed: int, done: bool] = 105 | if data.len == 0: 106 | return (0, true) 107 | 108 | var dataPos = 0 109 | 110 | if payload.len == 0: 111 | let n = lenBE32.toOpenArray(pos, lenBE32.high()).copyFrom(data) 112 | pos += n 113 | dataPos += n 114 | 115 | if pos < 4: 116 | return (dataPos, false) 117 | 118 | let messageSize = uint32.fromBytesBE(lenBE32) 119 | if uint64(messageSize) > uint64(maxMessageSize): 120 | error = 121 | (ref TransportLimitError)(msg: "Maximum length exceeded: " & $messageSize) 122 | return (dataPos, true) 123 | 124 | if messageSize == 0: 125 | return (dataPos, true) 126 | 127 | payload = newSeqUninit[byte](int(messageSize)) 128 | pos = 0 129 | 130 | let n = payload.toOpenArray(pos, payload.high()).copyFrom( 131 | data.toOpenArray(dataPos, data.high()) 132 | ) 133 | 134 | pos += n 135 | dataPos += n 136 | 137 | (dataPos, pos == payload.len()) 138 | 139 | await transport.readMessage(predicate) 140 | 141 | if error != nil: 142 | raise error 143 | 144 | payload 145 | 146 | proc sendMsgLengthHeaderBE32( 147 | transport: StreamTransport, msg: seq[byte] 148 | ) {.async: (raises: [CancelledError, TransportError]).} = 149 | var header = msg.len.uint32.toBytesBE() 150 | discard await transport.write(addr header[0], header.len) 151 | discard await transport.write(msg) 152 | 153 | proc lengthHeaderBE32*(T: type Framing): T = 154 | ## Framing using a HTTP-like `Content-Length` header followed by two newlines 155 | ## to delimit each message. 156 | T(recvMsg: recvMsgLengthHeaderBE32, sendMsg: sendMsgLengthHeaderBE32) 157 | 158 | proc new*( 159 | T: type RpcSocketClient, 160 | maxMessageSize = defaultMaxMessageSize, 161 | router = default(ref RpcRouter), 162 | framing = Framing.newLine(), 163 | ): T = 164 | let router = 165 | if router != nil: 166 | proc( 167 | request: RequestBatchRx 168 | ): Future[seq[byte]] {.async: (raises: [], raw: true).} = 169 | router[].route(request) 170 | else: 171 | nil 172 | 173 | T(maxMessageSize: maxMessageSize, router: router, framing: framing) 174 | 175 | proc newRpcSocketClient*( 176 | maxMessageSize = defaultMaxMessageSize, 177 | router = default(ref RpcRouter), 178 | framing = Framing.newLine(), 179 | ): RpcSocketClient = 180 | ## Creates a new client instance. 181 | RpcSocketClient.new(maxMessageSize, router, framing) 182 | 183 | method send*( 184 | client: RpcSocketClient, reqData: seq[byte] 185 | ) {.async: (raises: [CancelledError, JsonRpcError]).} = 186 | if client.transport.isNil: 187 | raise newException( 188 | RpcTransportError, "Transport is not initialised (missing a call to connect?)" 189 | ) 190 | try: 191 | await client.framing.sendMsg(client.transport, reqData) 192 | except CancelledError as exc: 193 | raise exc 194 | except TransportError as exc: 195 | raise (ref RpcPostError)(msg: exc.msg, parent: exc) 196 | 197 | method request( 198 | client: RpcSocketClient, reqData: seq[byte] 199 | ): Future[seq[byte]] {.async: (raises: [CancelledError, JsonRpcError]).} = 200 | ## Remotely calls the specified RPC method. 201 | let transport = client.transport 202 | if transport.isNil: 203 | raise newException( 204 | RpcTransportError, "Transport is not initialised (missing a call to connect?)" 205 | ) 206 | 207 | client.withPendingFut(fut): 208 | try: 209 | await client.framing.sendMsg(client.transport, reqData) 210 | except CatchableError as exc: 211 | # If there's an error sending, the "next messages" facility will be 212 | # broken since we don't know if the server observed the message or not 213 | transport.close() 214 | raise (ref RpcPostError)(msg: exc.msg, parent: exc) 215 | 216 | await fut 217 | 218 | proc processMessages*(client: RpcSocketClient) {.async: (raises: []).} = 219 | # Provide backwards compat with consumers that don't set a max message size 220 | # for example by constructing RpcWebSocketHandler without going through init 221 | let maxMessageSize = 222 | if client.maxMessageSize == 0: defaultMaxMessageSize else: client.maxMessageSize 223 | 224 | var lastError: ref JsonRpcError 225 | while not client.transport.atEof(): 226 | try: 227 | let data = await client.framing.recvMsg(client.transport, maxMessageSize) 228 | if data.len == 0: 229 | break 230 | 231 | let fallback = client.callOnProcessMessage(data).valueOr: 232 | lastError = (ref RequestDecodeError)(msg: error, payload: data) 233 | break 234 | 235 | if not fallback: 236 | continue 237 | 238 | let resp = await client.processMessage(data) 239 | 240 | if resp.len > 0: 241 | await client.framing.sendMsg(client.transport, resp) 242 | except CatchableError as exc: 243 | lastError = (ref RpcTransportError)(msg: exc.msg, parent: exc) 244 | break 245 | 246 | if lastError == nil: 247 | lastError = (ref RpcTransportError)(msg: "Connection closed") 248 | 249 | client.clearPending(lastError) 250 | 251 | await client.transport.closeWait() 252 | client.transport = nil 253 | if not client.onDisconnect.isNil: 254 | client.onDisconnect() 255 | 256 | proc connect*( 257 | client: RpcSocketClient, address: TransportAddress 258 | ) {.async: (raises: [CancelledError, JsonRpcError]).} = 259 | client.transport = 260 | try: 261 | await connect(address) 262 | except TransportError as exc: 263 | raise (ref RpcTransportError)(msg: exc.msg, parent: exc) 264 | 265 | client.address = address 266 | client.remote = $client.address 267 | client.loop = processMessages(client) 268 | 269 | proc connect*( 270 | client: RpcSocketClient, address: string, port: Port 271 | ) {.async: (raises: [CancelledError, JsonRpcError]).} = 272 | let addresses = 273 | try: 274 | resolveTAddress(address, port) 275 | except TransportError as exc: 276 | raise (ref RpcTransportError)(msg: exc.msg, parent: exc) 277 | 278 | await client.connect(addresses[0]) 279 | 280 | method close*(client: RpcSocketClient) {.async: (raises: [], raw: true).} = 281 | client.loop.cancelAndWait() 282 | -------------------------------------------------------------------------------- /tests/test_client_hook.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | std/importutils, 12 | unittest2, 13 | chronicles, 14 | websock/websock, 15 | ../json_rpc/rpcclient, 16 | ../json_rpc/rpcserver, 17 | ./private/helpers 18 | 19 | createRpcSigsFromNim(RpcClient): 20 | proc get_Banana(id: int): int 21 | 22 | proc installHandlers(s: RpcServer) = 23 | s.rpc("get_Banana") do(id: int) -> JsonString: 24 | if id == 99: 25 | return "123".JsonString 26 | elif id == 100: 27 | return "\"stop\"".JsonString 28 | else: 29 | return "\"error\"".JsonString 30 | 31 | type 32 | Shadow = ref object 33 | something: int 34 | 35 | proc setupClientHook(client: RpcClient): Shadow = 36 | var shadow = Shadow(something: 0) 37 | client.onProcessMessage = proc(client: RpcClient, line: string): 38 | Result[bool, string] {.gcsafe, raises: [].} = 39 | 40 | try: 41 | let m = JrpcConv.decode(line, JsonNode) 42 | if m["result"].kind == JString: 43 | if m["result"].str == "stop": 44 | shadow.something = 123 45 | return ok(false) 46 | else: 47 | shadow.something = 77 48 | return err("not stop") 49 | 50 | return ok(true) 51 | except CatchableError as exc: 52 | return err(exc.msg) 53 | shadow 54 | 55 | suite "test client features": 56 | setup: 57 | var server = newRpcWebSocketServer("127.0.0.1", Port(0)) 58 | server.installHandlers() 59 | var client = newRpcWebSocketClient() 60 | let shadow = client.setupClientHook() 61 | 62 | server.start() 63 | waitFor client.connect("ws://" & $server.localAddress()) 64 | teardown: 65 | server.stop() 66 | waitFor server.closeWait() 67 | waitFor client.close() 68 | 69 | test "hook success": 70 | let res = waitFor client.get_Banana(99) 71 | check res == 123 72 | check shadow.something == 0 73 | 74 | test "hook error": 75 | expect JsonRpcError: 76 | let res2 = waitFor client.get_Banana(123) 77 | check res2 == 0 78 | check shadow.something == 77 79 | 80 | # test "hook invalid": 81 | # expect InvalidResponse: 82 | # let res2 = waitFor client.get_Banana(100) 83 | # check res2 == 0 84 | # check shadow.something == 123 85 | 86 | type 87 | TestSocketServer = ref object of RpcSocketServer 88 | getData: proc(): string {.gcsafe, raises: [].} 89 | 90 | proc processClient(server: StreamServer, transport: StreamTransport) {.async: (raises: []), gcsafe.} = 91 | ## Process transport data to the RPC server 92 | try: 93 | var rpc = getUserData[TestSocketServer](server) 94 | while true: 95 | var 96 | value = await transport.readLine() 97 | if value == "": 98 | await transport.closeWait() 99 | break 100 | 101 | let res = rpc.getData() 102 | discard await transport.write(res & "\r\n") 103 | except TransportError as ex: 104 | error "Transport closed during processing client", msg=ex.msg 105 | except CatchableError as ex: 106 | error "Error occured during processing client", msg=ex.msg 107 | 108 | proc addStreamServer(server: TestSocketServer, address: TransportAddress) = 109 | privateAccess(RpcSocketServer) 110 | try: 111 | info "Starting JSON-RPC socket server", address = $address 112 | var transportServer = createStreamServer(address, processClient, {ReuseAddr}, udata = server) 113 | server.servers.add(transportServer) 114 | except CatchableError as exc: 115 | error "Failed to create server", address = $address, message = exc.msg 116 | 117 | if len(server.servers) == 0: 118 | # Server was not bound, critical error. 119 | raise newException(RpcBindError, "Unable to create server!") 120 | 121 | proc new(T: type TestSocketServer, getData: proc(): string {.gcsafe, raises: [].}): T = 122 | T( 123 | router: RpcRouter.init(), 124 | getData: getData, 125 | ) 126 | 127 | 128 | suite "test rpc socket client": 129 | let server = TestSocketServer.new(proc(): string {.gcsafe, raises: [].} = 130 | return """{"jsonrpc":"2.0","result":10}""" 131 | ) 132 | let serverAddress = initTAddress("127.0.0.1:0") 133 | server.addStreamServer(serverAddress) 134 | 135 | var client = newRpcSocketClient() 136 | server.start() 137 | waitFor client.connect(server.localAddress()[0]) 138 | 139 | test "missing id in server response": 140 | expect JsonRpcError: 141 | let res = waitFor client.get_Banana(11) 142 | discard res 143 | 144 | server.stop() 145 | waitFor server.closeWait() 146 | 147 | 148 | type 149 | TestHttpServer = ref object of RpcHttpServer 150 | getData: proc(): string {.gcsafe, raises: [].} 151 | 152 | proc processClientRpc(rpcServer: TestHttpServer): HttpProcessCallback2 = 153 | return proc (req: RequestFence): Future[HttpResponseRef] 154 | {.async: (raises: [CancelledError]).} = 155 | if not req.isOk(): 156 | return defaultResponse() 157 | 158 | let 159 | request = req.get() 160 | headers = HttpTable.init([("Content-Type", 161 | "application/json; charset=utf-8")]) 162 | try: 163 | let data = rpcServer.getData() 164 | let res = await request.respond(Http200, data, headers) 165 | trace "JSON-RPC result has been sent" 166 | return res 167 | except CancelledError as exc: 168 | raise exc 169 | except CatchableError as exc: 170 | debug "Internal error while processing JSON-RPC call" 171 | return defaultResponse(exc) 172 | 173 | proc addHttpServer( 174 | rpcServer: TestHttpServer, 175 | address: TransportAddress, 176 | socketFlags: set[ServerFlags] = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}, 177 | serverUri = Uri(), 178 | serverIdent = "", 179 | maxConnections: int = -1, 180 | bufferSize: int = 4096, 181 | backlogSize: int = 100, 182 | httpHeadersTimeout = 10.seconds, 183 | maxHeadersSize: int = 8192, 184 | maxRequestBodySize: int = 1_048_576) = 185 | let server = HttpServerRef.new( 186 | address, 187 | processClientRpc(rpcServer), 188 | {}, 189 | socketFlags, 190 | serverUri, "nim-json-rpc", maxConnections, backlogSize, 191 | bufferSize, httpHeadersTimeout, maxHeadersSize, maxRequestBodySize 192 | ).valueOr: 193 | error "Failed to create server", address = $address, 194 | message = error 195 | raise newException(RpcBindError, "Unable to create server: " & $error) 196 | info "Starting JSON-RPC HTTP server", url = "http://" & $address 197 | 198 | privateAccess(RpcHttpServer) 199 | rpcServer.httpServers.add server 200 | 201 | proc new(T: type TestHttpServer, getData: proc(): string {.gcsafe, raises: [].}): T = 202 | T( 203 | router: RpcRouter.init(), 204 | maxChunkSize: 8192, 205 | getData: getData, 206 | ) 207 | 208 | suite "test rpc http client": 209 | let server = TestHttpServer.new(proc(): string {.gcsafe, raises: [].} = 210 | return """{"jsonrpc":"2.0","result":10}""" 211 | ) 212 | let serverAddress = initTAddress("127.0.0.1:0") 213 | server.addHttpServer(serverAddress) 214 | 215 | var client = newRpcHttpClient() 216 | server.start() 217 | waitFor client.connect("http://" & $server.localAddress()[0]) 218 | 219 | test "missing id in server response": 220 | expect JsonRpcError: 221 | let res = waitFor client.get_Banana(11) 222 | discard res 223 | 224 | waitFor server.stop() 225 | waitFor server.closeWait() 226 | 227 | 228 | type 229 | TestWsServer = ref object of RpcWebSocketServer 230 | getData: proc(): string {.gcsafe, raises: [].} 231 | 232 | proc handleRequest(rpc: TestWsServer, request: websock.HttpRequest) 233 | {.async: (raises: [CancelledError]).} = 234 | try: 235 | let server = rpc.wsserver 236 | let ws = await server.handleRequest(request) 237 | if ws.readyState != ReadyState.Open: 238 | error "Failed to open websocket connection" 239 | return 240 | 241 | trace "Websocket handshake completed" 242 | while ws.readyState != ReadyState.Closed: 243 | let recvData = await ws.recvMsg() 244 | trace "Client message: ", size = recvData.len, binary = ws.binary 245 | 246 | if ws.readyState == ReadyState.Closed: 247 | # if session already terminated by peer, 248 | # no need to send response 249 | break 250 | 251 | if recvData.len == 0: 252 | await ws.close( 253 | reason = "cannot process zero length message" 254 | ) 255 | break 256 | 257 | let data = rpc.getData() 258 | 259 | trace "RPC result has been sent", address = $request.uri 260 | await ws.send(data) 261 | 262 | except WebSocketError as exc: 263 | error "WebSocket error:", exception = exc.msg 264 | 265 | except CancelledError as exc: 266 | raise exc 267 | 268 | except CatchableError as exc: 269 | error "Something error", msg=exc.msg 270 | 271 | proc newWsServer(address: TransportAddress, getData: proc(): string {.gcsafe, raises: [].}): TestWsServer = 272 | 273 | let flags = {ServerFlags.TcpNoDelay,ServerFlags.ReuseAddr} 274 | var server = new(TestWsServer) 275 | proc processCallback(request: websock.HttpRequest): Future[void] = 276 | handleRequest(server, request) 277 | 278 | privateAccess(RpcWebSocketServer) 279 | 280 | server.getData = getData 281 | server.wsserver = WSServer.new(rng = HmacDrbgContext.new()) 282 | server.server = websock.HttpServer.create( 283 | address, 284 | processCallback, 285 | flags 286 | ) 287 | 288 | server 289 | 290 | suite "test ws http client": 291 | let serverAddress = initTAddress("127.0.0.1:0") 292 | let server = newWsServer(serverAddress, proc(): string {.gcsafe, raises: [].} = 293 | return """{"jsonrpc":"2.0","result":10}""" 294 | ) 295 | 296 | var client = newRpcWebSocketClient() 297 | server.start() 298 | waitFor client.connect("ws://" & $server.localAddress()) 299 | 300 | test "missing id in server response": 301 | expect JsonRpcError: 302 | let res = waitFor client.get_Banana(11) 303 | discard res 304 | 305 | server.stop() 306 | waitFor server.closeWait() 307 | -------------------------------------------------------------------------------- /json_rpc/servers/httpserver.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | std/sequtils, 14 | chronicles, httputils, chronos, 15 | chronos/apps/http/[httpserver, shttpserver], 16 | json_serialization/std/net as jsnet, 17 | ../private/utils, 18 | ../errors, 19 | ../server 20 | 21 | export 22 | server, shttpserver, jsnet 23 | 24 | logScope: 25 | topics = "jsonrpc server http" 26 | 27 | const 28 | JsonRpcIdent = "nim-json-rpc" 29 | 30 | type 31 | # HttpAuthHook: handle CORS, JWT auth, etc. in HTTP header 32 | # before actual request processed 33 | # return value: 34 | # - nil: auth success, continue execution 35 | # - HttpResponse: could not authenticate, stop execution 36 | # and return the response 37 | HttpAuthHook* = 38 | proc(request: HttpRequestRef): Future[HttpResponseRef] {.async: (raises: [CatchableError]).} 39 | 40 | # This inheritance arrangement is useful for 41 | # e.g. combo HTTP server 42 | RpcHttpHandler* = ref object of RpcServer 43 | maxChunkSize*: int 44 | 45 | RpcHttpServer* = ref object of RpcHttpHandler 46 | httpServers: seq[HttpServerRef] 47 | authHooks: seq[HttpAuthHook] 48 | 49 | proc serveHTTP*(rpcServer: RpcHttpHandler, request: HttpRequestRef): 50 | Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = 51 | try: 52 | let req = await request.getBody() 53 | debug "Received JSON-RPC request", 54 | address = request.remote().valueOr(default(TransportAddress)), 55 | len = req.len 56 | 57 | let 58 | data = await rpcServer.route(req) 59 | chunkSize = rpcServer.maxChunkSize 60 | streamType = 61 | if data.len <= chunkSize: 62 | HttpResponseStreamType.Plain 63 | else: 64 | HttpResponseStreamType.Chunked 65 | response = request.getResponse() 66 | 67 | response.addHeader("Content-Type", "application/json") 68 | 69 | await response.prepare(streamType) 70 | let maxLen = data.len 71 | 72 | var len = data.len 73 | while len > chunkSize: 74 | await response.send(data[maxLen - len].unsafeAddr, chunkSize) 75 | len -= chunkSize 76 | 77 | if len > 0: 78 | await response.send(data[maxLen - len].unsafeAddr, len) 79 | 80 | await response.finish() 81 | response 82 | except CancelledError as exc: 83 | raise exc 84 | except CatchableError as exc: 85 | debug "Internal error while processing JSON-RPC call", msg=exc.msg 86 | defaultResponse(exc) 87 | 88 | proc processClientRpc(rpcServer: RpcHttpServer): HttpProcessCallback2 = 89 | return proc (req: RequestFence): Future[HttpResponseRef] 90 | {.async: (raises: [CancelledError]).} = 91 | if not req.isOk(): 92 | debug "Got invalid request", err = req.error() 93 | return defaultResponse() 94 | 95 | let request = req.get() 96 | # if hook result is not nil, 97 | # it means we should return immediately 98 | try: 99 | for hook in rpcServer.authHooks: 100 | let res = await hook(request) 101 | if not res.isNil: 102 | return res 103 | except CancelledError as exc: 104 | raise exc 105 | except CatchableError as exc: 106 | error "Internal error while processing JSON-RPC hook", msg=exc.msg 107 | return defaultResponse(exc) 108 | 109 | return await rpcServer.serveHTTP(request) 110 | 111 | proc addHttpServer*( 112 | rpcServer: RpcHttpServer, 113 | address: TransportAddress, 114 | socketFlags: set[ServerFlags] = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}, 115 | serverUri = Uri(), 116 | serverIdent = "", 117 | maxConnections: int = -1, 118 | bufferSize: int = 4096, 119 | backlogSize: int = 100, 120 | httpHeadersTimeout = 10.seconds, 121 | maxHeadersSize: int = 8192, 122 | maxRequestBodySize: int = defaultMaxMessageSize) {.raises: [JsonRpcError].} = 123 | let server = HttpServerRef.new( 124 | address, 125 | processClientRpc(rpcServer), 126 | {}, 127 | socketFlags, 128 | serverUri, JsonRpcIdent, maxConnections, backlogSize, 129 | bufferSize, httpHeadersTimeout, maxHeadersSize, maxRequestBodySize 130 | ).valueOr: 131 | error "Failed to create server", address = $address, 132 | message = error 133 | raise newException(RpcBindError, "Unable to create server: " & $error) 134 | 135 | rpcServer.httpServers.add server 136 | 137 | proc addSecureHttpServer*( 138 | rpcServer: RpcHttpServer, 139 | address: TransportAddress, 140 | tlsPrivateKey: TLSPrivateKey, 141 | tlsCertificate: TLSCertificate, 142 | socketFlags: set[ServerFlags] = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}, 143 | serverUri = Uri(), 144 | serverIdent: string = JsonRpcIdent, 145 | secureFlags: set[TLSFlags] = {}, 146 | maxConnections: int = -1, 147 | backlogSize: int = 100, 148 | bufferSize: int = 4096, 149 | httpHeadersTimeout = 10.seconds, 150 | maxHeadersSize: int = 8192, 151 | maxRequestBodySize: int = defaultMaxMessageSize) {.raises: [JsonRpcError].} = 152 | let server = SecureHttpServerRef.new( 153 | address, 154 | processClientRpc(rpcServer), 155 | tlsPrivateKey, 156 | tlsCertificate, 157 | {HttpServerFlags.Secure}, 158 | socketFlags, 159 | serverUri, JsonRpcIdent, secureFlags, maxConnections, backlogSize, 160 | bufferSize, httpHeadersTimeout, maxHeadersSize, maxRequestBodySize 161 | ).valueOr: 162 | error "Failed to create server", address = $address, 163 | message = error 164 | raise newException(RpcBindError, "Unable to create server: " & $error) 165 | 166 | rpcServer.httpServers.add server 167 | 168 | proc addHttpServers*(server: RpcHttpServer, 169 | addresses: openArray[TransportAddress]) {.raises: [JsonRpcError].} = 170 | ## Start a server on at least one of the given addresses, or raise 171 | if addresses.len == 0: 172 | return 173 | 174 | var lastExc: ref JsonRpcError 175 | for item in addresses: 176 | try: 177 | server.addHttpServer(item) 178 | except JsonRpcError as exc: 179 | lastExc = exc 180 | if server.httpServers.len == 0: 181 | raise lastExc 182 | 183 | proc addSecureHttpServers*(server: RpcHttpServer, 184 | addresses: openArray[TransportAddress], 185 | tlsPrivateKey: TLSPrivateKey, 186 | tlsCertificate: TLSCertificate) {.raises: [JsonRpcError].} = 187 | ## Start a server on at least one of the given addresses, or raise 188 | if addresses.len == 0: 189 | return 190 | 191 | var lastExc: ref JsonRpcError 192 | for item in addresses: 193 | try: 194 | server.addSecureHttpServer(item, tlsPrivateKey, tlsCertificate) 195 | except JsonRpcError as exc: 196 | lastExc = exc 197 | if server.httpServers.len == 0: 198 | raise lastExc 199 | 200 | proc addHttpServer*(server: RpcHttpServer, address: string) {.raises: [JsonRpcError].} = 201 | ## Create new server and assign it to addresses ``addresses``. 202 | addHttpServers(server, toSeq(resolveIP([address]))) 203 | 204 | proc addSecureHttpServer*(server: RpcHttpServer, 205 | address: string, 206 | tlsPrivateKey: TLSPrivateKey, 207 | tlsCertificate: TLSCertificate) {.raises: [JsonRpcError].} = 208 | addSecureHttpServers(server, toSeq(resolveIP([address])), tlsPrivateKey, tlsCertificate) 209 | 210 | proc addHttpServers*(server: RpcHttpServer, addresses: openArray[string]) {.raises: [JsonRpcError].} = 211 | addHttpServers(server, toSeq(resolveIP(addresses))) 212 | 213 | proc addHttpServer*(server: RpcHttpServer, address: string, port: Port) {.raises: [JsonRpcError].} = 214 | addHttpServers(server, toSeq(resolveIP(address, port))) 215 | 216 | proc addSecureHttpServer*(server: RpcHttpServer, 217 | address: string, 218 | port: Port, 219 | tlsPrivateKey: TLSPrivateKey, 220 | tlsCertificate: TLSCertificate) {.raises: [JsonRpcError].} = 221 | addSecureHttpServers(server, toSeq(resolveIP(address, port)), tlsPrivateKey, tlsCertificate) 222 | 223 | proc new*(T: type RpcHttpServer, authHooks: seq[HttpAuthHook] = @[]): T = 224 | T(router: RpcRouter.init(), httpServers: @[], authHooks: authHooks, maxChunkSize: 8192) 225 | 226 | proc new*(T: type RpcHttpServer, router: RpcRouter, authHooks: seq[HttpAuthHook] = @[]): T = 227 | T(router: router, httpServers: @[], authHooks: authHooks, maxChunkSize: 8192) 228 | 229 | proc newRpcHttpServer*(authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer = 230 | RpcHttpServer.new(authHooks) 231 | 232 | proc newRpcHttpServer*(router: RpcRouter, authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer = 233 | RpcHttpServer.new(router, authHooks) 234 | 235 | proc newRpcHttpServer*(addresses: openArray[TransportAddress], authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer {.raises: [JsonRpcError].} = 236 | ## Create new server and assign it to addresses ``addresses``. 237 | result = newRpcHttpServer(authHooks) 238 | result.addHttpServers(addresses) 239 | 240 | proc newRpcHttpServer*(addresses: openArray[string], authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer {.raises: [JsonRpcError].} = 241 | ## Create new server and assign it to addresses ``addresses``. 242 | result = newRpcHttpServer(authHooks) 243 | result.addHttpServers(addresses) 244 | 245 | proc newRpcHttpServer*(addresses: openArray[string], router: RpcRouter, authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer {.raises: [JsonRpcError].} = 246 | ## Create new server and assign it to addresses ``addresses``. 247 | result = newRpcHttpServer(router, authHooks) 248 | result.addHttpServers(addresses) 249 | 250 | proc newRpcHttpServer*(addresses: openArray[TransportAddress], router: RpcRouter, authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer {.raises: [JsonRpcError].} = 251 | ## Create new server and assign it to addresses ``addresses``. 252 | result = newRpcHttpServer(router, authHooks) 253 | result.addHttpServers(addresses) 254 | 255 | proc start*(server: RpcHttpServer) = 256 | ## Start the RPC server. 257 | for item in server.httpServers: 258 | info "Starting JSON-RPC HTTP server", url = item.baseUri 259 | item.start() 260 | 261 | proc stop*(server: RpcHttpServer) {.async: (raises: []).} = 262 | ## Stop the RPC server. 263 | for item in server.httpServers: 264 | await item.stop() 265 | info "Stopped JSON-RPC HTTP server", url = item.baseUri 266 | 267 | proc closeWait*(server: RpcHttpServer) {.async: (raises: []).} = 268 | ## Cleanup resources of RPC server. 269 | for item in server.httpServers: 270 | await item.closeWait() 271 | 272 | proc localAddress*(server: RpcHttpServer): seq[TransportAddress] = 273 | for item in server.httpServers: 274 | result.add item.instance.localAddress() 275 | 276 | proc setMaxChunkSize*(server: RpcHttpServer, maxChunkSize: int) = 277 | server.maxChunkSize = maxChunkSize 278 | -------------------------------------------------------------------------------- /json_rpc/private/server_handler_wrapper.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | std/[macros, typetraits], 14 | stew/[byteutils, objects], 15 | json_serialization, 16 | json_serialization/std/[options], 17 | json_serialization/pkg/results, 18 | ../errors, 19 | ./jrpc_sys, 20 | ./shared_wrapper, 21 | ../jsonmarshal 22 | 23 | export 24 | jsonmarshal 25 | 26 | type 27 | RpcSetup = object 28 | numFields: int 29 | numOptionals: int 30 | minLength: int 31 | 32 | # ------------------------------------------------------------------------------ 33 | # Optional resolvers 34 | # ------------------------------------------------------------------------------ 35 | 36 | template rpc_isOptional(_: auto): bool = false 37 | template rpc_isOptional[T](_: results.Opt[T]): bool = true 38 | template rpc_isOptional[T](_: options.Option[T]): bool = true 39 | 40 | # ------------------------------------------------------------------------------ 41 | # Run time helpers 42 | # ------------------------------------------------------------------------------ 43 | 44 | func unpackArg(args: JsonString, argName: string, argType: type): argType 45 | {.gcsafe, raises: [JsonRpcError].} = 46 | ## This where input parameters are decoded from JSON into 47 | ## Nim data types 48 | try: 49 | result = JrpcConv.decode(args.string, argType) 50 | except CatchableError as err: 51 | raise newException(RequestDecodeError, 52 | "Parameter [" & argName & "] of type '" & 53 | $argType & "' could not be decoded: " & err.msg) 54 | 55 | # ------------------------------------------------------------------------------ 56 | # Compile time helpers 57 | # ------------------------------------------------------------------------------ 58 | func hasOptionals(setup: RpcSetup): bool {.compileTime.} = 59 | setup.numOptionals > 0 60 | 61 | func rpcSetupImpl[T](val: T): RpcSetup {.compileTime.} = 62 | ## Counting number of fields, optional fields, and 63 | ## minimum fields needed by a rpc method 64 | mixin rpc_isOptional 65 | var index = 1 66 | for field in fields(val): 67 | inc result.numFields 68 | if rpc_isOptional(field): 69 | inc result.numOptionals 70 | else: 71 | result.minLength = index 72 | inc index 73 | 74 | func rpcSetupFromType(T: type): RpcSetup {.compileTime.} = 75 | var dummy: T 76 | rpcSetupImpl(dummy) 77 | 78 | template expectOptionalParamsLen(params: RequestParamsRx, 79 | minLength, maxLength: static[int]) = 80 | ## Make sure positional params with optional fields 81 | ## meets the handler expectation 82 | let 83 | expected = "Expected at least " & $minLength & " and maximum " & 84 | $maxLength & " Json parameter(s) but got " 85 | 86 | if params.positional.len < minLength: 87 | raise newException(RequestDecodeError, 88 | expected & $params.positional.len) 89 | 90 | template expectParamsLen(params: RequestParamsRx, length: static[int]) = 91 | ## Make sure positional params meets the handler expectation 92 | 93 | # https://github.com/nim-lang/Nim/issues/24228 especially in a Chronos async 94 | # context, with `{.compileTime.}` `$` `int`, `uint64`, and `int64` overloads 95 | # https://github.com/nim-lang/Nim/blob/v2.0.10/lib/system/dollars.nim#L28-L34 96 | # provides for for compile-time evaluation from, can cause JSON-RPC code not 97 | # to compile. Explicitly choose the non-CTFE overloads, which do not trigger 98 | # this Nim issue. 99 | let 100 | nonConstLength = length 101 | expected = "Expected " & $nonConstLength & " JSON parameter(s) but got " 102 | 103 | if params.positional.len != length: 104 | raise newException(RequestDecodeError, 105 | expected & $params.positional.len) 106 | 107 | template setupPositional(setup: static[RpcSetup], params: RequestParamsRx) = 108 | ## Generate code to check positional params length 109 | when setup.hasOptionals: 110 | expectOptionalParamsLen(params, setup.minLength, setup.numFields) 111 | else: 112 | expectParamsLen(params, setup.numFields) 113 | 114 | template len(params: RequestParamsRx): int = 115 | params.positional.len 116 | 117 | template notNull(params: RequestParamsRx, pos: int): bool = 118 | params.positional[pos].kind != JsonValueKind.Null 119 | 120 | template val(params: RequestParamsRx, pos: int): auto = 121 | params.positional[pos].param 122 | 123 | template unpackPositional(params: RequestParamsRx, 124 | paramVar: auto, 125 | paramName: static[string], 126 | pos: static[int], 127 | setup: static[RpcSetup], 128 | paramType: type) = 129 | ## Convert a positional parameter from Json into Nim 130 | 131 | when not defined(nimHasTemplateRedefinitionPragma): 132 | {.pragma: redefine.} 133 | 134 | template innerNode() {.redefine.} = 135 | paramVar = unpackArg(params.val(pos), paramName, paramType) 136 | 137 | # e.g. (A: int, B: Option[int], C: string, D: Option[int], E: Option[string]) 138 | when rpc_isOptional(paramVar): 139 | when pos >= setup.minLength: 140 | # allow both empty and null after mandatory args 141 | # D & E fall into this category 142 | if params.len > pos and params.notNull(pos): 143 | innerNode() 144 | else: 145 | # allow null param for optional args between/before mandatory args 146 | # B fall into this category 147 | if params.notNull(pos): 148 | innerNode() 149 | else: 150 | # mandatory args 151 | # A and C fall into this category 152 | # unpack Nim type and assign from JSON 153 | if params.notNull(pos): 154 | innerNode() 155 | 156 | func makeType(typeName, params: NimNode): NimNode = 157 | ## Generate type section contains an object definition 158 | ## with fields of handler params 159 | let typeSec = quote do: 160 | type `typeName` = object 161 | 162 | let obj = typeSec[0][2] 163 | let recList = newNimNode(nnkRecList) 164 | if params.len > 1: 165 | for i in 1.. 1: 174 | for i in 1.. 1 # not including return type 267 | rpcSetup = ident"rpcSetup" 268 | handler = makeHandler(handlerName, params, procBody, returnType) 269 | named = setupNamed(paramsObj, paramsIdent, params) 270 | 271 | if hasParams: 272 | setup.add makeType(typeName, params) 273 | setup.add quote do: 274 | const `rpcSetup` = rpcSetupFromType(`typeName`) 275 | var `paramsObj`: `typeName` 276 | 277 | # unpack each parameter and provide assignments 278 | var 279 | pos = 0 280 | positional = newStmtList() 281 | executeParams: seq[NimNode] 282 | 283 | for paramIdent, paramType in paramsIter(params): 284 | let paramName = $paramIdent 285 | positional.add quote do: 286 | unpackPositional(`paramsIdent`, 287 | `paramsObj`.`paramIdent`, 288 | `paramName`, 289 | `pos`, 290 | `rpcSetup`, 291 | `paramType`) 292 | 293 | executeParams.add quote do: 294 | `paramsObj`.`paramIdent` 295 | inc pos 296 | 297 | if hasParams: 298 | setup.add quote do: 299 | if `paramsIdent`.kind == rpPositional: 300 | setupPositional(`rpcSetup`, `paramsIdent`) 301 | `positional` 302 | else: 303 | `named` 304 | else: 305 | # even though there is no parameters expected 306 | # but the numbers of received params should 307 | # still be checked (RPC spec) 308 | setup.add quote do: 309 | if `paramsIdent`.kind == rpPositional: 310 | expectParamsLen(`paramsIdent`, 0) 311 | 312 | let 313 | executeCall = newCall(handlerName, executeParams) 314 | 315 | result = newStmtList() 316 | result.add handler 317 | result.add quote do: 318 | proc `procWrapper`(`paramsIdent`: RequestParamsRx): Future[JsonString] {.async.} = 319 | # Avoid 'yield in expr not lowered' with an intermediate variable. 320 | # See: https://github.com/nim-lang/Nim/issues/17849 321 | `setup` 322 | let resFut = `executeCall` 323 | maybeWrapServerResult(resFut) 324 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /tests/testrpcmacro.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2023 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | import 11 | unittest2, 12 | chronicles, 13 | ../json_rpc/rpcserver, 14 | ./private/helpers, 15 | json_serialization/std/options 16 | 17 | type 18 | # some nested types to check object parsing 19 | Test2 = object 20 | x: array[0..2, int] 21 | y: string 22 | 23 | Test = object 24 | a: array[0..1, int] 25 | b: Test2 26 | 27 | MyObject = object 28 | a: int 29 | b: Test 30 | c: float 31 | 32 | MyOptional = object 33 | maybeInt: Option[int] 34 | 35 | MyOptionalNotBuiltin = object 36 | val: Option[Test2] 37 | 38 | MyEnum = enum 39 | Enum0 40 | Enum1 41 | 42 | MuscleCar = object 43 | color: string 44 | wheel: int 45 | 46 | MyObject.useDefaultSerializationIn JrpcConv 47 | Test.useDefaultSerializationIn JrpcConv 48 | Test2.useDefaultSerializationIn JrpcConv 49 | MyOptional.useDefaultSerializationIn JrpcConv 50 | MyOptionalNotBuiltin.useDefaultSerializationIn JrpcConv 51 | MuscleCar.useDefaultSerializationIn JrpcConv 52 | 53 | proc readValue*(r: var JsonReader[JrpcConv], val: var MyEnum) 54 | {.gcsafe, raises: [IOError, SerializationError].} = 55 | let intVal = r.parseInt(int) 56 | if intVal < low(MyEnum).int or intVal > high(MyEnum).int: 57 | r.raiseUnexpectedValue("invalid enum range " & $intVal) 58 | val = MyEnum(intVal) 59 | 60 | let 61 | testObj = %*{ 62 | "a": %1, 63 | "b": %*{ 64 | "a": %[5, 0], 65 | "b": %*{ 66 | "x": %[1, 2, 3], 67 | "y": %"test" 68 | } 69 | }, 70 | "c": %1.0} 71 | 72 | var s = newRpcSocketServer(["127.0.0.1:0"]) 73 | 74 | # RPC definitions 75 | s.rpc("rpc.simplePath"): 76 | return %1 77 | 78 | s.rpc("rpc.enumParam") do(e: MyEnum): 79 | return %[$e] 80 | 81 | s.rpc("rpc.differentParams") do(a: int, b: string): 82 | return %[%a, %b] 83 | 84 | s.rpc("rpc.arrayParam") do(arr: array[0..5, byte], b: string): 85 | var res = %arr 86 | res.add %b 87 | return %res 88 | 89 | s.rpc("rpc.seqParam") do(a: string, s: seq[int]): 90 | var res = newJArray() 91 | res.add %a 92 | for item in s: 93 | res.add %int(item) 94 | return res 95 | 96 | s.rpc("rpc.objParam") do(a: string, obj: MyObject): 97 | return %obj 98 | 99 | s.rpc("rpc.returnTypeSimple") do(i: int) -> int: 100 | return i 101 | 102 | s.rpc("rpc.returnTypeComplex") do(i: int) -> Test2: 103 | return Test2(x: [1, i, 3], y: "test") 104 | 105 | s.rpc("rpc.testReturns") do() -> int: 106 | return 1234 107 | 108 | s.rpc("rpc.multiVarsOfOneType") do(a, b: string) -> string: 109 | return a & " " & b 110 | 111 | s.rpc("rpc.optional") do(obj: MyOptional) -> MyOptional: 112 | return obj 113 | 114 | s.rpc("rpc.optionalArg") do(val: int, obj: Option[MyOptional]) -> MyOptional: 115 | return if obj.isSome(): 116 | obj.get() 117 | else: 118 | MyOptional(maybeInt: some(val)) 119 | 120 | s.rpc("rpc.optionalArg2") do(a, b: string, c, d: Option[string]) -> string: 121 | var ret = a & b 122 | if c.isSome: ret.add c.get() 123 | if d.isSome: ret.add d.get() 124 | return ret 125 | 126 | s.rpc("echo") do(car: MuscleCar) -> JsonString: 127 | return JrpcConv.encode(car).JsonString 128 | 129 | type 130 | OptionalFields = object 131 | a: int 132 | b: Option[int] 133 | c: string 134 | d: Option[int] 135 | e: Option[string] 136 | 137 | OptionalFields.useDefaultSerializationIn JrpcConv 138 | 139 | s.rpc("rpc.mixedOptionalArg") do(a: int, b: Option[int], c: string, 140 | d: Option[int], e: Option[string]) -> OptionalFields: 141 | 142 | result.a = a 143 | result.b = b 144 | result.c = c 145 | result.d = d 146 | result.e = e 147 | 148 | s.rpc("rpc.optionalArgNotBuiltin") do(obj: Option[MyOptionalNotBuiltin]) -> string: 149 | return if obj.isSome: 150 | let val = obj.get.val 151 | if val.isSome: 152 | obj.get.val.get.y 153 | else: 154 | "Empty2" 155 | else: 156 | "Empty1" 157 | 158 | type 159 | MaybeOptions = object 160 | o1: Option[bool] 161 | o2: Option[bool] 162 | o3: Option[bool] 163 | 164 | MaybeOptions.useDefaultSerializationIn JrpcConv 165 | 166 | s.rpc("rpc.optInObj") do(data: string, options: Option[MaybeOptions]) -> int: 167 | if options.isSome: 168 | let o = options.get 169 | if o.o1.isSome: result += 1 170 | if o.o2.isSome: result += 2 171 | if o.o3.isSome: result += 4 172 | 173 | proc installMoreApiHandlers*(s: RpcServer, prefix: static string) = 174 | s.rpc(prefix & ".optionalStringArg") do(a: Option[string]) -> string: 175 | if a.isSome: 176 | return a.get() 177 | else: 178 | return "nope" 179 | 180 | s.installMoreApiHandlers("rpc") 181 | 182 | # Tests 183 | suite "Server types": 184 | test "On macro registration": 185 | check s.hasMethod("rpc.simplePath") 186 | check s.hasMethod("rpc.differentParams") 187 | check s.hasMethod("rpc.arrayParam") 188 | check s.hasMethod("rpc.seqParam") 189 | check s.hasMethod("rpc.objParam") 190 | check s.hasMethod("rpc.returnTypeSimple") 191 | check s.hasMethod("rpc.returnTypeComplex") 192 | check s.hasMethod("rpc.testReturns") 193 | check s.hasMethod("rpc.multiVarsOfOneType") 194 | check s.hasMethod("rpc.optionalArg") 195 | check s.hasMethod("rpc.mixedOptionalArg") 196 | check s.hasMethod("rpc.optionalArgNotBuiltin") 197 | check s.hasMethod("rpc.optInObj") 198 | check s.hasMethod("rpc.optionalStringArg") 199 | 200 | test "Simple paths": 201 | let r = waitFor s.executeMethod("rpc.simplePath", %[]) 202 | check r == "1" 203 | 204 | test "Enum param paths": 205 | block: 206 | let r = waitFor s.executeMethod("rpc.enumParam", %[%int64(Enum1)]) 207 | check r == "[\"Enum1\"]" 208 | 209 | expect(JsonRpcError): 210 | discard waitFor s.executeMethod("rpc.enumParam", %[(int64(42))]) 211 | 212 | test "Different param types": 213 | let 214 | inp = %[%1, %"abc"] 215 | r = waitFor s.executeMethod("rpc.differentParams", inp) 216 | check r == inp 217 | 218 | test "Array parameters": 219 | let r1 = waitFor s.executeMethod("rpc.arrayParam", %[%[1, 2, 3], %"hello"]) 220 | var ckR1 = %[1, 2, 3, 0, 0, 0] 221 | ckR1.elems.add %"hello" 222 | check r1 == ckR1 223 | 224 | test "Seq parameters": 225 | let r2 = waitFor s.executeMethod("rpc.seqParam", %[%"abc", %[1, 2, 3, 4, 5]]) 226 | var ckR2 = %["abc"] 227 | for i in 0..4: ckR2.add %(i + 1) 228 | check r2 == ckR2 229 | 230 | test "Object parameters": 231 | let r = waitFor s.executeMethod("rpc.objParam", %[%"abc", testObj]) 232 | check r == testObj 233 | 234 | test "Simple return types": 235 | let 236 | inp = %99 237 | r1 = waitFor s.executeMethod("rpc.returnTypeSimple", %[%inp]) 238 | check r1 == inp 239 | 240 | test "Complex return types": 241 | let 242 | inp = 99 243 | r1 = waitFor s.executeMethod("rpc.returnTypeComplex", %[%inp]) 244 | check r1 == %*{"x": %[1, inp, 3], "y": "test"} 245 | 246 | test "Option types": 247 | let 248 | inp1 = MyOptional(maybeInt: some(75)) 249 | inp2 = MyOptional() 250 | r1 = waitFor s.executeMethod("rpc.optional", %[%inp1]) 251 | r2 = waitFor s.executeMethod("rpc.optional", %[%inp2]) 252 | check r1.string == JrpcConv.encode inp1 253 | check r2.string == JrpcConv.encode inp2 254 | 255 | test "Return statement": 256 | let r = waitFor s.executeMethod("rpc.testReturns", %[]) 257 | check r == JrpcConv.encode 1234 258 | 259 | test "Runtime errors": 260 | expect JsonRpcError: 261 | # root param not array 262 | discard waitFor s.executeMethod("rpc.arrayParam", %"test") 263 | expect JsonRpcError: 264 | # too big for array 265 | discard waitFor s.executeMethod("rpc.arrayParam", %[%[0, 1, 2, 3, 4, 5, 6], %"hello"]) 266 | expect JsonRpcError: 267 | # wrong sub parameter type 268 | discard waitFor s.executeMethod("rpc.arrayParam", %[%"test", %"hello"]) 269 | expect JsonRpcError: 270 | # wrong param type 271 | discard waitFor s.executeMethod("rpc.differentParams", %[%"abc", %1]) 272 | 273 | test "Multiple variables of one type": 274 | let r = waitFor s.executeMethod("rpc.multiVarsOfOneType", %[%"hello", %"world"]) 275 | check r == JrpcConv.encode "hello world" 276 | 277 | test "Optional arg": 278 | let 279 | int1 = MyOptional(maybeInt: some(75)) 280 | int2 = MyOptional(maybeInt: some(117)) 281 | r1 = waitFor s.executeMethod("rpc.optionalArg", %[%117, %int1]) 282 | r2 = waitFor s.executeMethod("rpc.optionalArg", %[%117]) 283 | r3 = waitFor s.executeMethod("rpc.optionalArg", %[%117, newJNull()]) 284 | check r1 == JrpcConv.encode int1 285 | check r2 == JrpcConv.encode int2 286 | check r3 == JrpcConv.encode int2 287 | 288 | test "Optional arg2": 289 | let r1 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B"]) 290 | check r1 == JrpcConv.encode "AB" 291 | 292 | let r2 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull()]) 293 | check r2 == JrpcConv.encode "AB" 294 | 295 | let r3 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull(), newJNull()]) 296 | check r3 == JrpcConv.encode "AB" 297 | 298 | let r4 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull(), %"D"]) 299 | check r4 == JrpcConv.encode "ABD" 300 | 301 | let r5 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C", %"D"]) 302 | check r5 == JrpcConv.encode "ABCD" 303 | 304 | let r6 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C", newJNull()]) 305 | check r6 == JrpcConv.encode "ABC" 306 | 307 | let r7 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C"]) 308 | check r7 == JrpcConv.encode "ABC" 309 | 310 | test "Mixed optional arg": 311 | var ax = waitFor s.executeMethod("rpc.mixedOptionalArg", %[%10, %11, %"hello", %12, %"world"]) 312 | check ax == JrpcConv.encode OptionalFields(a: 10, b: some(11), c: "hello", d: some(12), e: some("world")) 313 | var bx = waitFor s.executeMethod("rpc.mixedOptionalArg", %[%10, newJNull(), %"hello"]) 314 | check bx == JrpcConv.encode OptionalFields(a: 10, c: "hello") 315 | 316 | test "Non-built-in optional types": 317 | let 318 | t2 = Test2(x: [1, 2, 3], y: "Hello") 319 | testOpts1 = MyOptionalNotBuiltin(val: some(t2)) 320 | testOpts2 = MyOptionalNotBuiltin() 321 | var r = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[%testOpts1]) 322 | check r == JrpcConv.encode t2.y 323 | var r2 = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[]) 324 | check r2 == JrpcConv.encode "Empty1" 325 | var r3 = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[%testOpts2]) 326 | check r3 == JrpcConv.encode "Empty2" 327 | 328 | test "Manually set up JSON for optionals": 329 | # Check manual set up json with optionals 330 | let opts1 = parseJson("""{"o1": true}""") 331 | var r1 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts1]) 332 | check r1 == JrpcConv.encode 1 333 | let opts2 = parseJson("""{"o2": true}""") 334 | var r2 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts2]) 335 | check r2 == JrpcConv.encode 2 336 | let opts3 = parseJson("""{"o3": true}""") 337 | var r3 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts3]) 338 | check r3 == JrpcConv.encode 4 339 | # Combinations 340 | let opts4 = parseJson("""{"o1": true, "o3": true}""") 341 | var r4 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts4]) 342 | check r4 == JrpcConv.encode 5 343 | let opts5 = parseJson("""{"o2": true, "o3": true}""") 344 | var r5 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts5]) 345 | check r5 == JrpcConv.encode 6 346 | let opts6 = parseJson("""{"o1": true, "o2": true}""") 347 | var r6 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts6]) 348 | check r6 == JrpcConv.encode 3 349 | 350 | test "Optional String Arg": 351 | let 352 | data = some("some string") 353 | r1 = waitFor s.executeMethod("rpc.optionalStringArg", %[%data]) 354 | r2 = waitFor s.executeMethod("rpc.optionalStringArg", %[]) 355 | r3 = waitFor s.executeMethod("rpc.optionalStringArg", %[newJNull()]) 356 | check r1 == %data.get() 357 | check r2 == %"nope" 358 | check r3 == %"nope" 359 | 360 | test "Null object fields": 361 | let r = waitFor s.executeMethod("echo", """{"car":{"color":"red","wheel":null}}""".JsonString) 362 | check r == """{"color":"red","wheel":0}""" 363 | 364 | let x = waitFor s.executeMethod("echo", """{"car":{"color":null,"wheel":77}}""".JsonString) 365 | check x == """{"color":"","wheel":77}""" 366 | 367 | s.stop() 368 | waitFor s.closeWait() 369 | -------------------------------------------------------------------------------- /json_rpc/private/jrpc_sys.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2023-2025 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import 13 | std/hashes, 14 | results, 15 | json_serialization, 16 | json_serialization/pkg/results as jsresults 17 | 18 | export results, json_serialization, jsresults 19 | 20 | # This module implements JSON-RPC 2.0 Specification 21 | # https://www.jsonrpc.org/specification 22 | 23 | type 24 | # Special object of Json-RPC 2.0 25 | JsonRPC2* = object 26 | 27 | RequestParamKind* = enum 28 | rpPositional 29 | rpNamed 30 | 31 | ParamDescRx* = object 32 | kind* : JsonValueKind 33 | param*: JsonString 34 | 35 | ParamDescNamed* = object 36 | name*: string 37 | value*: JsonString 38 | 39 | # Request params received by server 40 | RequestParamsRx* = object 41 | case kind*: RequestParamKind 42 | of rpPositional: 43 | positional*: seq[ParamDescRx] 44 | of rpNamed: 45 | named*: seq[ParamDescNamed] 46 | 47 | # Request params sent by client 48 | RequestParamsTx* = object 49 | case kind*: RequestParamKind 50 | of rpPositional: 51 | positional*: seq[JsonString] 52 | of rpNamed: 53 | named*: seq[ParamDescNamed] 54 | 55 | RequestIdKind* = enum 56 | riNull 57 | riNumber 58 | riString 59 | 60 | RequestId* = object 61 | case kind*: RequestIdKind 62 | of riNull: 63 | discard 64 | of riNumber: 65 | num*: int 66 | of riString: 67 | str*: string 68 | 69 | ReqRespHeader* = object 70 | ## Helper type to extract id from message (for example for logging) 71 | id* : results.Opt[RequestId] 72 | 73 | # Request received by server 74 | # TODO used in nim-web3 - remove eventually 75 | RequestRx* {.deprecated: "ResultsRx2".} = object 76 | jsonrpc* : results.Opt[JsonRPC2] 77 | `method`*: results.Opt[string] 78 | params* : RequestParamsRx 79 | id* : RequestId 80 | 81 | RequestRx2* = object 82 | jsonrpc* : JsonRPC2 83 | `method`*: string 84 | params* : RequestParamsRx 85 | id* : results.Opt[RequestId] 86 | 87 | # Request sent by client 88 | RequestTx* = object 89 | jsonrpc* : JsonRPC2 90 | `method`*: string 91 | params* : RequestParamsTx 92 | id* : results.Opt[RequestId] 93 | 94 | ResponseError* = object 95 | code* : int 96 | message*: string 97 | data* : results.Opt[JsonString] 98 | 99 | ResponseKind* = enum 100 | rkError 101 | rkResult 102 | 103 | # Response sent by server 104 | ResponseTx* = object 105 | jsonrpc* : JsonRPC2 106 | case kind*{.dontSerialize.}: ResponseKind 107 | of rkResult: 108 | result* : JsonString 109 | of rkError: 110 | error* : ResponseError 111 | id* : RequestId 112 | 113 | # Response received by client 114 | # TODO used in nim-web3 tests 115 | ResponseRx* {.deprecated.} = object 116 | jsonrpc*: results.Opt[JsonRPC2] 117 | id* : results.Opt[RequestId] 118 | result* : JsonString 119 | error* : results.Opt[ResponseError] 120 | 121 | ResponseRx2* = object 122 | jsonrpc*: JsonRPC2 123 | case kind*: ResponseKind 124 | of rkResult: 125 | result* : JsonString 126 | of rkError: 127 | error* : ResponseError 128 | id* : RequestId 129 | 130 | ReBatchKind* = enum 131 | rbkSingle 132 | rbkMany 133 | 134 | RequestBatchRx* = object 135 | case kind*: ReBatchKind 136 | of rbkMany: 137 | many* : seq[RequestRx2] 138 | of rbkSingle: 139 | single*: RequestRx2 140 | 141 | ResponseBatchRx* = object 142 | case kind*: ReBatchKind 143 | of rbkMany: 144 | many* : seq[ResponseRx2] 145 | of rbkSingle: 146 | single*: ResponseRx2 147 | 148 | # don't mix the json-rpc system encoding with the 149 | # actual response/params encoding 150 | createJsonFlavor JrpcSys, 151 | automaticObjectSerialization = false, 152 | requireAllFields = true, 153 | omitOptionalFields = true, # Skip optional fields==none in Writer 154 | allowUnknownFields = true, 155 | skipNullFields = false # Skip optional fields==null in Reader 156 | 157 | ReqRespHeader.useDefaultReaderIn JrpcSys 158 | RequestRx.useDefaultReaderIn JrpcSys 159 | RequestRx2.useDefaultReaderIn JrpcSys 160 | 161 | ParamDescNamed.useDefaultWriterIn JrpcSys 162 | RequestTx.useDefaultWriterIn JrpcSys 163 | ResponseTx.useDefaultWriterIn JrpcSys 164 | 165 | ResponseError.useDefaultSerializationIn JrpcSys 166 | 167 | const 168 | JsonRPC2Literal = JsonString("\"2.0\"") 169 | MaxIdStringLength = 256 170 | ## Maximum length of id, when represented as a string (to avoid spam) 171 | 172 | func hash*(x: RequestId): hashes.Hash = 173 | var h = 0.Hash 174 | case x.kind: 175 | of riNumber: h = h !& hash(x.num) 176 | of riString: h = h !& hash(x.str) 177 | of riNull: h = h !& hash("null") 178 | result = !$(h) 179 | 180 | func `$`*(x: RequestId): string = 181 | case x.kind: 182 | of riNumber: $x.num 183 | of riString: x.str 184 | of riNull: "null" 185 | 186 | func `==`*(a, b: RequestId): bool = 187 | if a.kind != b.kind: 188 | return false 189 | case a.kind 190 | of riNumber: a.num == b.num 191 | of riString: a.str == b.str 192 | of riNull: true 193 | 194 | func meth*(rx: RequestRx | RequestRx2): string = 195 | rx.`method` 196 | 197 | template shouldWriteObjectField*(field: RequestParamsTx): bool = 198 | case field.kind 199 | of rpPositional: 200 | field.positional.len > 0 201 | of rpNamed: 202 | field.named.len > 0 203 | 204 | func isFieldExpected*(_: type RequestParamsRx): bool {.compileTime.} = 205 | # A Structured value that holds the parameter values to be used during the 206 | # invocation of the method. This member MAY be omitted. 207 | 208 | false 209 | 210 | proc readValue*(r: var JsonReader[JrpcSys], val: var JsonRPC2) 211 | {.gcsafe, raises: [IOError, JsonReaderError].} = 212 | let version = r.parseAsString() 213 | if version != JsonRPC2Literal: 214 | r.raiseUnexpectedValue("Invalid JSON-RPC version, want=" & 215 | JsonRPC2Literal.string & " got=" & version.string) 216 | 217 | proc readValue*( 218 | r: var JsonReader, value: var results.Opt[RequestId] 219 | ) {.raises: [IOError, SerializationError].} = 220 | # Unlike the default reader in `results`, pass `null` to RequestId reader that 221 | # will handle it 222 | mixin readValue 223 | 224 | value.ok r.readValue(RequestId) 225 | 226 | proc writeValue*(w: var JsonWriter[JrpcSys], val: JsonRPC2) 227 | {.gcsafe, raises: [IOError].} = 228 | w.writeValue JsonRPC2Literal 229 | 230 | proc readValue*(r: var JsonReader[JrpcSys], val: var RequestId) 231 | {.gcsafe, raises: [IOError, JsonReaderError].} = 232 | let tok = r.tokKind 233 | case tok 234 | of JsonValueKind.Number: 235 | val = RequestId(kind: riNumber, num: r.parseInt(int)) 236 | of JsonValueKind.String: 237 | val = RequestId(kind: riString, str: r.parseString(MaxIdStringLength)) 238 | of JsonValueKind.Null: 239 | val = RequestId(kind: riNull) 240 | r.parseNull() 241 | else: 242 | r.raiseUnexpectedValue("Invalid RequestId, must be Number, String, or Null, got=" & $tok) 243 | 244 | proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestId) 245 | {.gcsafe, raises: [IOError].} = 246 | case val.kind 247 | of riNumber: w.writeValue val.num 248 | of riString: w.writeValue val.str 249 | of riNull: w.writeValue JsonString("null") 250 | 251 | proc readValue*(r: var JsonReader[JrpcSys], val: var RequestParamsRx) 252 | {.gcsafe, raises: [IOError, SerializationError].} = 253 | let tok = r.tokKind 254 | case tok 255 | of JsonValueKind.Array: 256 | val = RequestParamsRx(kind: rpPositional) 257 | r.parseArray: 258 | val.positional.add ParamDescRx( 259 | kind: r.tokKind(), 260 | param: r.parseAsString(), 261 | ) 262 | of JsonValueKind.Object: 263 | val = RequestParamsRx(kind: rpNamed) 264 | for key in r.readObjectFields(): 265 | val.named.add ParamDescNamed( 266 | name: key, 267 | value: r.parseAsString(), 268 | ) 269 | else: 270 | r.raiseUnexpectedValue("RequestParam must be either array or object, got=" & $tok) 271 | 272 | proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestParamsTx) 273 | {.gcsafe, raises: [IOError].} = 274 | case val.kind 275 | of rpPositional: 276 | w.writeArray val.positional 277 | of rpNamed: 278 | w.beginRecord RequestParamsTx 279 | for x in val.named: 280 | w.writeField(x.name, x.value) 281 | w.endRecord() 282 | 283 | proc readValue*(r: var JsonReader[JrpcSys], val: var ResponseRx) 284 | {.gcsafe, raises: [IOError, SerializationError].} = 285 | # We need to overload ResponseRx reader because 286 | # we don't want to skip null fields 287 | r.parseObjectWithoutSkip(key): 288 | case key 289 | of "jsonrpc": r.readValue(val.jsonrpc) 290 | of "id" : r.readValue(val.id) 291 | of "result" : val.result = r.parseAsString() 292 | of "error" : r.readValue(val.error) 293 | else: discard 294 | 295 | proc readValue*(r: var JsonReader[JrpcSys], val: var ResponseRx2) 296 | {.gcsafe, raises: [IOError, SerializationError].} = 297 | # https://www.jsonrpc.org/specification#response_object 298 | 299 | var 300 | jsonrpcOpt: Opt[JsonRPC2] 301 | idOpt: Opt[RequestId] 302 | resultOpt: Opt[JsonString] 303 | errorOpt: Opt[ResponseError] 304 | 305 | r.parseObjectWithoutSkip(key): 306 | case key 307 | of "jsonrpc": r.readValue(jsonrpcOpt) 308 | of "id" : r.readValue(idOpt) 309 | of "result" : resultOpt.ok r.parseAsString() 310 | of "error" : r.readValue(errorOpt) 311 | else: discard 312 | 313 | if jsonrpcOpt.isNone: 314 | r.raiseIncompleteObject("Missing or invalid `jsonrpc` version") 315 | let id = idOpt.valueOr: 316 | r.raiseIncompleteObject("Missing `id` field") 317 | 318 | if resultOpt.isNone() and errorOpt.isNone(): 319 | r.raiseIncompleteObject("Missing `result` or `error` field") 320 | 321 | if errorOpt.isSome(): 322 | if resultOpt.isSome(): 323 | r.raiseUnexpectedValue("Both `result` and `error` fields present") 324 | 325 | val = ResponseRx2(id: id, kind: ResponseKind.rkError, error: move(errorOpt[])) 326 | else: 327 | val = ResponseRx2(id: id, kind: ResponseKind.rkResult, result: move(resultOpt[])) 328 | 329 | proc readValue*(r: var JsonReader[JrpcSys], val: var RequestBatchRx) 330 | {.gcsafe, raises: [IOError, SerializationError].} = 331 | let tok = r.tokKind 332 | case tok 333 | of JsonValueKind.Array: 334 | val = RequestBatchRx(kind: rbkMany) 335 | r.readValue(val.many) 336 | if val.many.len == 0: 337 | r.raiseUnexpectedValue("Batch must contain at least one message") 338 | of JsonValueKind.Object: 339 | val = RequestBatchRx(kind: rbkSingle) 340 | r.readValue(val.single) 341 | else: 342 | r.raiseUnexpectedValue("RequestBatch must be either array or object, got=" & $tok) 343 | 344 | proc readValue*(r: var JsonReader[JrpcSys], val: var ResponseBatchRx) 345 | {.gcsafe, raises: [IOError, SerializationError].} = 346 | let tok = r.tokKind 347 | case tok 348 | of JsonValueKind.Array: 349 | val = ResponseBatchRx(kind: rbkMany) 350 | r.readValue(val.many) 351 | if val.many.len == 0: 352 | r.raiseUnexpectedValue("Batch must contain at least one message") 353 | of JsonValueKind.Object: 354 | val = ResponseBatchRx(kind: rbkSingle) 355 | r.readValue(val.single) 356 | else: 357 | r.raiseUnexpectedValue("ResponseBatch must be either array or object, got=" & $tok) 358 | 359 | func toTx*(params: RequestParamsRx): RequestParamsTx = 360 | case params.kind: 361 | of rpPositional: 362 | result = RequestParamsTx(kind: rpPositional) 363 | for x in params.positional: 364 | result.positional.add x.param 365 | of rpNamed: 366 | result = RequestParamsTx(kind: rpNamed) 367 | result.named = params.named 368 | 369 | template writeRequest*(writer: var JrpcSys.Writer, name: string, params: RequestParamsTx, id: int) = 370 | writer.writeObject: 371 | writer.writeMember("jsonrpc", JsonRPC2()) 372 | writer.writeMember("method", name) 373 | writer.writeMember("params", params) 374 | writer.writeMember("id", id) 375 | 376 | template writeNotification*(writer: var JrpcSys.Writer, name: string, params: RequestParamsTx) = 377 | writer.writeObject: 378 | writer.writeMember("jsonrpc", JsonRPC2()) 379 | writer.writeMember("method", name) 380 | writer.writeMember("params", params) 381 | 382 | template withWriter*(_: type JrpcSys, writer, body: untyped): seq[byte] = 383 | var stream = memoryOutput() 384 | 385 | {.cast(noSideEffect), cast(raises: []).}: 386 | var writer = JrpcSys.Writer.init(stream) 387 | body 388 | 389 | stream.getOutput(seq[byte]) 390 | 391 | {.pop.} 392 | -------------------------------------------------------------------------------- /json_rpc/client.nim: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | # Copyright (c) 2019-2024 Status Research & Development GmbH 3 | # Licensed under either of 4 | # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 5 | # * MIT license ([LICENSE-MIT](LICENSE-MIT)) 6 | # at your option. 7 | # This file may not be copied, modified, or distributed except according to 8 | # those terms. 9 | 10 | {.push raises: [], gcsafe.} 11 | 12 | import chronos/futures 13 | 14 | # json_rpc seems to frequently trigger this bug so add a workaround here 15 | when (NimMajor, NimMinor, NimPatch) < (2, 2, 6): 16 | proc json_rpc_workaround_24844_future_string*() {.exportc.} = 17 | # TODO https://github.com/nim-lang/Nim/issues/24844 18 | discard Future[string]().value() 19 | 20 | import 21 | std/[deques, hashes, json, tables, macros], 22 | chronos, 23 | chronicles, 24 | stew/byteutils, 25 | results, 26 | ./private/[client_handler_wrapper, jrpc_sys, shared_wrapper], 27 | ./[errors, jsonmarshal, router] 28 | 29 | from strutils import replace 30 | 31 | export 32 | chronos, deques, tables, jsonmarshal, RequestParamsTx, ResponseBatchRx, RequestIdKind, 33 | RequestId, RequestTx, RequestParamKind, results 34 | 35 | logScope: 36 | topics = "jsonrpc client" 37 | 38 | const defaultMaxMessageSize* = 128 * 1024 * 1024 # 128 MB (JSON encoded) 39 | 40 | type 41 | RpcBatchItem* = object 42 | meth*: string 43 | params*: RequestParamsTx 44 | 45 | RpcBatchCallRef* = ref object of RootRef 46 | client*: RpcClient 47 | batch*: seq[RpcBatchItem] 48 | 49 | RpcBatchResponse* = object 50 | error*: Opt[string] 51 | result*: JsonString 52 | 53 | ResponseFut* = Future[seq[byte]].Raising([CancelledError, JsonRpcError]) 54 | RpcClient* = ref object of RootRef 55 | lastId: int 56 | onDisconnect*: proc() {.gcsafe, raises: [].} 57 | onProcessMessage* {.deprecated.}: proc(client: RpcClient, line: string): 58 | Result[bool, string] {.gcsafe, raises: [].} 59 | pendingRequests*: Deque[ResponseFut] 60 | remote*: string 61 | # Client identifier, for logging 62 | maxMessageSize*: int 63 | 64 | RpcConnection* = ref object of RpcClient 65 | router*: 66 | proc(request: RequestBatchRx): Future[seq[byte]] {.async: (raises: []).} 67 | ## Router used for transports that support bidirectional communication 68 | 69 | GetJsonRpcRequestHeaders* = proc(): seq[(string, string)] {.gcsafe, raises: [].} 70 | 71 | func hash*(v: RpcClient): Hash = 72 | cast[Hash](addr v[]) 73 | 74 | func parseResponse*(payload: openArray[byte], T: type): T {.raises: [JsonRpcError].} = 75 | try: 76 | JrpcSys.decode(payload, T) 77 | except SerializationError as exc: 78 | raise (ref InvalidResponse)( 79 | msg: exc.formatMsg("msg"), payload: @payload, parent: exc 80 | ) 81 | 82 | proc processsSingleResponse( 83 | response: sink ResponseRx2, id: int 84 | ): JsonString {.raises: [JsonRpcError].} = 85 | if response.id.kind != RequestIdKind.riNumber or response.id.num != id: 86 | raise 87 | (ref InvalidResponse)(msg: "Expected `id` " & $id & ", got " & $response.id) 88 | 89 | case response.kind 90 | of ResponseKind.rkError: 91 | raise (ref JsonRpcError)(msg: JrpcSys.encode(response.error)) 92 | of ResponseKind.rkResult: 93 | move(response.result) 94 | 95 | proc processsSingleResponse*( 96 | body: openArray[byte], id: int 97 | ): JsonString {.raises: [JsonRpcError].} = 98 | processsSingleResponse(parseResponse(body, ResponseRx2), id) 99 | 100 | template withPendingFut*(client, fut, body: untyped): untyped = 101 | let fut = ResponseFut.init("jsonrpc.client.pending") 102 | client.pendingRequests.addLast fut 103 | body 104 | 105 | method send( 106 | client: RpcClient, data: seq[byte] 107 | ) {.base, async: (raises: [CancelledError, JsonRpcError]).} = 108 | raiseAssert("`RpcClient.send` not implemented") 109 | 110 | proc callOnProcessMessage*( 111 | client: RpcClient, line: openArray[byte] 112 | ): Result[bool, string] = 113 | if client.onProcessMessage.isNil.not: 114 | client.onProcessMessage(client, string.fromBytes(line)) 115 | else: 116 | ok(true) 117 | 118 | proc processMessage*( 119 | client: RpcConnection, line: seq[byte] 120 | ): Future[seq[byte]] {.async: (raises: []).} = 121 | let request = 122 | try: 123 | JrpcSys.decode(line, RequestBatchRx) 124 | except IncompleteObjectError: 125 | if client.pendingRequests.len() == 0: 126 | debug "Received message even though there's nothing queued, dropping", 127 | id = ( 128 | block: 129 | JrpcSys.decode(line, ReqRespHeader).id 130 | ) 131 | return default(seq[byte]) 132 | 133 | let fut = client.pendingRequests.popFirst() 134 | 135 | # Messages are assumed to arrive one by one - even if the future was cancelled, 136 | # we therefore consume one message for every line we don't have to process 137 | if fut.finished(): # probably cancelled 138 | debug "Future already finished, dropping", state = fut.state() 139 | return default(seq[byte]) 140 | 141 | fut.complete(line) 142 | 143 | return default(seq[byte]) 144 | except SerializationError as exc: 145 | return wrapError(router.INVALID_REQUEST, exc.msg) 146 | 147 | if client.router != nil: 148 | await client.router(request) 149 | else: 150 | default(seq[byte]) 151 | 152 | proc clearPending*(client: RpcClient, exc: ref JsonRpcError) = 153 | while client.pendingRequests.len > 0: 154 | let fut = client.pendingRequests.popFirst() 155 | if not fut.finished(): 156 | fut.fail(exc) 157 | 158 | # ------------------------------------------------------------------------------ 159 | # Public functions 160 | # ------------------------------------------------------------------------------ 161 | 162 | proc getNextId(client: RpcClient): int = 163 | client.lastId += 1 164 | client.lastId 165 | 166 | method request( 167 | client: RpcClient, reqData: seq[byte] 168 | ): Future[seq[byte]] {.base, async: (raises: [CancelledError, JsonRpcError]).} = 169 | raiseAssert("`RpcClient.request` not implemented") 170 | 171 | method close*(client: RpcClient): Future[void] {.base, async: (raises: []).} = 172 | raiseAssert("`RpcClient.close` not implemented") 173 | 174 | proc notify*( 175 | client: RpcClient, name: string, params: RequestParamsTx 176 | ) {.async: (raises: [CancelledError, JsonRpcError], raw: true).} = 177 | ## Perform a "notification", ie a JSON-RPC request without response 178 | let requestData = JrpcSys.withWriter(writer): 179 | writer.writeNotification(name, params) 180 | 181 | debug "Sending JSON-RPC notification", 182 | name, len = requestData.len, remote = client.remote 183 | trace "Parameters", params 184 | 185 | # Release params memory earlier by using a raw proc for the initial 186 | # processing 187 | proc complete( 188 | client: RpcClient, request: auto 189 | ) {.async: (raises: [CancelledError, JsonRpcError]).} = 190 | try: 191 | await request 192 | except JsonRpcError as exc: 193 | debug "JSON-RPC notification failed", err = exc.msg, remote = client.remote 194 | raise exc 195 | 196 | let req = client.send(requestData) 197 | client.complete(req) 198 | 199 | proc call*( 200 | client: RpcClient, name: string, params: RequestParamsTx 201 | ): Future[JsonString] {.async: (raises: [CancelledError, JsonRpcError], raw: true).} = 202 | ## Perform an RPC call returning the `result` of the call 203 | let 204 | # We don't really need an id since exchanges happen in order but using one 205 | # helps debugging, if nothing else 206 | id = client.getNextId() 207 | requestData = JrpcSys.withWriter(writer): 208 | writer.writeRequest(name, params, id) 209 | 210 | debug "Sending JSON-RPC request", 211 | name, len = requestData.len, id, remote = client.remote 212 | trace "Parameters", params 213 | 214 | # Release params memory earlier by using a raw proc for the initial 215 | # processing 216 | proc complete( 217 | client: RpcClient, request: auto, id: int 218 | ): Future[JsonString] {.async: (raises: [CancelledError, JsonRpcError]).} = 219 | try: 220 | let resData = await request 221 | 222 | debug "Processing JSON-RPC response", 223 | len = resData.len, id, remote = client.remote 224 | processsSingleResponse(resData, id) 225 | except JsonRpcError as exc: 226 | debug "JSON-RPC request failed", err = exc.msg, id, remote = client.remote 227 | raise exc 228 | 229 | let req = client.request(requestData) 230 | client.complete(req, id) 231 | 232 | proc call*( 233 | client: RpcClient, name: string, params: JsonNode 234 | ): Future[JsonString] {.async: (raises: [CancelledError, JsonRpcError], raw: true).} = 235 | client.call(name, params.paramsTx) 236 | 237 | proc callBatch*( 238 | client: RpcClient, calls: seq[RequestTx] 239 | ): Future[seq[ResponseRx2]] {. 240 | async: (raises: [CancelledError, JsonRpcError], raw: true) 241 | .} = 242 | if calls.len == 0: 243 | let res = Future[seq[ResponseRx2]].Raising([CancelledError, JsonRpcError]).init( 244 | "empty batch" 245 | ) 246 | res.complete(default(seq[ResponseRx2])) 247 | return res 248 | 249 | let requestData = JrpcSys.withWriter(writer): 250 | writer.writeArray: 251 | for call in calls: 252 | writer.writeValue(call) 253 | 254 | debug "Sending JSON-RPC batch", len = requestData.len, remote = client.remote 255 | 256 | proc complete( 257 | client: RpcClient, request: auto 258 | ): Future[seq[ResponseRx2]] {.async: (raises: [CancelledError, JsonRpcError]).} = 259 | try: 260 | let resData = await request 261 | debug "Processing JSON-RPC batch response", 262 | len = resData.len, remote = client.remote 263 | parseResponse(resData, seq[ResponseRx2]) 264 | except JsonRpcError as exc: 265 | debug "JSON-RPC batch request failed", err = exc.msg, remote = client.remote 266 | raise exc 267 | 268 | let req = client.request(requestData) 269 | client.complete(req) 270 | 271 | proc prepareBatch*(client: RpcClient): RpcBatchCallRef = 272 | RpcBatchCallRef(client: client) 273 | 274 | proc send*( 275 | batch: RpcBatchCallRef 276 | ): Future[Result[seq[RpcBatchResponse], string]] {. 277 | async: (raises: [CancelledError], raw: true) 278 | .} = 279 | if batch.batch.len == 0: 280 | let res = Future[Result[seq[RpcBatchResponse], string]] 281 | .Raising([CancelledError]) 282 | .init("empty batch") 283 | res.complete( 284 | Result[seq[RpcBatchResponse], string].ok(default(seq[RpcBatchResponse])) 285 | ) 286 | return res 287 | 288 | var lastId: int 289 | var map = initTable[int, int]() 290 | 291 | let requestData = JrpcSys.withWriter(writer): 292 | writer.writeArray: 293 | for i, item in batch.batch: 294 | lastId = batch.client.getNextId() 295 | map[lastId] = i 296 | writer.writeValue(requestTx(item.meth, item.params, lastId)) 297 | 298 | debug "Sending JSON-RPC batch", 299 | len = requestData.len, lastId, remote = batch.client.remote 300 | 301 | proc complete( 302 | client: RpcClient, request: auto, map: sink Table[int, int], lastId: int 303 | ): Future[Result[seq[RpcBatchResponse], string]] {.async: (raises: [CancelledError]).} = 304 | var 305 | map = move(map) # 2.0 compat 306 | res = 307 | try: 308 | let resData = await request 309 | debug "Processing JSON-RPC batch response", 310 | len = resData.len, lastId, remote = client.remote 311 | 312 | parseResponse(resData, seq[ResponseRx2]) 313 | except JsonRpcError as exc: 314 | debug "JSON-RPC batch request failed", err = exc.msg, remote = client.remote 315 | 316 | return err(exc.msg) 317 | responses = newSeq[RpcBatchResponse](map.len) 318 | 319 | for i, response in res.mpairs(): 320 | let id = response.id.num 321 | var index: int 322 | if not map.pop(id, index): 323 | return err("cannot find message id: " & $lastId & " in response " & $i) 324 | 325 | case response.kind 326 | of ResponseKind.rkError: 327 | responses[index] = 328 | RpcBatchResponse(error: Opt.some(JrpcSys.encode(response.error))) 329 | of ResponseKind.rkResult: 330 | responses[index] = RpcBatchResponse(result: move(response.result)) 331 | 332 | # In case the response is incomplete, we should say something about the 333 | # missing requests 334 | for _, index in map: 335 | responses[index] = RpcBatchResponse( 336 | error: Opt.some( 337 | JrpcSys.encode( 338 | ResponseError(code: INTERNAL_ERROR, message: "Missing response from server") 339 | ) 340 | ) 341 | ) 342 | 343 | ok(responses) 344 | let req = batch.client.request(requestData) 345 | batch.client.complete(req, map, lastId) 346 | 347 | # ------------------------------------------------------------------------------ 348 | # Signature processing 349 | # ------------------------------------------------------------------------------ 350 | 351 | macro createRpcSigs*(clientType: untyped, filePath: static[string]): untyped = 352 | ## Takes a file of forward declarations in Nim and builds them into RPC 353 | ## calls, based on their parameters. 354 | ## Inputs are marshalled to json, and results are put into the signature's 355 | ## Nim type. 356 | cresteSignaturesFromString(clientType, staticRead($filePath.replace('\\', '/'))) 357 | 358 | macro createRpcSigsFromString*(clientType: untyped, sigString: static[string]): untyped = 359 | ## Takes a string of forward declarations in Nim and builds them into RPC 360 | ## calls, based on their parameters. 361 | ## Inputs are marshalled to json, and results are put into the signature's 362 | ## Nim type. 363 | cresteSignaturesFromString(clientType, sigString) 364 | 365 | macro createSingleRpcSig*(clientType: untyped, alias: static[string], procDecl: untyped): untyped = 366 | ## Takes a single forward declarations in Nim and builds them into RPC 367 | ## calls, based on their parameters. 368 | ## Inputs are marshalled to json, and results are put into the signature's 369 | ## Nim type. 370 | doAssert procDecl.len == 1, "Only accept single proc definition" 371 | let procDecl = procDecl[0] 372 | procDecl.expectKind nnkProcDef 373 | result = createRpcFromSig(clientType, procDecl, ident(alias)) 374 | 375 | macro createRpcSigsFromNim*(clientType: untyped, procList: untyped): untyped = 376 | ## Takes a list of forward declarations in Nim and builds them into RPC 377 | ## calls, based on their parameters. 378 | ## Inputs are marshalled to json, and results are put into the signature's 379 | ## Nim type. 380 | processRpcSigs(clientType, procList) 381 | 382 | {.pop.} 383 | --------------------------------------------------------------------------------