├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── config.nims ├── json_rpc.nimble ├── json_rpc ├── client.nim ├── clients │ ├── httpclient.nim │ ├── socketclient.nim │ ├── websocketclient.nim │ └── websocketclientimpl.nim ├── errors.nim ├── jsonmarshal.nim ├── private │ ├── client_handler_wrapper.nim │ ├── jrpc_sys.nim │ ├── server_handler_wrapper.nim │ ├── shared_wrapper.nim │ └── utils.nim ├── router.nim ├── rpcclient.nim ├── rpcproxy.nim ├── rpcserver.nim ├── server.nim └── servers │ ├── httpserver.nim │ ├── socketserver.nim │ └── websocketserver.nim ├── nim.cfg └── tests ├── all.nim ├── nim.cfg ├── private ├── ethcallsigs.nim ├── ethhexstrings.nim ├── ethprocs.nim ├── ethtypes.nim ├── file_callsigs.nim ├── helpers.nim └── stintjson.nim ├── test_batch_call.nim ├── test_callsigs.nim ├── test_client_hook.nim ├── test_jrpc_sys.nim ├── test_router_rpc.nim ├── testethcalls.nim ├── testhook.nim ├── testhttp.nim ├── testhttps.nim ├── testproxy.nim ├── testrpcmacro.nim └── testserverclient.nim /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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.0" 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 & < 4.1.0", 25 | "httputils >= 0.3.0 & < 0.4.0", 26 | "chronicles", 27 | "websock >= 0.2.0 & < 0.3.0", 28 | "json_serialization", 29 | "unittest2" 30 | 31 | let nimc = getEnv("NIMC", "nim") # Which nim compiler to use 32 | let lang = getEnv("NIMLANG", "c") # Which backend (c/cpp/js) 33 | let flags = getEnv("NIMFLAGS", "") # Extra flags for the compiler 34 | let verbose = getEnv("V", "") notin ["", "0"] 35 | 36 | let cfg = 37 | " --styleCheck:usages --styleCheck:error" & 38 | (if verbose: "" else: " --verbosity:0") & 39 | " --skipParentCfg --skipUserCfg --outdir:build --nimcache:build/nimcache -f" & 40 | " --threads:on -d:chronicles_log_level=ERROR" 41 | 42 | proc build(args, path: string) = 43 | exec nimc & " " & lang & " " & cfg & " " & flags & " " & args & " " & path 44 | 45 | proc run(args, path: string) = 46 | build args & " --mm:refc -r", path 47 | if (NimMajor, NimMinor) > (1, 6): 48 | build args & " --mm:orc -r", path 49 | 50 | proc buildOnly(args, path: string) = 51 | build args & " --mm:refc", path 52 | if (NimMajor, NimMinor) > (1, 6): 53 | build args & " --mm:orc", path 54 | 55 | task test, "run tests": 56 | run "", "tests/all" 57 | 58 | when not defined(windows): 59 | # on windows, socker server build failed 60 | buildOnly "-d:chronicles_log_level=TRACE -d:\"chronicles_sinks=textlines[dynamic],json[dynamic]\"", "tests/all" 61 | -------------------------------------------------------------------------------- /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 13 | std/[json, tables, macros], 14 | chronicles, 15 | chronos, 16 | results, 17 | ./jsonmarshal, 18 | ./private/jrpc_sys, 19 | ./private/client_handler_wrapper, 20 | ./private/shared_wrapper, 21 | ./errors 22 | 23 | from strutils import replace 24 | 25 | export 26 | chronos, 27 | tables, 28 | jsonmarshal, 29 | RequestParamsTx, 30 | RequestBatchTx, 31 | ResponseBatchRx, 32 | results 33 | 34 | logScope: 35 | topics = "JSONRPC-CLIENT" 36 | 37 | const MaxMessageBodyBytes* = 128 * 1024 * 1024 # 128 MB (JSON encoded) 38 | 39 | type 40 | RpcBatchItem* = object 41 | meth*: string 42 | params*: RequestParamsTx 43 | 44 | RpcBatchCallRef* = ref object of RootRef 45 | client*: RpcClient 46 | batch*: seq[RpcBatchItem] 47 | 48 | RpcBatchResponse* = object 49 | error*: Opt[string] 50 | result*: JsonString 51 | 52 | RpcClient* = ref object of RootRef 53 | awaiting*: Table[RequestId, Future[JsonString]] 54 | lastId: int 55 | onDisconnect*: proc() {.gcsafe, raises: [].} 56 | onProcessMessage*: proc(client: RpcClient, line: string): 57 | Result[bool, string] {.gcsafe, raises: [].} 58 | batchFut*: Future[ResponseBatchRx] 59 | 60 | GetJsonRpcRequestHeaders* = proc(): seq[(string, string)] {.gcsafe, raises: [].} 61 | 62 | # ------------------------------------------------------------------------------ 63 | # Public helpers 64 | # ------------------------------------------------------------------------------ 65 | 66 | func validateResponse(resIndex: int, res: ResponseRx): Result[void, string] = 67 | if res.jsonrpc.isNone: 68 | return err("missing or invalid `jsonrpc` in response " & $resIndex) 69 | 70 | if res.id.isNone: 71 | if res.error.isSome: 72 | let error = JrpcSys.encode(res.error.get) 73 | return err(error) 74 | else: 75 | return err("missing or invalid response id in response " & $resIndex) 76 | 77 | if res.error.isSome: 78 | let error = JrpcSys.encode(res.error.get) 79 | return err(error) 80 | 81 | # Up to this point, the result should contains something 82 | if res.result.string.len == 0: 83 | return err("missing or invalid response result in response " & $resIndex) 84 | 85 | ok() 86 | 87 | proc processResponse(resIndex: int, 88 | map: var Table[RequestId, int], 89 | responses: var seq[RpcBatchResponse], 90 | response: ResponseRx): Result[void, string] = 91 | let r = validateResponse(resIndex, response) 92 | if r.isErr: 93 | if response.id.isSome: 94 | let id = response.id.get 95 | var index: int 96 | if not map.pop(id, index): 97 | return err("cannot find message id: " & $id & " in response " & $resIndex) 98 | responses[index] = RpcBatchResponse( 99 | error: Opt.some(r.error) 100 | ) 101 | else: 102 | return err(r.error) 103 | else: 104 | let id = response.id.get 105 | var index: int 106 | if not map.pop(id, index): 107 | return err("cannot find message id: " & $id & " in response " & $resIndex) 108 | responses[index] = RpcBatchResponse( 109 | result: response.result 110 | ) 111 | 112 | ok() 113 | 114 | # ------------------------------------------------------------------------------ 115 | # Public helpers 116 | # ------------------------------------------------------------------------------ 117 | 118 | func requestTxEncode*(name: string, params: RequestParamsTx, id: RequestId): string = 119 | let req = requestTx(name, params, id) 120 | JrpcSys.encode(req) 121 | 122 | func requestBatchEncode*(calls: RequestBatchTx): string = 123 | JrpcSys.encode(calls) 124 | 125 | # ------------------------------------------------------------------------------ 126 | # Public functions 127 | # ------------------------------------------------------------------------------ 128 | 129 | proc getNextId*(client: RpcClient): RequestId = 130 | client.lastId += 1 131 | RequestId(kind: riNumber, num: client.lastId) 132 | 133 | method call*(client: RpcClient, name: string, 134 | params: RequestParamsTx): Future[JsonString] 135 | {.base, async.} = 136 | raiseAssert("`RpcClient.call` not implemented") 137 | 138 | proc call*(client: RpcClient, name: string, 139 | params: JsonNode): Future[JsonString] 140 | {.async: (raw: true).} = 141 | client.call(name, params.paramsTx) 142 | 143 | method close*(client: RpcClient): Future[void] {.base, async.} = 144 | raiseAssert("`RpcClient.close` not implemented") 145 | 146 | method callBatch*(client: RpcClient, 147 | calls: RequestBatchTx): Future[ResponseBatchRx] 148 | {.base, async.} = 149 | raiseAssert("`RpcClient.callBatch` not implemented") 150 | 151 | proc processMessage*(client: RpcClient, line: string): Result[void, string] = 152 | if client.onProcessMessage.isNil.not: 153 | let fallBack = client.onProcessMessage(client, line).valueOr: 154 | return err(error) 155 | if not fallBack: 156 | return ok() 157 | 158 | try: 159 | let batch = JrpcSys.decode(line, ResponseBatchRx) 160 | if batch.kind == rbkMany: 161 | if client.batchFut.isNil or client.batchFut.finished(): 162 | client.batchFut = newFuture[ResponseBatchRx]() 163 | client.batchFut.complete(batch) 164 | return ok() 165 | 166 | let response = batch.single 167 | if response.jsonrpc.isNone: 168 | return err("missing or invalid `jsonrpc`") 169 | 170 | let id = response.id.valueOr: 171 | if response.error.isSome: 172 | let error = JrpcSys.encode(response.error.get) 173 | return err(error) 174 | else: 175 | return err("missing or invalid response id") 176 | 177 | var requestFut: Future[JsonString] 178 | if not client.awaiting.pop(id, requestFut): 179 | return err("Cannot find message id \"" & $id & "\"") 180 | 181 | if response.error.isSome: 182 | let error = JrpcSys.encode(response.error.get) 183 | requestFut.fail(newException(JsonRpcError, error)) 184 | return ok() 185 | 186 | # Up to this point, the result should contains something 187 | if response.result.string.len == 0: 188 | let msg = "missing or invalid response result" 189 | requestFut.fail(newException(JsonRpcError, msg)) 190 | return ok() 191 | 192 | debug "Received JSON-RPC response", 193 | len = string(response.result).len, id 194 | requestFut.complete(response.result) 195 | return ok() 196 | 197 | except CatchableError as exc: 198 | return err(exc.msg) 199 | 200 | proc prepareBatch*(client: RpcClient): RpcBatchCallRef = 201 | RpcBatchCallRef(client: client) 202 | 203 | proc send*(batch: RpcBatchCallRef): 204 | Future[Result[seq[RpcBatchResponse], string]] {. 205 | async: (raises: []).} = 206 | var 207 | calls = RequestBatchTx( 208 | kind: rbkMany, 209 | many: newSeqOfCap[RequestTx](batch.batch.len), 210 | ) 211 | responses = newSeq[RpcBatchResponse](batch.batch.len) 212 | map = initTable[RequestId, int]() 213 | 214 | for item in batch.batch: 215 | let id = batch.client.getNextId() 216 | map[id] = calls.many.len 217 | calls.many.add requestTx(item.meth, item.params, id) 218 | 219 | try: 220 | let res = await batch.client.callBatch(calls) 221 | if res.kind == rbkSingle: 222 | let r = processResponse(0, map, responses, res.single) 223 | if r.isErr: 224 | return err(r.error) 225 | else: 226 | for i, z in res.many: 227 | let r = processResponse(i, map, responses, z) 228 | if r.isErr: 229 | return err(r.error) 230 | except CatchableError as exc: 231 | return err(exc.msg) 232 | 233 | return ok(responses) 234 | 235 | # ------------------------------------------------------------------------------ 236 | # Signature processing 237 | # ------------------------------------------------------------------------------ 238 | 239 | macro createRpcSigs*(clientType: untyped, filePath: static[string]): untyped = 240 | ## Takes a file of forward declarations in Nim and builds them into RPC 241 | ## calls, based on their parameters. 242 | ## Inputs are marshalled to json, and results are put into the signature's 243 | ## Nim type. 244 | cresteSignaturesFromString(clientType, staticRead($filePath.replace('\\', '/'))) 245 | 246 | macro createRpcSigsFromString*(clientType: untyped, sigString: static[string]): untyped = 247 | ## Takes a string of forward declarations in Nim and builds them into RPC 248 | ## calls, based on their parameters. 249 | ## Inputs are marshalled to json, and results are put into the signature's 250 | ## Nim type. 251 | cresteSignaturesFromString(clientType, sigString) 252 | 253 | macro createSingleRpcSig*(clientType: untyped, alias: static[string], procDecl: untyped): untyped = 254 | ## Takes a single forward declarations in Nim and builds them into RPC 255 | ## calls, based on their parameters. 256 | ## Inputs are marshalled to json, and results are put into the signature's 257 | ## Nim type. 258 | doAssert procDecl.len == 1, "Only accept single proc definition" 259 | let procDecl = procDecl[0] 260 | procDecl.expectKind nnkProcDef 261 | result = createRpcFromSig(clientType, procDecl, ident(alias)) 262 | 263 | macro createRpcSigsFromNim*(clientType: untyped, procList: untyped): untyped = 264 | ## Takes a list of forward declarations in Nim and builds them into RPC 265 | ## calls, based on their parameters. 266 | ## Inputs are marshalled to json, and results are put into the signature's 267 | ## Nim type. 268 | processRpcSigs(clientType, procList) 269 | 270 | {.pop.} 271 | -------------------------------------------------------------------------------- /json_rpc/clients/httpclient.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/[tables, uri], 14 | stew/byteutils, 15 | results, 16 | chronos/apps/http/httpclient as chronosHttpClient, 17 | chronicles, httputils, json_serialization/std/net, 18 | ../client, 19 | ../errors, 20 | ../private/jrpc_sys 21 | 22 | export 23 | client, errors, HttpClientFlag, HttpClientFlags 24 | 25 | logScope: 26 | topics = "JSONRPC-HTTP-CLIENT" 27 | 28 | type 29 | HttpClientOptions* = object 30 | httpMethod: HttpMethod 31 | 32 | RpcHttpClient* = ref object of RpcClient 33 | httpSession: HttpSessionRef 34 | httpAddress: HttpResult[HttpAddress] 35 | maxBodySize: int 36 | getHeaders: GetJsonRpcRequestHeaders 37 | 38 | # ------------------------------------------------------------------------------ 39 | # Private helpers 40 | # ------------------------------------------------------------------------------ 41 | 42 | proc `$`(v: HttpAddress): string = 43 | v.id 44 | 45 | proc new( 46 | T: type RpcHttpClient, maxBodySize = MaxMessageBodyBytes, secure = false, 47 | getHeaders: GetJsonRpcRequestHeaders = nil, flags: HttpClientFlags = {}): T = 48 | 49 | var moreFlags: HttpClientFlags 50 | if secure: 51 | moreFlags.incl HttpClientFlag.NoVerifyHost 52 | moreFlags.incl HttpClientFlag.NoVerifyServerName 53 | 54 | T( 55 | maxBodySize: maxBodySize, 56 | httpSession: HttpSessionRef.new(flags = flags + moreFlags), 57 | getHeaders: getHeaders 58 | ) 59 | 60 | template closeRefs(req, res: untyped) = 61 | # We can't trust try/finally in async/await in all nim versions, so we 62 | # do it manually instead 63 | if req != nil: 64 | try: 65 | await req.closeWait() 66 | except CatchableError as exc: # shouldn't happen 67 | debug "Error closing JSON-RPC HTTP resuest/response", err = exc.msg 68 | discard exc 69 | 70 | if res != nil: 71 | try: 72 | await res.closeWait() 73 | except CatchableError as exc: # shouldn't happen 74 | debug "Error closing JSON-RPC HTTP resuest/response", err = exc.msg 75 | discard exc 76 | 77 | proc callImpl(client: RpcHttpClient, reqBody: string): Future[string] {.async.} = 78 | doAssert client.httpSession != nil 79 | if client.httpAddress.isErr: 80 | raise newException(RpcAddressUnresolvableError, client.httpAddress.error) 81 | 82 | var headers = 83 | if not isNil(client.getHeaders): 84 | client.getHeaders() 85 | else: 86 | @[] 87 | headers.add(("Content-Type", "application/json")) 88 | 89 | var req: HttpClientRequestRef 90 | var res: HttpClientResponseRef 91 | 92 | req = HttpClientRequestRef.post(client.httpSession, 93 | client.httpAddress.get, 94 | body = reqBody.toOpenArrayByte(0, reqBody.len - 1), 95 | headers = headers) 96 | res = 97 | try: 98 | await req.send() 99 | except CancelledError as e: 100 | debug "Cancelled POST Request with JSON-RPC", e = e.msg 101 | closeRefs(req, res) 102 | raise e 103 | except CatchableError as e: 104 | debug "Failed to send POST Request with JSON-RPC", e = e.msg 105 | closeRefs(req, res) 106 | raise (ref RpcPostError)(msg: "Failed to send POST Request with JSON-RPC: " & e.msg, parent: e) 107 | 108 | if res.status < 200 or res.status >= 300: # res.status is not 2xx (success) 109 | debug "Unsuccessful POST Request with JSON-RPC", 110 | status = res.status, reason = res.reason 111 | closeRefs(req, res) 112 | raise (ref ErrorResponse)(status: res.status, msg: res.reason) 113 | 114 | let resBytes = 115 | try: 116 | await res.getBodyBytes(client.maxBodySize) 117 | except CancelledError as e: 118 | debug "Cancelled POST Response for JSON-RPC", e = e.msg 119 | closeRefs(req, res) 120 | raise e 121 | except CatchableError as e: 122 | debug "Failed to read POST Response for JSON-RPC", e = e.msg 123 | closeRefs(req, res) 124 | raise (ref FailedHttpResponse)(msg: "Failed to read POST Response for JSON-RPC: " & e.msg, parent: e) 125 | 126 | result = string.fromBytes(resBytes) 127 | trace "Response", text = result 128 | closeRefs(req, res) 129 | 130 | # ------------------------------------------------------------------------------ 131 | # Public functions 132 | # ------------------------------------------------------------------------------ 133 | 134 | proc newRpcHttpClient*( 135 | maxBodySize = MaxMessageBodyBytes, secure = false, 136 | getHeaders: GetJsonRpcRequestHeaders = nil, 137 | flags: HttpClientFlags = {}): RpcHttpClient = 138 | RpcHttpClient.new(maxBodySize, secure, getHeaders, flags) 139 | 140 | method call*(client: RpcHttpClient, name: string, 141 | params: RequestParamsTx): Future[JsonString] 142 | {.async.} = 143 | 144 | let 145 | id = client.getNextId() 146 | reqBody = requestTxEncode(name, params, id) 147 | 148 | debug "Sending JSON-RPC request", 149 | address = client.httpAddress, len = len(reqBody), name, id 150 | trace "Message", msg = reqBody 151 | 152 | let resText = await client.callImpl(reqBody) 153 | 154 | # completed by processMessage - the flow is quite weird here to accomodate 155 | # socket and ws clients, but could use a more thorough refactoring 156 | var newFut = newFuture[JsonString]() 157 | # add to awaiting responses 158 | client.awaiting[id] = newFut 159 | 160 | # Might error for all kinds of reasons 161 | let msgRes = client.processMessage(resText) 162 | if msgRes.isErr: 163 | # Need to clean up in case the answer was invalid 164 | let exc = newException(JsonRpcError, msgRes.error) 165 | newFut.fail(exc) 166 | client.awaiting.del(id) 167 | raise exc 168 | 169 | client.awaiting.del(id) 170 | 171 | # processMessage should have completed this future - if it didn't, `read` will 172 | # raise, which is reasonable 173 | if newFut.finished: 174 | return newFut.read() 175 | else: 176 | # TODO: Provide more clarity regarding the failure here 177 | debug "Invalid POST Response for JSON-RPC" 178 | raise newException(InvalidResponse, "Invalid response") 179 | 180 | method callBatch*(client: RpcHttpClient, 181 | calls: RequestBatchTx): Future[ResponseBatchRx] 182 | {.async.} = 183 | let reqBody = requestBatchEncode(calls) 184 | debug "Sending JSON-RPC batch", 185 | address = $client.httpAddress, len = len(reqBody) 186 | let resText = await client.callImpl(reqBody) 187 | 188 | if client.batchFut.isNil or client.batchFut.finished(): 189 | client.batchFut = newFuture[ResponseBatchRx]() 190 | 191 | # Might error for all kinds of reasons 192 | let msgRes = client.processMessage(resText) 193 | if msgRes.isErr: 194 | # Need to clean up in case the answer was invalid 195 | debug "Failed to process POST Response for JSON-RPC", msg = msgRes.error 196 | let exc = newException(JsonRpcError, msgRes.error) 197 | client.batchFut.fail(exc) 198 | raise exc 199 | 200 | # processMessage should have completed this future - if it didn't, `read` will 201 | # raise, which is reasonable 202 | if client.batchFut.finished: 203 | return client.batchFut.read() 204 | else: 205 | # TODO: Provide more clarity regarding the failure here 206 | debug "Invalid POST Response for JSON-RPC" 207 | raise newException(InvalidResponse, "Invalid response") 208 | 209 | proc connect*(client: RpcHttpClient, url: string) {.async.} = 210 | client.httpAddress = client.httpSession.getAddress(url) 211 | if client.httpAddress.isErr: 212 | raise newException(RpcAddressUnresolvableError, client.httpAddress.error) 213 | 214 | proc connect*(client: RpcHttpClient, address: string, port: Port, secure: bool) {.async.} = 215 | var uri = Uri( 216 | scheme: if secure: "https" else: "http", 217 | hostname: address, 218 | port: $port) 219 | 220 | let res = getAddress(client.httpSession, uri) 221 | if res.isOk: 222 | client.httpAddress = res 223 | else: 224 | raise newException(RpcAddressUnresolvableError, res.error) 225 | 226 | method close*(client: RpcHttpClient) {.async.} = 227 | if not client.httpSession.isNil: 228 | await client.httpSession.closeWait() 229 | 230 | {.pop.} 231 | -------------------------------------------------------------------------------- /json_rpc/clients/socketclient.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/tables, 14 | chronicles, 15 | results, 16 | chronos, 17 | ../client, 18 | ../errors, 19 | ../private/jrpc_sys 20 | 21 | export client, errors 22 | 23 | logScope: 24 | topics = "JSONRPC-SOCKET-CLIENT" 25 | 26 | type 27 | RpcSocketClient* = ref object of RpcClient 28 | transport*: StreamTransport 29 | address*: TransportAddress 30 | loop*: Future[void] 31 | 32 | const defaultMaxRequestLength* = 1024 * 128 33 | 34 | proc new*(T: type RpcSocketClient): T = 35 | T() 36 | 37 | proc newRpcSocketClient*: RpcSocketClient = 38 | ## Creates a new client instance. 39 | RpcSocketClient.new() 40 | 41 | method call*(client: RpcSocketClient, name: string, 42 | params: RequestParamsTx): Future[JsonString] {.async.} = 43 | ## Remotely calls the specified RPC method. 44 | if client.transport.isNil: 45 | raise newException(JsonRpcError, 46 | "Transport is not initialised (missing a call to connect?)") 47 | 48 | let 49 | id = client.getNextId() 50 | reqBody = requestTxEncode(name, params, id) & "\r\n" 51 | newFut = newFuture[JsonString]() # completed by processMessage 52 | 53 | # add to awaiting responses 54 | client.awaiting[id] = newFut 55 | 56 | debug "Sending JSON-RPC request", 57 | address = $client.address, len = len(reqBody), name, id 58 | 59 | let res = await client.transport.write(reqBody) 60 | # TODO: Add actions when not full packet was send, e.g. disconnect peer. 61 | doAssert(res == reqBody.len) 62 | 63 | return await newFut 64 | 65 | method callBatch*(client: RpcSocketClient, 66 | calls: RequestBatchTx): Future[ResponseBatchRx] 67 | {.async.} = 68 | if client.transport.isNil: 69 | raise newException(JsonRpcError, 70 | "Transport is not initialised (missing a call to connect?)") 71 | 72 | if client.batchFut.isNil or client.batchFut.finished(): 73 | client.batchFut = newFuture[ResponseBatchRx]() 74 | 75 | let reqBody = requestBatchEncode(calls) & "\r\n" 76 | debug "Sending JSON-RPC batch", 77 | address = $client.address, len = len(reqBody) 78 | let res = await client.transport.write(reqBody) 79 | 80 | # TODO: Add actions when not full packet was send, e.g. disconnect peer. 81 | doAssert(res == reqBody.len) 82 | 83 | return await client.batchFut 84 | 85 | proc processData(client: RpcSocketClient) {.async: (raises: []).} = 86 | while true: 87 | var localException: ref JsonRpcError 88 | while true: 89 | try: 90 | var value = await client.transport.readLine(defaultMaxRequestLength) 91 | if value == "": 92 | # transmission ends 93 | await client.transport.closeWait() 94 | break 95 | 96 | let res = client.processMessage(value) 97 | if res.isErr: 98 | localException = newException(JsonRpcError, res.error) 99 | break 100 | except TransportError as exc: 101 | localException = newException(JsonRpcError, exc.msg) 102 | await client.transport.closeWait() 103 | break 104 | except CancelledError as exc: 105 | localException = newException(JsonRpcError, exc.msg) 106 | await client.transport.closeWait() 107 | break 108 | 109 | if localException.isNil.not: 110 | for _,fut in client.awaiting: 111 | fut.fail(localException) 112 | if client.batchFut.isNil.not and not client.batchFut.completed(): 113 | client.batchFut.fail(localException) 114 | 115 | # async loop reconnection and waiting 116 | try: 117 | info "Reconnect to server", address=`$`(client.address) 118 | client.transport = await connect(client.address) 119 | except TransportError as exc: 120 | error "Error when reconnecting to server", msg=exc.msg 121 | break 122 | except CancelledError as exc: 123 | debug "Server connection was cancelled", msg=exc.msg 124 | break 125 | 126 | proc connect*(client: RpcSocketClient, address: string, port: Port) {.async.} = 127 | let addresses = resolveTAddress(address, port) 128 | client.transport = await connect(addresses[0]) 129 | client.address = addresses[0] 130 | client.loop = processData(client) 131 | 132 | proc connect*(client: RpcSocketClient, address: TransportAddress) {.async.} = 133 | client.transport = await connect(address) 134 | client.address = address 135 | client.loop = processData(client) 136 | 137 | method close*(client: RpcSocketClient) {.async.} = 138 | await client.loop.cancelAndWait() 139 | if not client.transport.isNil: 140 | client.transport.close() 141 | client.transport = nil 142 | -------------------------------------------------------------------------------- /json_rpc/clients/websocketclient.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 | ./websocketclientimpl, 14 | ../client 15 | 16 | # this weird arrangement is to avoid clash 17 | # between Json.encode and Base64Pad.encode 18 | 19 | export 20 | websocketclientimpl, 21 | client, 22 | errors 23 | -------------------------------------------------------------------------------- /json_rpc/clients/websocketclientimpl.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/uri, 14 | pkg/websock/[websock, extensions/compression/deflate], 15 | pkg/[chronos, chronos/apps/http/httptable, chronicles], 16 | stew/byteutils, 17 | ../errors 18 | 19 | # avoid clash between Json.encode and Base64Pad.encode 20 | import ../client except encode 21 | 22 | logScope: 23 | topics = "JSONRPC-WS-CLIENT" 24 | 25 | type 26 | RpcWebSocketClient* = ref object of RpcClient 27 | transport*: WSSession 28 | uri*: Uri 29 | loop*: Future[void] 30 | getHeaders*: GetJsonRpcRequestHeaders 31 | 32 | proc new*( 33 | T: type RpcWebSocketClient, getHeaders: GetJsonRpcRequestHeaders = nil): T = 34 | T(getHeaders: getHeaders) 35 | 36 | proc newRpcWebSocketClient*( 37 | getHeaders: GetJsonRpcRequestHeaders = nil): RpcWebSocketClient = 38 | ## Creates a new client instance. 39 | RpcWebSocketClient.new(getHeaders) 40 | 41 | method call*(client: RpcWebSocketClient, name: string, 42 | params: RequestParamsTx): Future[JsonString] {.async.} = 43 | ## Remotely calls the specified RPC method. 44 | if client.transport.isNil: 45 | raise newException(JsonRpcError, 46 | "Transport is not initialised (missing a call to connect?)") 47 | 48 | let 49 | id = client.getNextId() 50 | reqBody = requestTxEncode(name, params, id) & "\r\n" 51 | newFut = newFuture[JsonString]() # completed by processMessage 52 | # add to awaiting responses 53 | client.awaiting[id] = newFut 54 | 55 | debug "Sending JSON-RPC request", 56 | address = $client.uri, len = len(reqBody), name 57 | 58 | await client.transport.send(reqBody) 59 | return await newFut 60 | 61 | method callBatch*(client: RpcWebSocketClient, 62 | calls: RequestBatchTx): Future[ResponseBatchRx] 63 | {.async.} = 64 | if client.transport.isNil: 65 | raise newException(JsonRpcError, 66 | "Transport is not initialised (missing a call to connect?)") 67 | 68 | if client.batchFut.isNil or client.batchFut.finished(): 69 | client.batchFut = newFuture[ResponseBatchRx]() 70 | 71 | let reqBody = requestBatchEncode(calls) & "\r\n" 72 | debug "Sending JSON-RPC batch", 73 | address = $client.uri, len = len(reqBody) 74 | await client.transport.send(reqBody) 75 | 76 | return await client.batchFut 77 | 78 | proc processData(client: RpcWebSocketClient) {.async.} = 79 | var error: ref CatchableError 80 | 81 | template processError() = 82 | for k, v in client.awaiting: 83 | v.fail(error) 84 | if client.batchFut.isNil.not and not client.batchFut.completed(): 85 | client.batchFut.fail(error) 86 | client.awaiting.clear() 87 | 88 | let ws = client.transport 89 | try: 90 | while ws.readyState != ReadyState.Closed: 91 | var value = await ws.recvMsg(MaxMessageBodyBytes) 92 | 93 | if value.len == 0: 94 | # transmission ends 95 | break 96 | 97 | let res = client.processMessage(string.fromBytes(value)) 98 | if res.isErr: 99 | error = newException(JsonRpcError, res.error) 100 | processError() 101 | 102 | except CatchableError as e: 103 | error = e 104 | 105 | await client.transport.close() 106 | 107 | client.transport = nil 108 | 109 | if client.awaiting.len != 0: 110 | if error.isNil: 111 | error = newException(IOError, "Transport was closed while waiting for response") 112 | processError() 113 | if not client.onDisconnect.isNil: 114 | client.onDisconnect() 115 | 116 | proc addExtraHeaders( 117 | headers: var HttpTable, 118 | client: RpcWebSocketClient, 119 | extraHeaders: HttpTable) = 120 | # Apply client instance overrides 121 | if client.getHeaders != nil: 122 | for header in client.getHeaders(): 123 | headers.add(header[0], header[1]) 124 | 125 | # Apply call specific overrides 126 | for header in extraHeaders.stringItems: 127 | headers.add(header.key, header.value) 128 | 129 | # Apply default origin 130 | discard headers.hasKeyOrPut("Origin", "http://localhost") 131 | 132 | proc connect*( 133 | client: RpcWebSocketClient, 134 | uri: string, 135 | extraHeaders: HttpTable = default(HttpTable), 136 | compression = false, 137 | hooks: seq[Hook] = @[], 138 | flags: set[TLSFlags] = {}) {.async.} = 139 | proc headersHook(ctx: Hook, headers: var HttpTable): Result[void, string] = 140 | headers.addExtraHeaders(client, extraHeaders) 141 | ok() 142 | var ext: seq[ExtFactory] = if compression: @[deflateFactory()] 143 | else: @[] 144 | let uri = parseUri(uri) 145 | let ws = await WebSocket.connect( 146 | uri=uri, 147 | factories=ext, 148 | hooks=hooks & Hook(append: headersHook), 149 | flags=flags) 150 | client.transport = ws 151 | client.uri = uri 152 | client.loop = processData(client) 153 | 154 | method close*(client: RpcWebSocketClient) {.async.} = 155 | await client.loop.cancelAndWait() 156 | if not client.transport.isNil: 157 | await client.transport.close() 158 | client.transport = nil 159 | -------------------------------------------------------------------------------- /json_rpc/errors.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 results, json_serialization 13 | 14 | type 15 | JsonRpcError* = object of CatchableError 16 | ## Base type of all nim-json-rpc errors 17 | 18 | ErrorResponse* = object of JsonRpcError 19 | status*: int 20 | ## raised when the server responded with an error 21 | 22 | InvalidResponse* = object of JsonRpcError 23 | ## raised when the server response violates the JSON-RPC protocol 24 | 25 | FailedHttpResponse* = object of JsonRpcError 26 | ## raised when fail to read the underlying HTTP server response 27 | 28 | RpcPostError* = object of JsonRpcError 29 | ## raised when the client fails to send the POST request with JSON-RPC 30 | 31 | RpcBindError* = object of JsonRpcError 32 | RpcAddressUnresolvableError* = object of JsonRpcError 33 | 34 | InvalidRequest* = object of JsonRpcError 35 | ## raised when the server recieves an invalid JSON request object 36 | code*: int 37 | 38 | RequestDecodeError* = object of JsonRpcError 39 | ## raised when fail to decode RequestRx 40 | 41 | ParamsEncodeError* = object of JsonRpcError 42 | ## raised when fail to encode RequestParamsTx 43 | 44 | ApplicationError* = object of JsonRpcError 45 | ## Error to be raised by the application request handlers when the server 46 | ## needs to respond with a custom application error. The error code should 47 | ## be outside the range of -32768 to -32000. A custom JSON data object may 48 | ## be provided. 49 | code*: int 50 | message*: string 51 | data*: results.Opt[JsonString] 52 | -------------------------------------------------------------------------------- /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 | 25 | # JrpcConv is a namespace/flavor for encoding and decoding 26 | # parameters and return value of a rpc method. 27 | -------------------------------------------------------------------------------- /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 | # make proc async 26 | result.addPragma ident"async" 27 | # export this proc 28 | result[0] = nnkPostfix.newTree(ident"*", newIdentNode($procName)) 29 | 30 | func createBatchCallProc(procName, parameters, callBody: NimNode): NimNode = 31 | # parameters come as a tree 32 | var paramList = newSeq[NimNode]() 33 | for p in parameters: paramList.add(p) 34 | 35 | # build proc 36 | result = newProc(procName, paramList, callBody) 37 | 38 | # export this proc 39 | result[0] = nnkPostfix.newTree(ident"*", newIdentNode($procName)) 40 | 41 | func setupConversion(reqParams, params: NimNode): NimNode = 42 | # populate json params 43 | # even rpcs with no parameters have an empty json array node sent 44 | 45 | params.expectKind nnkFormalParams 46 | result = newStmtList() 47 | result.add quote do: 48 | var `reqParams` = RequestParamsTx(kind: rpPositional) 49 | 50 | for parName, parType in paramsIter(params): 51 | result.add quote do: 52 | `reqParams`.positional.add encode(JrpcConv, `parName`).JsonString 53 | 54 | template maybeUnwrapClientResult*(client, meth, reqParams, returnType): auto = 55 | ## Don't decode e.g. JsonString, return as is 56 | when noWrap(typeof returnType): 57 | await client.call(meth, reqParams) 58 | else: 59 | let res = await client.call(meth, reqParams) 60 | decode(JrpcConv, res.string, typeof returnType) 61 | 62 | func createRpcFromSig*(clientType, rpcDecl: NimNode, alias = NimNode(nil)): NimNode = 63 | ## This procedure will generate something like this: 64 | ## - Currently it always send positional parameters to the server 65 | ## 66 | ## proc rpcApi(client: RpcClient; paramA: TypeA; paramB: TypeB): Future[RetType] = 67 | ## {.gcsafe.}: 68 | ## var reqParams = RequestParamsTx(kind: rpPositional) 69 | ## reqParams.positional.add encode(JrpcConv, paramA).JsonString 70 | ## reqParams.positional.add encode(JrpcConv, paramB).JsonString 71 | ## let res = await client.call("rpcApi", reqParams) 72 | ## result = decode(JrpcConv, res.string, typeof RetType) 73 | ## 74 | ## 2nd version to handle batch request after calling client.prepareBatch() 75 | ## proc rpcApi(batch: RpcBatchCallRef; paramA: TypeA; paramB: TypeB) = 76 | ## var reqParams = RequestParamsTx(kind: rpPositional) 77 | ## reqParams.positional.add encode(JrpcConv, paramA).JsonString 78 | ## reqParams.positional.add encode(JrpcConv, paramB).JsonString 79 | ## batch.batch.add RpcBatchItem(meth: "rpcApi", params: reqParams) 80 | 81 | # Each input parameter in the rpc signature is converted 82 | # to json using JrpcConv.encode. 83 | # Return types are then converted back to native Nim types. 84 | 85 | let 86 | params = rpcDecl.findChild(it.kind == nnkFormalParams).ensureReturnType 87 | procName = if alias.isNil: rpcDecl.name else: alias 88 | pathStr = $rpcDecl.name 89 | returnType = params[0] 90 | reqParams = ident "reqParams" 91 | setup = setupConversion(reqParams, params) 92 | clientIdent = ident"client" 93 | batchParams = params.copy 94 | batchIdent = ident "batch" 95 | 96 | # insert rpc client as first parameter 97 | params.insert(1, nnkIdentDefs.newTree( 98 | clientIdent, 99 | ident($clientType), 100 | newEmptyNode() 101 | )) 102 | 103 | # convert return type to Future 104 | params[0] = nnkBracketExpr.newTree(ident"Future", returnType) 105 | 106 | # perform rpc call 107 | let callBody = quote do: 108 | # populate request params 109 | `setup` 110 | maybeUnwrapClientResult(`clientIdent`, `pathStr`, `reqParams`, `returnType`) 111 | 112 | # insert RpcBatchCallRef as first parameter 113 | batchParams.insert(1, nnkIdentDefs.newTree( 114 | batchIdent, 115 | ident "RpcBatchCallRef", 116 | newEmptyNode() 117 | )) 118 | 119 | # remove return type 120 | batchParams[0] = newEmptyNode() 121 | 122 | let batchCallBody = quote do: 123 | `setup` 124 | `batchIdent`.batch.add RpcBatchItem( 125 | meth: `pathStr`, 126 | params: `reqParams` 127 | ) 128 | 129 | # create rpc proc 130 | result = newStmtList() 131 | result.add createRpcProc(procName, params, callBody) 132 | result.add createBatchCallProc(procName, batchParams, batchCallBody) 133 | 134 | when defined(nimDumpRpcs): 135 | echo pathStr, ":\n", result.repr 136 | 137 | func processRpcSigs*(clientType, parsedCode: NimNode): NimNode = 138 | result = newStmtList() 139 | 140 | for line in parsedCode: 141 | if line.kind == nnkProcDef: 142 | var procDef = createRpcFromSig(clientType, line) 143 | result.add(procDef) 144 | 145 | func cresteSignaturesFromString*(clientType: NimNode, sigStrings: string): NimNode = 146 | try: 147 | result = processRpcSigs(clientType, sigStrings.parseStmt()) 148 | except ValueError as exc: 149 | doAssert(false, exc.msg) 150 | 151 | {.pop.} 152 | -------------------------------------------------------------------------------- /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/stew/results as jser_results 17 | 18 | export 19 | results, 20 | json_serialization 21 | 22 | # This module implements JSON-RPC 2.0 Specification 23 | # https://www.jsonrpc.org/specification 24 | 25 | type 26 | # Special object of Json-RPC 2.0 27 | JsonRPC2* = object 28 | 29 | RequestParamKind* = enum 30 | rpPositional 31 | rpNamed 32 | 33 | ParamDescRx* = object 34 | kind* : JsonValueKind 35 | param*: JsonString 36 | 37 | ParamDescNamed* = object 38 | name*: string 39 | value*: JsonString 40 | 41 | # Request params received by server 42 | RequestParamsRx* = object 43 | case kind*: RequestParamKind 44 | of rpPositional: 45 | positional*: seq[ParamDescRx] 46 | of rpNamed: 47 | named*: seq[ParamDescNamed] 48 | 49 | # Request params sent by client 50 | RequestParamsTx* = object 51 | case kind*: RequestParamKind 52 | of rpPositional: 53 | positional*: seq[JsonString] 54 | of rpNamed: 55 | named*: seq[ParamDescNamed] 56 | 57 | RequestIdKind* = enum 58 | riNull 59 | riNumber 60 | riString 61 | 62 | RequestId* = object 63 | case kind*: RequestIdKind 64 | of riNumber: 65 | num*: int 66 | of riString: 67 | str*: string 68 | of riNull: 69 | discard 70 | 71 | # Request received by server 72 | RequestRx* = object 73 | jsonrpc* : results.Opt[JsonRPC2] 74 | id* : RequestId 75 | `method`*: results.Opt[string] 76 | params* : RequestParamsRx 77 | 78 | # Request sent by client 79 | RequestTx* = object 80 | jsonrpc* : JsonRPC2 81 | id* : results.Opt[RequestId] 82 | `method`*: string 83 | params* : RequestParamsTx 84 | 85 | ResponseError* = object 86 | code* : int 87 | message*: string 88 | data* : results.Opt[JsonString] 89 | 90 | ResponseKind* = enum 91 | rkResult 92 | rkError 93 | 94 | # Response sent by server 95 | ResponseTx* = object 96 | jsonrpc* : JsonRPC2 97 | id* : RequestId 98 | case kind*: ResponseKind 99 | of rkResult: 100 | result* : JsonString 101 | of rkError: 102 | error* : ResponseError 103 | 104 | # Response received by client 105 | ResponseRx* = object 106 | jsonrpc*: results.Opt[JsonRPC2] 107 | id* : results.Opt[RequestId] 108 | result* : JsonString 109 | error* : results.Opt[ResponseError] 110 | 111 | ReBatchKind* = enum 112 | rbkSingle 113 | rbkMany 114 | 115 | RequestBatchRx* = object 116 | case kind*: ReBatchKind 117 | of rbkMany: 118 | many* : seq[RequestRx] 119 | of rbkSingle: 120 | single*: RequestRx 121 | 122 | RequestBatchTx* = object 123 | case kind*: ReBatchKind 124 | of rbkMany: 125 | many* : seq[RequestTx] 126 | of rbkSingle: 127 | single*: RequestTx 128 | 129 | ResponseBatchRx* = object 130 | case kind*: ReBatchKind 131 | of rbkMany: 132 | many* : seq[ResponseRx] 133 | of rbkSingle: 134 | single*: ResponseRx 135 | 136 | ResponseBatchTx* = object 137 | case kind*: ReBatchKind 138 | of rbkMany: 139 | many* : seq[ResponseTx] 140 | of rbkSingle: 141 | single*: ResponseTx 142 | 143 | # don't mix the json-rpc system encoding with the 144 | # actual response/params encoding 145 | createJsonFlavor JrpcSys, 146 | automaticObjectSerialization = false, 147 | requireAllFields = false, 148 | omitOptionalFields = true, # Skip optional fields==none in Writer 149 | allowUnknownFields = true, 150 | skipNullFields = true # Skip optional fields==null in Reader 151 | 152 | ResponseError.useDefaultSerializationIn JrpcSys 153 | RequestTx.useDefaultWriterIn JrpcSys 154 | RequestRx.useDefaultReaderIn JrpcSys 155 | 156 | const 157 | JsonRPC2Literal = JsonString("\"2.0\"") 158 | 159 | func hash*(x: RequestId): hashes.Hash = 160 | var h = 0.Hash 161 | case x.kind: 162 | of riNumber: h = h !& hash(x.num) 163 | of riString: h = h !& hash(x.str) 164 | of riNull: h = h !& hash("null") 165 | result = !$(h) 166 | 167 | func `$`*(x: RequestId): string = 168 | case x.kind: 169 | of riNumber: $x.num 170 | of riString: x.str 171 | of riNull: "null" 172 | 173 | func `==`*(a, b: RequestId): bool = 174 | if a.kind != b.kind: 175 | return false 176 | case a.kind 177 | of riNumber: a.num == b.num 178 | of riString: a.str == b.str 179 | of riNull: true 180 | 181 | func meth*(rx: RequestRx): Opt[string] = 182 | rx.`method` 183 | 184 | proc readValue*(r: var JsonReader[JrpcSys], val: var JsonRPC2) 185 | {.gcsafe, raises: [IOError, JsonReaderError].} = 186 | let version = r.parseAsString() 187 | if version != JsonRPC2Literal: 188 | r.raiseUnexpectedValue("Invalid JSON-RPC version, want=" & 189 | JsonRPC2Literal.string & " got=" & version.string) 190 | 191 | proc writeValue*(w: var JsonWriter[JrpcSys], val: JsonRPC2) 192 | {.gcsafe, raises: [IOError].} = 193 | w.writeValue JsonRPC2Literal 194 | 195 | proc readValue*(r: var JsonReader[JrpcSys], val: var RequestId) 196 | {.gcsafe, raises: [IOError, JsonReaderError].} = 197 | let tok = r.tokKind 198 | case tok 199 | of JsonValueKind.Number: 200 | val = RequestId(kind: riNumber, num: r.parseInt(int)) 201 | of JsonValueKind.String: 202 | val = RequestId(kind: riString, str: r.parseString()) 203 | of JsonValueKind.Null: 204 | val = RequestId(kind: riNull) 205 | r.parseNull() 206 | else: 207 | r.raiseUnexpectedValue("Invalid RequestId, must be Number, String, or Null, got=" & $tok) 208 | 209 | proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestId) 210 | {.gcsafe, raises: [IOError].} = 211 | case val.kind 212 | of riNumber: w.writeValue val.num 213 | of riString: w.writeValue val.str 214 | of riNull: w.writeValue JsonString("null") 215 | 216 | proc readValue*(r: var JsonReader[JrpcSys], val: var RequestParamsRx) 217 | {.gcsafe, raises: [IOError, SerializationError].} = 218 | let tok = r.tokKind 219 | case tok 220 | of JsonValueKind.Array: 221 | val = RequestParamsRx(kind: rpPositional) 222 | r.parseArray: 223 | val.positional.add ParamDescRx( 224 | kind: r.tokKind(), 225 | param: r.parseAsString(), 226 | ) 227 | of JsonValueKind.Object: 228 | val = RequestParamsRx(kind: rpNamed) 229 | for key in r.readObjectFields(): 230 | val.named.add ParamDescNamed( 231 | name: key, 232 | value: r.parseAsString(), 233 | ) 234 | else: 235 | r.raiseUnexpectedValue("RequestParam must be either array or object, got=" & $tok) 236 | 237 | proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestParamsTx) 238 | {.gcsafe, raises: [IOError].} = 239 | case val.kind 240 | of rpPositional: 241 | w.writeArray val.positional 242 | of rpNamed: 243 | w.beginRecord RequestParamsTx 244 | for x in val.named: 245 | w.writeField(x.name, x.value) 246 | w.endRecord() 247 | 248 | proc writeValue*(w: var JsonWriter[JrpcSys], val: ResponseTx) 249 | {.gcsafe, raises: [IOError].} = 250 | w.beginRecord ResponseTx 251 | w.writeField("jsonrpc", val.jsonrpc) 252 | w.writeField("id", val.id) 253 | if val.kind == rkResult: 254 | w.writeField("result", val.result) 255 | else: 256 | w.writeField("error", val.error) 257 | w.endRecord() 258 | 259 | proc readValue*(r: var JsonReader[JrpcSys], val: var ResponseRx) 260 | {.gcsafe, raises: [IOError, SerializationError].} = 261 | # We need to overload ResponseRx reader because 262 | # we don't want to skip null fields 263 | r.parseObjectWithoutSkip(key): 264 | case key 265 | of "jsonrpc": r.readValue(val.jsonrpc) 266 | of "id" : r.readValue(val.id) 267 | of "result" : val.result = r.parseAsString() 268 | of "error" : r.readValue(val.error) 269 | else: discard 270 | 271 | proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestBatchTx) 272 | {.gcsafe, raises: [IOError].} = 273 | if val.kind == rbkMany: 274 | w.writeArray(val.many) 275 | else: 276 | w.writeValue(val.single) 277 | 278 | proc readValue*(r: var JsonReader[JrpcSys], val: var RequestBatchRx) 279 | {.gcsafe, raises: [IOError, SerializationError].} = 280 | let tok = r.tokKind 281 | case tok 282 | of JsonValueKind.Array: 283 | val = RequestBatchRx(kind: rbkMany) 284 | r.readValue(val.many) 285 | of JsonValueKind.Object: 286 | val = RequestBatchRx(kind: rbkSingle) 287 | r.readValue(val.single) 288 | else: 289 | r.raiseUnexpectedValue("RequestBatch must be either array or object, got=" & $tok) 290 | 291 | proc writeValue*(w: var JsonWriter[JrpcSys], val: ResponseBatchTx) 292 | {.gcsafe, raises: [IOError].} = 293 | if val.kind == rbkMany: 294 | w.writeArray(val.many) 295 | else: 296 | w.writeValue(val.single) 297 | 298 | proc readValue*(r: var JsonReader[JrpcSys], val: var ResponseBatchRx) 299 | {.gcsafe, raises: [IOError, SerializationError].} = 300 | let tok = r.tokKind 301 | case tok 302 | of JsonValueKind.Array: 303 | val = ResponseBatchRx(kind: rbkMany) 304 | r.readValue(val.many) 305 | of JsonValueKind.Object: 306 | val = ResponseBatchRx(kind: rbkSingle) 307 | r.readValue(val.single) 308 | else: 309 | r.raiseUnexpectedValue("ResponseBatch must be either array or object, got=" & $tok) 310 | 311 | func toTx*(params: RequestParamsRx): RequestParamsTx = 312 | case params.kind: 313 | of rpPositional: 314 | result = RequestParamsTx(kind: rpPositional) 315 | for x in params.positional: 316 | result.positional.add x.param 317 | of rpNamed: 318 | result = RequestParamsTx(kind: rpNamed) 319 | result.named = params.named 320 | 321 | {.pop.} 322 | -------------------------------------------------------------------------------- /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/stew/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 | -------------------------------------------------------------------------------- /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 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: RequestParamsTx, id: RequestId): RequestTx = 67 | RequestTx( 68 | id: Opt.some(id), 69 | `method`: name, 70 | params: params, 71 | ) 72 | -------------------------------------------------------------------------------- /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/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, tables, json], 14 | chronicles, 15 | chronos, 16 | ./private/server_handler_wrapper, 17 | ./errors, 18 | ./private/jrpc_sys, 19 | ./jsonmarshal 20 | 21 | export 22 | chronos, 23 | jsonmarshal, 24 | json 25 | 26 | type 27 | # Procedure signature accepted as an RPC call by server 28 | RpcProc* = proc(params: RequestParamsRx): Future[JsonString] 29 | {.async.} 30 | 31 | RpcRouter* = object 32 | procs*: Table[string, RpcProc] 33 | 34 | const 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 | defaultMaxRequestLength* = 1024 * 128 44 | 45 | # ------------------------------------------------------------------------------ 46 | # Private helpers 47 | # ------------------------------------------------------------------------------ 48 | 49 | func invalidRequest(msg: string): ResponseError = 50 | ResponseError(code: INVALID_REQUEST, message: msg) 51 | 52 | func methodNotFound(msg: string): ResponseError = 53 | ResponseError(code: METHOD_NOT_FOUND, message: msg) 54 | 55 | func serverError(msg: string, data: JsonString): ResponseError = 56 | ResponseError(code: SERVER_ERROR, message: msg, data: Opt.some(data)) 57 | 58 | func somethingError(code: int, msg: string): ResponseError = 59 | ResponseError(code: code, message: msg) 60 | 61 | func applicationError(code: int, msg: string, data: Opt[JsonString]): ResponseError = 62 | ResponseError(code: code, message: msg, data: data) 63 | 64 | proc validateRequest(router: RpcRouter, req: RequestRx): 65 | Result[RpcProc, ResponseError] = 66 | if req.jsonrpc.isNone: 67 | return invalidRequest("'jsonrpc' missing or invalid").err 68 | 69 | if req.id.kind == riNull: 70 | return invalidRequest("'id' missing or invalid").err 71 | 72 | if req.meth.isNone: 73 | return invalidRequest("'method' missing or invalid").err 74 | 75 | let 76 | methodName = req.meth.get 77 | rpcProc = router.procs.getOrDefault(methodName) 78 | 79 | if rpcProc.isNil: 80 | return methodNotFound("'" & methodName & 81 | "' is not a registered RPC method").err 82 | 83 | ok(rpcProc) 84 | 85 | proc wrapError(err: ResponseError, id: RequestId): ResponseTx = 86 | ResponseTx( 87 | id: id, 88 | kind: rkError, 89 | error: err, 90 | ) 91 | 92 | proc wrapError(code: int, msg: string, id: RequestId): ResponseTx = 93 | ResponseTx( 94 | id: id, 95 | kind: rkError, 96 | error: somethingError(code, msg), 97 | ) 98 | 99 | proc wrapReply(res: JsonString, id: RequestId): ResponseTx = 100 | ResponseTx( 101 | id: id, 102 | kind: rkResult, 103 | result: res, 104 | ) 105 | 106 | proc wrapError(code: int, msg: string): string = 107 | """{"jsonrpc":"2.0","id":null,"error":{"code":""" & $code & 108 | ""","message":""" & escapeJson(msg) & "}}" 109 | 110 | # ------------------------------------------------------------------------------ 111 | # Public functions 112 | # ------------------------------------------------------------------------------ 113 | 114 | proc init*(T: type RpcRouter): T = discard 115 | 116 | proc register*(router: var RpcRouter, path: string, call: RpcProc) = 117 | # this proc should not raise exception 118 | try: 119 | router.procs[path] = call 120 | except CatchableError as exc: 121 | doAssert(false, exc.msg) 122 | 123 | proc clear*(router: var RpcRouter) = 124 | router.procs.clear 125 | 126 | proc hasMethod*(router: RpcRouter, methodName: string): bool = 127 | router.procs.hasKey(methodName) 128 | 129 | proc route*(router: RpcRouter, req: RequestRx): 130 | Future[ResponseTx] {.async: (raises: []).} = 131 | let rpcProc = router.validateRequest(req).valueOr: 132 | return wrapError(error, req.id) 133 | 134 | try: 135 | debug "Processing JSON-RPC request", id = req.id, name = req.`method`.get() 136 | let res = await rpcProc(req.params) 137 | debug "Returning JSON-RPC response", 138 | id = req.id, name = req.`method`.get(), len = string(res).len 139 | return wrapReply(res, req.id) 140 | except ApplicationError as err: 141 | return wrapError(applicationError(err.code, err.msg, err.data), req.id) 142 | except InvalidRequest as err: 143 | # TODO: deprecate / remove this usage and use InvalidRequest only for 144 | # internal errors. 145 | return wrapError(err.code, err.msg, req.id) 146 | except CatchableError as err: 147 | # Note: Errors that are not specifically raised as `ApplicationError`s will 148 | # be returned as custom server errors. 149 | let methodName = req.meth.get # this Opt already validated 150 | debug "Error occurred within RPC", 151 | methodName = methodName, err = err.msg 152 | return serverError("`" & methodName & "` raised an exception", 153 | escapeJson(err.msg).JsonString). 154 | wrapError(req.id) 155 | 156 | proc route*(router: RpcRouter, data: string|seq[byte]): 157 | Future[string] {.async: (raises: []).} = 158 | ## Route to RPC from string data. Data is expected to be able to be 159 | ## converted to Json. 160 | ## Returns string of Json from RPC result/error node 161 | let request = 162 | try: 163 | JrpcSys.decode(data, RequestBatchRx) 164 | except CatchableError as err: 165 | return wrapError(JSON_PARSE_ERROR, err.msg) 166 | 167 | try: 168 | if request.kind == rbkSingle: 169 | let response = await router.route(request.single) 170 | JrpcSys.encode(response) 171 | elif request.many.len == 0: 172 | wrapError(INVALID_REQUEST, "no request object in request array") 173 | else: 174 | var resFut: seq[Future[ResponseTx]] 175 | for req in request.many: 176 | resFut.add router.route(req) 177 | await noCancel(allFutures(resFut)) 178 | var response = ResponseBatchTx(kind: rbkMany) 179 | for fut in resFut: 180 | response.many.add fut.read() 181 | JrpcSys.encode(response) 182 | except CatchableError as err: 183 | wrapError(JSON_ENCODE_ERROR, err.msg) 184 | 185 | macro rpc*(server: RpcRouter, path: static[string], body: untyped): untyped = 186 | ## Define a remote procedure call. 187 | ## Input and return parameters are defined using the ``do`` notation. 188 | ## For example: 189 | ## .. code-block:: nim 190 | ## myServer.rpc("path") do(param1: int, param2: float) -> string: 191 | ## result = $param1 & " " & $param2 192 | ## ``` 193 | ## Input parameters are automatically marshalled from json to Nim types, 194 | ## and output parameters are automatically marshalled to json for transport. 195 | let 196 | params = body.findChild(it.kind == nnkFormalParams) 197 | procBody = if body.kind == nnkStmtList: body else: body.body 198 | procWrapper = genSym(nskProc, $path & "_rpcWrapper") 199 | 200 | result = wrapServerHandler($path, params, procBody, procWrapper) 201 | 202 | result.add quote do: 203 | `server`.register(`path`, `procWrapper`) 204 | 205 | when defined(nimDumpRpcs): 206 | echo "\n", path, ": ", result.repr 207 | 208 | {.pop.} 209 | -------------------------------------------------------------------------------- /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/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.} = 58 | let res = await client.call(name, params.toTx) 59 | return res 60 | 61 | proc getClient*(proxy: RpcProxy): RpcClient = 62 | case proxy.kind 63 | of Http: 64 | proxy.httpClient 65 | of WebSocket: 66 | proxy.webSocketClient 67 | 68 | proc new*(T: type RpcProxy, server: RpcHttpServer, cfg: ClientConfig): T = 69 | case cfg.kind 70 | of Http: 71 | let client = newRpcHttpClient() 72 | return T(rpcHttpServer: server, kind: Http, httpUri: cfg.httpUri, httpClient: client) 73 | of WebSocket: 74 | let client = newRpcWebSocketClient() 75 | return T( 76 | rpcHttpServer: server, 77 | kind: WebSocket, 78 | wsUri: cfg.wsUri, 79 | webSocketClient: client, 80 | compression: cfg.compression, 81 | flags: cfg.flags 82 | ) 83 | 84 | proc new*( 85 | T: type RpcProxy, 86 | listenAddresses: openArray[TransportAddress], 87 | cfg: ClientConfig, 88 | authHooks: seq[HttpAuthHook] = @[] 89 | ): T {.raises: [CatchableError].} = 90 | RpcProxy.new(newRpcHttpServer(listenAddresses, RpcRouter.init(), authHooks), cfg) 91 | 92 | proc new*( 93 | T: type RpcProxy, 94 | listenAddresses: openArray[string], 95 | cfg: ClientConfig, 96 | authHooks: seq[HttpAuthHook] = @[]): T {.raises: [CatchableError].} = 97 | RpcProxy.new(newRpcHttpServer(listenAddresses, RpcRouter.init(), authHooks), cfg) 98 | 99 | proc connectToProxy(proxy: RpcProxy): Future[void] = 100 | case proxy.kind 101 | of Http: 102 | return proxy.httpClient.connect(proxy.httpUri) 103 | of WebSocket: 104 | return proxy.webSocketClient.connect( 105 | uri = proxy.wsUri, 106 | compression = proxy.compression, 107 | flags = proxy.flags) 108 | 109 | proc start*(proxy: RpcProxy) {.async.} = 110 | proxy.rpcHttpServer.start() 111 | await proxy.connectToProxy() 112 | 113 | template rpc*(server: RpcProxy, path: string, body: untyped): untyped = 114 | server.rpcHttpServer.rpc(path, body) 115 | 116 | proc registerProxyMethod*(proxy: var RpcProxy, methodName: string) = 117 | try: 118 | proxy.rpcHttpServer.register(methodName, proxyCall(proxy.getClient(), methodName)) 119 | except CatchableError as err: 120 | # Adding proc type to table gives invalid exception tracking, see Nim bug: https://github.com/nim-lang/Nim/issues/18376 121 | raiseAssert err.msg 122 | 123 | proc stop*(proxy: RpcProxy) {.async.} = 124 | await proxy.getClient().close() 125 | await proxy.rpcHttpServer.stop() 126 | 127 | proc closeWait*(proxy: RpcProxy) {.async.} = 128 | await proxy.rpcHttpServer.closeWait() 129 | 130 | func localAddress*(proxy: RpcProxy): seq[TransportAddress] = 131 | proxy.rpcHttpServer.localAddress() 132 | 133 | {.pop.} 134 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, 14 | chronos, 15 | ./router, 16 | ./jsonmarshal, 17 | ./private/jrpc_sys, 18 | ./private/shared_wrapper, 19 | ./errors 20 | 21 | export 22 | chronos, 23 | jsonmarshal, 24 | router 25 | 26 | type 27 | RpcServer* = ref object of RootRef 28 | router*: RpcRouter 29 | 30 | # ------------------------------------------------------------------------------ 31 | # Constructors 32 | # ------------------------------------------------------------------------------ 33 | 34 | proc new*(T: type RpcServer): T = 35 | T(router: RpcRouter.init()) 36 | 37 | # ------------------------------------------------------------------------------ 38 | # Public functions 39 | # ------------------------------------------------------------------------------ 40 | 41 | template rpc*(server: RpcServer, path: string, body: untyped): untyped = 42 | server.router.rpc(path, body) 43 | 44 | template hasMethod*(server: RpcServer, methodName: string): bool = 45 | server.router.hasMethod(methodName) 46 | 47 | proc executeMethod*(server: RpcServer, 48 | methodName: string, 49 | params: RequestParamsTx): Future[JsonString] {.async: (raises: [CancelledError, JsonRpcError]).} = 50 | 51 | let 52 | req = requestTx(methodName, params, RequestId(kind: riNumber, num: 0)) 53 | reqData = JrpcSys.encode(req) 54 | respData = await server.router.route(reqData) 55 | resp = try: 56 | JrpcSys.decode(respData, ResponseRx) 57 | except CatchableError as exc: 58 | raise (ref JsonRpcError)(msg: exc.msg) 59 | 60 | if resp.error.isSome: 61 | raise (ref JsonRpcError)(msg: $resp.error.get) 62 | resp.result 63 | 64 | proc executeMethod*(server: RpcServer, 65 | methodName: string, 66 | args: JsonNode): Future[JsonString] {.async: (raises: [CancelledError, JsonRpcError], raw: true).} = 67 | 68 | let params = paramsTx(args) 69 | server.executeMethod(methodName, params) 70 | 71 | proc executeMethod*(server: RpcServer, 72 | methodName: string, 73 | args: JsonString): Future[JsonString] {.async: (raises: [CancelledError, JsonRpcError]).} = 74 | 75 | let params = try: 76 | let x = JrpcSys.decode(args.string, RequestParamsRx) 77 | x.toTx 78 | except SerializationError as exc: 79 | raise newException(JsonRpcError, exc.msg) 80 | 81 | await server.executeMethod(methodName, params) 82 | 83 | # Wrapper for message processing 84 | 85 | proc route*(server: RpcServer, line: string): Future[string] {.async: (raises: [], raw: true).} = 86 | server.router.route(line) 87 | proc route*(server: RpcServer, line: seq[byte]): Future[string] {.async: (raises: [], raw: true).} = 88 | server.router.route(line) 89 | 90 | # Server registration 91 | 92 | proc register*(server: RpcServer, name: string, rpc: RpcProc) = 93 | ## Add a name/code pair to the RPC server. 94 | server.router.register(name, rpc) 95 | 96 | proc unRegisterAll*(server: RpcServer) = 97 | # Remove all remote procedure calls from this server. 98 | server.router.clear 99 | 100 | {.pop.} 101 | -------------------------------------------------------------------------------- /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 | ../private/utils, 17 | ../errors, 18 | ../server 19 | 20 | export 21 | server, shttpserver 22 | 23 | logScope: 24 | topics = "JSONRPC-HTTP-SERVER" 25 | 26 | const 27 | JsonRpcIdent = "nim-json-rpc" 28 | 29 | type 30 | # HttpAuthHook: handle CORS, JWT auth, etc. in HTTP header 31 | # before actual request processed 32 | # return value: 33 | # - nil: auth success, continue execution 34 | # - HttpResponse: could not authenticate, stop execution 35 | # and return the response 36 | HttpAuthHook* = 37 | proc(request: HttpRequestRef): Future[HttpResponseRef] {.async: (raises: [CatchableError]).} 38 | 39 | # This inheritance arrangement is useful for 40 | # e.g. combo HTTP server 41 | RpcHttpHandler* = ref object of RpcServer 42 | maxChunkSize*: int 43 | 44 | RpcHttpServer* = ref object of RpcHttpHandler 45 | httpServers: seq[HttpServerRef] 46 | authHooks: seq[HttpAuthHook] 47 | 48 | proc serveHTTP*(rpcServer: RpcHttpHandler, request: HttpRequestRef): 49 | Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = 50 | try: 51 | let req = await request.getBody() 52 | debug "Received JSON-RPC request", 53 | address = request.remote().valueOr(default(TransportAddress)), 54 | len = req.len 55 | 56 | let 57 | data = await rpcServer.route(req) 58 | chunkSize = rpcServer.maxChunkSize 59 | streamType = 60 | if data.len <= chunkSize: 61 | HttpResponseStreamType.Plain 62 | else: 63 | HttpResponseStreamType.Chunked 64 | response = request.getResponse() 65 | 66 | response.addHeader("Content-Type", "application/json") 67 | 68 | await response.prepare(streamType) 69 | let maxLen = data.len 70 | 71 | var len = data.len 72 | while len > chunkSize: 73 | await response.send(data[maxLen - len].unsafeAddr, chunkSize) 74 | len -= chunkSize 75 | 76 | if len > 0: 77 | await response.send(data[maxLen - len].unsafeAddr, len) 78 | 79 | await response.finish() 80 | response 81 | except CancelledError as exc: 82 | raise exc 83 | except CatchableError as exc: 84 | debug "Internal error while processing JSON-RPC call", msg=exc.msg 85 | defaultResponse(exc) 86 | 87 | proc processClientRpc(rpcServer: RpcHttpServer): HttpProcessCallback2 = 88 | return proc (req: RequestFence): Future[HttpResponseRef] 89 | {.async: (raises: [CancelledError]).} = 90 | if not req.isOk(): 91 | debug "Got invalid request", err = req.error() 92 | return defaultResponse() 93 | 94 | let request = req.get() 95 | # if hook result is not nil, 96 | # it means we should return immediately 97 | try: 98 | for hook in rpcServer.authHooks: 99 | let res = await hook(request) 100 | if not res.isNil: 101 | return res 102 | except CancelledError as exc: 103 | raise exc 104 | except CatchableError as exc: 105 | error "Internal error while processing JSON-RPC hook", msg=exc.msg 106 | return defaultResponse(exc) 107 | 108 | return await rpcServer.serveHTTP(request) 109 | 110 | proc addHttpServer*( 111 | rpcServer: RpcHttpServer, 112 | address: TransportAddress, 113 | socketFlags: set[ServerFlags] = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}, 114 | serverUri = Uri(), 115 | serverIdent = "", 116 | maxConnections: int = -1, 117 | bufferSize: int = 4096, 118 | backlogSize: int = 100, 119 | httpHeadersTimeout = 10.seconds, 120 | maxHeadersSize: int = 8192, 121 | maxRequestBodySize: int = 1_048_576) {.raises: [JsonRpcError].} = 122 | let server = HttpServerRef.new( 123 | address, 124 | processClientRpc(rpcServer), 125 | {}, 126 | socketFlags, 127 | serverUri, JsonRpcIdent, maxConnections, backlogSize, 128 | bufferSize, httpHeadersTimeout, maxHeadersSize, maxRequestBodySize 129 | ).valueOr: 130 | error "Failed to create server", address = $address, 131 | message = error 132 | raise newException(RpcBindError, "Unable to create server: " & $error) 133 | 134 | rpcServer.httpServers.add server 135 | 136 | proc addSecureHttpServer*( 137 | rpcServer: RpcHttpServer, 138 | address: TransportAddress, 139 | tlsPrivateKey: TLSPrivateKey, 140 | tlsCertificate: TLSCertificate, 141 | socketFlags: set[ServerFlags] = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}, 142 | serverUri = Uri(), 143 | serverIdent: string = JsonRpcIdent, 144 | secureFlags: set[TLSFlags] = {}, 145 | maxConnections: int = -1, 146 | backlogSize: int = 100, 147 | bufferSize: int = 4096, 148 | httpHeadersTimeout = 10.seconds, 149 | maxHeadersSize: int = 8192, 150 | maxRequestBodySize: int = 1_048_576) {.raises: [JsonRpcError].} = 151 | let server = SecureHttpServerRef.new( 152 | address, 153 | processClientRpc(rpcServer), 154 | tlsPrivateKey, 155 | tlsCertificate, 156 | {HttpServerFlags.Secure}, 157 | socketFlags, 158 | serverUri, JsonRpcIdent, secureFlags, maxConnections, backlogSize, 159 | bufferSize, httpHeadersTimeout, maxHeadersSize, maxRequestBodySize 160 | ).valueOr: 161 | error "Failed to create server", address = $address, 162 | message = error 163 | raise newException(RpcBindError, "Unable to create server: " & $error) 164 | 165 | rpcServer.httpServers.add server 166 | 167 | proc addHttpServers*(server: RpcHttpServer, 168 | addresses: openArray[TransportAddress]) {.raises: [JsonRpcError].} = 169 | ## Start a server on at least one of the given addresses, or raise 170 | if addresses.len == 0: 171 | return 172 | 173 | var lastExc: ref JsonRpcError 174 | for item in addresses: 175 | try: 176 | server.addHttpServer(item) 177 | except JsonRpcError as exc: 178 | lastExc = exc 179 | if server.httpServers.len == 0: 180 | raise lastExc 181 | 182 | proc addSecureHttpServers*(server: RpcHttpServer, 183 | addresses: openArray[TransportAddress], 184 | tlsPrivateKey: TLSPrivateKey, 185 | tlsCertificate: TLSCertificate) {.raises: [JsonRpcError].} = 186 | ## Start a server on at least one of the given addresses, or raise 187 | if addresses.len == 0: 188 | return 189 | 190 | var lastExc: ref JsonRpcError 191 | for item in addresses: 192 | try: 193 | server.addSecureHttpServer(item, tlsPrivateKey, tlsCertificate) 194 | except JsonRpcError as exc: 195 | lastExc = exc 196 | if server.httpServers.len == 0: 197 | raise lastExc 198 | 199 | proc addHttpServer*(server: RpcHttpServer, address: string) {.raises: [JsonRpcError].} = 200 | ## Create new server and assign it to addresses ``addresses``. 201 | addHttpServers(server, toSeq(resolveIP([address]))) 202 | 203 | proc addSecureHttpServer*(server: RpcHttpServer, 204 | address: string, 205 | tlsPrivateKey: TLSPrivateKey, 206 | tlsCertificate: TLSCertificate) {.raises: [JsonRpcError].} = 207 | addSecureHttpServers(server, toSeq(resolveIP([address])), tlsPrivateKey, tlsCertificate) 208 | 209 | proc addHttpServers*(server: RpcHttpServer, addresses: openArray[string]) {.raises: [JsonRpcError].} = 210 | addHttpServers(server, toSeq(resolveIP(addresses))) 211 | 212 | proc addHttpServer*(server: RpcHttpServer, address: string, port: Port) {.raises: [JsonRpcError].} = 213 | addHttpServers(server, toSeq(resolveIP(address, port))) 214 | 215 | proc addSecureHttpServer*(server: RpcHttpServer, 216 | address: string, 217 | port: Port, 218 | tlsPrivateKey: TLSPrivateKey, 219 | tlsCertificate: TLSCertificate) {.raises: [JsonRpcError].} = 220 | addSecureHttpServers(server, toSeq(resolveIP(address, port)), tlsPrivateKey, tlsCertificate) 221 | 222 | proc new*(T: type RpcHttpServer, authHooks: seq[HttpAuthHook] = @[]): T = 223 | T(router: RpcRouter.init(), httpServers: @[], authHooks: authHooks, maxChunkSize: 8192) 224 | 225 | proc new*(T: type RpcHttpServer, router: RpcRouter, authHooks: seq[HttpAuthHook] = @[]): T = 226 | T(router: router, httpServers: @[], authHooks: authHooks, maxChunkSize: 8192) 227 | 228 | proc newRpcHttpServer*(authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer = 229 | RpcHttpServer.new(authHooks) 230 | 231 | proc newRpcHttpServer*(router: RpcRouter, authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer = 232 | RpcHttpServer.new(router, authHooks) 233 | 234 | proc newRpcHttpServer*(addresses: openArray[TransportAddress], authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer {.raises: [JsonRpcError].} = 235 | ## Create new server and assign it to addresses ``addresses``. 236 | result = newRpcHttpServer(authHooks) 237 | result.addHttpServers(addresses) 238 | 239 | proc newRpcHttpServer*(addresses: openArray[string], authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer {.raises: [JsonRpcError].} = 240 | ## Create new server and assign it to addresses ``addresses``. 241 | result = newRpcHttpServer(authHooks) 242 | result.addHttpServers(addresses) 243 | 244 | proc newRpcHttpServer*(addresses: openArray[string], router: RpcRouter, authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer {.raises: [JsonRpcError].} = 245 | ## Create new server and assign it to addresses ``addresses``. 246 | result = newRpcHttpServer(router, authHooks) 247 | result.addHttpServers(addresses) 248 | 249 | proc newRpcHttpServer*(addresses: openArray[TransportAddress], router: RpcRouter, authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer {.raises: [JsonRpcError].} = 250 | ## Create new server and assign it to addresses ``addresses``. 251 | result = newRpcHttpServer(router, authHooks) 252 | result.addHttpServers(addresses) 253 | 254 | proc start*(server: RpcHttpServer) = 255 | ## Start the RPC server. 256 | for item in server.httpServers: 257 | info "Starting JSON-RPC HTTP server", url = item.baseUri 258 | item.start() 259 | 260 | proc stop*(server: RpcHttpServer) {.async.} = 261 | ## Stop the RPC server. 262 | for item in server.httpServers: 263 | await item.stop() 264 | info "Stopped JSON-RPC HTTP server", url = item.baseUri 265 | 266 | proc closeWait*(server: RpcHttpServer) {.async.} = 267 | ## Cleanup resources of RPC server. 268 | for item in server.httpServers: 269 | await item.closeWait() 270 | 271 | proc localAddress*(server: RpcHttpServer): seq[TransportAddress] = 272 | for item in server.httpServers: 273 | result.add item.instance.localAddress() 274 | 275 | proc setMaxChunkSize*(server: RpcHttpServer, maxChunkSize: int) = 276 | server.maxChunkSize = maxChunkSize 277 | -------------------------------------------------------------------------------- /json_rpc/servers/socketserver.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, 15 | json_serialization/std/net, 16 | ../private/utils, 17 | ../errors, 18 | ../server 19 | 20 | export errors, server 21 | 22 | type 23 | RpcSocketServer* = ref object of RpcServer 24 | servers: seq[StreamServer] 25 | processClientHook: StreamCallback2 26 | 27 | proc processClient(server: StreamServer, transport: StreamTransport) {.async: (raises: []).} = 28 | ## Process transport data to the RPC server 29 | try: 30 | var rpc = getUserData[RpcSocketServer](server) 31 | while true: 32 | let req = await transport.readLine(defaultMaxRequestLength) 33 | if req == "": 34 | await transport.closeWait() 35 | break 36 | 37 | debug "Received JSON-RPC request", 38 | address = transport.remoteAddress(), 39 | len = req.len 40 | 41 | let res = await rpc.route(req) 42 | discard await transport.write(res & "\r\n") 43 | except TransportError as ex: 44 | error "Transport closed during processing client", 45 | address = transport.remoteAddress(), 46 | msg=ex.msg 47 | except CancelledError: 48 | error "JSON-RPC request processing cancelled", 49 | address = transport.remoteAddress() 50 | 51 | # Utility functions for setting up servers using stream transport addresses 52 | 53 | proc addStreamServer*(server: RpcSocketServer, address: TransportAddress) {.raises: [JsonRpcError].} = 54 | try: 55 | var transportServer = createStreamServer(address, server.processClientHook, {ReuseAddr}, udata = server) 56 | server.servers.add(transportServer) 57 | except CatchableError as exc: 58 | error "Failed to create server", address = $address, message = exc.msg 59 | raise newException(RpcBindError, "Unable to create stream server: " & exc.msg) 60 | 61 | proc addStreamServers*(server: RpcSocketServer, addresses: openArray[TransportAddress]) {.raises: [JsonRpcError].} = 62 | var lastExc: ref JsonRpcError 63 | for item in addresses: 64 | try: 65 | server.addStreamServer(item) 66 | except JsonRpcError as exc: 67 | lastExc = exc 68 | if server.servers.len == 0: 69 | raise lastExc 70 | 71 | proc addStreamServer*(server: RpcSocketServer, address: string) {.raises: [JsonRpcError].} = 72 | ## Create new server and assign it to addresses ``addresses``. 73 | addStreamServers(server, toSeq(resolveIP([address]))) 74 | 75 | proc addStreamServers*(server: RpcSocketServer, addresses: openArray[string]) {.raises: [JsonRpcError].} = 76 | addStreamServers(server, toSeq(resolveIP(addresses))) 77 | 78 | proc addStreamServer*(server: RpcSocketServer, address: string, port: Port) {.raises: [JsonRpcError].} = 79 | addStreamServers(server, toSeq(resolveIP(address, port))) 80 | 81 | proc new(T: type RpcSocketServer): T = 82 | T(router: RpcRouter.init(), servers: @[], processClientHook: processClient) 83 | 84 | proc newRpcSocketServer*(): RpcSocketServer = 85 | RpcSocketServer.new() 86 | 87 | proc newRpcSocketServer*(addresses: openArray[TransportAddress]): RpcSocketServer {.raises: [JsonRpcError].} = 88 | ## Create new server and assign it to addresses ``addresses``. 89 | result = RpcSocketServer.new() 90 | result.addStreamServers(addresses) 91 | 92 | proc newRpcSocketServer*(addresses: openArray[string]): RpcSocketServer {.raises: [JsonRpcError].} = 93 | ## Create new server and assign it to addresses ``addresses``. 94 | result = RpcSocketServer.new() 95 | result.addStreamServers(addresses) 96 | 97 | proc newRpcSocketServer*(address: string, port: Port = Port(8545)): RpcSocketServer {.raises: [JsonRpcError].} = 98 | # Create server on specified port 99 | result = RpcSocketServer.new() 100 | result.addStreamServer(address, port) 101 | 102 | proc newRpcSocketServer*(processClientHook: StreamCallback2): RpcSocketServer = 103 | ## Create new server with custom processClientHook. 104 | result = RpcSocketServer.new() 105 | result.processClientHook = processClientHook 106 | 107 | proc start*(server: RpcSocketServer) {.raises: [JsonRpcError].} = 108 | ## Start the RPC server. 109 | for item in server.servers: 110 | try: 111 | info "Starting JSON-RPC socket server", address = item.localAddress 112 | item.start() 113 | except TransportOsError as exc: 114 | # TODO stop already-started servers 115 | raise (ref RpcBindError)(msg: exc.msg, parent: exc) 116 | 117 | proc stop*(server: RpcSocketServer) = 118 | ## Stop the RPC server. 119 | for item in server.servers: 120 | try: 121 | item.stop() 122 | except TransportOsError as exc: 123 | warn "Could not stop transport", err = exc.msg 124 | 125 | proc close*(server: RpcSocketServer) = 126 | ## Cleanup resources of RPC server. 127 | for item in server.servers: 128 | item.close() 129 | 130 | proc closeWait*(server: RpcSocketServer) {.async.} = 131 | ## Cleanup resources of RPC server. 132 | for item in server.servers: 133 | await item.closeWait() 134 | 135 | proc localAddress*(server: RpcSocketServer): seq[TransportAddress] = 136 | for x in server.servers: 137 | result.add x.localAddress 138 | -------------------------------------------------------------------------------- /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, chronos, websock/[websock, types], 14 | websock/extensions/compression/deflate, 15 | stew/byteutils, json_serialization/std/net, 16 | ".."/[errors, server] 17 | 18 | export errors, server, net 19 | 20 | logScope: 21 | topics = "JSONRPC-WS-SERVER" 22 | 23 | type 24 | # WsAuthHook: handle CORS, JWT auth, etc. in HTTP header 25 | # before actual request processed 26 | # return value: 27 | # - true: auth success, continue execution 28 | # - false: could not authenticate, stop execution 29 | # and return the response 30 | WsAuthHook* = 31 | proc(request: HttpRequest): Future[bool] {.async: (raises: [CatchableError]).} 32 | 33 | # This inheritance arrangement is useful for 34 | # e.g. combo HTTP server 35 | RpcWebSocketHandler* = ref object of RpcServer 36 | wsserver*: WSServer 37 | 38 | RpcWebSocketServer* = ref object of RpcWebSocketHandler 39 | server: StreamServer 40 | authHooks: seq[WsAuthHook] 41 | 42 | proc serveHTTP*(rpc: RpcWebSocketHandler, request: HttpRequest) 43 | {.async: (raises: [CancelledError]).} = 44 | try: 45 | let server = rpc.wsserver 46 | let ws = await server.handleRequest(request) 47 | if ws.readyState != ReadyState.Open: 48 | error "Failed to open websocket connection", 49 | address = $request.uri 50 | return 51 | 52 | trace "Websocket handshake completed" 53 | while ws.readyState != ReadyState.Closed: 54 | let req = await ws.recvMsg() 55 | debug "Received JSON-RPC request", 56 | address = $request.uri, 57 | len = req.len 58 | 59 | if ws.readyState == ReadyState.Closed: 60 | # if session already terminated by peer, 61 | # no need to send response 62 | break 63 | 64 | if req.len == 0: 65 | await ws.close( 66 | reason = "cannot process zero length message" 67 | ) 68 | break 69 | 70 | let data = await rpc.route(req) 71 | 72 | trace "RPC result has been sent", address = $request.uri 73 | await ws.send(data) 74 | 75 | except WebSocketError as exc: 76 | error "WebSocket error:", 77 | address = $request.uri, msg = exc.msg 78 | 79 | except CancelledError as exc: 80 | raise exc 81 | 82 | except CatchableError as exc: 83 | debug "Internal error while processing JSON-RPC call", msg=exc.msg 84 | 85 | proc handleRequest(rpc: RpcWebSocketServer, request: HttpRequest) 86 | {.async: (raises: [CancelledError]).} = 87 | trace "Handling request:", uri = request.uri.path 88 | trace "Initiating web socket connection." 89 | 90 | # if hook result is false, 91 | # it means we should return immediately 92 | try: 93 | for hook in rpc.authHooks: 94 | let res = await hook(request) 95 | if not res: 96 | return 97 | except CancelledError as exc: 98 | raise exc 99 | except CatchableError as exc: 100 | debug "Internal error while processing JSON-RPC hook", msg=exc.msg 101 | try: 102 | await request.sendResponse(Http503, 103 | data = "", 104 | content = "Internal error, processing JSON-RPC hook: " & exc.msg) 105 | return 106 | except CatchableError as exc: 107 | debug "Something error", msg=exc.msg 108 | return 109 | 110 | await rpc.serveHTTP(request) 111 | 112 | proc initWebsocket(rpc: RpcWebSocketServer, compression: bool, 113 | authHooks: seq[WsAuthHook], 114 | rng: ref HmacDrbgContext) = 115 | if compression: 116 | let deflateFactory = deflateFactory() 117 | rpc.wsserver = WSServer.new(factories = [deflateFactory], rng = rng) 118 | else: 119 | rpc.wsserver = WSServer.new(rng = rng) 120 | rpc.authHooks = authHooks 121 | 122 | proc newRpcWebSocketServer*( 123 | address: TransportAddress, 124 | compression: bool = false, 125 | flags: set[ServerFlags] = {ServerFlags.TcpNoDelay,ServerFlags.ReuseAddr}, 126 | authHooks: seq[WsAuthHook] = @[], 127 | rng = HmacDrbgContext.new()): RpcWebSocketServer {.raises: [JsonRpcError].} = 128 | 129 | var server = new(RpcWebSocketServer) 130 | proc processCallback(request: HttpRequest): Future[void] = 131 | handleRequest(server, request) 132 | 133 | server.initWebsocket(compression, authHooks, rng) 134 | try: 135 | server.server = HttpServer.create( 136 | address, 137 | processCallback, 138 | flags 139 | ) 140 | except CatchableError as exc: 141 | raise (ref RpcBindError)(msg: "Unable to create server: " & exc.msg, parent: exc) 142 | 143 | server 144 | 145 | proc newRpcWebSocketServer*( 146 | host: string, 147 | port: Port, 148 | compression: bool = false, 149 | flags: set[ServerFlags] = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}, 150 | authHooks: seq[WsAuthHook] = @[], 151 | rng = HmacDrbgContext.new()): RpcWebSocketServer {.raises: [JsonRpcError].} = 152 | 153 | try: 154 | newRpcWebSocketServer( 155 | initTAddress(host, port), 156 | compression, 157 | flags, 158 | authHooks, 159 | rng 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()): RpcWebSocketServer {.raises: [JsonRpcError].} = 176 | 177 | var server = new(RpcWebSocketServer) 178 | proc processCallback(request: HttpRequest): Future[void] = 179 | handleRequest(server, request) 180 | 181 | server.initWebsocket(compression, authHooks, rng) 182 | try: 183 | server.server = TlsHttpServer.create( 184 | address, 185 | tlsPrivateKey, 186 | tlsCertificate, 187 | processCallback, 188 | flags, 189 | tlsFlags, 190 | tlsMinVersion, 191 | tlsMaxVersion 192 | ) 193 | except CatchableError as exc: 194 | raise (ref RpcBindError)(msg: "Unable to create server: " & exc.msg, parent: exc) 195 | 196 | server 197 | 198 | proc newRpcWebSocketServer*( 199 | host: string, 200 | port: Port, 201 | tlsPrivateKey: TLSPrivateKey, 202 | tlsCertificate: TLSCertificate, 203 | compression: bool = false, 204 | flags: set[ServerFlags] = {ServerFlags.TcpNoDelay, 205 | ServerFlags.ReuseAddr}, 206 | tlsFlags: set[TLSFlags] = {}, 207 | tlsMinVersion = TLSVersion.TLS12, 208 | tlsMaxVersion = TLSVersion.TLS12, 209 | authHooks: seq[WsAuthHook] = @[], 210 | rng = HmacDrbgContext.new()): RpcWebSocketServer {.raises: [JsonRpcError].} = 211 | 212 | try: 213 | newRpcWebSocketServer( 214 | initTAddress(host, port), 215 | tlsPrivateKey, 216 | tlsCertificate, 217 | compression, 218 | flags, 219 | tlsFlags, 220 | tlsMinVersion, 221 | tlsMaxVersion, 222 | authHooks, 223 | rng 224 | ) 225 | except TransportError as exc: 226 | raise (ref RpcBindError)(msg: "Unable to create server: " & exc.msg, parent: exc) 227 | 228 | proc start*(server: RpcWebSocketServer) {.raises: [JsonRpcError].} = 229 | ## Start the RPC server. 230 | try: 231 | info "Starting JSON-RPC WebSocket server", address = server.server.local 232 | server.server.start() 233 | except TransportOsError as exc: 234 | raise (ref RpcBindError)(msg: "Unable to start server: " & exc.msg, parent: exc) 235 | 236 | proc stop*(server: RpcWebSocketServer) = 237 | ## Stop the RPC server. 238 | try: 239 | server.server.stop() 240 | notice "Stopped JSON-RPC WebSocket server", address = server.server.local 241 | except TransportOsError as exc: 242 | warn "Could not stop JSON-RPC WebSocket server", err = exc.msg 243 | 244 | proc close*(server: RpcWebSocketServer) = 245 | ## Cleanup resources of RPC server. 246 | server.server.close() 247 | 248 | proc closeWait*(server: RpcWebSocketServer) {.async.} = 249 | ## Cleanup resources of RPC server. 250 | await server.server.closeWait() 251 | 252 | proc localAddress*(server: RpcWebSocketServer): TransportAddress = 253 | server.server.localAddress() 254 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/private/ethhexstrings.nim: -------------------------------------------------------------------------------- 1 | import 2 | ../../json_rpc/errors 3 | 4 | type 5 | HexQuantityStr* = distinct string 6 | HexDataStr* = distinct string 7 | 8 | # Hex validation 9 | 10 | {.push gcsafe, raises: [].} 11 | 12 | template stripLeadingZeros(value: string): string = 13 | var cidx = 0 14 | # ignore the last character so we retain '0' on zero value 15 | while cidx < value.len - 1 and value[cidx] == '0': 16 | cidx.inc 17 | value[cidx .. ^1] 18 | 19 | proc encodeQuantity*(value: SomeUnsignedInt): string = 20 | var hValue = value.toHex.stripLeadingZeros 21 | result = "0x" & hValue 22 | 23 | func hasHexHeader*(value: string): bool = 24 | if value != "" and value[0] == '0' and value[1] in {'x', 'X'} and value.len > 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.. 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 | -------------------------------------------------------------------------------- /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 16 | from strutils import rsplit 17 | template sourceDir: string = currentSourcePath.rsplit(DirSep, 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 | -------------------------------------------------------------------------------- /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 | 18 | createRpcSigsFromNim(RpcClient): 19 | proc get_Banana(id: int): int 20 | 21 | proc installHandlers(s: RpcServer) = 22 | s.rpc("get_Banana") do(id: int) -> JsonString: 23 | if id == 99: 24 | return "123".JsonString 25 | elif id == 100: 26 | return "\"stop\"".JsonString 27 | else: 28 | return "\"error\"".JsonString 29 | 30 | type 31 | Shadow = ref object 32 | something: int 33 | 34 | proc setupClientHook(client: RpcClient): Shadow = 35 | var shadow = Shadow(something: 0) 36 | client.onProcessMessage = proc(client: RpcClient, line: string): 37 | Result[bool, string] {.gcsafe, raises: [].} = 38 | 39 | try: 40 | let m = JrpcConv.decode(line, JsonNode) 41 | if m["result"].kind == JString: 42 | if m["result"].str == "stop": 43 | shadow.something = 123 44 | return ok(false) 45 | else: 46 | shadow.something = 77 47 | return err("not stop") 48 | 49 | return ok(true) 50 | except CatchableError as exc: 51 | return err(exc.msg) 52 | shadow 53 | 54 | suite "test client features": 55 | var server = newRpcHttpServer(["127.0.0.1:0"]) 56 | server.installHandlers() 57 | var client = newRpcHttpClient() 58 | let shadow = client.setupClientHook() 59 | 60 | server.start() 61 | waitFor client.connect("http://" & $server.localAddress()[0]) 62 | 63 | test "client onProcessMessage hook": 64 | let res = waitFor client.get_Banana(99) 65 | check res == 123 66 | check shadow.something == 0 67 | 68 | expect JsonRpcError: 69 | let res2 = waitFor client.get_Banana(123) 70 | check res2 == 0 71 | check shadow.something == 77 72 | 73 | expect InvalidResponse: 74 | let res2 = waitFor client.get_Banana(100) 75 | check res2 == 0 76 | check shadow.something == 123 77 | 78 | waitFor server.stop() 79 | waitFor server.closeWait() 80 | 81 | 82 | type 83 | TestSocketServer = ref object of RpcSocketServer 84 | getData: proc(): string {.gcsafe, raises: [].} 85 | 86 | proc processClient(server: StreamServer, transport: StreamTransport) {.async: (raises: []), gcsafe.} = 87 | ## Process transport data to the RPC server 88 | try: 89 | var rpc = getUserData[TestSocketServer](server) 90 | while true: 91 | var 92 | value = await transport.readLine(router.defaultMaxRequestLength) 93 | if value == "": 94 | await transport.closeWait() 95 | break 96 | 97 | let res = rpc.getData() 98 | discard await transport.write(res & "\r\n") 99 | except TransportError as ex: 100 | error "Transport closed during processing client", msg=ex.msg 101 | except CatchableError as ex: 102 | error "Error occured during processing client", msg=ex.msg 103 | 104 | proc addStreamServer(server: TestSocketServer, address: TransportAddress) = 105 | privateAccess(RpcSocketServer) 106 | try: 107 | info "Starting JSON-RPC socket server", address = $address 108 | var transportServer = createStreamServer(address, processClient, {ReuseAddr}, udata = server) 109 | server.servers.add(transportServer) 110 | except CatchableError as exc: 111 | error "Failed to create server", address = $address, message = exc.msg 112 | 113 | if len(server.servers) == 0: 114 | # Server was not bound, critical error. 115 | raise newException(RpcBindError, "Unable to create server!") 116 | 117 | proc new(T: type TestSocketServer, getData: proc(): string {.gcsafe, raises: [].}): T = 118 | T( 119 | router: RpcRouter.init(), 120 | getData: getData, 121 | ) 122 | 123 | 124 | suite "test rpc socket client": 125 | let server = TestSocketServer.new(proc(): string {.gcsafe, raises: [].} = 126 | return """{"jsonrpc":"2.0","result":10}""" 127 | ) 128 | let serverAddress = initTAddress("127.0.0.1:0") 129 | server.addStreamServer(serverAddress) 130 | 131 | var client = newRpcSocketClient() 132 | server.start() 133 | waitFor client.connect(server.localAddress()[0]) 134 | 135 | test "missing id in server response": 136 | expect JsonRpcError: 137 | let res = waitFor client.get_Banana(11) 138 | discard res 139 | 140 | server.stop() 141 | waitFor server.closeWait() 142 | 143 | 144 | type 145 | TestHttpServer = ref object of RpcHttpServer 146 | getData: proc(): string {.gcsafe, raises: [].} 147 | 148 | proc processClientRpc(rpcServer: TestHttpServer): HttpProcessCallback2 = 149 | return proc (req: RequestFence): Future[HttpResponseRef] 150 | {.async: (raises: [CancelledError]).} = 151 | if not req.isOk(): 152 | return defaultResponse() 153 | 154 | let 155 | request = req.get() 156 | headers = HttpTable.init([("Content-Type", 157 | "application/json; charset=utf-8")]) 158 | try: 159 | let data = rpcServer.getData() 160 | let res = await request.respond(Http200, data, headers) 161 | trace "JSON-RPC result has been sent" 162 | return res 163 | except CancelledError as exc: 164 | raise exc 165 | except CatchableError as exc: 166 | debug "Internal error while processing JSON-RPC call" 167 | return defaultResponse(exc) 168 | 169 | proc addHttpServer( 170 | rpcServer: TestHttpServer, 171 | address: TransportAddress, 172 | socketFlags: set[ServerFlags] = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}, 173 | serverUri = Uri(), 174 | serverIdent = "", 175 | maxConnections: int = -1, 176 | bufferSize: int = 4096, 177 | backlogSize: int = 100, 178 | httpHeadersTimeout = 10.seconds, 179 | maxHeadersSize: int = 8192, 180 | maxRequestBodySize: int = 1_048_576) = 181 | let server = HttpServerRef.new( 182 | address, 183 | processClientRpc(rpcServer), 184 | {}, 185 | socketFlags, 186 | serverUri, "nim-json-rpc", maxConnections, backlogSize, 187 | bufferSize, httpHeadersTimeout, maxHeadersSize, maxRequestBodySize 188 | ).valueOr: 189 | error "Failed to create server", address = $address, 190 | message = error 191 | raise newException(RpcBindError, "Unable to create server: " & $error) 192 | info "Starting JSON-RPC HTTP server", url = "http://" & $address 193 | 194 | privateAccess(RpcHttpServer) 195 | rpcServer.httpServers.add server 196 | 197 | proc new(T: type TestHttpServer, getData: proc(): string {.gcsafe, raises: [].}): T = 198 | T( 199 | router: RpcRouter.init(), 200 | maxChunkSize: 8192, 201 | getData: getData, 202 | ) 203 | 204 | suite "test rpc http client": 205 | let server = TestHttpServer.new(proc(): string {.gcsafe, raises: [].} = 206 | return """{"jsonrpc":"2.0","result":10}""" 207 | ) 208 | let serverAddress = initTAddress("127.0.0.1:0") 209 | server.addHttpServer(serverAddress) 210 | 211 | var client = newRpcHttpClient() 212 | server.start() 213 | waitFor client.connect("http://" & $server.localAddress()[0]) 214 | 215 | test "missing id in server response": 216 | expect JsonRpcError: 217 | let res = waitFor client.get_Banana(11) 218 | discard res 219 | 220 | waitFor server.stop() 221 | waitFor server.closeWait() 222 | 223 | 224 | type 225 | TestWsServer = ref object of RpcWebSocketServer 226 | getData: proc(): string {.gcsafe, raises: [].} 227 | 228 | proc handleRequest(rpc: TestWsServer, request: websock.HttpRequest) 229 | {.async: (raises: [CancelledError]).} = 230 | try: 231 | let server = rpc.wsserver 232 | let ws = await server.handleRequest(request) 233 | if ws.readyState != ReadyState.Open: 234 | error "Failed to open websocket connection" 235 | return 236 | 237 | trace "Websocket handshake completed" 238 | while ws.readyState != ReadyState.Closed: 239 | let recvData = await ws.recvMsg() 240 | trace "Client message: ", size = recvData.len, binary = ws.binary 241 | 242 | if ws.readyState == ReadyState.Closed: 243 | # if session already terminated by peer, 244 | # no need to send response 245 | break 246 | 247 | if recvData.len == 0: 248 | await ws.close( 249 | reason = "cannot process zero length message" 250 | ) 251 | break 252 | 253 | let data = rpc.getData() 254 | 255 | trace "RPC result has been sent", address = $request.uri 256 | await ws.send(data) 257 | 258 | except WebSocketError as exc: 259 | error "WebSocket error:", exception = exc.msg 260 | 261 | except CancelledError as exc: 262 | raise exc 263 | 264 | except CatchableError as exc: 265 | error "Something error", msg=exc.msg 266 | 267 | proc newWsServer(address: TransportAddress, getData: proc(): string {.gcsafe, raises: [].}): TestWsServer = 268 | 269 | let flags = {ServerFlags.TcpNoDelay,ServerFlags.ReuseAddr} 270 | var server = new(TestWsServer) 271 | proc processCallback(request: websock.HttpRequest): Future[void] = 272 | handleRequest(server, request) 273 | 274 | privateAccess(RpcWebSocketServer) 275 | 276 | server.getData = getData 277 | server.wsserver = WSServer.new(rng = HmacDrbgContext.new()) 278 | server.server = websock.HttpServer.create( 279 | address, 280 | processCallback, 281 | flags 282 | ) 283 | 284 | server 285 | 286 | suite "test ws http client": 287 | let serverAddress = initTAddress("127.0.0.1:0") 288 | let server = newWsServer(serverAddress, proc(): string {.gcsafe, raises: [].} = 289 | return """{"jsonrpc":"2.0","result":10}""" 290 | ) 291 | 292 | var client = newRpcWebSocketClient() 293 | server.start() 294 | waitFor client.connect("ws://" & $server.localAddress()) 295 | 296 | test "missing id in server response": 297 | expect JsonRpcError: 298 | let res = waitFor client.get_Banana(11) 299 | discard res 300 | 301 | server.stop() 302 | waitFor server.closeWait() 303 | -------------------------------------------------------------------------------- /tests/test_jrpc_sys.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 | unittest2, 12 | ../json_rpc/private/jrpc_sys 13 | 14 | func id(): RequestId = 15 | RequestId(kind: riNull) 16 | 17 | func id(x: string): RequestId = 18 | RequestId(kind: riString, str: x) 19 | 20 | func id(x: int): RequestId = 21 | RequestId(kind: riNumber, num: x) 22 | 23 | func req(id: int or string, meth: string, params: RequestParamsTx): RequestTx = 24 | RequestTx( 25 | id: Opt.some(id(id)), 26 | `method`: meth, 27 | params: params 28 | ) 29 | 30 | func reqNull(meth: string, params: RequestParamsTx): RequestTx = 31 | RequestTx( 32 | id: Opt.some(id()), 33 | `method`: meth, 34 | params: params 35 | ) 36 | 37 | func reqNoId(meth: string, params: RequestParamsTx): RequestTx = 38 | RequestTx( 39 | `method`: meth, 40 | params: params 41 | ) 42 | 43 | func toParams(params: varargs[(string, JsonString)]): seq[ParamDescNamed] = 44 | for x in params: 45 | result.add ParamDescNamed(name:x[0], value:x[1]) 46 | 47 | func namedPar(params: varargs[(string, JsonString)]): RequestParamsTx = 48 | RequestParamsTx( 49 | kind: rpNamed, 50 | named: toParams(params) 51 | ) 52 | 53 | func posPar(params: varargs[JsonString]): RequestParamsTx = 54 | RequestParamsTx( 55 | kind: rpPositional, 56 | positional: @params 57 | ) 58 | 59 | func res(id: int or string, r: JsonString): ResponseTx = 60 | ResponseTx( 61 | id: id(id), 62 | kind: rkResult, 63 | result: r, 64 | ) 65 | 66 | func res(id: int or string, err: ResponseError): ResponseTx = 67 | ResponseTx( 68 | id: id(id), 69 | kind: rkError, 70 | error: err, 71 | ) 72 | 73 | func resErr(code: int, msg: string): ResponseError = 74 | ResponseError( 75 | code: code, 76 | message: msg, 77 | ) 78 | 79 | func resErr(code: int, msg: string, data: JsonString): ResponseError = 80 | ResponseError( 81 | code: code, 82 | message: msg, 83 | data: Opt.some(data) 84 | ) 85 | 86 | func reqBatch(args: varargs[RequestTx]): RequestBatchTx = 87 | if args.len == 1: 88 | RequestBatchTx( 89 | kind: rbkSingle, single: args[0] 90 | ) 91 | else: 92 | RequestBatchTx( 93 | kind: rbkMany, many: @args 94 | ) 95 | 96 | func resBatch(args: varargs[ResponseTx]): ResponseBatchTx = 97 | if args.len == 1: 98 | ResponseBatchTx( 99 | kind: rbkSingle, single: args[0] 100 | ) 101 | else: 102 | ResponseBatchTx( 103 | kind: rbkMany, many: @args 104 | ) 105 | 106 | suite "jrpc_sys conversion": 107 | let np1 = namedPar(("banana", JsonString("true")), ("apple", JsonString("123"))) 108 | let pp1 = posPar(JsonString("123"), JsonString("true"), JsonString("\"hello\"")) 109 | 110 | test "RequestTx -> RequestRx: id(int), positional": 111 | let tx = req(123, "int_positional", pp1) 112 | let txBytes = JrpcSys.encode(tx) 113 | let rx = JrpcSys.decode(txBytes, RequestRx) 114 | 115 | check: 116 | rx.jsonrpc.isSome 117 | rx.id.kind == riNumber 118 | rx.id.num == 123 119 | rx.meth.get == "int_positional" 120 | rx.params.kind == rpPositional 121 | rx.params.positional.len == 3 122 | rx.params.positional[0].kind == JsonValueKind.Number 123 | rx.params.positional[1].kind == JsonValueKind.Bool 124 | rx.params.positional[2].kind == JsonValueKind.String 125 | 126 | test "RequestTx -> RequestRx: id(string), named": 127 | let tx = req("word", "string_named", np1) 128 | let txBytes = JrpcSys.encode(tx) 129 | let rx = JrpcSys.decode(txBytes, RequestRx) 130 | 131 | check: 132 | rx.jsonrpc.isSome 133 | rx.id.kind == riString 134 | rx.id.str == "word" 135 | rx.meth.get == "string_named" 136 | rx.params.kind == rpNamed 137 | rx.params.named[0].name == "banana" 138 | rx.params.named[0].value.string == "true" 139 | rx.params.named[1].name == "apple" 140 | rx.params.named[1].value.string == "123" 141 | 142 | test "RequestTx -> RequestRx: id(null), named": 143 | let tx = reqNull("null_named", np1) 144 | let txBytes = JrpcSys.encode(tx) 145 | let rx = JrpcSys.decode(txBytes, RequestRx) 146 | 147 | check: 148 | rx.jsonrpc.isSome 149 | rx.id.kind == riNull 150 | rx.meth.get == "null_named" 151 | rx.params.kind == rpNamed 152 | rx.params.named[0].name == "banana" 153 | rx.params.named[0].value.string == "true" 154 | rx.params.named[1].name == "apple" 155 | rx.params.named[1].value.string == "123" 156 | 157 | test "RequestTx -> RequestRx: none, none": 158 | let tx = reqNoId("none_positional", posPar()) 159 | let txBytes = JrpcSys.encode(tx) 160 | let rx = JrpcSys.decode(txBytes, RequestRx) 161 | 162 | check: 163 | rx.jsonrpc.isSome 164 | rx.id.kind == riNull 165 | rx.meth.get == "none_positional" 166 | rx.params.kind == rpPositional 167 | rx.params.positional.len == 0 168 | 169 | test "ResponseTx -> ResponseRx: id(int), res": 170 | let tx = res(777, JsonString("true")) 171 | let txBytes = JrpcSys.encode(tx) 172 | let rx = JrpcSys.decode(txBytes, ResponseRx) 173 | check: 174 | rx.jsonrpc.isSome 175 | rx.id.isSome 176 | rx.id.get.num == 777 177 | rx.result.string.len > 0 178 | rx.result == JsonString("true") 179 | rx.error.isNone 180 | 181 | test "ResponseTx -> ResponseRx: id(string), err: nodata": 182 | let tx = res("gum", resErr(999, "fatal")) 183 | let txBytes = JrpcSys.encode(tx) 184 | let rx = JrpcSys.decode(txBytes, ResponseRx) 185 | check: 186 | rx.jsonrpc.isSome 187 | rx.id.isSome 188 | rx.id.get.str == "gum" 189 | rx.result.string.len == 0 190 | rx.error.isSome 191 | rx.error.get.code == 999 192 | rx.error.get.message == "fatal" 193 | rx.error.get.data.isNone 194 | 195 | test "ResponseTx -> ResponseRx: id(string), err: some data": 196 | let tx = res("gum", resErr(999, "fatal", JsonString("888.999"))) 197 | let txBytes = JrpcSys.encode(tx) 198 | let rx = JrpcSys.decode(txBytes, ResponseRx) 199 | check: 200 | rx.jsonrpc.isSome 201 | rx.id.isSome 202 | rx.id.get.str == "gum" 203 | rx.result.string.len == 0 204 | rx.error.isSome 205 | rx.error.get.code == 999 206 | rx.error.get.message == "fatal" 207 | rx.error.get.data.get == JsonString("888.999") 208 | 209 | test "RequestBatchTx -> RequestBatchRx: single": 210 | let tx1 = req(123, "int_positional", pp1) 211 | let tx = reqBatch(tx1) 212 | let txBytes = JrpcSys.encode(tx) 213 | let rx = JrpcSys.decode(txBytes, RequestBatchRx) 214 | check: 215 | rx.kind == rbkSingle 216 | 217 | test "RequestBatchTx -> RequestBatchRx: many": 218 | let tx1 = req(123, "int_positional", pp1) 219 | let tx2 = req("word", "string_named", np1) 220 | let tx3 = reqNull("null_named", np1) 221 | let tx4 = reqNoId("none_positional", posPar()) 222 | let tx = reqBatch(tx1, tx2, tx3, tx4) 223 | let txBytes = JrpcSys.encode(tx) 224 | let rx = JrpcSys.decode(txBytes, RequestBatchRx) 225 | check: 226 | rx.kind == rbkMany 227 | rx.many.len == 4 228 | 229 | test "ResponseBatchTx -> ResponseBatchRx: single": 230 | let tx1 = res(777, JsonString("true")) 231 | let tx = resBatch(tx1) 232 | let txBytes = JrpcSys.encode(tx) 233 | let rx = JrpcSys.decode(txBytes, ResponseBatchRx) 234 | check: 235 | rx.kind == rbkSingle 236 | 237 | test "ResponseBatchTx -> ResponseBatchRx: many": 238 | let tx1 = res(777, JsonString("true")) 239 | let tx2 = res("gum", resErr(999, "fatal")) 240 | let tx3 = res("gum", resErr(999, "fatal", JsonString("888.999"))) 241 | let tx = resBatch(tx1, tx2, tx3) 242 | let txBytes = JrpcSys.encode(tx) 243 | let rx = JrpcSys.decode(txBytes, ResponseBatchRx) 244 | check: 245 | rx.kind == rbkMany 246 | rx.many.len == 3 247 | 248 | test "skip null value": 249 | let jsonBytes = """{"jsonrpc":null, "id":null, "method":null, "params":null}""" 250 | let x = JrpcSys.decode(jsonBytes, RequestRx) 251 | check: 252 | x.jsonrpc.isNone 253 | x.id.kind == riNull 254 | x.`method`.isNone 255 | x.params.kind == rpPositional 256 | -------------------------------------------------------------------------------- /tests/test_router_rpc.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 | unittest2, 12 | ../json_rpc/router, 13 | json_serialization/std/options, 14 | json_serialization/stew/results 15 | 16 | var server = RpcRouter() 17 | 18 | type 19 | OptAlias[T] = results.Opt[T] 20 | 21 | server.rpc("std_option") do(A: int, B: Option[int], C: string, D: Option[int], E: Option[string]) -> 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", "id":0, "method": """ & 67 | "\"" & meth & "\", \"params\": " & params & "}" 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","id":0,"result":"A: 44, B: 99, C: apple, D: 33, E: none"}""" 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","id":0,"result":"A: 44, B: 99, C: apple, D: 77, E: none"}""" 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","id":0,"result":"A: 44, B: 567, C: apple, D: 77, E: none"}""" 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","id":0,"error":{"code":-32000,"message":"`std_option` raised an exception","data":"Parameter [D] of type 'Option[system.int]' could not be decoded: number expected"}}""" 90 | elif meth == "results_opt": 91 | check res == """{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"`results_opt` raised an exception","data":"Parameter [D] of type 'Opt[system.int]' could not be decoded: number expected"}}""" 92 | elif meth == "mixed_opt": 93 | check res == """{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"`mixed_opt` raised an exception","data":"Parameter [D] of type 'Option[system.int]' could not be decoded: number expected"}}""" 94 | else: 95 | check res == """{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"`alias_opt` raised an exception","data":"Parameter [D] of type 'Option[system.int]' could not be decoded: number expected"}}""" 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","id":0,"result":"A: 44, B: 567, C: apple, D: 999, E: banana"}""" 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","id":0,"result":"A: 33, B: 99, C: banana, D: 77, E: none"}""" 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","id":0,"result":"A: 33, B: 99, C: banana, D: 8887, E: none"}""" 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","id":0,"result":"A: 33, B: 99, C: banana, D: 8887, E: none"}""" 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","id":0,"result":123}""" 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","id":0,"error":{"code":-32000,"message":"`noParams` raised an exception","data":"Expected 0 JSON parameter(s) but got 1"}}""" 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","id":0,"result":777}""" 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","id":0,"result":21}""" 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","id":0,"result":21}""" 147 | -------------------------------------------------------------------------------- /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 20 | from strutils import rsplit 21 | template sourceDir: string = currentSourcePath.rsplit(DirSep, 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/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 | -------------------------------------------------------------------------------- /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 | 14 | const TestsCount = 100 15 | const bigChunkSize = 4 * 8192 16 | 17 | suite "JSON-RPC/http": 18 | setup: 19 | var httpsrv = newRpcHttpServer(["127.0.0.1:0"]) 20 | # Create RPC on server 21 | httpsrv.rpc("myProc") do(input: string, data: array[0..3, int]): 22 | result = %("Hello " & input & " data: " & $data) 23 | httpsrv.rpc("noParamsProc") do(): 24 | result = %("Hello world") 25 | 26 | httpsrv.rpc("bigchunkMethod") do() -> seq[byte]: 27 | result = newSeq[byte](bigChunkSize) 28 | for i in 0.. 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 | let y = waitFor s.executeMethod("echo", """{"car":null}""".JsonString) 368 | check y == """{"color":"","wheel":0}""" 369 | 370 | let z = waitFor s.executeMethod("echo", "[null]".JsonString) 371 | check z == """{"color":"","wheel":0}""" 372 | 373 | s.stop() 374 | waitFor s.closeWait() 375 | -------------------------------------------------------------------------------- /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 | unittest2, 12 | ../json_rpc/[rpcclient, rpcserver] 13 | 14 | # Create RPC on server 15 | proc setupServer*(srv: RpcServer) = 16 | srv.rpc("myProc") do(input: string, data: array[0..3, int]): 17 | return %("Hello " & input & " data: " & $data) 18 | 19 | srv.rpc("myError") do(input: string, data: array[0..3, int]): 20 | raise (ref ValueError)(msg: "someMessage") 21 | 22 | srv.rpc("invalidRequest") do(): 23 | raise (ref InvalidRequest)(code: -32001, msg: "Unknown payload") 24 | 25 | suite "Socket Server/Client RPC": 26 | var srv = newRpcSocketServer(["127.0.0.1:0"]) 27 | var client = newRpcSocketClient() 28 | 29 | srv.setupServer() 30 | srv.start() 31 | waitFor client.connect(srv.localAddress()[0]) 32 | 33 | test "Successful RPC call": 34 | let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]]) 35 | check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" 36 | 37 | test "Missing params": 38 | expect(CatchableError): 39 | discard waitFor client.call("myProc", %[%"abc"]) 40 | 41 | test "Error RPC call": 42 | expect(CatchableError): # The error type wont be translated 43 | discard waitFor client.call("myError", %[%"abc", %[1, 2, 3, 4]]) 44 | 45 | test "Invalid request exception": 46 | try: 47 | discard waitFor client.call("invalidRequest", %[]) 48 | check false 49 | except CatchableError as e: 50 | check e.msg == """{"code":-32001,"message":"Unknown payload"}""" 51 | 52 | srv.stop() 53 | waitFor srv.closeWait() 54 | 55 | suite "Websocket Server/Client RPC": 56 | var srv = newRpcWebSocketServer("127.0.0.1", Port(0)) 57 | var client = newRpcWebSocketClient() 58 | 59 | srv.setupServer() 60 | srv.start() 61 | waitFor client.connect("ws://" & $srv.localAddress()) 62 | 63 | test "Successful RPC call": 64 | let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]]) 65 | check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" 66 | 67 | test "Missing params": 68 | expect(CatchableError): 69 | discard waitFor client.call("myProc", %[%"abc"]) 70 | 71 | test "Error RPC call": 72 | expect(CatchableError): # The error type wont be translated 73 | discard waitFor client.call("myError", %[%"abc", %[1, 2, 3, 4]]) 74 | 75 | test "Invalid request exception": 76 | try: 77 | discard waitFor client.call("invalidRequest", %[]) 78 | check false 79 | except CatchableError as e: 80 | check e.msg == """{"code":-32001,"message":"Unknown payload"}""" 81 | 82 | srv.stop() 83 | waitFor srv.closeWait() 84 | 85 | suite "Websocket Server/Client RPC with Compression": 86 | var srv = newRpcWebSocketServer("127.0.0.1", Port(0), 87 | compression = true) 88 | var client = newRpcWebSocketClient() 89 | 90 | srv.setupServer() 91 | srv.start() 92 | waitFor client.connect("ws://" & $srv.localAddress(), 93 | compression = true) 94 | 95 | test "Successful RPC call": 96 | let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]]) 97 | check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" 98 | 99 | test "Missing params": 100 | expect(CatchableError): 101 | discard waitFor client.call("myProc", %[%"abc"]) 102 | 103 | test "Error RPC call": 104 | expect(CatchableError): # The error type wont be translated 105 | discard waitFor client.call("myError", %[%"abc", %[1, 2, 3, 4]]) 106 | 107 | test "Invalid request exception": 108 | try: 109 | discard waitFor client.call("invalidRequest", %[]) 110 | check false 111 | except CatchableError as e: 112 | check e.msg == """{"code":-32001,"message":"Unknown payload"}""" 113 | 114 | srv.stop() 115 | waitFor srv.closeWait() 116 | 117 | suite "Custom processClient": 118 | test "Should be able to use custom processClient": 119 | var wasCalled: bool = false 120 | 121 | proc processClientHook(server: StreamServer, transport: StreamTransport) {.async: (raises: []).} = 122 | wasCalled = true 123 | 124 | var srv = newRpcSocketServer(processClientHook) 125 | srv.addStreamServer("localhost", Port(8888)) 126 | var client = newRpcSocketClient() 127 | srv.setupServer() 128 | srv.start() 129 | waitFor client.connect(srv.localAddress()[0]) 130 | asyncCheck client.call("", %[]) 131 | srv.stop() 132 | waitFor srv.closeWait() 133 | check wasCalled 134 | --------------------------------------------------------------------------------