From cc44cd257bb78513a43d93765b7e686ad69b2e6c Mon Sep 17 00:00:00 2001 From: coffeepots Date: Tue, 5 Jun 2018 14:03:58 +0100 Subject: [PATCH] Refactor to just `client`, `server` and `jsonmarshal`, add some comments --- eth-rpc/{client => }/client.nim | 2 +- eth-rpc/server.nim | 226 ++++++++++++++++++++++++++ eth-rpc/server/asyncutils.nim | 16 -- eth-rpc/server/jsonutils.nim | 38 ----- eth-rpc/server/private/debugutils.nim | 2 - eth-rpc/server/rpcconsts.nim | 7 - eth-rpc/server/server.nim | 56 ------- eth-rpc/server/servertypes.nim | 90 ---------- rpcclient.nim | 2 +- rpcserver.nim | 4 +- 10 files changed, 230 insertions(+), 213 deletions(-) rename eth-rpc/{client => }/client.nim (99%) create mode 100644 eth-rpc/server.nim delete mode 100644 eth-rpc/server/asyncutils.nim delete mode 100644 eth-rpc/server/jsonutils.nim delete mode 100644 eth-rpc/server/private/debugutils.nim delete mode 100644 eth-rpc/server/rpcconsts.nim delete mode 100644 eth-rpc/server/server.nim delete mode 100644 eth-rpc/server/servertypes.nim diff --git a/eth-rpc/client/client.nim b/eth-rpc/client.nim similarity index 99% rename from eth-rpc/client/client.nim rename to eth-rpc/client.nim index f6d24a7..5d42642 100644 --- a/eth-rpc/client/client.nim +++ b/eth-rpc/client.nim @@ -1,5 +1,5 @@ import asyncnet, asyncdispatch, tables, json, macros -import ../ jsonmarshal +import jsonmarshal type RpcClient* = ref object diff --git a/eth-rpc/server.nim b/eth-rpc/server.nim new file mode 100644 index 0000000..a5079b3 --- /dev/null +++ b/eth-rpc/server.nim @@ -0,0 +1,226 @@ +import asyncdispatch, asyncnet, json, tables, strutils, options, jsonmarshal, macros +export asyncdispatch, asyncnet, json, jsonmarshal + +type + RpcJsonError* = enum rjeInvalidJson, rjeVersionError, rjeNoMethod, rjeNoId + + RpcJsonErrorContainer* = tuple[err: RpcJsonError, msg: string] + + # Procedure signature accepted as an RPC call by server + RpcProc* = proc (params: JsonNode): Future[JsonNode] + + RpcServer* = ref object + socket*: AsyncSocket + port*: Port + address*: string + procs*: TableRef[string, RpcProc] + + RpcProcError* = ref object of Exception + code*: int + data*: JsonNode + +const + JSON_PARSE_ERROR* = -32700 + INVALID_REQUEST* = -32600 + METHOD_NOT_FOUND* = -32601 + INVALID_PARAMS* = -32602 + INTERNAL_ERROR* = -32603 + SERVER_ERROR* = -32000 + + jsonErrorMessages*: array[RpcJsonError, (int, string)] = + [ + (JSON_PARSE_ERROR, "Invalid JSON"), + (INVALID_REQUEST, "JSON 2.0 required"), + (INVALID_REQUEST, "No method requested"), + (INVALID_REQUEST, "No id specified") + ] + +template ifDebug*(actions: untyped): untyped = + # TODO: Replace with nim-chronicle + when not defined(release): actions else: discard + +proc `$`*(port: Port): string = $int(port) + +proc newRpcServer*(address = "localhost", port: Port = Port(8545)): RpcServer = + result = RpcServer( + socket: newAsyncSocket(), + port: port, + address: address, + procs: newTable[string, RpcProc]() + ) + +# Json state checking + +template jsonValid*(jsonString: string, node: var JsonNode): (bool, string) = + var + valid = true + msg = "" + try: node = parseJson(line) + except: + valid = false + msg = getCurrentExceptionMsg() + (valid, msg) + +proc checkJsonErrors*(line: string, node: var JsonNode): Option[RpcJsonErrorContainer] = + ## Tries parsing line into node, if successful checks required fields + ## Returns: error state or none + let res = jsonValid(line, node) + if not res[0]: + return some((rjeInvalidJson, res[1])) + if not node.hasKey("jsonrpc"): + return some((rjeVersionError, "")) + if not node.hasKey("method"): + return some((rjeNoMethod, "")) + if not node.hasKey("id"): + return some((rjeNoId, "")) + return none(RpcJsonErrorContainer) + +# Json reply wrappers + +proc wrapReply*(id: JsonNode, value: JsonNode, error: JsonNode): string = + let node = %{"jsonrpc": %"2.0", "result": value, "error": error, "id": id} + return $node & "\c\l" + +proc sendError*(client: AsyncSocket, code: int, msg: string, id: JsonNode, data: JsonNode = newJNull()) {.async.} = + ## Send error message to client + let error = %{"code": %(code), "message": %msg, "data": data} + ifDebug: echo "Send error json: ", wrapReply(newJNull(), error, id) + result = client.send(wrapReply(id, newJNull(), error)) + +proc sendJsonError*(state: RpcJsonError, client: AsyncSocket, id: JsonNode, data = newJNull()) {.async.} = + ## Send client response for invalid json state + let errMsgs = jsonErrorMessages[state] + await client.sendError(errMsgs[0], errMsgs[1], id, data) + +# Server message processing + +proc processMessage(server: RpcServer, client: AsyncSocket, line: string) {.async.} = + var + node: JsonNode + jsonErrorState = checkJsonErrors(line, node) # set up node and/or flag errors + if jsonErrorState.isSome: + let errState = jsonErrorState.get + var id: JsonNode + if errState.err == rjeInvalidJson: id = newJNull() # id cannot be retrieved + else: id = node["id"] + await errState.err.sendJsonError(client, id, %errState.msg) + else: + let + methodName = node["method"].str + id = node["id"] + + if not server.procs.hasKey(methodName): + await client.sendError(METHOD_NOT_FOUND, "Method not found", id, %(methodName & " is not a registered method.")) + else: + let callRes = await server.procs[methodName](node["params"]) + await client.send(wrapReply(id, callRes, newJNull())) + +proc processClient(server: RpcServer, client: AsyncSocket) {.async.} = + while true: + let line = await client.recvLine() + if line == "": + # Disconnected. + client.close() + break + + ifDebug: echo "Process client: ", server.port, ":" & line + + let future = processMessage(server, client, line) + await future + if future.failed: + if future.readError of RpcProcError: + let err = future.readError.RpcProcError + await client.sendError(err.code, err.msg, err.data) + elif future.readError of ValueError: + let err = future.readError[].ValueError + await client.sendError(INVALID_PARAMS, err.msg, %"") + else: + await client.sendError(SERVER_ERROR, "Error: Unknown error occurred", %"") + +proc serve*(server: RpcServer) {.async.} = + ## Start the RPC server. + server.socket.bindAddr(server.port, server.address) + server.socket.listen() + + while true: + let client = await server.socket.accept() + asyncCheck server.processClient(client) + +# Server registration and RPC generation + +proc register*(server: RpcServer, name: string, rpc: RpcProc) = + ## Add a name/code pair to the RPC server. + server.procs[name] = rpc + +proc unRegisterAll*(server: RpcServer) = + # Remove all remote procedure calls from this server. + server.procs.clear + +proc makeProcName(s: string): string = + result = "" + for c in s: + if c.isAlphaNumeric: result.add c + +proc hasReturnType(params: NimNode): bool = + if params != nil and params.len > 0 and params[0] != nil and params[0].kind != nnkEmpty: + result = true + +macro rpc*(server: RpcServer, path: string, body: untyped): untyped = + ## Define a remote procedure call. + ## Input and return parameters are defined using the ``do`` notation. + ## For example: + ## .. code-block:: nim + ## myServer.rpc("path") do(param1: int, param2: float) -> string: + ## result = $param1 & " " & $param2 + ## ``` + ## Input parameters are automatically marshalled from json to Nim types, + ## and output parameters are automatically marshalled to json for transport. + result = newStmtList() + let + parameters = body.findChild(it.kind == nnkFormalParams) + paramsIdent = newIdentNode"params" # all remote calls have a single parameter: `params: JsonNode` + pathStr = $path # procs are generated from the stripped path + procNameStr = pathStr.makeProcName # strip non alphanumeric + procName = newIdentNode(procNameStr) # public rpc proc + doMain = newIdentNode(procNameStr & "DoMain") # when parameters present: proc that contains our rpc body + res = newIdentNode("result") # async result + var + setup = jsonToNim(parameters, paramsIdent) + procBody = if body.kind == nnkStmtList: body else: body.body + + if parameters.hasReturnType: + let returnType = parameters[0] + + # delegate async proc allows return and setting of result as native type + result.add(quote do: + proc `doMain`(`paramsIdent`: JsonNode): Future[`returnType`] {.async.} = + `setup` + `procBody` + ) + + if returnType == ident"JsonNode": + # `JsonNode` results don't need conversion + result.add( quote do: + proc `procName`(`paramsIdent`: JsonNode): Future[JsonNode] {.async.} = + `res` = await `doMain`(`paramsIdent`) + ) + else: + result.add(quote do: + proc `procName`(`paramsIdent`: JsonNode): Future[JsonNode] {.async.} = + `res` = %await `doMain`(`paramsIdent`) + ) + else: + # no return types, inline contents + result.add(quote do: + proc `procName`(`paramsIdent`: JsonNode): Future[JsonNode] {.async.} = + `setup` + `procBody` + ) + result.add( quote do: + `server`.register(`path`, `procName`) + ) + + when defined(nimDumpRpcs): + echo "\n", pathStr, ": ", result.repr + +# TODO: Allow cross checking between client signatures and server calls diff --git a/eth-rpc/server/asyncutils.nim b/eth-rpc/server/asyncutils.nim deleted file mode 100644 index d8e5827..0000000 --- a/eth-rpc/server/asyncutils.nim +++ /dev/null @@ -1,16 +0,0 @@ -import json, asyncdispatch, asyncnet, jsonutils, private / debugutils - -proc wrapReply*(id: JsonNode, value: JsonNode, error: JsonNode): string = - let node = %{"jsonrpc": %"2.0", "result": value, "error": error, "id": id} - return $node & "\c\l" - -proc sendError*(client: AsyncSocket, code: int, msg: string, id: JsonNode, data: JsonNode = newJNull()) {.async.} = - ## Send error message to client - let error = %{"code": %(code), "message": %msg, "data": data} - ifDebug: echo "Send error json: ", wrapReply(newJNull(), error, id) - result = client.send(wrapReply(id, newJNull(), error)) - -proc sendJsonError*(state: RpcJsonError, client: AsyncSocket, id: JsonNode, data = newJNull()) {.async.} = - ## Send client response for invalid json state - let errMsgs = jsonErrorMessages[state] - await client.sendError(errMsgs[0], errMsgs[1], id, data) diff --git a/eth-rpc/server/jsonutils.nim b/eth-rpc/server/jsonutils.nim deleted file mode 100644 index 52a6b0b..0000000 --- a/eth-rpc/server/jsonutils.nim +++ /dev/null @@ -1,38 +0,0 @@ -import rpcconsts, options, json - -type - RpcJsonError* = enum rjeInvalidJson, rjeVersionError, rjeNoMethod, rjeNoId - RpcJsonErrorContainer* = tuple[err: RpcJsonError, msg: string] - -const - jsonErrorMessages*: array[RpcJsonError, (int, string)] = - [ - (JSON_PARSE_ERROR, "Invalid JSON"), - (INVALID_REQUEST, "JSON 2.0 required"), - (INVALID_REQUEST, "No method requested"), - (INVALID_REQUEST, "No id specified") - ] - -template jsonValid*(jsonString: string, node: var JsonNode): (bool, string) = - var - valid = true - msg = "" - try: node = parseJson(line) - except: - valid = false - msg = getCurrentExceptionMsg() - (valid, msg) - -proc checkJsonErrors*(line: string, node: var JsonNode): Option[RpcJsonErrorContainer] = - ## Tries parsing line into node, if successful checks required fields - ## Returns: error state or none - let res = jsonValid(line, node) - if not res[0]: - return some((rjeInvalidJson, res[1])) - if not node.hasKey("jsonrpc"): - return some((rjeVersionError, "")) - if not node.hasKey("method"): - return some((rjeNoMethod, "")) - if not node.hasKey("id"): - return some((rjeNoId, "")) - return none(RpcJsonErrorContainer) diff --git a/eth-rpc/server/private/debugutils.nim b/eth-rpc/server/private/debugutils.nim deleted file mode 100644 index 5de259c..0000000 --- a/eth-rpc/server/private/debugutils.nim +++ /dev/null @@ -1,2 +0,0 @@ -template ifDebug*(actions: untyped): untyped = - when not defined(release): actions else: discard \ No newline at end of file diff --git a/eth-rpc/server/rpcconsts.nim b/eth-rpc/server/rpcconsts.nim deleted file mode 100644 index 2d69042..0000000 --- a/eth-rpc/server/rpcconsts.nim +++ /dev/null @@ -1,7 +0,0 @@ -const - JSON_PARSE_ERROR* = -32700 - INVALID_REQUEST* = -32600 - METHOD_NOT_FOUND* = -32601 - INVALID_PARAMS* = -32602 - INTERNAL_ERROR* = -32603 - SERVER_ERROR* = -32000 \ No newline at end of file diff --git a/eth-rpc/server/server.nim b/eth-rpc/server/server.nim deleted file mode 100644 index 3522874..0000000 --- a/eth-rpc/server/server.nim +++ /dev/null @@ -1,56 +0,0 @@ -import asyncdispatch, asyncnet, json, tables, strutils, - servertypes, rpcconsts, private / debugutils, jsonutils, asyncutils, - options - -proc processMessage(server: RpcServer, client: AsyncSocket, line: string) {.async.} = - var - node: JsonNode - jsonErrorState = checkJsonErrors(line, node) # set up node and/or flag errors - if jsonErrorState.isSome: - let errState = jsonErrorState.get - var id: JsonNode - if errState.err == rjeInvalidJson: id = newJNull() # id cannot be retrieved - else: id = node["id"] - await errState.err.sendJsonError(client, id, %errState.msg) - else: - let - methodName = node["method"].str - id = node["id"] - - if not server.procs.hasKey(methodName): - await client.sendError(METHOD_NOT_FOUND, "Method not found", id, %(methodName & " is not a registered method.")) - else: - let callRes = await server.procs[methodName](node["params"]) - await client.send(wrapReply(id, callRes, newJNull())) - -proc processClient(server: RpcServer, client: AsyncSocket) {.async.} = - while true: - let line = await client.recvLine() - if line == "": - # Disconnected. - client.close() - break - - ifDebug: echo "Process client: ", server.port, ":" & line - - let future = processMessage(server, client, line) - await future - if future.failed: - if future.readError of RpcProcError: - let err = future.readError.RpcProcError - await client.sendError(err.code, err.msg, err.data) - elif future.readError of ValueError: - let err = future.readError[].ValueError - await client.sendError(INVALID_PARAMS, err.msg, %"") - else: - await client.sendError(SERVER_ERROR, "Error: Unknown error occurred", %"") - -proc serve*(server: RpcServer) {.async.} = - server.socket.bindAddr(server.port, server.address) - server.socket.listen() - - while true: - let client = await server.socket.accept() - asyncCheck server.processClient(client) - - diff --git a/eth-rpc/server/servertypes.nim b/eth-rpc/server/servertypes.nim deleted file mode 100644 index 4b8597a..0000000 --- a/eth-rpc/server/servertypes.nim +++ /dev/null @@ -1,90 +0,0 @@ -import asyncdispatch, asyncnet, json, tables, macros, strutils, ../ jsonmarshal -export asyncdispatch, asyncnet, json, jsonmarshal - -type - RpcProc* = proc (params: JsonNode): Future[JsonNode] - - RpcServer* = ref object - socket*: AsyncSocket - port*: Port - address*: string - procs*: TableRef[string, RpcProc] - - RpcProcError* = ref object of Exception - code*: int - data*: JsonNode - -proc register*(server: RpcServer, name: string, rpc: RpcProc) = - server.procs[name] = rpc - -proc unRegisterAll*(server: RpcServer) = server.procs.clear - -proc newRpcServer*(address = "localhost", port: Port = Port(8545)): RpcServer = - result = RpcServer( - socket: newAsyncSocket(), - port: port, - address: address, - procs: newTable[string, RpcProc]() - ) - -proc `$`*(port: Port): string = $int(port) - -proc makeProcName(s: string): string = - result = "" - for c in s: - if c.isAlphaNumeric: result.add c - -proc hasReturnType(params: NimNode): bool = - if params != nil and params.len > 0 and params[0] != nil and params[0].kind != nnkEmpty: - result = true - -macro rpc*(server: RpcServer, path: string, body: untyped): untyped = - result = newStmtList() - let - parameters = body.findChild(it.kind == nnkFormalParams) - paramsIdent = newIdentNode"params" # all remote calls have a single parameter: `params: JsonNode` - pathStr = $path # procs are generated from the stripped path - procNameStr = pathStr.makeProcName # strip non alphanumeric - procName = newIdentNode(procNameStr) # public rpc proc - doMain = newIdentNode(procNameStr & "DoMain") # when parameters present: proc that contains our rpc body - res = newIdentNode("result") # async result - var - setup = jsonToNim(parameters, paramsIdent) - procBody = if body.kind == nnkStmtList: body else: body.body - - if parameters.hasReturnType: - let returnType = parameters[0] - - # delegate async proc allows return and setting of result as native type - result.add(quote do: - proc `doMain`(`paramsIdent`: JsonNode): Future[`returnType`] {.async.} = - `setup` - `procBody` - ) - - if returnType == ident"JsonNode": - # `JsonNode` results don't need conversion - result.add( quote do: - proc `procName`(`paramsIdent`: JsonNode): Future[JsonNode] {.async.} = - `res` = await `doMain`(`paramsIdent`) - ) - else: - result.add(quote do: - proc `procName`(`paramsIdent`: JsonNode): Future[JsonNode] {.async.} = - `res` = %await `doMain`(`paramsIdent`) - ) - else: - # no return types, inline contents - result.add(quote do: - proc `procName`(`paramsIdent`: JsonNode): Future[JsonNode] {.async.} = - `setup` - `procBody` - ) - result.add( quote do: - `server`.register(`path`, `procName`) - ) - - when defined(nimDumpRpcs): - echo "\n", pathStr, ": ", result.repr - -# TODO: Allow cross checking between client signatures and server calls diff --git a/rpcclient.nim b/rpcclient.nim index e25c15e..c1a3431 100644 --- a/rpcclient.nim +++ b/rpcclient.nim @@ -1,3 +1,3 @@ -import eth-rpc / client / client +import eth-rpc / client export client diff --git a/rpcserver.nim b/rpcserver.nim index 18c499e..905d53e 100644 --- a/rpcserver.nim +++ b/rpcserver.nim @@ -1,2 +1,2 @@ -import r"eth-rpc/server" / [servertypes, rpcconsts, server] -export servertypes, rpcconsts, server +import eth-rpc / server +export server