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.
|
# those terms.
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[options, strutils, times, os],
|
std/[
|
||||||
|
options,
|
||||||
|
strutils,
|
||||||
|
times,
|
||||||
|
os,
|
||||||
|
uri
|
||||||
|
],
|
||||||
pkg/[
|
pkg/[
|
||||||
chronicles,
|
chronicles,
|
||||||
confutils,
|
confutils,
|
||||||
|
@ -300,45 +306,6 @@ type
|
||||||
defaultValueDesc: $DiscoveryType.V4
|
defaultValueDesc: $DiscoveryType.V4
|
||||||
name: "discovery" .}: DiscoveryType
|
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* {.
|
nodeKeyHex* {.
|
||||||
desc: "P2P node private key (as 32 bytes hex string)"
|
desc: "P2P node private key (as 32 bytes hex string)"
|
||||||
defaultValue: ""
|
defaultValue: ""
|
||||||
|
@ -409,6 +376,51 @@ type
|
||||||
defaultValueDesc: $RpcFlag.Eth
|
defaultValueDesc: $RpcFlag.Eth
|
||||||
name: "ws-api" }: seq[string]
|
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/
|
# github.com/ethereum/execution-apis/
|
||||||
# /blob/v1.0.0-alpha.8/src/engine/authentication.md#key-distribution
|
# /blob/v1.0.0-alpha.8/src/engine/authentication.md#key-distribution
|
||||||
jwtSecret* {.
|
jwtSecret* {.
|
||||||
|
@ -641,6 +653,10 @@ proc getStaticPeers*(conf: NimbusConf): seq[ENode] =
|
||||||
result.append(conf.staticPeers)
|
result.append(conf.staticPeers)
|
||||||
loadStaticPeersFile(string conf.staticPeersFile, result)
|
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 =
|
proc makeConfig*(cmdLine = commandLineParams()): NimbusConf =
|
||||||
{.push warning[ProveInit]: off.}
|
{.push warning[ProveInit]: off.}
|
||||||
result = NimbusConf.load(
|
result = NimbusConf.load(
|
||||||
|
@ -672,12 +688,12 @@ proc makeConfig*(cmdLine = commandLineParams()): NimbusConf =
|
||||||
if result.customNetwork.isNone:
|
if result.customNetwork.isNone:
|
||||||
result.networkParams = networkParams(result.networkId)
|
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:
|
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 result.udpPort == Port(0):
|
||||||
# if udpPort not set in cli, then
|
# if udpPort not set in cli, then
|
||||||
result.udpPort = result.tcpPort
|
result.udpPort = result.tcpPort
|
||||||
|
|
|
@ -1312,11 +1312,17 @@ proc setupGraphqlContext*(chainDB: BaseChainDB,
|
||||||
proc setupGraphqlHttpServer*(conf: NimbusConf,
|
proc setupGraphqlHttpServer*(conf: NimbusConf,
|
||||||
chainDB: BaseChainDB,
|
chainDB: BaseChainDB,
|
||||||
ethNode: EthereumNode,
|
ethNode: EthereumNode,
|
||||||
txPool: TxPoolRef): GraphqlHttpServerRef =
|
txPool: TxPoolRef,
|
||||||
|
authHooks: seq[AuthHook] = @[]): GraphqlHttpServerRef =
|
||||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||||
let ctx = setupGraphqlContext(chainDB, ethNode, txPool)
|
let ctx = setupGraphqlContext(chainDB, ethNode, txPool)
|
||||||
let address = initTAddress(conf.graphqlAddress, conf.graphqlPort)
|
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():
|
if sres.isErr():
|
||||||
echo sres.error
|
echo sres.error
|
||||||
quit(QuitFailure)
|
quit(QuitFailure)
|
||||||
|
|
|
@ -27,7 +27,7 @@ import
|
||||||
./db/[storage_types, db_chain, select_backend],
|
./db/[storage_types, db_chain, select_backend],
|
||||||
./graphql/ethapi,
|
./graphql/ethapi,
|
||||||
./p2p/[chain, clique/clique_desc, clique/clique_sealer],
|
./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],
|
./sync/[fast, protocol, snap],
|
||||||
./utils/tx_pool
|
./utils/tx_pool
|
||||||
|
|
||||||
|
@ -178,9 +178,11 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||||
msg = $(rc.unsafeError) # avoid side effects
|
msg = $(rc.unsafeError) # avoid side effects
|
||||||
quit(QuitFailure)
|
quit(QuitFailure)
|
||||||
rc.value
|
rc.value
|
||||||
|
let allowedOrigins = conf.getAllowedOrigins()
|
||||||
|
|
||||||
# Provide JWT authentication handler for rpcHttpServer
|
# Provide JWT authentication handler for rpcHttpServer
|
||||||
let httpJwtAuthHook = httpJwtAuth(jwtKey)
|
let httpJwtAuthHook = httpJwtAuth(jwtKey)
|
||||||
|
let httpCorsHook = httpCors(allowedOrigins)
|
||||||
|
|
||||||
# Creating RPC Server
|
# Creating RPC Server
|
||||||
if conf.rpcEnabled:
|
if conf.rpcEnabled:
|
||||||
|
@ -188,9 +190,9 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||||
conf.engineApiPort == conf.rpcPort
|
conf.engineApiPort == conf.rpcPort
|
||||||
|
|
||||||
let hooks = if enableAuthHook:
|
let hooks = if enableAuthHook:
|
||||||
@[httpJwtAuthHook]
|
@[httpJwtAuthHook, httpCorsHook]
|
||||||
else:
|
else:
|
||||||
@[]
|
@[httpCorsHook]
|
||||||
|
|
||||||
nimbus.rpcServer = newRpcHttpServer(
|
nimbus.rpcServer = newRpcHttpServer(
|
||||||
[initTAddress(conf.rpcAddress, conf.rpcPort)],
|
[initTAddress(conf.rpcAddress, conf.rpcPort)],
|
||||||
|
@ -215,6 +217,7 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||||
|
|
||||||
# Provide JWT authentication handler for rpcWebsocketServer
|
# Provide JWT authentication handler for rpcWebsocketServer
|
||||||
let wsJwtAuthHook = wsJwtAuth(jwtKey)
|
let wsJwtAuthHook = wsJwtAuth(jwtKey)
|
||||||
|
let wsCorsHook = wsCors(allowedOrigins)
|
||||||
|
|
||||||
# Creating Websocket RPC Server
|
# Creating Websocket RPC Server
|
||||||
if conf.wsEnabled:
|
if conf.wsEnabled:
|
||||||
|
@ -222,9 +225,9 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||||
conf.engineApiWsPort == conf.wsPort
|
conf.engineApiWsPort == conf.wsPort
|
||||||
|
|
||||||
let hooks = if enableAuthHook:
|
let hooks = if enableAuthHook:
|
||||||
@[wsJwtAuthHook]
|
@[wsJwtAuthHook, wsCorsHook]
|
||||||
else:
|
else:
|
||||||
@[]
|
@[wsCorsHook]
|
||||||
|
|
||||||
# Construct server object
|
# Construct server object
|
||||||
nimbus.wsRpcServer = newRpcWebSocketServer(
|
nimbus.wsRpcServer = newRpcWebSocketServer(
|
||||||
|
@ -244,7 +247,13 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||||
nimbus.wsRpcServer.start()
|
nimbus.wsRpcServer.start()
|
||||||
|
|
||||||
if conf.graphqlEnabled:
|
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()
|
nimbus.graphqlServer.start()
|
||||||
|
|
||||||
if conf.engineSigner != ZERO_ADDRESS:
|
if conf.engineSigner != ZERO_ADDRESS:
|
||||||
|
@ -288,7 +297,7 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
|
||||||
if conf.engineApiPort != conf.rpcPort:
|
if conf.engineApiPort != conf.rpcPort:
|
||||||
nimbus.engineApiServer = newRpcHttpServer(
|
nimbus.engineApiServer = newRpcHttpServer(
|
||||||
[initTAddress(conf.engineApiAddress, conf.engineApiPort)],
|
[initTAddress(conf.engineApiAddress, conf.engineApiPort)],
|
||||||
authHooks = @[httpJwtAuthHook]
|
authHooks = @[httpJwtAuthHook, httpCorsHook]
|
||||||
)
|
)
|
||||||
setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiServer)
|
setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiServer)
|
||||||
setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, 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:
|
if conf.engineApiWsPort != conf.wsPort:
|
||||||
nimbus.engineApiWsServer = newRpcWebSocketServer(
|
nimbus.engineApiWsServer = newRpcWebSocketServer(
|
||||||
initTAddress(conf.engineApiWsAddress, conf.engineApiWsPort),
|
initTAddress(conf.engineApiWsAddress, conf.engineApiWsPort),
|
||||||
authHooks = @[wsJwtAuthHook]
|
authHooks = @[wsJwtAuthHook, wsCorsHook]
|
||||||
)
|
)
|
||||||
setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiWsServer)
|
setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiWsServer)
|
||||||
setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiWsServer)
|
setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiWsServer)
|
||||||
|
|
|
@ -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…
Reference in New Issue