mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-12 05:14:14 +00:00
add CORS support for HTTP local services(json-rpc, engine-api, graphql)
There is also a stub for websocket CORS handler. We still need to add non immediate response management to websock.
This commit is contained in:
parent
a48f69a89a
commit
4721fc7a54
@ -7,7 +7,13 @@
|
||||
# those terms.
|
||||
|
||||
import
|
||||
std/[options, strutils, times, os],
|
||||
std/[
|
||||
options,
|
||||
strutils,
|
||||
times,
|
||||
os,
|
||||
uri
|
||||
],
|
||||
pkg/[
|
||||
chronicles,
|
||||
confutils,
|
||||
@ -300,45 +306,6 @@ type
|
||||
defaultValueDesc: $DiscoveryType.V4
|
||||
name: "discovery" .}: DiscoveryType
|
||||
|
||||
terminalTotalDifficulty* {.
|
||||
desc: "The terminal total difficulty of the eth2 merge transition block." &
|
||||
" It takes precedence over terminalTotalDifficulty in config file."
|
||||
name: "terminal-total-difficulty" .}: Option[UInt256]
|
||||
|
||||
engineApiEnabled* {.
|
||||
desc: "Enable the Engine API"
|
||||
defaultValue: false
|
||||
name: "engine-api" .}: bool
|
||||
|
||||
engineApiPort* {.
|
||||
desc: "Listening port for the Engine API"
|
||||
defaultValue: defaultEngineApiPort
|
||||
defaultValueDesc: $defaultEngineApiPort
|
||||
name: "engine-api-port" .}: Port
|
||||
|
||||
engineApiAddress* {.
|
||||
desc: "Listening address for the Engine API"
|
||||
defaultValue: defaultAdminListenAddress
|
||||
defaultValueDesc: $defaultAdminListenAddressDesc
|
||||
name: "engine-api-address" .}: ValidIpAddress
|
||||
|
||||
engineApiWsEnabled* {.
|
||||
desc: "Enable the WebSocket Engine API"
|
||||
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" .}: ValidIpAddress
|
||||
|
||||
nodeKeyHex* {.
|
||||
desc: "P2P node private key (as 32 bytes hex string)"
|
||||
defaultValue: ""
|
||||
@ -409,6 +376,51 @@ type
|
||||
defaultValueDesc: $RpcFlag.Eth
|
||||
name: "ws-api" }: seq[string]
|
||||
|
||||
engineApiEnabled* {.
|
||||
desc: "Enable the Engine API"
|
||||
defaultValue: false
|
||||
name: "engine-api" .}: bool
|
||||
|
||||
engineApiPort* {.
|
||||
desc: "Listening port for the Engine API"
|
||||
defaultValue: defaultEngineApiPort
|
||||
defaultValueDesc: $defaultEngineApiPort
|
||||
name: "engine-api-port" .}: Port
|
||||
|
||||
engineApiAddress* {.
|
||||
desc: "Listening address for the Engine API"
|
||||
defaultValue: defaultAdminListenAddress
|
||||
defaultValueDesc: $defaultAdminListenAddressDesc
|
||||
name: "engine-api-address" .}: ValidIpAddress
|
||||
|
||||
engineApiWsEnabled* {.
|
||||
desc: "Enable the WebSocket Engine API"
|
||||
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" .}: ValidIpAddress
|
||||
|
||||
terminalTotalDifficulty* {.
|
||||
desc: "The terminal total difficulty of the eth2 merge transition block." &
|
||||
" It takes precedence over terminalTotalDifficulty in config file."
|
||||
name: "terminal-total-difficulty" .}: Option[UInt256]
|
||||
|
||||
allowedOrigins* {.
|
||||
desc: "Comma separated list of domains from which to accept cross origin requests"
|
||||
defaultValue: @[]
|
||||
defaultValueDesc: "*"
|
||||
name: "allowed-origins" .}: seq[string]
|
||||
|
||||
# github.com/ethereum/execution-apis/
|
||||
# /blob/v1.0.0-alpha.8/src/engine/authentication.md#key-distribution
|
||||
jwtSecret* {.
|
||||
@ -641,6 +653,10 @@ proc getStaticPeers*(conf: NimbusConf): seq[ENode] =
|
||||
result.append(conf.staticPeers)
|
||||
loadStaticPeersFile(string conf.staticPeersFile, result)
|
||||
|
||||
proc getAllowedOrigins*(conf: NimbusConf): seq[Uri] =
|
||||
for item in repeatingList(conf.allowedOrigins):
|
||||
result.add parseUri(item)
|
||||
|
||||
proc makeConfig*(cmdLine = commandLineParams()): NimbusConf =
|
||||
{.push warning[ProveInit]: off.}
|
||||
result = NimbusConf.load(
|
||||
@ -672,12 +688,12 @@ proc makeConfig*(cmdLine = commandLineParams()): NimbusConf =
|
||||
if result.customNetwork.isNone:
|
||||
result.networkParams = networkParams(result.networkId)
|
||||
|
||||
# ttd from cli takes precedence over ttd from config-file
|
||||
if result.terminalTotalDifficulty.isSome:
|
||||
result.networkParams.config.terminalTotalDifficulty =
|
||||
result.terminalTotalDifficulty
|
||||
|
||||
if result.cmd == noCommand:
|
||||
# ttd from cli takes precedence over ttd from config-file
|
||||
if result.terminalTotalDifficulty.isSome:
|
||||
result.networkParams.config.terminalTotalDifficulty =
|
||||
result.terminalTotalDifficulty
|
||||
|
||||
if result.udpPort == Port(0):
|
||||
# if udpPort not set in cli, then
|
||||
result.udpPort = result.tcpPort
|
||||
|
@ -1312,11 +1312,17 @@ proc setupGraphqlContext*(chainDB: BaseChainDB,
|
||||
proc setupGraphqlHttpServer*(conf: NimbusConf,
|
||||
chainDB: BaseChainDB,
|
||||
ethNode: EthereumNode,
|
||||
txPool: TxPoolRef): GraphqlHttpServerRef =
|
||||
txPool: TxPoolRef,
|
||||
authHooks: seq[AuthHook] = @[]): GraphqlHttpServerRef =
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let ctx = setupGraphqlContext(chainDB, ethNode, txPool)
|
||||
let address = initTAddress(conf.graphqlAddress, conf.graphqlPort)
|
||||
let sres = GraphqlHttpServerRef.new(ctx, address, socketFlags = socketFlags)
|
||||
let sres = GraphqlHttpServerRef.new(
|
||||
ctx,
|
||||
address,
|
||||
socketFlags = socketFlags,
|
||||
authHooks = authHooks
|
||||
)
|
||||
if sres.isErr():
|
||||
echo sres.error
|
||||
quit(QuitFailure)
|
||||
|
@ -27,7 +27,7 @@ import
|
||||
./db/[storage_types, db_chain, select_backend],
|
||||
./graphql/ethapi,
|
||||
./p2p/[chain, clique/clique_desc, clique/clique_sealer],
|
||||
./rpc/[common, debug, engine_api, jwt_auth, p2p],
|
||||
./rpc/[common, debug, engine_api, jwt_auth, p2p, cors],
|
||||
./sync/[fast, protocol, snap],
|
||||
./utils/tx_pool
|
||||
|
||||
@ -178,9 +178,11 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||
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:
|
||||
@ -188,9 +190,9 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||
conf.engineApiPort == conf.rpcPort
|
||||
|
||||
let hooks = if enableAuthHook:
|
||||
@[httpJwtAuthHook]
|
||||
@[httpJwtAuthHook, httpCorsHook]
|
||||
else:
|
||||
@[]
|
||||
@[httpCorsHook]
|
||||
|
||||
nimbus.rpcServer = newRpcHttpServer(
|
||||
[initTAddress(conf.rpcAddress, conf.rpcPort)],
|
||||
@ -215,6 +217,7 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||
|
||||
# Provide JWT authentication handler for rpcWebsocketServer
|
||||
let wsJwtAuthHook = wsJwtAuth(jwtKey)
|
||||
let wsCorsHook = wsCors(allowedOrigins)
|
||||
|
||||
# Creating Websocket RPC Server
|
||||
if conf.wsEnabled:
|
||||
@ -222,9 +225,9 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||
conf.engineApiWsPort == conf.wsPort
|
||||
|
||||
let hooks = if enableAuthHook:
|
||||
@[wsJwtAuthHook]
|
||||
@[wsJwtAuthHook, wsCorsHook]
|
||||
else:
|
||||
@[]
|
||||
@[wsCorsHook]
|
||||
|
||||
# Construct server object
|
||||
nimbus.wsRpcServer = newRpcWebSocketServer(
|
||||
@ -244,7 +247,13 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||
nimbus.wsRpcServer.start()
|
||||
|
||||
if conf.graphqlEnabled:
|
||||
nimbus.graphqlServer = setupGraphqlHttpServer(conf, chainDB, nimbus.ethNode, nimbus.txPool)
|
||||
nimbus.graphqlServer = setupGraphqlHttpServer(
|
||||
conf,
|
||||
chainDB,
|
||||
nimbus.ethNode,
|
||||
nimbus.txPool,
|
||||
@[httpCorsHook]
|
||||
)
|
||||
nimbus.graphqlServer.start()
|
||||
|
||||
if conf.engineSigner != ZERO_ADDRESS:
|
||||
@ -288,7 +297,7 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||
if conf.engineApiPort != conf.rpcPort:
|
||||
nimbus.engineApiServer = newRpcHttpServer(
|
||||
[initTAddress(conf.engineApiAddress, conf.engineApiPort)],
|
||||
authHooks = @[httpJwtAuthHook]
|
||||
authHooks = @[httpJwtAuthHook, httpCorsHook]
|
||||
)
|
||||
setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiServer)
|
||||
setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiServer)
|
||||
@ -302,7 +311,7 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||
if conf.engineApiWsPort != conf.wsPort:
|
||||
nimbus.engineApiWsServer = newRpcWebSocketServer(
|
||||
initTAddress(conf.engineApiWsAddress, conf.engineApiWsPort),
|
||||
authHooks = @[wsJwtAuthHook]
|
||||
authHooks = @[wsJwtAuthHook, wsCorsHook]
|
||||
)
|
||||
setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiWsServer)
|
||||
setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiWsServer)
|
||||
|
88
nimbus/rpc/cors.nim
Normal file
88
nimbus/rpc/cors.nim
Normal file
@ -0,0 +1,88 @@
|
||||
# Nimbus
|
||||
# Copyright (c) 2022 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
|
||||
std/[uri],
|
||||
chronos,
|
||||
chronos/apps/http/[httptable, httpserver],
|
||||
json_rpc/rpcserver,
|
||||
httputils,
|
||||
websock/websock as ws,
|
||||
../config
|
||||
|
||||
proc sameOrigin(a, b: Uri): bool =
|
||||
a.hostname == b.hostname and
|
||||
a.scheme == b.scheme and
|
||||
a.port == b.port
|
||||
|
||||
proc containsOrigin(list: seq[Uri], origin: Uri): bool =
|
||||
for x in list:
|
||||
if x.sameOrigin(origin): return true
|
||||
|
||||
const
|
||||
HookOK = HttpResponseRef(nil)
|
||||
|
||||
proc httpCors*(allowedOrigins: seq[Uri]): HttpAuthHook =
|
||||
proc handler(req: HttpRequestRef): Future[HttpResponseRef] {.async.} =
|
||||
let origins = req.headers.getList("Origin")
|
||||
let everyOriginAllowed = allowedOrigins.len == 0
|
||||
|
||||
if origins.len > 1:
|
||||
return await req.respond(Http400,
|
||||
"Only a single Origin header must be specified")
|
||||
|
||||
if origins.len == 0:
|
||||
# maybe not a CORS request
|
||||
return HookOK
|
||||
|
||||
# this section shared by all http method
|
||||
let origin = parseUri(origins[0])
|
||||
let resp = req.getResponse()
|
||||
if not allowedOrigins.containsOrigin(origin):
|
||||
return await req.respond(Http403, "Origin not allowed")
|
||||
|
||||
if everyOriginAllowed:
|
||||
resp.addHeader("Access-Control-Allow-Origin", "*")
|
||||
else:
|
||||
# The Vary: Origin header to must be set to prevent
|
||||
# potential cache poisoning attacks:
|
||||
# https://textslashplain.com/2018/08/02/cors-and-vary/
|
||||
resp.addHeader("Vary", "Origin")
|
||||
resp.addHeader("Access-Control-Allow-Origin", origins[0])
|
||||
|
||||
if req.meth == MethodOptions:
|
||||
# Preflight request
|
||||
let meth = resp.getHeader("Access-Control-Request-Method", "?")
|
||||
if meth != "?":
|
||||
# TODO: get actual methods supported by respective server
|
||||
# e.g. JSON-RPC, GRAPHQL, ENGINE-API
|
||||
resp.addHeader("Access-Control-Allow-Methods", "GET, POST")
|
||||
resp.addHeader("Vary", "Access-Control-Request-Method")
|
||||
|
||||
let heads = resp.getHeader("Access-Control-Request-Headers", "?")
|
||||
if heads != "?":
|
||||
# TODO: get actual headers supported by each server?
|
||||
resp.addHeader("Access-Control-Allow-Headers", heads)
|
||||
resp.addHeader("Vary", "Access-Control-Request-Headers")
|
||||
return await req.respond(Http400)
|
||||
|
||||
# other method such as POST or GET will fill
|
||||
# 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)
|
Loading…
x
Reference in New Issue
Block a user