diff --git a/nimbus/config.nim b/nimbus/config.nim index 37ea66a34..1e5e50cc9 100644 --- a/nimbus/config.nim +++ b/nimbus/config.nim @@ -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 diff --git a/nimbus/graphql/ethapi.nim b/nimbus/graphql/ethapi.nim index 1c95035ee..b0fe59d8f 100644 --- a/nimbus/graphql/ethapi.nim +++ b/nimbus/graphql/ethapi.nim @@ -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) diff --git a/nimbus/nimbus.nim b/nimbus/nimbus.nim index 12dab0c91..34e9e1ccf 100644 --- a/nimbus/nimbus.nim +++ b/nimbus/nimbus.nim @@ -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) diff --git a/nimbus/rpc/cors.nim b/nimbus/rpc/cors.nim new file mode 100644 index 000000000..d148b99b1 --- /dev/null +++ b/nimbus/rpc/cors.nim @@ -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)