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:
jangko 2022-07-19 15:15:18 +07:00
parent a48f69a89a
commit 4721fc7a54
No known key found for this signature in database
GPG Key ID: 31702AE10541E6B9
4 changed files with 174 additions and 55 deletions

View File

@ -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

View File

@ -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)

View File

@ -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
View 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)