Implement combo http server for rpc, engine_api, and graphql services (#1992)

* Combo HTTP server implementation

* Use json flavor for jwt_auth decoder
This commit is contained in:
andri lim 2024-01-29 20:20:04 +07:00 committed by GitHub
parent 61abdac2d7
commit c635e160d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1007 additions and 501 deletions

View File

@ -42,7 +42,7 @@ proc getClient(env: TestEnv, token: string): RpcHttpClient =
@[("Authorization", "Bearer " & token)]
let client = newRpcHttpClient(getHeaders = authHeaders)
waitFor client.connect("127.0.0.1", env.engine.rpcPort, false)
waitFor client.connect("127.0.0.1", env.engine.httpPort, false)
return client
template genAuthTest(procName: untyped, timeDriftSeconds: int64, customAuthSecretBytes: string, authOK: bool) =

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2023 Status Research & Development GmbH
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
@ -10,7 +10,6 @@
import
std/strutils,
chronicles,
./engine_spec,
../../../../nimbus/common/hardforks

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2023 Status Research & Development GmbH
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
@ -9,14 +9,12 @@
# according to those terms.
import
std/strutils,
chronicles,
./engine_spec,
../helper,
../cancun/customizer,
../../../../nimbus/common
# Generate test cases for each field of NewPayload, where the payload contains a single invalid field and a valid hash.
type
InvalidPayloadTestCase* = ref object of EngineSpec

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2023 Status Research & Development GmbH
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
@ -9,7 +9,6 @@
# according to those terms.
import
std/strutils,
eth/common,
chronicles,
stew/byteutils,

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2023 Status Research & Development GmbH
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
@ -9,7 +9,6 @@
# according to those terms.
import
std/strutils,
eth/common,
chronicles,
./engine_spec

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2023 Status Research & Development GmbH
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
@ -111,7 +111,10 @@ proc newEngineEnv*(conf: var NimbusConf, chainFile: string, enableAuth: bool): E
let
hooks = if enableAuth: @[httpJwtAuth(key)]
else: @[]
server = newRpcHttpServerWithParams("127.0.0.1:" & $conf.rpcPort, hooks)
server = newRpcHttpServerWithParams("127.0.0.1:" & $conf.httpPort, hooks).valueOr:
echo "Failed to create rpc server: ", error
quit(QuitFailure)
sealer = SealingEngineRef.new(
chain, ctx, conf.engineSigner,
txPool, EngineStopped)
@ -135,7 +138,7 @@ proc newEngineEnv*(conf: var NimbusConf, chainFile: string, enableAuth: bool): E
server.start()
let client = newRpcHttpClient()
waitFor client.connect("127.0.0.1", conf.rpcPort, false)
waitFor client.connect("127.0.0.1", conf.httpPort, false)
if com.ttd().isSome:
sync.start()
@ -167,8 +170,8 @@ proc setRealTTD*(env: EngineEnv) =
env.com.setTTD some(realTTD)
env.ttd = realTTD
func rpcPort*(env: EngineEnv): Port =
env.conf.rpcPort
func httpPort*(env: EngineEnv): Port =
env.conf.httpPort
func client*(env: EngineEnv): RpcHttpClient =
env.client

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2023 Status Research & Development GmbH
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
@ -36,28 +36,28 @@ type
chainFile : string
enableAuth: bool
port : int
rpcPort : int
httpPort : int
clients : ClientPool
sender : TxSender
clMock* : CLMocker
proc makeEnv(conf: NimbusConf): TestEnv =
TestEnv(
conf : conf,
port : 30303,
rpcPort: 8545,
clients: ClientPool(),
sender : TxSender.new(conf.networkParams),
conf : conf,
port : 30303,
httpPort: 8545,
clients : ClientPool(),
sender : TxSender.new(conf.networkParams),
)
proc addEngine(env: TestEnv, conf: var NimbusConf): EngineEnv =
conf.tcpPort = Port env.port
conf.udpPort = Port env.port
conf.rpcPort = Port env.rpcPort
conf.httpPort = Port env.httpPort
let engine = newEngineEnv(conf, env.chainFile, env.enableAuth)
env.clients.add engine
inc env.port
inc env.rpcPort
inc env.httpPort
engine
proc setup(env: TestEnv, conf: var NimbusConf, chainFile: string, enableAuth: bool) =

View File

@ -46,17 +46,7 @@ proc manageAccounts(ctx: EthContext, conf: NimbusConf) =
proc setupRpcServer(ctx: EthContext, com: CommonRef,
ethNode: EthereumNode, txPool: TxPoolRef,
conf: NimbusConf): RpcServer =
let rpcServer = newRpcHttpServer([initTAddress(conf.rpcAddress, conf.rpcPort)])
setupCommonRpc(ethNode, conf, rpcServer)
setupEthRpc(ethNode, ctx, com, txPool, rpcServer)
rpcServer.start()
rpcServer
proc setupWsRpcServer(ctx: EthContext, com: CommonRef,
ethNode: EthereumNode, txPool: TxPoolRef,
conf: NimbusConf): RpcServer =
let rpcServer = newRpcWebSocketServer(initTAddress(conf.wsAddress, conf.wsPort))
let rpcServer = newRpcHttpServer([initTAddress(conf.httpAddress, conf.httpPort)])
setupCommonRpc(ethNode, conf, rpcServer)
setupEthRpc(ethNode, ctx, com, txPool, rpcServer)
@ -68,11 +58,6 @@ proc stopRpcHttpServer(srv: RpcServer) =
waitFor rpcServer.stop()
waitFor rpcServer.closeWait()
proc stopRpcWsServer(srv: RpcServer) =
let rpcServer = RpcWebSocketServer(srv)
rpcServer.stop()
waitFor rpcServer.closeWait()
proc setupEnv*(): TestEnv =
let conf = makeConfig(@[
"--prune-mode:archive",
@ -83,12 +68,8 @@ proc setupEnv*(): TestEnv =
"--custom-network:" & initPath / "genesis.json",
"--rpc",
"--rpc-api:eth,debug",
# "--rpc-address:0.0.0.0",
"--rpc-port:8545",
"--ws",
"--ws-api:eth,debug",
# "--ws-address:0.0.0.0",
"--ws-port:8546"
# "--http-address:0.0.0.0",
"--http-port:8545",
])
let

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2021 Status Research & Development GmbH
# Copyright (c) 2021-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
@ -7,12 +7,13 @@
# This file may not be copied, modified, or distributed except according to
# those terms.
{.push raises: [].}
{.push gcsafe, raises: [].}
import
std/[os, json, tables, strutils],
stew/[byteutils, results],
eth/[keyfile, common, keys]
eth/[keyfile, common, keys],
json_serialization
from nimcrypto/utils import burnMem
@ -28,31 +29,17 @@ type
proc init*(_: type AccountsManager): AccountsManager =
discard
proc loadKeystores*(am: var AccountsManager, path: string): Result[void, string]
{.gcsafe, raises: [OSError].}=
proc loadKeystores*(am: var AccountsManager, path: string):
Result[void, string] =
try:
createDir(path)
except OSError, IOError:
return err("keystore: cannot create directory")
for filename in walkDirRec(path):
try:
var data = json.parseFile(filename)
for filename in walkDirRec(path):
var data = Json.loadFile(filename, JsonNode)
let address: EthAddress = hexToByteArray[20](data["address"].getStr())
am.accounts[address] = NimbusAccount(keystore: data, unlocked: false)
except JsonParsingError:
return err("keystore: json parsing error " & filename)
except ValueError:
return err("keystore: data parsing error")
except IOError:
return err("keystore: data read error")
except CatchableError as e: # json raises Exception
return err("keystore: " & e.msg)
except Exception as e:
{.warning: "Kludge(BareExcept): `parseFile()` in json vendor package needs to be updated".}
raiseAssert "Ooops loadKeystores(): name=" & $e.name & " msg=" & e.msg
except CatchableError as exc:
return err("loadKeystrores: " & exc.msg)
ok()
proc getAccount*(am: var AccountsManager, address: EthAddress): Result[NimbusAccount, string] =
@ -110,3 +97,5 @@ proc importPrivateKey*(am: var AccountsManager, fileName: string): Result[void,
return ok()
except CatchableError as ex:
return err(ex.msg)
{.pop.}

View File

@ -87,11 +87,8 @@ const
defaultDataDirDesc = defaultDataDir()
defaultPort = 30303
defaultMetricsServerPort = 9093
defaultEthRpcPort = 8545
defaultEthWsPort = 8546
defaultEthGraphqlPort = 8547
defaultHttpPort = 8545
defaultEngineApiPort = 8550
defaultEngineApiWsPort = 8551
defaultListenAddress = (static parseIpAddress("0.0.0.0"))
defaultAdminListenAddress = (static parseIpAddress("127.0.0.1"))
defaultListenAddressDesc = $defaultListenAddress & ", meaning all network interfaces"
@ -380,26 +377,26 @@ type
defaultValue: NimbusCmd.noCommand }: NimbusCmd
of noCommand:
httpPort* {.
separator: "\pLOCAL SERVICES OPTIONS:"
desc: "Listening port of the HTTP server(rpc, ws, graphql)"
defaultValue: defaultHttpPort
defaultValueDesc: $defaultHttpPort
name: "http-port" }: Port
httpAddress* {.
desc: "Listening IP address of the HTTP server(rpc, ws, graphql)"
defaultValue: defaultAdminListenAddress
defaultValueDesc: $defaultAdminListenAddressDesc
name: "http-address" }: IpAddress
rpcEnabled* {.
separator: "\pLOCAL SERVICE OPTIONS:"
desc: "Enable the JSON-RPC server"
defaultValue: false
name: "rpc" }: bool
rpcPort* {.
desc: "Listening port of the JSON-RPC server"
defaultValue: defaultEthRpcPort
defaultValueDesc: $defaultEthRpcPort
name: "rpc-port" }: Port
rpcAddress* {.
desc: "Listening IP address of the JSON-RPC server"
defaultValue: defaultAdminListenAddress
defaultValueDesc: $defaultAdminListenAddressDesc
name: "rpc-address" }: IpAddress
rpcApi {.
desc: "Enable specific set of RPC API (available: eth, debug)"
desc: "Enable specific set of RPC API (available: eth, debug, exp)"
defaultValue: @[]
defaultValueDesc: $RpcFlag.Eth
name: "rpc-api" }: seq[string]
@ -409,37 +406,30 @@ type
defaultValue: false
name: "ws" }: bool
wsPort* {.
desc: "Listening port of the Websocket JSON-RPC server"
defaultValue: defaultEthWsPort
defaultValueDesc: $defaultEthWsPort
name: "ws-port" }: Port
wsAddress* {.
desc: "Listening IP address of the Websocket JSON-RPC server"
defaultValue: defaultAdminListenAddress
defaultValueDesc: $defaultAdminListenAddressDesc
name: "ws-address" }: IpAddress
wsApi {.
desc: "Enable specific set of Websocket RPC API (available: eth, debug)"
desc: "Enable specific set of Websocket RPC API (available: eth, debug, exp)"
defaultValue: @[]
defaultValueDesc: $RpcFlag.Eth
name: "ws-api" }: seq[string]
graphqlEnabled* {.
desc: "Enable the GraphQL HTTP server"
defaultValue: false
name: "graphql" }: bool
engineApiEnabled* {.
desc: "Enable the Engine API"
defaultValue: false
name: "engine-api" .}: bool
engineApiPort* {.
desc: "Listening port for the Engine API"
desc: "Listening port for the Engine API(http and ws)"
defaultValue: defaultEngineApiPort
defaultValueDesc: $defaultEngineApiPort
name: "engine-api-port" .}: Port
engineApiAddress* {.
desc: "Listening address for the Engine API"
desc: "Listening address for the Engine API(http and ws)"
defaultValue: defaultAdminListenAddress
defaultValueDesc: $defaultAdminListenAddressDesc
name: "engine-api-address" .}: IpAddress
@ -449,18 +439,6 @@ type
defaultValue: false
name: "engine-api-ws" .}: bool
engineApiWsPort* {.
desc: "Listening port for the WebSocket Engine API"
defaultValue: defaultEngineApiWsPort
defaultValueDesc: $defaultEngineApiWsPort
name: "engine-api-ws-port" .}: Port
engineApiWsAddress* {.
desc: "Listening address for the WebSocket Engine API"
defaultValue: defaultAdminListenAddress
defaultValueDesc: $defaultAdminListenAddressDesc
name: "engine-api-ws-address" .}: IpAddress
terminalTotalDifficulty* {.
desc: "The terminal total difficulty of the eth2 merge transition block." &
" It takes precedence over terminalTotalDifficulty in config file."
@ -481,23 +459,6 @@ type
defaultValueDesc: "\"jwt.hex\" in the data directory (see --data-dir)"
name: "jwt-secret" .}: Option[InputFile]
graphqlEnabled* {.
desc: "Enable the GraphQL HTTP server"
defaultValue: false
name: "graphql" }: bool
graphqlPort* {.
desc: "Listening port of the GraphQL HTTP server"
defaultValue: defaultEthGraphqlPort
defaultValueDesc: $defaultEthGraphqlPort
name: "graphql-port" }: Port
graphqlAddress* {.
desc: "Listening IP address of the GraphQL HTTP server"
defaultValue: defaultAdminListenAddress
defaultValueDesc: $defaultAdminListenAddressDesc
name: "graphql-address" }: IpAddress
metricsEnabled* {.
desc: "Enable the built-in metrics HTTP server"
defaultValue: false
@ -685,7 +646,7 @@ proc getProtocolFlags*(conf: NimbusConf): set[ProtocolFlag] =
proc getRpcFlags(api: openArray[string]): set[RpcFlag] =
if api.len == 0:
return {RpcFlag.Eth}
for item in repeatingList(api):
case item.toLowerAscii()
of "eth": result.incl RpcFlag.Eth
@ -719,7 +680,7 @@ proc fromEnr*(T: type ENode, r: enr.Record): ENodeResult[ENode] =
ok(ENode(
pubkey: pk,
address: Address(
ip: ipv4(tr.ip.get()),
ip: utils.ipv4(tr.ip.get()),
udpPort: Port(tr.udp.get()),
tcpPort: Port(tr.tcp.get())
)
@ -779,6 +740,18 @@ proc getAllowedOrigins*(conf: NimbusConf): seq[Uri] =
for item in repeatingList(conf.allowedOrigins):
result.add parseUri(item)
func engineApiServerEnabled*(conf: NimbusConf): bool =
conf.engineApiEnabled or conf.engineApiWsEnabled
func shareServerWithEngineApi*(conf: NimbusConf): bool =
conf.engineApiServerEnabled and
conf.engineApiPort == conf.httpPort
func httpServerEnabled*(conf: NimbusConf): bool =
conf.graphqlEnabled or
conf.wsEnabled or
conf.rpcEnabled
# KLUDGE: The `load()` template does currently not work within any exception
# annotated environment.
{.pop.}
@ -834,13 +807,6 @@ proc makeConfig*(cmdLine = commandLineParams()): NimbusConf
# if udpPort not set in cli, then
result.udpPort = result.tcpPort
# enable rpc server or ws server if they share common port with engine api
let rpcMustEnabled = result.engineApiEnabled and (result.engineApiPort == result.rpcPort)
let wsMustEnabled = result.engineApiWsEnabled and (result.engineApiWsPort == result.wsPort)
result.rpcEnabled = result.rpcEnabled or rpcMustEnabled
result.wsEnabled = result.wsEnabled or wsMustEnabled
# see issue #1346
if result.keyStore.string == defaultKeystoreDir() and
result.dataDir.string != defaultDataDir():

View File

@ -1459,23 +1459,10 @@ proc setupGraphqlContext*(com: CommonRef,
ctx.initEthApi()
ctx
proc setupGraphqlHttpServer*(conf: NimbusConf,
com: CommonRef,
ethNode: EthereumNode,
txPool: TxPoolRef,
authHooks: seq[AuthHook] = @[]): GraphqlHttpServerRef =
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
proc setupGraphqlHttpHandler*(com: CommonRef,
ethNode: EthereumNode,
txPool: TxPoolRef): GraphqlHttpHandlerRef =
let ctx = setupGraphqlContext(com, ethNode, txPool)
let address = initTAddress(conf.graphqlAddress, conf.graphqlPort)
let sres = GraphqlHttpServerRef.new(
ctx,
address,
socketFlags = socketFlags,
authHooks = authHooks
)
if sres.isErr():
echo sres.error
quit(QuitFailure)
sres.get()
GraphqlHttpHandlerRef.new(ctx)
{.pop.}

View File

@ -13,23 +13,25 @@ import
import
std/[os, strutils, net],
chronicles,
chronos,
eth/[keys, net/nat],
eth/p2p as eth_p2p,
json_rpc/rpcserver,
eth/keys,
eth/net/nat,
metrics,
metrics/[chronos_httpserver, chronicles_support],
websock/websock as ws,
metrics/chronicles_support,
kzg4844/kzg_ex as kzg,
./rpc,
./version,
./constants,
./nimbus_desc,
./core/eip4844,
"."/[config, constants, version, rpc, common],
./db/[core_db/persistent, select_backend],
./graphql/ethapi,
./core/[chain, sealer, clique/clique_desc,
clique/clique_sealer, tx_pool, block_import],
./beacon/beacon_engine,
./sync/[beacon, legacy, full, protocol, snap, stateless,
protocol/les_protocol, handlers, peers],
./core/block_import,
./db/select_backend,
./db/core_db/persistent,
./core/clique/clique_desc,
./core/clique/clique_sealer,
./sync/protocol,
./sync/handlers,
./sync/stateless,
./sync/protocol/les_protocol,
./evm/async/data_sources/json_rpc_data_source
when defined(evmc_enabled):
@ -40,31 +42,6 @@ when defined(evmc_enabled):
## * No multiple bind addresses support
## * No database support
type
NimbusState = enum
Starting, Running, Stopping
NimbusNode = ref object
rpcServer: RpcHttpServer
engineApiServer: RpcHttpServer
engineApiWsServer: RpcWebSocketServer
ethNode: EthereumNode
state: NimbusState
graphqlServer: GraphqlHttpServerRef
wsRpcServer: RpcWebSocketServer
sealingEngine: SealingEngineRef
ctx: EthContext
chainRef: ChainRef
txPool: TxPoolRef
networkLoop: Future[void]
peerManager: PeerManagerRef
legaSyncRef: LegacySyncRef
snapSyncRef: SnapSyncRef
fullSyncRef: FullSyncRef
beaconSyncRef: BeaconSyncRef
statelessSyncRef: StatelessSyncRef
beaconEngine: BeaconEngineRef
proc importBlocks(conf: NimbusConf, com: CommonRef) =
if string(conf.blocksFile).len > 0:
# success or not, we quit after importing blocks
@ -244,97 +221,7 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
discard setTimer(Moment.fromNow(conf.logMetricsInterval.seconds), logMetrics)
discard setTimer(Moment.fromNow(conf.logMetricsInterval.seconds), logMetrics)
# Provide JWT authentication handler for rpcHttpServer
let jwtKey = block:
# Create or load shared secret
let rc = nimbus.ctx.rng.jwtSharedSecret(conf)
if rc.isErr:
fatal "Failed create or load shared secret",
msg = $(rc.unsafeError) # avoid side effects
quit(QuitFailure)
rc.value
let allowedOrigins = conf.getAllowedOrigins()
# Provide JWT authentication handler for rpcHttpServer
let httpJwtAuthHook = httpJwtAuth(jwtKey)
let httpCorsHook = httpCors(allowedOrigins)
# Creating RPC Server
if conf.rpcEnabled:
let enableAuthHook = conf.engineApiEnabled and
conf.engineApiPort == conf.rpcPort
let hooks = if enableAuthHook:
@[httpJwtAuthHook, httpCorsHook]
else:
@[httpCorsHook]
nimbus.rpcServer = newRpcHttpServerWithParams(
initTAddress(conf.rpcAddress, conf.rpcPort),
authHooks = hooks
)
setupCommonRpc(nimbus.ethNode, conf, nimbus.rpcServer)
# Enable RPC APIs based on RPC flags and protocol flags
let rpcFlags = conf.getRpcFlags()
if (RpcFlag.Eth in rpcFlags and ProtocolFlag.Eth in protocols) or
(conf.engineApiPort == conf.rpcPort):
setupEthRpc(nimbus.ethNode, nimbus.ctx, com, nimbus.txPool, nimbus.rpcServer)
if RpcFlag.Debug in rpcFlags:
setupDebugRpc(com, nimbus.rpcServer)
if RpcFlag.Exp in rpcFlags:
setupExpRpc(com, nimbus.rpcServer)
nimbus.rpcServer.rpc("admin_quit") do() -> string:
{.gcsafe.}:
nimbus.state = Stopping
result = "EXITING"
nimbus.rpcServer.start()
# Provide JWT authentication handler for rpcWebsocketServer
let wsJwtAuthHook = wsJwtAuth(jwtKey)
let wsCorsHook = wsCors(allowedOrigins)
# Creating Websocket RPC Server
if conf.wsEnabled:
let enableAuthHook = conf.engineApiWsEnabled and
conf.engineApiWsPort == conf.wsPort
let hooks = if enableAuthHook:
@[wsJwtAuthHook, wsCorsHook]
else:
@[wsCorsHook]
# Construct server object
nimbus.wsRpcServer = newRpcWebSocketServer(
initTAddress(conf.wsAddress, conf.wsPort),
authHooks = hooks,
rng = nimbus.ctx.rng
)
setupCommonRpc(nimbus.ethNode, conf, nimbus.wsRpcServer)
# Enable Websocket RPC APIs based on RPC flags and protocol flags
let wsFlags = conf.getWsFlags()
if (RpcFlag.Eth in wsFlags and ProtocolFlag.Eth in protocols) or
(conf.engineApiWsPort == conf.wsPort):
setupEthRpc(nimbus.ethNode, nimbus.ctx, com, nimbus.txPool, nimbus.wsRpcServer)
if RpcFlag.Debug in wsFlags:
setupDebugRpc(com, nimbus.wsRpcServer)
if RpcFlag.Exp in wsFlags:
setupExpRpc(com, nimbus.wsRpcServer)
nimbus.wsRpcServer.start()
if conf.graphqlEnabled:
nimbus.graphqlServer = setupGraphqlHttpServer(
conf,
com,
nimbus.ethNode,
nimbus.txPool,
@[httpCorsHook]
)
nimbus.graphqlServer.start()
nimbus.setupRpc(conf, com, protocols)
if conf.engineSigner != ZERO_ADDRESS and not com.forkGTE(MergeFork):
let res = nimbus.ctx.am.getAccount(conf.engineSigner)
@ -370,40 +257,16 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
if conf.engineSigner != ZERO_ADDRESS:
nimbus.sealingEngine.start()
if conf.engineApiEnabled:
#let maybeAsyncDataSource = maybeStatelessAsyncDataSource(nimbus, conf)
if conf.engineApiPort != conf.rpcPort:
nimbus.engineApiServer = newRpcHttpServerWithParams(
initTAddress(conf.engineApiAddress, conf.engineApiPort),
authHooks = @[httpJwtAuthHook, httpCorsHook]
)
setupEngineAPI(nimbus.beaconEngine, nimbus.engineApiServer)
setupEthRpc(nimbus.ethNode, nimbus.ctx, com, nimbus.txPool, nimbus.engineApiServer)
nimbus.engineApiServer.start()
else:
setupEngineAPI(nimbus.beaconEngine, nimbus.rpcServer)
info "Starting engine API server", port = conf.engineApiPort
if conf.engineApiWsEnabled:
#let maybeAsyncDataSource = maybeStatelessAsyncDataSource(nimbus, conf)
if conf.engineApiWsPort != conf.wsPort:
nimbus.engineApiWsServer = newRpcWebSocketServer(
initTAddress(conf.engineApiWsAddress, conf.engineApiWsPort),
authHooks = @[wsJwtAuthHook, wsCorsHook]
)
setupEngineAPI(nimbus.beaconEngine, nimbus.engineApiWsServer)
setupEthRpc(nimbus.ethNode, nimbus.ctx, com, nimbus.txPool, nimbus.engineApiWsServer)
nimbus.engineApiWsServer.start()
else:
setupEngineAPI(nimbus.beaconEngine, nimbus.wsRpcServer)
info "Starting WebSocket engine API server", port = conf.engineApiWsPort
# metrics server
if conf.metricsEnabled:
info "Starting metrics HTTP server", address = conf.metricsAddress, port = conf.metricsPort
startMetricsHttpServer($conf.metricsAddress, conf.metricsPort)
let res = MetricsHttpServerRef.new($conf.metricsAddress, conf.metricsPort)
if res.isErr:
fatal "Failed to create metrics server", msg=res.error
quit(QuitFailure)
nimbus.metricsServer = res.get
waitFor nimbus.metricsServer.start()
proc start(nimbus: NimbusNode, conf: NimbusConf) =
## logging
@ -468,42 +331,13 @@ proc start(nimbus: NimbusNode, conf: NimbusConf) =
of SyncMode.Snap:
nimbus.snapSyncRef.start
if nimbus.state == Starting:
if nimbus.state == NimbusState.Starting:
# it might have been set to "Stopping" with Ctrl+C
nimbus.state = Running
proc stop*(nimbus: NimbusNode, conf: NimbusConf) {.async, gcsafe.} =
trace "Graceful shutdown"
if conf.rpcEnabled:
await nimbus.rpcServer.stop()
# nimbus.engineApiServer can be nil if conf.engineApiPort == conf.rpcPort
if conf.engineApiEnabled and nimbus.engineApiServer.isNil.not:
await nimbus.engineApiServer.stop()
if conf.wsEnabled:
nimbus.wsRpcServer.stop()
# nimbus.engineApiWsServer can be nil if conf.engineApiWsPort == conf.wsPort
if conf.engineApiWsEnabled and nimbus.engineApiWsServer.isNil.not:
nimbus.engineApiWsServer.stop()
if conf.graphqlEnabled:
await nimbus.graphqlServer.stop()
if conf.engineSigner != ZERO_ADDRESS and nimbus.sealingEngine.isNil.not:
await nimbus.sealingEngine.stop()
if conf.maxPeers > 0:
await nimbus.networkLoop.cancelAndWait()
if nimbus.peerManager.isNil.not:
await nimbus.peerManager.stop()
if nimbus.statelessSyncRef.isNil.not:
nimbus.statelessSyncRef.stop()
if nimbus.snapSyncRef.isNil.not:
nimbus.snapSyncRef.stop()
if nimbus.fullSyncRef.isNil.not:
nimbus.fullSyncRef.stop()
if nimbus.beaconSyncRef.isNil.not:
nimbus.beaconSyncRef.stop()
nimbus.state = NimbusState.Running
proc process*(nimbus: NimbusNode, conf: NimbusConf) =
# Main event loop
while nimbus.state == Running:
while nimbus.state == NimbusState.Running:
try:
poll()
except CatchableError as e:
@ -514,14 +348,14 @@ proc process*(nimbus: NimbusNode, conf: NimbusConf) =
waitFor nimbus.stop(conf)
when isMainModule:
var nimbus = NimbusNode(state: Starting, ctx: newEthContext())
var nimbus = NimbusNode(state: NimbusState.Starting, ctx: newEthContext())
## Ctrl+C handling
proc controlCHandler() {.noconv.} =
when defined(windows):
# workaround for https://github.com/nim-lang/Nim/issues/4057
setupForeignThreadGc()
nimbus.state = Stopping
nimbus.state = NimbusState.Stopping
echo "\nCtrl+C pressed. Waiting for a graceful shutdown."
setControlCHook(controlCHandler)

94
nimbus/nimbus_desc.nim Normal file
View File

@ -0,0 +1,94 @@
# Nimbus
# Copyright (c) 2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
import
chronos,
eth/p2p,
metrics/chronos_httpserver,
./rpc/rpc_server,
./core/sealer,
./core/chain,
./core/tx_pool,
./sync/peers,
./sync/beacon,
./sync/legacy,
./sync/snap,
./sync/stateless,
./sync/full,
./beacon/beacon_engine,
./common,
./config
export
chronos,
p2p,
chronos_httpserver,
rpc_server,
sealer,
chain,
tx_pool,
peers,
beacon,
legacy,
snap,
stateless,
full,
beacon_engine,
common,
config
type
NimbusState* = enum
Starting, Running, Stopping
NimbusNode* = ref object
httpServer*: NimbusHttpServerRef
engineApiServer*: NimbusHttpServerRef
ethNode*: EthereumNode
state*: NimbusState
sealingEngine*: SealingEngineRef
ctx*: EthContext
chainRef*: ChainRef
txPool*: TxPoolRef
networkLoop*: Future[void]
peerManager*: PeerManagerRef
legaSyncRef*: LegacySyncRef
snapSyncRef*: SnapSyncRef
fullSyncRef*: FullSyncRef
beaconSyncRef*: BeaconSyncRef
statelessSyncRef*: StatelessSyncRef
beaconEngine*: BeaconEngineRef
metricsServer*: MetricsHttpServerRef
{.push gcsafe, raises: [].}
proc stop*(nimbus: NimbusNode, conf: NimbusConf) {.async, gcsafe.} =
trace "Graceful shutdown"
if nimbus.httpServer.isNil.not:
await nimbus.httpServer.stop()
if nimbus.engineApiServer.isNil.not:
await nimbus.engineApiServer.stop()
if conf.engineSigner != ZERO_ADDRESS and nimbus.sealingEngine.isNil.not:
await nimbus.sealingEngine.stop()
if conf.maxPeers > 0:
await nimbus.networkLoop.cancelAndWait()
if nimbus.peerManager.isNil.not:
await nimbus.peerManager.stop()
if nimbus.statelessSyncRef.isNil.not:
nimbus.statelessSyncRef.stop()
if nimbus.snapSyncRef.isNil.not:
nimbus.snapSyncRef.stop()
if nimbus.fullSyncRef.isNil.not:
nimbus.fullSyncRef.stop()
if nimbus.beaconSyncRef.isNil.not:
nimbus.beaconSyncRef.stop()
if nimbus.metricsServer.isNil.not:
await nimbus.metricsServer.stop()
{.pop.}

View File

@ -8,6 +8,10 @@
# those terms.
import
chronicles,
websock/websock,
json_rpc/rpcserver,
graphql/httpserver,
./rpc/common,
./rpc/debug,
./rpc/engine_api,
@ -15,7 +19,9 @@ import
./rpc/jwt_auth,
./rpc/cors,
./rpc/rpc_server,
./rpc/experimental
./rpc/experimental,
./nimbus_desc,
./graphql/ethapi
export
common,
@ -26,3 +32,252 @@ export
cors,
rpc_server,
experimental
{.push gcsafe, raises: [].}
const DefaultChunkSize = 8192
func serverEnabled(conf: NimbusConf): bool =
conf.httpServerEnabled or
conf.engineApiServerEnabled
func combinedServer(conf: NimbusConf): bool =
conf.httpServerEnabled and
conf.shareServerWithEngineApi
proc installRPC(server: RpcServer,
nimbus: NimbusNode,
conf: NimbusConf,
com: CommonRef,
flags: set[RpcFlag]) =
setupCommonRpc(nimbus.ethNode, conf, server)
if RpcFlag.Eth in flags:
setupEthRpc(nimbus.ethNode, nimbus.ctx, com, nimbus.txPool, server)
if RpcFlag.Debug in flags:
setupDebugRpc(com, server)
if RpcFlag.Exp in flags:
setupExpRpc(com, server)
server.rpc("admin_quit") do() -> string:
{.gcsafe.}:
nimbus.state = NimbusState.Stopping
result = "EXITING"
proc newRpcWebsocketHandler(): RpcWebSocketHandler =
let rng = HmacDrbgContext.new()
RpcWebSocketHandler(
wsserver: WSServer.new(rng = rng),
)
proc newRpcHttpHandler(): RpcHttpHandler =
RpcHttpHandler(
maxChunkSize: DefaultChunkSize,
)
proc addHandler(handlers: var seq[RpcHandlerProc],
server: RpcHttpHandler) =
proc handlerProc(request: HttpRequestRef):
Future[RpcHandlerResult] {.async: (raises: []).} =
try:
let res = await server.serveHTTP(request)
if res.isNil:
return RpcHandlerResult(status: RpcHandlerStatus.Skip)
else:
return RpcHandlerResult(status: RpcHandlerStatus.Response, response: res)
except CancelledError:
return RpcHandlerResult(status: RpcHandlerStatus.Error)
handlers.add handlerProc
proc addHandler(handlers: var seq[RpcHandlerProc],
server: RpcWebSocketHandler) =
proc handlerProc(request: HttpRequestRef):
Future[RpcHandlerResult] {.async: (raises: []).} =
if not request.headers.contains("Sec-WebSocket-Version"):
return RpcHandlerResult(status: RpcHandlerStatus.Skip)
let stream = websock.AsyncStream(
reader: request.connection.mainReader,
writer: request.connection.mainWriter,
)
let req = websock.HttpRequest(
meth: request.meth,
uri: request.uri,
version: request.version,
headers: request.headers,
stream: stream,
)
try:
await server.serveHTTP(req)
return RpcHandlerResult(status: RpcHandlerStatus.KeepConnection)
except CancelledError:
return RpcHandlerResult(status: RpcHandlerStatus.Error)
handlers.add handlerProc
proc addHandler(handlers: var seq[RpcHandlerProc],
server: GraphqlHttpHandlerRef) =
proc handlerProc(request: HttpRequestRef):
Future[RpcHandlerResult] {.async: (raises: []).} =
try:
let res = await server.serveHTTP(request)
if res.isNil:
return RpcHandlerResult(status: RpcHandlerStatus.Skip)
else:
return RpcHandlerResult(status: RpcHandlerStatus.Response, response: res)
except CatchableError:
return RpcHandlerResult(status: RpcHandlerStatus.Error)
handlers.add handlerProc
proc addHttpServices(handlers: var seq[RpcHandlerProc],
nimbus: NimbusNode, conf: NimbusConf,
com: CommonRef, protocols: set[ProtocolFlag]) =
# The order is important: graphql, ws, rpc
# graphql depends on /graphl path
# ws depends on Sec-WebSocket-Version header
# json-rpc have no reliable identification
if conf.graphqlEnabled:
let ctx = setupGraphqlContext(com, nimbus.ethNode, nimbus.txPool)
let server = GraphqlHttpHandlerRef.new(ctx)
handlers.addHandler(server)
if conf.wsEnabled:
let server = newRpcWebsocketHandler()
var rpcFlags = conf.getWsFlags()
if ProtocolFlag.Eth in protocols: rpcFlags.incl RpcFlag.Eth
installRPC(server, nimbus, conf, com, rpcFlags)
handlers.addHandler(server)
if conf.rpcEnabled:
let server = newRpcHttpHandler()
var rpcFlags = conf.getRpcFlags()
if ProtocolFlag.Eth in protocols: rpcFlags.incl RpcFlag.Eth
installRPC(server, nimbus, conf, com, rpcFlags)
handlers.addHandler(server)
proc addEngineApiServices(handlers: var seq[RpcHandlerProc],
nimbus: NimbusNode, conf: NimbusConf,
com: CommonRef) =
# The order is important: ws, rpc
if conf.engineApiWsEnabled:
let server = newRpcWebsocketHandler()
setupEngineAPI(nimbus.beaconEngine, server)
installRPC(server, nimbus, conf, com, {RpcFlag.Eth})
handlers.addHandler(server)
if conf.engineApiEnabled:
let server = newRpcHttpHandler()
setupEngineAPI(nimbus.beaconEngine, server)
installRPC(server, nimbus, conf, com, {RpcFlag.Eth})
handlers.addHandler(server)
proc addServices(handlers: var seq[RpcHandlerProc],
nimbus: NimbusNode, conf: NimbusConf,
com: CommonRef, protocols: set[ProtocolFlag]) =
# The order is important: graphql, ws, rpc
if conf.graphqlEnabled:
let ctx = setupGraphqlContext(com, nimbus.ethNode, nimbus.txPool)
let server = GraphqlHttpHandlerRef.new(ctx)
handlers.addHandler(server)
if conf.wsEnabled or conf.engineApiWsEnabled:
let server = newRpcWebsocketHandler()
if conf.engineApiWsEnabled:
setupEngineAPI(nimbus.beaconEngine, server)
if not conf.wsEnabled:
installRPC(server, nimbus, conf, com, {RpcFlag.Eth})
if conf.wsEnabled:
var rpcFlags = conf.getWsFlags()
if ProtocolFlag.Eth in protocols: rpcFlags.incl RpcFlag.Eth
installRPC(server, nimbus, conf, com, rpcFlags)
handlers.addHandler(server)
if conf.rpcEnabled or conf.engineApiEnabled:
let server = newRpcHttpHandler()
if conf.engineApiEnabled:
setupEngineAPI(nimbus.beaconEngine, server)
if not conf.rpcEnabled:
installRPC(server, nimbus, conf, com, {RpcFlag.Eth})
if conf.rpcEnabled:
var rpcFlags = conf.getRpcFlags()
if ProtocolFlag.Eth in protocols: rpcFlags.incl RpcFlag.Eth
installRPC(server, nimbus, conf, com, rpcFlags)
handlers.addHandler(server)
proc setupRpc*(nimbus: NimbusNode, conf: NimbusConf,
com: CommonRef, protocols: set[ProtocolFlag]) =
if not conf.serverEnabled:
return
# Provide JWT authentication handler for rpcHttpServer
let jwtKey = block:
# Create or load shared secret
let rc = nimbus.ctx.rng.jwtSharedSecret(conf)
if rc.isErr:
fatal "Failed create or load shared secret",
msg = $(rc.unsafeError) # avoid side effects
quit(QuitFailure)
rc.value
let
allowedOrigins = conf.getAllowedOrigins()
jwtAuthHook = httpJwtAuth(jwtKey)
corsHook = httpCors(allowedOrigins)
if conf.combinedServer:
let hooks = @[jwtAuthHook, corsHook]
var handlers: seq[RpcHandlerProc]
handlers.addServices(nimbus, conf, com, protocols)
let address = initTAddress(conf.httpAddress, conf.httpPort)
let res = newHttpServerWithParams(address, hooks, handlers)
if res.isErr:
fatal "Cannot create RPC server", msg=res.error
quit(QuitFailure)
nimbus.httpServer = res.get
nimbus.httpServer.start()
return
if conf.httpServerEnabled:
let hooks = @[corsHook]
var handlers: seq[RpcHandlerProc]
handlers.addHttpServices(nimbus, conf, com, protocols)
let address = initTAddress(conf.httpAddress, conf.httpPort)
let res = newHttpServerWithParams(address, hooks, handlers)
if res.isErr:
fatal "Cannot create RPC server", msg=res.error
quit(QuitFailure)
nimbus.httpServer = res.get
nimbus.httpServer.start()
if conf.engineApiServerEnabled:
let hooks = @[jwtAuthHook, corsHook]
var handlers: seq[RpcHandlerProc]
handlers.addEngineApiServices(nimbus, conf, com)
let address = initTAddress(conf.engineApiAddress, conf.engineApiPort)
let res = newHttpServerWithParams(address, hooks, handlers)
if res.isErr:
fatal "Cannot create RPC server", msg=res.error
quit(QuitFailure)
nimbus.engineApiServer = res.get
nimbus.engineApiServer.start()
{.pop.}

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2022 Status Research & Development GmbH
# Copyright (c) 2022-2024 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at
# https://opensource.org/licenses/MIT).
@ -11,12 +11,12 @@
import
std/[uri],
chronos,
chronos/apps/http/[httptable, httpserver],
json_rpc/rpcserver,
chronos/apps/http/httptable,
chronos/apps/http/httpserver,
httputils,
websock/websock as ws
./rpc_server
{.push raises: [].}
{.push gcsafe, raises: [].}
proc sameOrigin(a, b: Uri): bool =
a.hostname == b.hostname and
@ -30,8 +30,9 @@ proc containsOrigin(list: seq[Uri], origin: Uri): bool =
const
HookOK = HttpResponseRef(nil)
proc httpCors*(allowedOrigins: seq[Uri]): HttpAuthHook =
proc handler(req: HttpRequestRef): Future[HttpResponseRef] {.async.} =
proc httpCors*(allowedOrigins: seq[Uri]): RpcAuthHook =
proc handler(req: HttpRequestRef): Future[HttpResponseRef]
{.gcsafe, async: (raises: [CatchableError]).} =
let origins = req.headers.getList("Origin")
let everyOriginAllowed = allowedOrigins.len == 0
@ -87,12 +88,4 @@ proc httpCors*(allowedOrigins: seq[Uri]): HttpAuthHook =
# the rest of response in server
return HookOK
result = HttpAuthHook(handler)
proc wsCors*(allowedOrigins: seq[Uri]): WsAuthHook =
proc handler(req: ws.HttpRequest): Future[bool] {.async.} =
# TODO: implement websock equivalent of
# request.getResponse
return true
result = WsAuthHook(handler)
result = handler

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2022 Status Research & Development GmbH
# Copyright (c) 2022-2024 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at
# https://opensource.org/licenses/MIT).
@ -12,20 +12,21 @@
# nimbus-eth2/beacon_chain/spec/engine_authentication.nim
# go-ethereum/node/jwt_handler.go
{.push raises: [].}
{.push gcsafe, raises: [].}
import
std/[base64, json, options, os, strutils, times],
std/[base64, options, strutils, times],
bearssl/rand,
chronicles,
chronos,
chronos/apps/http/[httptable, httpserver],
json_rpc/rpcserver,
chronos/apps/http/httptable,
chronos/apps/http/httpserver,
httputils,
websock/websock as ws,
nimcrypto/[hmac, utils],
stew/[byteutils, objects, results],
../config
../config,
./jwt_auth_helper,
./rpc_server
logScope:
topics = "Jwt/HS256 auth"
@ -70,14 +71,7 @@ type
jwtMethodUnsupported = "token protected header provides unsupported method"
jwtTimeValidationError = "token time validation failed"
jwtTokenValidationError = "token signature validation failed"
JwtHeader = object ##\
## Template used for JSON unmarshalling
typ, alg: string
JwtIatPayload = object ##\
## Template used for JSON unmarshalling
iat: uint64
jwtCreationError = "Cannot create jwt secret"
# ------------------------------------------------------------------------------
# Private functions
@ -113,7 +107,7 @@ proc verifyTokenHS256(token: string; key: JwtSharedKey): Result[void,JwtError] =
let jsonHeader = p[0].base64urlDecode
error = jwtProtHeaderInvJson
let jwtHeader = jsonHeader.parseJson.to(JwtHeader)
let jwtHeader = jsonHeader.decodeJwtHeader()
# The following JSON decoded object is required
if jwtHeader.typ != "JWT" and jwtHeader.alg != "HS256":
@ -124,18 +118,16 @@ proc verifyTokenHS256(token: string; key: JwtSharedKey): Result[void,JwtError] =
let jsonPayload = p[1].base64urlDecode
error = jwtIatPayloadInvJson
let jwtPayload = jsonPayload.parseJson.to(JwtIatPayload)
let jwtPayload = jsonPayload.decodeJwtIatPayload()
time = jwtPayload.iat.int64
except CatchableError as e:
discard e
debug "JWT token decoding error",
protectedHeader = p[0],
payload = p[1],
msg = e.msg,
error
return err(error)
except Exception as e:
{.warning: "Kludge(BareExcept): `parseJson()` in vendor package needs to be updated".}
raiseAssert "Ooops verifyTokenHS256(): name=" & $e.name & " msg=" & e.msg
# github.com/ethereum/
# /execution-apis/blob/v1.0.0-beta.3/src/engine/authentication.md#jwt-claims
@ -195,8 +187,7 @@ proc jwtGenSecret*(rng: ref rand.HmacDrbgContext): JwtGenSecret =
proc jwtSharedSecret*(
rndSecret: JwtGenSecret;
config: NimbusConf;
): Result[JwtSharedKey, JwtError]
{.gcsafe, raises: [CatchableError].}=
): Result[JwtSharedKey, JwtError] =
## Return a key for jwt authentication preferable from the argument file
## `config.jwtSecret` (which contains at least 32 bytes hex encoded random
## data.) Otherwise it creates a key and stores it in the `config.dataDir`.
@ -226,18 +217,19 @@ proc jwtSharedSecret*(
# github.com/ethereum/
# /execution-apis/blob/v1.0.0-alpha.8/src/engine/
# /authentication.md#key-distribution
let
jwtSecretPath = config.dataDir.string / jwtSecretFile
newSecret = rndSecret()
let jwtSecretPath = config.dataDir.string & "/" & jwtSecretFile
try:
let newSecret = rndSecret()
jwtSecretPath.writeFile(newSecret.JwtSharedKeyRaw.to0xHex)
return ok(newSecret)
except IOError as e:
# Allow continuing to run, though this is effectively fatal for a merge
# client using authentication. This keeps it lower-risk initially.
warn "Could not write JWT secret to data directory",
jwtSecretPath
discard e
return ok(newSecret)
except CatchableError:
return err(jwtCreationError)
try:
let lines = config.jwtSecret.get.string.readLines(1)
@ -254,13 +246,16 @@ proc jwtSharedSecret*(
return err(jwtKeyInvalidHexString)
proc jwtSharedSecret*(rng: ref rand.HmacDrbgContext; config: NimbusConf):
Result[JwtSharedKey, JwtError]
{.gcsafe, raises: [CatchableError].} =
Result[JwtSharedKey, JwtError] =
## Variant of `jwtSharedSecret()` with explicit random generator argument.
rng.jwtGenSecret.jwtSharedSecret(config)
try:
rng.jwtGenSecret.jwtSharedSecret(config)
except CatchableError:
return err(jwtCreationError)
proc httpJwtAuth*(key: JwtSharedKey): HttpAuthHook =
proc handler(req: HttpRequestRef): Future[HttpResponseRef] {.async.} =
proc httpJwtAuth*(key: JwtSharedKey): RpcAuthHook =
proc handler(req: HttpRequestRef): Future[HttpResponseRef]
{.gcsafe, async: (raises: [CatchableError]).} =
let auth = req.headers.getString("Authorization", "?")
if auth.len < 9 or auth[0..6].cmpIgnoreCase("Bearer ") != 0:
return await req.respond(Http403, "Missing authorization token")
@ -278,31 +273,7 @@ proc httpJwtAuth*(key: JwtSharedKey): HttpAuthHook =
else:
return await req.respond(Http403, "Malformed token")
result = HttpAuthHook(handler)
proc wsJwtAuth*(key: JwtSharedKey): WsAuthHook =
proc handler(req: ws.HttpRequest): Future[bool] {.async.} =
let auth = req.headers.getString("Authorization", "?")
if auth.len < 9 or auth[0..6].cmpIgnoreCase("Bearer ") != 0:
await req.sendResponse(code = Http403, data = "Missing authorization token")
return false
let rc = auth[7..^1].strip.verifyTokenHS256(key)
if rc.isOk:
return true
debug "Could not authenticate",
error = rc.error
case rc.error:
of jwtTokenValidationError, jwtMethodUnsupported:
await req.sendResponse(code = Http403, data = "Unauthorized access")
else:
await req.sendResponse(code = Http403, data = "Malformed token")
return false
result = WsAuthHook(handler)
result = handler
# ------------------------------------------------------------------------------
# End

View File

@ -0,0 +1,45 @@
# Nimbus
# Copyright (c) 2024 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at
# https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at
# https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
import
json_serialization
type
JwtHeader* = object ##\
## Template used for JSON unmarshalling
typ*, alg*: string
JwtIatPayload* = object ##\
## Template used for JSON unmarshalling
iat*: uint64
createJsonFlavor JAuth,
automaticObjectSerialization = false,
requireAllFields = false,
allowUnknownFields = true
JwtHeader.useDefaultSerializationIn JAuth
JwtIatPayload.useDefaultSerializationIn JAuth
# This file separated from jwt_auth.nim
# is to prevent generic resolution clash between
# json_serialization and base64
{.push gcsafe, raises: [].}
proc decodeJwtHeader*(jsonBytes: string): JwtHeader
{.gcsafe, raises: [SerializationError].} =
JAuth.decode(jsonBytes, JwtHeader)
proc decodeJwtIatPayload*(jsonBytes: string): JwtIatPayload
{.gcsafe, raises: [SerializationError].} =
JAuth.decode(jsonBytes, JwtIatPayload)
{.pop.}

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2023 Status Research & Development GmbH
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
@ -7,22 +7,57 @@
# This file may not be copied, modified, or distributed except according to
# those terms.
when false:
import
std/importutils
import
chronos,
json_rpc/rpcserver
json_rpc/servers/httpserver as jrpc_server,
chronos/apps/http/httpserver {.all.}
type
RpcHttpServerParams = object
serverFlags: set[HttpServerFlags]
socketFlags: set[ServerFlags]
serverUri: Uri
serverIdent: string
maxConnections: int
bufferSize: int
backlogSize: int
httpHeadersTimeout: chronos.Duration
httpHeadersTimeout: Duration
maxHeadersSize: int
maxRequestBodySize: int
RpcHandlerStatus* {.pure.} = enum
Skip
Response
KeepConnection
Error
RpcHandlerResult* = object
status*: RpcHandlerStatus
response*: HttpResponseRef
RpcProcessExitType* {.pure.} = enum
KeepAlive
Graceful
Immediate
KeepConnection
RpcAuthHook* = proc(request: HttpRequestRef): Future[HttpResponseRef]
{.gcsafe, async: (raises: [CatchableError]).}
RpcHandlerProc* = proc(request: HttpRequestRef): Future[RpcHandlerResult]
{.async: (raises: []).}
NimbusHttpServer* = object of RootObj
server: HttpServerRef
authHooks: seq[RpcAuthHook]
handlers: seq[RpcHandlerProc]
NimbusHttpServerRef* = ref NimbusHttpServer
{.push gcsafe, raises: [].}
func defaultRpcHttpServerParams(): RpcHttpServerParams =
RpcHttpServerParams(
@ -37,40 +72,44 @@ func defaultRpcHttpServerParams(): RpcHttpServerParams =
maxRequestBodySize: 2 * 1024 * 1024,
)
template processResolvedAddresses =
if tas4.len + tas6.len == 0:
# Addresses could not be resolved, critical error.
raise newException(RpcAddressUnresolvableError, "Unable to get address!")
proc resolvedAddress(address: string): Result[TransportAddress, string] =
var tas: seq[TransportAddress]
for r in tas4:
yield r
if tas4.len == 0: # avoid ipv4 + ipv6 running together
for r in tas6:
yield r
iterator resolvedAddresses(address: string): TransportAddress =
var
tas4: seq[TransportAddress]
tas6: seq[TransportAddress]
# Attempt to resolve `address` for IPv4 address space.
try:
tas4 = resolveTAddress(address, AddressFamily.IPv4)
tas = resolveTAddress(address, AddressFamily.IPv4)
if tas.len == 1:
return ok(tas[0])
if tas.len > 1:
return err("Too much address for HTTP server: " & $tas.len)
except CatchableError:
# It might be an IPv6
discard
# Attempt to resolve `address` for IPv6 address space.
try:
tas6 = resolveTAddress(address, AddressFamily.IPv6)
tas = resolveTAddress(address, AddressFamily.IPv6)
if tas.len == 1:
return ok(tas[0])
if tas.len > 1:
return err("Too much address for HTTP server: " & $tas.len)
if tas.len == 0:
return err("No address found for HTTP server")
except CatchableError:
discard
return err("Failed to decode HTTP server address")
processResolvedAddresses()
proc createServer(address: TransportAddress,
params: RpcHttpServerParams): HttpResult[HttpServerRef] =
proc addServer*(server: RpcHttpServer, address: TransportAddress, params: RpcHttpServerParams) =
server.addHttpServer(
proc processCallback(req: RequestFence):
Future[HttpResponseRef] {.
async: (raises: [CancelledError]).} =
# This is a dummy callback because we are going to
# create our own callback
return nil
HttpServerRef.new(
address,
processCallback,
params.serverFlags,
params.socketFlags,
params.serverUri,
params.serverIdent,
@ -81,21 +120,233 @@ proc addServer*(server: RpcHttpServer, address: TransportAddress, params: RpcHtt
params.maxHeadersSize,
params.maxRequestBodySize)
proc addServer*(server: RpcHttpServer, address: string, params: RpcHttpServerParams) =
## Create new server and assign it to addresses ``addresses``.
for a in resolvedAddresses(address):
# TODO handle partial failures, ie when 1/N addresses fail
server.addServer(a, params)
proc createServer(address: string,
params: RpcHttpServerParams): HttpResult[HttpServerRef] =
## Create new server and assign it to ``address``.
let serverAddress = resolvedAddress(address).valueOr:
return err(error)
createServer(serverAddress, params)
proc newRpcHttpServerWithParams*(address: TransportAddress, authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer =
proc newHttpServerWithParams*(address: TransportAddress or string,
authHooks: sink seq[RpcAuthHook] = @[],
handlers: sink seq[RpcHandlerProc]):
HttpResult[NimbusHttpServerRef] =
## Create new server and assign it to ``address``.
let params = defaultRpcHttpServerParams()
let inner = createServer(address, params)
if inner.isErr:
return err(inner.error)
let server = NimbusHttpServerRef(
server: inner.get,
authHooks: system.move(authHooks),
handlers: system.move(handlers),
)
return ok(server)
proc invokeProcessCallback(nserver: NimbusHttpServerRef,
req: RequestFence): Future[RpcHandlerResult] {.
async: (raises: []).} =
when false:
let server = nserver.server
privateAccess(type server)
if len(server.middlewares) > 0:
server.middlewares[0](req)
else:
server.processCallback(req)
if req.isErr:
return RpcHandlerResult(
status: RpcHandlerStatus.Response,
response: defaultResponse(),
)
let request = req.get()
# If hook result is not nil,
# it means we should return immediately
try:
for hook in nserver.authHooks:
let res = await hook(request)
if not res.isNil:
return RpcHandlerResult(
status: RpcHandlerStatus.Response,
response: res,
)
except CatchableError as exc:
return RpcHandlerResult(
status: RpcHandlerStatus.Response,
response: defaultResponse(exc),
)
# If handler result.status != Skip,
# return immediately
for handler in nserver.handlers:
let res = await handler(request)
if res.status != RpcHandlerStatus.Skip:
return res
# not handled
return RpcHandlerResult(
status: RpcHandlerStatus.Response,
response: defaultResponse(),
)
proc processRequest(nserver: NimbusHttpServerRef,
connection: HttpConnectionRef,
connId: string): Future[RpcProcessExitType] {.
async: (raises: []).} =
let server = nserver.server
let requestFence = await getRequestFence(server, connection)
if requestFence.isErr():
case requestFence.error.kind
of HttpServerError.InterruptError:
# Cancelled, exiting
return RpcProcessExitType.Immediate
of HttpServerError.DisconnectError:
# Remote peer disconnected
if HttpServerFlags.NotifyDisconnect notin server.flags:
return RpcProcessExitType.Immediate
else:
# Request is incorrect or unsupported, sending notification
discard
try:
let response =
try:
await invokeProcessCallback(nserver, requestFence)
except CancelledError:
# Cancelled, exiting
return RpcProcessExitType.Immediate
case response.status
of RpcHandlerStatus.Skip: discard
of RpcHandlerStatus.Response:
let res = await connection.sendDefaultResponse(requestFence, response.response)
return RpcProcessExitType(res.ord)
of RpcHandlerStatus.KeepConnection:
return RpcProcessExitType.KeepConnection
of RpcHandlerStatus.Error:
return RpcProcessExitType.Immediate
finally:
if requestFence.isOk():
let request = requestFence.get()
if result == RpcProcessExitType.KeepConnection:
request.response = Opt.none(HttpResponseRef)
await request.closeWait()
proc processLoop(nserver: NimbusHttpServerRef, holder: HttpConnectionHolderRef) {.async: (raises: []).} =
let
server = holder.server
transp = holder.transp
connectionId = holder.connectionId
connection =
block:
let res = await getConnectionFence(server, transp)
if res.isErr():
if res.error.kind != HttpServerError.InterruptError:
discard await noCancel(
invokeProcessCallback(nserver, RequestFence.err(res.error)))
server.connections.del(connectionId)
return
res.get()
holder.connection = connection
var runLoop = RpcProcessExitType.KeepAlive
while runLoop == RpcProcessExitType.KeepAlive:
runLoop = await nserver.processRequest(connection, connectionId)
case runLoop
of RpcProcessExitType.KeepAlive:
await connection.closeWait()
of RpcProcessExitType.Immediate:
await connection.closeWait()
of RpcProcessExitType.Graceful:
await connection.gracefulCloseWait()
of RpcProcessExitType.KeepConnection:
discard
server.connections.del(connectionId)
proc acceptClientLoop(nserver: NimbusHttpServerRef) {.async: (raises: []).} =
let server = nserver.server
var runLoop = true
while runLoop:
try:
let transp = await server.instance.accept()
let resId = transp.getId()
if resId.isErr():
# We are unable to identify remote peer, it means that remote peer
# disconnected before identification.
await transp.closeWait()
runLoop = false
else:
let connId = resId.get()
let holder = HttpConnectionHolderRef.new(server, transp, resId.get())
server.connections[connId] = holder
holder.future = processLoop(nserver, holder)
except TransportTooManyError, TransportAbortedError:
# Non-critical error
discard
except CancelledError, TransportOsError, CatchableError:
# Critical, cancellation or unexpected error
runLoop = false
proc start*(server: NimbusHttpServerRef) =
if server.server.state == ServerStopped:
server.server.acceptLoop = acceptClientLoop(server)
proc stop*(server: NimbusHttpServerRef) {.async: (raises: []).} =
await server.server.stop()
proc closeWait*(server: NimbusHttpServerRef) {.async: (raises: []).} =
await server.server.closeWait()
func localAddress*(server: NimbusHttpServerRef): TransportAddress =
server.server.instance.localAddress()
proc addServer*(server: RpcHttpServer,
address: TransportAddress,
params: RpcHttpServerParams): Result[void, string] =
try:
server.addHttpServer(
address,
params.socketFlags,
params.serverUri,
params.serverIdent,
params.maxConnections,
params.bufferSize,
params.backlogSize,
params.httpHeadersTimeout,
params.maxHeadersSize,
params.maxRequestBodySize)
return ok()
except CatchableError as exc:
return err(exc.msg)
proc addServer*(server: RpcHttpServer,
address: string,
params: RpcHttpServerParams): Result[void, string] =
let serverAddress = resolvedAddress(address).valueOr:
return err(error)
server.addServer(serverAddress, params)
proc newRpcHttpServerWithParams*(address: TransportAddress,
authHooks: seq[HttpAuthHook] = @[]): Result[RpcHttpServer, string] =
## Create new server and assign it to addresses ``addresses``.
let server = RpcHttpServer.new(authHooks)
let params = defaultRpcHttpServerParams()
server.addServer(address, params)
server
server.addServer(address, params).isOkOr:
return err(error)
ok(server)
proc newRpcHttpServerWithParams*(address: string, authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer =
proc newRpcHttpServerWithParams*(address: string,
authHooks: seq[HttpAuthHook] = @[]): Result[RpcHttpServer, string] =
let server = RpcHttpServer.new(authHooks)
let params = defaultRpcHttpServerParams()
server.addServer(address, params)
server
server.addServer(address, params).isOkOr:
return err(error)
ok(server)
{.pop.}

View File

@ -17,9 +17,6 @@ import
../nimbus/common/[chain_config, context],
./test_helpers
proc `==`(a, b: ChainId): bool =
a.int == b.int
proc configurationMain*() =
suite "configuration test suite":
const
@ -226,32 +223,64 @@ proc configurationMain*() =
check conf.getBootnodes().len == 0
test "json-rpc enabled when json-engine api enabled and share same port":
let conf = makeConfig(@["--engine-api", "--engine-api-port:8545", "--rpc-port:8545"])
check conf.engineApiEnabled
check conf.rpcEnabled
check conf.wsEnabled == false
check conf.engineApiWsEnabled == false
let conf = makeConfig(@["--engine-api", "--engine-api-port:8545", "--http-port:8545"])
check:
conf.engineApiEnabled == true
conf.rpcEnabled == false
conf.wsEnabled == false
conf.engineApiWsEnabled == false
conf.graphqlEnabled == false
conf.engineApiServerEnabled
conf.httpServerEnabled == false
conf.shareServerWithEngineApi
test "ws-rpc enabled when ws-engine api enabled and share same port":
let conf = makeConfig(@["--engine-api-ws", "--engine-api-ws-port:8546", "--ws-port:8546"])
check conf.engineApiWsEnabled
check conf.wsEnabled
check conf.engineApiEnabled == false
check conf.rpcEnabled == false
let conf = makeConfig(@["--ws", "--engine-api-ws", "--engine-api-port:8546", "--http-port:8546"])
check:
conf.engineApiWsEnabled
conf.wsEnabled
conf.engineApiEnabled == false
conf.rpcEnabled == false
conf.graphqlEnabled == false
conf.engineApiServerEnabled
conf.httpServerEnabled
conf.shareServerWithEngineApi
test "json-rpc stay enabled when json-engine api enabled and using different port":
let conf = makeConfig(@["--rpc", "--engine-api", "--engine-api-port:8550", "--rpc-port:8545"])
check conf.engineApiEnabled
check conf.rpcEnabled
check conf.engineApiWsEnabled == false
check conf.wsEnabled == false
let conf = makeConfig(@["--rpc", "--engine-api", "--engine-api-port:8550", "--http-port:8545"])
check:
conf.engineApiEnabled
conf.rpcEnabled
conf.engineApiWsEnabled == false
conf.wsEnabled == false
conf.graphqlEnabled == false
conf.httpServerEnabled
conf.engineApiServerEnabled
conf.shareServerWithEngineApi == false
test "ws-rpc stay enabled when ws-engine api enabled and using different port":
let conf = makeConfig(@["--ws", "--engine-api-ws", "--engine-api-ws-port:8551", "--ws-port:8546"])
check conf.engineApiWsEnabled
check conf.wsEnabled
check conf.engineApiEnabled == false
check conf.rpcEnabled == false
let conf = makeConfig(@["--ws", "--engine-api-ws", "--engine-api-port:8551", "--http-port:8546"])
check:
conf.engineApiWsEnabled
conf.wsEnabled
conf.engineApiEnabled == false
conf.rpcEnabled == false
conf.graphqlEnabled == false
conf.httpServerEnabled
conf.engineApiServerEnabled
conf.shareServerWithEngineApi == false
test "graphql enabled. ws, rpc, and engine api not enabled":
let conf = makeConfig(@["--graphql"])
check:
conf.engineApiWsEnabled == false
conf.wsEnabled == false
conf.engineApiEnabled == false
conf.rpcEnabled == false
conf.graphqlEnabled == true
conf.httpServerEnabled == true
conf.engineApiServerEnabled == false
conf.shareServerWithEngineApi == false
let ctx = newEthContext()
test "net-key random":

View File

@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2022-2023 Status Research & Development GmbH
# Copyright (c) 2022-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
@ -15,18 +15,20 @@ import
std/[base64, json, options, os, strutils, times],
../nimbus/config,
../nimbus/rpc/jwt_auth,
../nimbus/rpc {.all.},
./replay/pp,
chronicles,
chronos/apps/http/httpclient as chronoshttpclient,
chronos/apps/http/httptable,
eth/[common, keys, p2p],
json_rpc/rpcserver,
nimcrypto/[hmac, utils],
stew/results,
stint,
unittest2,
graphql,
graphql/[httpserver, httpclient]
websock/websock,
graphql/[httpserver, httpclient],
json_rpc/[rpcserver, rpcclient]
type
UnGuardedKey =
@ -115,7 +117,7 @@ proc getHttpAuthReqHeader2(secret: JwtSharedKey; time: uint64): HttpTable =
let bearer = secret.UnGuardedKey.getSignedToken2($getIatToken(time))
result.add("aUtHoRiZaTiOn", "Bearer " & bearer)
proc createServer(serverAddress: TransportAddress, authHooks: seq[HttpAuthHook] = @[]): GraphqlHttpServerRef =
proc createServer(serverAddress: TransportAddress, authHooks: seq[AuthHook] = @[]): GraphqlHttpServerRef =
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
var ctx = GraphqlRef.new()
@ -141,6 +143,48 @@ proc createServer(serverAddress: TransportAddress, authHooks: seq[HttpAuthHook]
proc setupClient(address: TransportAddress): GraphqlHttpClientRef =
GraphqlHttpClientRef.new(address, secure = false).get()
func localAddress(server: GraphqlHttpServerRef): TransportAddress =
server.server.instance.localAddress()
# ------------------------------------------------------------------------------
# Http combo helpers
# ------------------------------------------------------------------------------
proc newGraphqlHandler(): GraphqlHttpHandlerRef =
const schema = """type Query {name: String}"""
let ctx = GraphqlRef.new()
let r = ctx.parseSchema(schema)
if r.isErr:
debugEcho r.error
# continue with empty schema
GraphqlHttpHandlerRef.new(ctx)
proc installRPC(server: RpcServer) =
server.rpc("rpc_echo") do(input: int) -> string:
result = "hello: " & $input
proc setupComboServer(hooks: sink seq[RpcAuthHook]): HttpResult[NimbusHttpServerRef] =
var handlers: seq[RpcHandlerProc]
let qlServer = newGraphqlHandler()
handlers.addHandler(qlServer)
let wsServer = newRpcWebsocketHandler()
wsServer.installRPC()
handlers.addHandler(wsServer)
let rpcServer = newRpcHttpHandler()
rpcServer.installRPC()
handlers.addHandler(rpcServer)
let address = initTAddress("127.0.0.1:0")
newHttpServerWithParams(address, hooks, handlers)
createRpcSigsFromNim(RpcClient):
proc rpc_echo(input: int): string
# ------------------------------------------------------------------------------
# Test Runners
# ------------------------------------------------------------------------------
@ -255,7 +299,7 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) =
authHook = secret.value.httpJwtAuth
const
serverAddress = initTAddress("127.0.0.1:8547")
serverAddress = initTAddress("127.0.0.1:0")
query = """{ __type(name: "ID") { kind }}"""
suite "EngineAuth: Http/rpc authentication mechanics":
@ -283,7 +327,7 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) =
setTraceLevel()
# Run http authorisation request
let client = setupClient(serverAddress)
let client = setupClient(server.localAddress)
let res = waitFor client.sendRequest(query, req.toList)
check res.isOk
if res.isErr:
@ -309,7 +353,7 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) =
setTraceLevel()
# Run http authorisation request
let client = setupClient(serverAddress)
let client = setupClient(server.localAddress)
let res = waitFor client.sendRequest(query, req.toList)
check res.isOk
if res.isErr:
@ -325,6 +369,73 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) =
waitFor server.closeWait()
suite "Test combo http server":
let res = setupComboServer(@[authHook])
if res.isErr:
debugEcho res.error
quit(QuitFailure)
let
server = res.get
time = getTime().toUnix.uint64
req = secret.value.getHttpAuthReqHeader(time)
server.start()
test "Graphql query no auth":
let client = setupClient(server.localAddress)
let res = waitFor client.sendRequest(query)
check res.isOk
let resp = res.get()
check resp.status == 403
check resp.reason == "Forbidden"
check resp.response == "Missing authorization token"
test "Graphql query with auth":
let client = setupClient(server.localAddress)
let res = waitFor client.sendRequest(query, req.toList)
check res.isOk
let resp = res.get()
check resp.status == 200
check resp.reason == "OK"
check resp.response == """{"data":{"__type":{"kind":"SCALAR"}}}"""
test "rpc query no auth":
let client = newRpcHttpClient()
waitFor client.connect("http://" & $server.localAddress)
try:
let res = waitFor client.rpc_echo(100)
discard res
check false
except ErrorResponse as exc:
check exc.msg == "Forbidden"
test "rpc query with uth":
proc authHeaders(): seq[(string, string)] =
req.toList
let client = newRpcHttpClient(getHeaders = authHeaders)
waitFor client.connect("http://" & $server.localAddress)
let res = waitFor client.rpc_echo(100)
check res == "hello: 100"
test "ws query no auth":
let client = newRpcWebSocketClient()
expect WSFailedUpgradeError:
waitFor client.connect("ws://" & $server.localAddress)
test "ws query with auth":
proc authHeaders(): seq[(string, string)] =
req.toList
let client = newRpcWebSocketClient(authHeaders)
waitFor client.connect("ws://" & $server.localAddress)
let res = waitFor client.rpc_echo(123)
check res == "hello: 123"
let res2 = waitFor client.rpc_echo(145)
check res2 == "hello: 145"
waitFor server.closeWait()
# ------------------------------------------------------------------------------
# Main function(s)
# ------------------------------------------------------------------------------

View File

@ -75,7 +75,7 @@ proc runTest(steps: Steps) =
com.initializeEmptyDb()
var
rpcServer = newRpcSocketServer(["127.0.0.1:" & $conf.rpcPort])
rpcServer = newRpcSocketServer(["127.0.0.1:" & $conf.httpPort])
client = newRpcSocketClient()
txPool = TxPoolRef.new(com, conf.engineSigner)
sealingEngine = SealingEngineRef.new(
@ -89,7 +89,7 @@ proc runTest(steps: Steps) =
sealingEngine.start()
rpcServer.start()
waitFor client.connect("127.0.0.1", conf.rpcPort)
waitFor client.connect("127.0.0.1", conf.httpPort)
suite "Engine API tests":
for i, step in steps:

View File

@ -155,7 +155,9 @@ proc rpcExperimentalJsonMain*() =
RPC_PORT = 0 # let the OS choose a port
var
rpcServer = newRpcHttpServerWithParams(initTAddress(RPC_HOST, RPC_PORT))
rpcServer = newRpcHttpServerWithParams(initTAddress(RPC_HOST, RPC_PORT)).valueOr:
echo "Failed to create RPC server: ", error
quit(QuitFailure)
client = newRpcHttpClient()
rpcServer.start()

@ -1 +1 @@
Subproject commit fa6e9b09e2dfe09be5b734e7a7a394a36d953831
Subproject commit 7568f1b7c3142d8e87c1f3dd42924238926affbe

2
vendor/nim-graphql vendored

@ -1 +1 @@
Subproject commit 4cc36cea8184233761a12c60aa805ed78295d787
Subproject commit c47f0c5d2f965e30e14ca65540797aac0e38f453

2
vendor/nim-json-rpc vendored

@ -1 +1 @@
Subproject commit 9a34452e235806f97387dd0c5711b82a24d47ba0
Subproject commit 85d6a67fbc4d490da90e58f3fe97253967401ca1