2021-02-15 12:45:51 +00:00
|
|
|
import
|
2021-03-26 12:17:00 +00:00
|
|
|
std/[strutils, tables, uri],
|
2021-05-29 16:15:03 +00:00
|
|
|
stew/[byteutils, results],
|
|
|
|
chronos/apps/http/httpclient as chronosHttpClient,
|
2021-03-26 12:17:00 +00:00
|
|
|
chronicles, httputils, json_serialization/std/net,
|
2021-05-29 16:15:03 +00:00
|
|
|
".."/[client, errors]
|
2018-07-14 07:51:54 +00:00
|
|
|
|
2021-05-29 16:15:03 +00:00
|
|
|
export
|
|
|
|
client
|
2021-03-26 12:17:00 +00:00
|
|
|
|
2022-03-21 14:19:49 +00:00
|
|
|
{.push raises: [Defect].}
|
|
|
|
|
2018-07-14 07:51:54 +00:00
|
|
|
logScope:
|
2019-01-16 10:59:40 +00:00
|
|
|
topics = "JSONRPC-HTTP-CLIENT"
|
2018-07-14 07:51:54 +00:00
|
|
|
|
|
|
|
type
|
2018-11-12 04:43:51 +00:00
|
|
|
HttpClientOptions* = object
|
|
|
|
httpMethod: HttpMethod
|
|
|
|
|
2018-07-14 07:51:54 +00:00
|
|
|
RpcHttpClient* = ref object of RpcClient
|
2021-05-29 16:15:03 +00:00
|
|
|
httpSession: HttpSessionRef
|
|
|
|
httpAddress: HttpResult[HttpAddress]
|
2021-02-15 12:45:51 +00:00
|
|
|
maxBodySize: int
|
2022-03-04 19:13:29 +00:00
|
|
|
getHeaders: GetJsonRpcRequestHeaders
|
2018-07-14 07:51:54 +00:00
|
|
|
|
|
|
|
const
|
2021-02-15 12:45:51 +00:00
|
|
|
MaxHttpRequestSize = 128 * 1024 * 1024 # maximum size of HTTP body in octets
|
2018-07-14 07:51:54 +00:00
|
|
|
|
2022-03-04 19:13:29 +00:00
|
|
|
proc new(
|
|
|
|
T: type RpcHttpClient, maxBodySize = MaxHttpRequestSize, secure = false,
|
|
|
|
getHeaders: GetJsonRpcRequestHeaders = nil): T =
|
2021-11-22 15:24:07 +00:00
|
|
|
let httpSessionFlags = if secure:
|
|
|
|
{
|
|
|
|
HttpClientFlag.NoVerifyHost,
|
|
|
|
HttpClientFlag.NoVerifyServerName
|
|
|
|
}
|
2021-11-22 13:09:13 +00:00
|
|
|
else:
|
2021-11-22 15:24:07 +00:00
|
|
|
{}
|
|
|
|
|
|
|
|
T(
|
|
|
|
maxBodySize: maxBodySize,
|
2022-03-04 19:13:29 +00:00
|
|
|
httpSession: HttpSessionRef.new(flags = httpSessionFlags),
|
|
|
|
getHeaders: getHeaders
|
2021-11-22 15:24:07 +00:00
|
|
|
)
|
2018-11-12 04:43:51 +00:00
|
|
|
|
2022-03-04 19:13:29 +00:00
|
|
|
proc newRpcHttpClient*(
|
|
|
|
maxBodySize = MaxHttpRequestSize, secure = false,
|
|
|
|
getHeaders: GetJsonRpcRequestHeaders = nil): RpcHttpClient =
|
|
|
|
RpcHttpClient.new(maxBodySize, secure, getHeaders)
|
2018-11-12 04:43:51 +00:00
|
|
|
|
2019-06-12 13:44:19 +00:00
|
|
|
method call*(client: RpcHttpClient, name: string,
|
2021-05-29 16:15:03 +00:00
|
|
|
params: JsonNode): Future[Response]
|
2022-03-21 14:19:49 +00:00
|
|
|
{.async, gcsafe.} =
|
2021-05-29 16:15:03 +00:00
|
|
|
doAssert client.httpSession != nil
|
|
|
|
if client.httpAddress.isErr:
|
|
|
|
raise newException(RpcAddressUnresolvableError, client.httpAddress.error)
|
2018-07-14 07:51:54 +00:00
|
|
|
|
2022-03-04 19:13:29 +00:00
|
|
|
var headers =
|
|
|
|
if not isNil(client.getHeaders):
|
|
|
|
client.getHeaders()
|
|
|
|
else:
|
|
|
|
@[]
|
|
|
|
headers.add(("Content-Type", "application/json"))
|
|
|
|
|
2021-02-15 12:45:51 +00:00
|
|
|
let
|
2021-05-29 16:15:03 +00:00
|
|
|
id = client.getNextId()
|
2021-02-15 12:45:51 +00:00
|
|
|
reqBody = $rpcCallNode(name, params, id)
|
2022-04-10 18:48:46 +00:00
|
|
|
|
|
|
|
var req: HttpClientRequestRef
|
|
|
|
var res: HttpClientResponseRef
|
|
|
|
|
|
|
|
defer:
|
|
|
|
# BEWARE!
|
|
|
|
# Using multiple defer statements in this function or multiple
|
|
|
|
# try/except blocks within a single defer statement doesn't
|
|
|
|
# produce the desired run-time code, so we use slightly bizzare
|
|
|
|
# code to ensure the exceptions safety of this function:
|
|
|
|
try:
|
|
|
|
var closeFutures = newSeq[Future[void]]()
|
|
|
|
if req != nil: closeFutures.add req.closeWait()
|
|
|
|
if res != nil: closeFutures.add res.closeWait()
|
|
|
|
if closeFutures.len > 0: await allFutures(closeFutures)
|
|
|
|
except CatchableError as err:
|
|
|
|
# TODO
|
|
|
|
# `close` functions shouldn't raise in general, but we first
|
|
|
|
# need to ensure this through exception tracking in Chronos
|
|
|
|
debug "Error closing JSON-RPC HTTP resuest/response", err = err.msg
|
|
|
|
|
|
|
|
req = HttpClientRequestRef.post(client.httpSession,
|
|
|
|
client.httpAddress.get,
|
|
|
|
body = reqBody.toOpenArrayByte(0, reqBody.len - 1),
|
|
|
|
headers = headers)
|
|
|
|
res =
|
|
|
|
try:
|
|
|
|
await req.send()
|
|
|
|
except CancelledError as e:
|
|
|
|
raise e
|
|
|
|
except CatchableError as e:
|
|
|
|
raise (ref RpcPostError)(msg: "Failed to send POST Request with JSON-RPC.", parent: e)
|
2021-11-22 13:09:13 +00:00
|
|
|
|
|
|
|
if res.status < 200 or res.status >= 300: # res.status is not 2xx (success)
|
|
|
|
raise newException(ErrorResponse, "POST Response: " & $res.status)
|
2021-02-15 12:45:51 +00:00
|
|
|
|
2021-05-29 16:15:03 +00:00
|
|
|
debug "Message sent to RPC server",
|
|
|
|
address = client.httpAddress, msg_len = len(reqBody)
|
2021-02-15 12:45:51 +00:00
|
|
|
trace "Message", msg = reqBody
|
|
|
|
|
2021-11-22 13:09:13 +00:00
|
|
|
let resBytes =
|
|
|
|
try:
|
|
|
|
await res.getBodyBytes(client.maxBodySize)
|
2021-11-22 15:24:07 +00:00
|
|
|
except CancelledError as e:
|
|
|
|
raise e
|
2021-11-22 13:09:13 +00:00
|
|
|
except CatchableError as exc:
|
|
|
|
raise (ref FailedHttpResponse)(msg: "Failed to read POST Response for JSON-RPC.", parent: exc)
|
|
|
|
|
|
|
|
let resText = string.fromBytes(resBytes)
|
2021-05-29 16:15:03 +00:00
|
|
|
trace "Response", text = resText
|
2018-07-14 07:51:54 +00:00
|
|
|
|
2021-02-15 12:45:51 +00:00
|
|
|
# completed by processMessage - the flow is quite weird here to accomodate
|
|
|
|
# socket and ws clients, but could use a more thorough refactoring
|
2018-07-14 07:51:54 +00:00
|
|
|
var newFut = newFuture[Response]()
|
|
|
|
# add to awaiting responses
|
|
|
|
client.awaiting[id] = newFut
|
2021-02-15 12:45:51 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
# Might raise for all kinds of reasons
|
2021-05-29 16:15:03 +00:00
|
|
|
client.processMessage(resText)
|
2021-02-15 12:45:51 +00:00
|
|
|
finally:
|
|
|
|
# Need to clean up in case the answer was invalid
|
|
|
|
client.awaiting.del(id)
|
|
|
|
|
|
|
|
# processMessage should have completed this future - if it didn't, `read` will
|
|
|
|
# raise, which is reasonable
|
2021-05-29 16:15:03 +00:00
|
|
|
if newFut.finished:
|
|
|
|
return newFut.read()
|
|
|
|
else:
|
|
|
|
# TODO: Provide more clarity regarding the failure here
|
|
|
|
raise newException(InvalidResponse, "Invalid response")
|
|
|
|
|
2022-03-21 14:19:49 +00:00
|
|
|
proc connect*(client: RpcHttpClient, url: string) {.async.} =
|
2021-05-29 16:15:03 +00:00
|
|
|
client.httpAddress = client.httpSession.getAddress(url)
|
|
|
|
if client.httpAddress.isErr:
|
|
|
|
raise newException(RpcAddressUnresolvableError, client.httpAddress.error)
|
2018-07-14 07:51:54 +00:00
|
|
|
|
2021-11-22 15:24:07 +00:00
|
|
|
proc connect*(client: RpcHttpClient, address: string, port: Port, secure: bool) {.async.} =
|
|
|
|
var uri = Uri(
|
|
|
|
scheme: if secure: "https" else: "http",
|
|
|
|
hostname: address,
|
|
|
|
port: $port)
|
2021-05-29 16:15:03 +00:00
|
|
|
|
2021-11-22 13:09:13 +00:00
|
|
|
let res = getAddress(client.httpSession, uri)
|
|
|
|
if res.isOk:
|
|
|
|
client.httpAddress = res
|
|
|
|
else:
|
|
|
|
raise newException(RpcAddressUnresolvableError, res.error)
|
2022-02-02 17:51:04 +00:00
|
|
|
|
|
|
|
method close*(client: RpcHttpClient) {.async.} =
|
|
|
|
if not client.httpSession.isNil:
|
|
|
|
await client.httpSession.closeWait()
|