From 9d4635d16162d5bb57e81621dc8dd703c71d557b Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Tue, 19 Mar 2024 12:12:25 +0000 Subject: [PATCH 1/5] add geth rpc fork and wire it with the host --- go/common/enclave.go | 2 +- go/common/gethencoding/geth_encoding.go | 2 +- go/common/host/host.go | 2 +- go/common/host/services.go | 2 +- go/common/log_events.go | 2 +- go/common/types.go | 2 +- go/enclave/components/batch_registry.go | 2 +- go/enclave/components/interfaces.go | 2 +- go/enclave/enclave.go | 2 +- go/enclave/events/subscription_manager.go | 2 +- go/enclave/evm/evm_facade.go | 2 +- go/enclave/evm/no_op_engine.go | 10 +- go/enclave/l2chain/interfaces.go | 2 +- go/enclave/l2chain/l2_chain.go | 2 +- go/enclave/rpc/EstimateGas.go | 2 +- go/enclave/rpc/GetBalance.go | 2 +- go/enclave/rpc/rpc_utils.go | 2 +- go/enclave/rpc_server.go | 2 +- go/host/container/host_container.go | 33 +- go/host/enclave/service.go | 2 +- go/host/events/logs.go | 2 +- go/host/host.go | 2 +- go/host/rpc/clientapi/client_api_eth.go | 2 +- go/host/rpc/clientapi/client_api_filter.go | 2 +- go/host/rpc/clientrpc/rpc_server.go | 67 -- go/host/rpc/enclaverpc/enclave_client.go | 2 +- go/obsclient/test_util.go | 2 +- go/rpc/client.go | 2 +- go/rpc/encrypted_client.go | 2 +- go/rpc/network_client.go | 2 +- integration/simulation/devnetwork/node.go | 13 +- .../simulation/p2p/in_mem_obscuro_client.go | 2 +- lib/gethfork/README.MD | 10 + lib/gethfork/node/api.go | 209 +++++ lib/gethfork/node/config.go | 323 ++++++++ lib/gethfork/node/defaults.go | 126 +++ lib/gethfork/node/doc.go | 124 +++ lib/gethfork/node/endpoints.go | 92 +++ lib/gethfork/node/errors.go | 41 + lib/gethfork/node/extract_params_handler.go | 32 + lib/gethfork/node/jwt_auth.go | 45 ++ lib/gethfork/node/jwt_handler.go | 80 ++ lib/gethfork/node/lifecycle.go | 31 + lib/gethfork/node/node.go | 635 +++++++++++++++ lib/gethfork/node/rpc_server.go | 91 +++ lib/gethfork/node/rpcstack.go | 658 ++++++++++++++++ lib/gethfork/rpc/client.go | 727 ++++++++++++++++++ lib/gethfork/rpc/client_opt.go | 145 ++++ lib/gethfork/rpc/context_headers.go | 56 ++ lib/gethfork/rpc/doc.go | 109 +++ lib/gethfork/rpc/endpoints.go | 52 ++ lib/gethfork/rpc/errors.go | 156 ++++ lib/gethfork/rpc/gw_auth.go | 3 + lib/gethfork/rpc/handler.go | 595 ++++++++++++++ lib/gethfork/rpc/http.go | 397 ++++++++++ lib/gethfork/rpc/inproc.go | 34 + lib/gethfork/rpc/ipc.go | 61 ++ lib/gethfork/rpc/ipc_js.go | 38 + lib/gethfork/rpc/ipc_unix.go | 62 ++ lib/gethfork/rpc/ipc_windows.go | 44 ++ lib/gethfork/rpc/json.go | 370 +++++++++ lib/gethfork/rpc/metrics.go | 50 ++ lib/gethfork/rpc/server.go | 238 ++++++ lib/gethfork/rpc/service.go | 249 ++++++ lib/gethfork/rpc/stdio.go | 71 ++ lib/gethfork/rpc/subscription.go | 387 ++++++++++ lib/gethfork/rpc/types.go | 253 ++++++ lib/gethfork/rpc/websocket.go | 380 +++++++++ tools/walletextension/common/responses.go | 2 +- .../subscriptions/subscriptions.go | 2 +- tools/walletextension/test/apis.go | 2 +- tools/walletextension/test/utils.go | 2 +- 72 files changed, 7034 insertions(+), 127 deletions(-) delete mode 100644 go/host/rpc/clientrpc/rpc_server.go create mode 100644 lib/gethfork/README.MD create mode 100644 lib/gethfork/node/api.go create mode 100644 lib/gethfork/node/config.go create mode 100644 lib/gethfork/node/defaults.go create mode 100644 lib/gethfork/node/doc.go create mode 100644 lib/gethfork/node/endpoints.go create mode 100644 lib/gethfork/node/errors.go create mode 100644 lib/gethfork/node/extract_params_handler.go create mode 100644 lib/gethfork/node/jwt_auth.go create mode 100644 lib/gethfork/node/jwt_handler.go create mode 100644 lib/gethfork/node/lifecycle.go create mode 100644 lib/gethfork/node/node.go create mode 100644 lib/gethfork/node/rpc_server.go create mode 100644 lib/gethfork/node/rpcstack.go create mode 100644 lib/gethfork/rpc/client.go create mode 100644 lib/gethfork/rpc/client_opt.go create mode 100644 lib/gethfork/rpc/context_headers.go create mode 100644 lib/gethfork/rpc/doc.go create mode 100644 lib/gethfork/rpc/endpoints.go create mode 100644 lib/gethfork/rpc/errors.go create mode 100644 lib/gethfork/rpc/gw_auth.go create mode 100644 lib/gethfork/rpc/handler.go create mode 100644 lib/gethfork/rpc/http.go create mode 100644 lib/gethfork/rpc/inproc.go create mode 100644 lib/gethfork/rpc/ipc.go create mode 100644 lib/gethfork/rpc/ipc_js.go create mode 100644 lib/gethfork/rpc/ipc_unix.go create mode 100644 lib/gethfork/rpc/ipc_windows.go create mode 100644 lib/gethfork/rpc/json.go create mode 100644 lib/gethfork/rpc/metrics.go create mode 100644 lib/gethfork/rpc/server.go create mode 100644 lib/gethfork/rpc/service.go create mode 100644 lib/gethfork/rpc/stdio.go create mode 100644 lib/gethfork/rpc/subscription.go create mode 100644 lib/gethfork/rpc/types.go create mode 100644 lib/gethfork/rpc/websocket.go diff --git a/go/common/enclave.go b/go/common/enclave.go index 0f353f4f91..4e923835a4 100644 --- a/go/common/enclave.go +++ b/go/common/enclave.go @@ -6,9 +6,9 @@ import ( "github.com/ten-protocol/go-ten/go/common/errutil" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common/tracers" "github.com/ten-protocol/go-ten/go/responses" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" gethcommon "github.com/ethereum/go-ethereum/common" ) diff --git a/go/common/gethencoding/geth_encoding.go b/go/common/gethencoding/geth_encoding.go index be0d26d165..09617dee91 100644 --- a/go/common/gethencoding/geth_encoding.go +++ b/go/common/gethencoding/geth_encoding.go @@ -27,7 +27,7 @@ import ( "github.com/ten-protocol/go-ten/go/common/gethapi" gethcommon "github.com/ethereum/go-ethereum/common" - gethrpc "github.com/ethereum/go-ethereum/rpc" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) const ( diff --git a/go/common/host/host.go b/go/common/host/host.go index 10dbd63377..154dc4f127 100644 --- a/go/common/host/host.go +++ b/go/common/host/host.go @@ -2,11 +2,11 @@ package host import ( "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common" "github.com/ten-protocol/go-ten/go/config" "github.com/ten-protocol/go-ten/go/host/db" "github.com/ten-protocol/go-ten/go/responses" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) // Host is the half of the Obscuro node that lives outside the enclave. diff --git a/go/common/host/services.go b/go/common/host/services.go index db73a52cdf..d40b236025 100644 --- a/go/common/host/services.go +++ b/go/common/host/services.go @@ -3,8 +3,8 @@ package host import ( "math/big" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/responses" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" diff --git a/go/common/log_events.go b/go/common/log_events.go index ab4c313b9f..fd01b0357c 100644 --- a/go/common/log_events.go +++ b/go/common/log_events.go @@ -4,8 +4,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/filters" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common/viewingkey" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) // LogSubscription is an authenticated subscription to logs. diff --git a/go/common/types.go b/go/common/types.go index 9e194acdc6..8c303bd7c2 100644 --- a/go/common/types.go +++ b/go/common/types.go @@ -6,8 +6,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/contracts/generated/MessageBus" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) type ( diff --git a/go/enclave/components/batch_registry.go b/go/enclave/components/batch_registry.go index 94eb0aede5..46f54247d4 100644 --- a/go/enclave/components/batch_registry.go +++ b/go/enclave/components/batch_registry.go @@ -12,13 +12,13 @@ import ( "github.com/ethereum/go-ethereum/core/state" gethlog "github.com/ethereum/go-ethereum/log" - gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common/async" "github.com/ten-protocol/go-ten/go/common/errutil" "github.com/ten-protocol/go-ten/go/common/log" "github.com/ten-protocol/go-ten/go/common/measure" "github.com/ten-protocol/go-ten/go/enclave/core" "github.com/ten-protocol/go-ten/go/enclave/limiters" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) type batchRegistry struct { diff --git a/go/enclave/components/interfaces.go b/go/enclave/components/interfaces.go index 68918849ab..a877553422 100644 --- a/go/enclave/components/interfaces.go +++ b/go/enclave/components/interfaces.go @@ -8,10 +8,10 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" - gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common" "github.com/ten-protocol/go-ten/go/enclave/core" "github.com/ten-protocol/go-ten/go/enclave/limiters" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) var ErrDuplicateRollup = errors.New("duplicate rollup received") diff --git a/go/enclave/enclave.go b/go/enclave/enclave.go index d401979cb2..ee73c7b773 100644 --- a/go/enclave/enclave.go +++ b/go/enclave/enclave.go @@ -51,7 +51,7 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core" gethlog "github.com/ethereum/go-ethereum/log" - gethrpc "github.com/ethereum/go-ethereum/rpc" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) var _noHeadBatch = big.NewInt(0) diff --git a/go/enclave/events/subscription_manager.go b/go/enclave/events/subscription_manager.go index b42dac4683..fcaeaaeb2e 100644 --- a/go/enclave/events/subscription_manager.go +++ b/go/enclave/events/subscription_manager.go @@ -6,8 +6,8 @@ import ( "math/big" "sync" - gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/enclave/vkhandler" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" "github.com/ten-protocol/go-ten/go/common/log" diff --git a/go/enclave/evm/evm_facade.go b/go/enclave/evm/evm_facade.go index 762d3e6d9f..3f9151b702 100644 --- a/go/enclave/evm/evm_facade.go +++ b/go/enclave/evm/evm_facade.go @@ -27,7 +27,7 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core" gethlog "github.com/ethereum/go-ethereum/log" - gethrpc "github.com/ethereum/go-ethereum/rpc" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) // ExecuteTransactions diff --git a/go/enclave/evm/no_op_engine.go b/go/enclave/evm/no_op_engine.go index 3c8cf06b68..9ef11e7717 100644 --- a/go/enclave/evm/no_op_engine.go +++ b/go/enclave/evm/no_op_engine.go @@ -22,11 +22,6 @@ type ObscuroNoOpConsensusEngine struct { logger gethlog.Logger } -func (e *ObscuroNoOpConsensusEngine) Finalize(_ consensus.ChainHeaderReader, _ *types.Header, _ *state.StateDB, _ []*types.Transaction, _ []*types.Header, _ []*types.Withdrawal) { - // TODO implement me - panic("implement me") -} - // Author is used to determine where to send the gas collected from the fees. func (e *ObscuroNoOpConsensusEngine) Author(_ *types.Header) (common.Address, error) { return PoolAddress, nil @@ -52,6 +47,11 @@ func (e *ObscuroNoOpConsensusEngine) Prepare(_ consensus.ChainHeaderReader, _ *t return nil } +func (e *ObscuroNoOpConsensusEngine) Finalize(_ consensus.ChainHeaderReader, _ *types.Header, _ *state.StateDB, _ []*types.Transaction, _ []*types.Header, _ []*types.Withdrawal) { + // TODO implement me + panic("implement me") +} + func (e *ObscuroNoOpConsensusEngine) FinalizeAndAssemble(_ consensus.ChainHeaderReader, _ *types.Header, _ *state.StateDB, _ []*types.Transaction, _ []*types.Header, _ []*types.Receipt, _ []*types.Withdrawal, ) (*types.Block, error) { diff --git a/go/enclave/l2chain/interfaces.go b/go/enclave/l2chain/interfaces.go index 4e8e3e2d5d..65c4b0a746 100644 --- a/go/enclave/l2chain/interfaces.go +++ b/go/enclave/l2chain/interfaces.go @@ -6,9 +6,9 @@ import ( gethcore "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/vm" - gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common/gethapi" "github.com/ten-protocol/go-ten/go/enclave/core" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) // ObscuroChain - the interface that provides the data access layer to the obscuro l2. diff --git a/go/enclave/l2chain/l2_chain.go b/go/enclave/l2chain/l2_chain.go index 28a0a8ce78..5765de720f 100644 --- a/go/enclave/l2chain/l2_chain.go +++ b/go/enclave/l2chain/l2_chain.go @@ -15,7 +15,6 @@ import ( "github.com/ethereum/go-ethereum/core/vm" gethlog "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" - gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/status-im/keycard-go/hexutils" "github.com/ten-protocol/go-ten/go/common/gethapi" "github.com/ten-protocol/go-ten/go/common/gethencoding" @@ -24,6 +23,7 @@ import ( "github.com/ten-protocol/go-ten/go/enclave/core" "github.com/ten-protocol/go-ten/go/enclave/evm" "github.com/ten-protocol/go-ten/go/enclave/genesis" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) type obscuroChain struct { diff --git a/go/enclave/rpc/EstimateGas.go b/go/enclave/rpc/EstimateGas.go index 2b6489d383..865bc7bae6 100644 --- a/go/enclave/rpc/EstimateGas.go +++ b/go/enclave/rpc/EstimateGas.go @@ -15,10 +15,10 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" gethcore "github.com/ethereum/go-ethereum/core" - gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common/gethapi" "github.com/ten-protocol/go-ten/go/common/gethencoding" "github.com/ten-protocol/go-ten/go/common/syserr" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) func EstimateGasValidate(reqParams []any, builder *CallBuilder[CallParamsWithBlock, hexutil.Uint64], _ *EncryptionManager) error { diff --git a/go/enclave/rpc/GetBalance.go b/go/enclave/rpc/GetBalance.go index dc0ece0bd9..7515d7a2a9 100644 --- a/go/enclave/rpc/GetBalance.go +++ b/go/enclave/rpc/GetBalance.go @@ -5,7 +5,7 @@ import ( "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/rpc" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ten-protocol/go-ten/go/common/gethencoding" diff --git a/go/enclave/rpc/rpc_utils.go b/go/enclave/rpc/rpc_utils.go index 7647418c44..8dc732b56b 100644 --- a/go/enclave/rpc/rpc_utils.go +++ b/go/enclave/rpc/rpc_utils.go @@ -3,8 +3,8 @@ package rpc import ( "fmt" - gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common/gethapi" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ten-protocol/go-ten/go/common" diff --git a/go/enclave/rpc_server.go b/go/enclave/rpc_server.go index 174df158d7..a504481a8e 100644 --- a/go/enclave/rpc_server.go +++ b/go/enclave/rpc_server.go @@ -20,7 +20,7 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" gethlog "github.com/ethereum/go-ethereum/log" - gethrpc "github.com/ethereum/go-ethereum/rpc" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) // RPCServer receives RPC calls to the enclave process and relays them to the enclave.Enclave. diff --git a/go/host/container/host_container.go b/go/host/container/host_container.go index 118955db78..033ab80bbd 100644 --- a/go/host/container/host_container.go +++ b/go/host/container/host_container.go @@ -4,10 +4,11 @@ import ( "fmt" "time" + "github.com/ten-protocol/go-ten/lib/gethfork/node" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ten-protocol/go-ten/go/host/l1" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common" "github.com/ten-protocol/go-ten/go/common/log" "github.com/ten-protocol/go-ten/go/common/metrics" @@ -17,9 +18,9 @@ import ( "github.com/ten-protocol/go-ten/go/host" "github.com/ten-protocol/go-ten/go/host/p2p" "github.com/ten-protocol/go-ten/go/host/rpc/clientapi" - "github.com/ten-protocol/go-ten/go/host/rpc/clientrpc" "github.com/ten-protocol/go-ten/go/host/rpc/enclaverpc" "github.com/ten-protocol/go-ten/go/wallet" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" gethlog "github.com/ethereum/go-ethereum/log" hostcommon "github.com/ten-protocol/go-ten/go/common/host" @@ -40,7 +41,7 @@ type HostContainer struct { host hostcommon.Host logger gethlog.Logger metricsService *metrics.Service - rpcServer clientrpc.Server + rpcServer node.Server } func (h *HostContainer) Start() error { @@ -137,7 +138,13 @@ func NewHostContainerFromConfig(parsedConfig *config.HostInputConfig, logger get aggP2P := p2p.NewSocketP2PLayer(cfg, services, p2pLogger, metricsService.Registry()) - rpcServer := clientrpc.NewServer(cfg, logger) + rpcServer := node.NewServer(&node.RPCConfig{ + EnableHTTP: cfg.HasClientRPCHTTP, + HTTPPort: int(cfg.ClientRPCPortHTTP), + EnableWs: cfg.HasClientRPCWebsockets, + WsPort: int(cfg.ClientRPCPortWS), + Host: cfg.ClientRPCHost, + }, logger) mgmtContractLib := mgmtcontractlib.NewMgmtContractLib(&cfg.ManagementContractAddress, logger) obscuroRelevantContracts := []gethcommon.Address{cfg.ManagementContractAddress, cfg.MessageBusAddress} @@ -148,7 +155,7 @@ func NewHostContainerFromConfig(parsedConfig *config.HostInputConfig, logger get // NewHostContainer builds a host container with dependency injection rather than from config. // Useful for testing etc. (want to be able to pass in logger, and also have option to mock out dependencies) -func NewHostContainer(cfg *config.HostConfig, services *host.ServicesRegistry, p2p hostcommon.P2PHostService, l1Client ethadapter.EthClient, l1Repo hostcommon.L1RepoService, enclaveClient common.Enclave, contractLib mgmtcontractlib.MgmtContractLib, hostWallet wallet.Wallet, rpcServer clientrpc.Server, logger gethlog.Logger, metricsService *metrics.Service) *HostContainer { +func NewHostContainer(cfg *config.HostConfig, services *host.ServicesRegistry, p2p hostcommon.P2PHostService, l1Client ethadapter.EthClient, l1Repo hostcommon.L1RepoService, enclaveClient common.Enclave, contractLib mgmtcontractlib.MgmtContractLib, hostWallet wallet.Wallet, rpcServer node.Server, logger gethlog.Logger, metricsService *metrics.Service) *HostContainer { h := host.NewHost(cfg, services, p2p, l1Client, l1Repo, enclaveClient, hostWallet, contractLib, logger, metricsService.Registry()) hostContainer := &HostContainer{ @@ -162,45 +169,31 @@ func NewHostContainer(cfg *config.HostConfig, services *host.ServicesRegistry, p rpcServer.RegisterAPIs([]rpc.API{ { Namespace: APINamespaceObscuro, - Version: APIVersion1, Service: clientapi.NewObscuroAPI(h), - Public: true, }, { Namespace: APINamespaceEth, - Version: APIVersion1, Service: clientapi.NewEthereumAPI(h, logger), - Public: true, }, { Namespace: APINamespaceTenScan, - Version: APIVersion1, Service: clientapi.NewTenScanAPI(h), - Public: true, }, { Namespace: APINamespaceNetwork, - Version: APIVersion1, Service: clientapi.NewNetworkAPI(h), - Public: true, }, { Namespace: APINamespaceTest, - Version: APIVersion1, Service: clientapi.NewTestAPI(hostContainer), - Public: true, }, { Namespace: APINamespaceEth, - Version: APIVersion1, Service: clientapi.NewFilterAPI(h, logger), - Public: true, }, { Namespace: APINamespaceScan, - Version: APIVersion1, Service: clientapi.NewScanAPI(h, logger), - Public: true, }, }) @@ -208,9 +201,7 @@ func NewHostContainer(cfg *config.HostConfig, services *host.ServicesRegistry, p rpcServer.RegisterAPIs([]rpc.API{ { Namespace: APINamespaceDebug, - Version: APIVersion1, Service: clientapi.NewNetworkDebug(h), - Public: true, }, }) } diff --git a/go/host/enclave/service.go b/go/host/enclave/service.go index 7a297cb463..402603ba75 100644 --- a/go/host/enclave/service.go +++ b/go/host/enclave/service.go @@ -6,12 +6,12 @@ import ( "sync/atomic" gethlog "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common" "github.com/ten-protocol/go-ten/go/common/errutil" "github.com/ten-protocol/go-ten/go/common/host" "github.com/ten-protocol/go-ten/go/common/log" "github.com/ten-protocol/go-ten/go/responses" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) // This private interface enforces the services that the enclaves service depends on diff --git a/go/host/events/logs.go b/go/host/events/logs.go index bdf7578461..9099811654 100644 --- a/go/host/events/logs.go +++ b/go/host/events/logs.go @@ -8,8 +8,8 @@ import ( "github.com/ten-protocol/go-ten/go/common/log" gethlog "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) type logSubsServiceLocator interface { diff --git a/go/host/host.go b/go/host/host.go index 2b1e46d166..60c2ec4b03 100644 --- a/go/host/host.go +++ b/go/host/host.go @@ -11,7 +11,6 @@ import ( "github.com/ten-protocol/go-ten/go/host/enclave" "github.com/ten-protocol/go-ten/go/host/l1" - "github.com/ethereum/go-ethereum/rpc" "github.com/naoina/toml" "github.com/ten-protocol/go-ten/go/common" "github.com/ten-protocol/go-ten/go/common/log" @@ -24,6 +23,7 @@ import ( "github.com/ten-protocol/go-ten/go/host/events" "github.com/ten-protocol/go-ten/go/responses" "github.com/ten-protocol/go-ten/go/wallet" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" gethlog "github.com/ethereum/go-ethereum/log" gethmetrics "github.com/ethereum/go-ethereum/metrics" diff --git a/go/host/rpc/clientapi/client_api_eth.go b/go/host/rpc/clientapi/client_api_eth.go index 9c15154e07..85bc103af0 100644 --- a/go/host/rpc/clientapi/client_api_eth.go +++ b/go/host/rpc/clientapi/client_api_eth.go @@ -8,11 +8,11 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common" "github.com/ten-protocol/go-ten/go/common/host" "github.com/ten-protocol/go-ten/go/common/log" "github.com/ten-protocol/go-ten/go/responses" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" gethcommon "github.com/ethereum/go-ethereum/common" gethlog "github.com/ethereum/go-ethereum/log" diff --git a/go/host/rpc/clientapi/client_api_filter.go b/go/host/rpc/clientapi/client_api_filter.go index 11a94b97f8..2c7ecfb626 100644 --- a/go/host/rpc/clientapi/client_api_filter.go +++ b/go/host/rpc/clientapi/client_api_filter.go @@ -14,7 +14,7 @@ import ( "github.com/ten-protocol/go-ten/go/common" - "github.com/ethereum/go-ethereum/rpc" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) // FilterAPI exposes a subset of Geth's PublicFilterAPI operations. diff --git a/go/host/rpc/clientrpc/rpc_server.go b/go/host/rpc/clientrpc/rpc_server.go deleted file mode 100644 index cc0cf7794d..0000000000 --- a/go/host/rpc/clientrpc/rpc_server.go +++ /dev/null @@ -1,67 +0,0 @@ -package clientrpc - -import ( - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ten-protocol/go-ten/go/common/log" - "github.com/ten-protocol/go-ten/go/config" - - gethlog "github.com/ethereum/go-ethereum/log" -) - -const ( - allOrigins = "*" -) - -// Server is the layer responsible for handling RPC requests from Obscuro client applications. -type Server interface { - Start() error - Stop() - RegisterAPIs(apis []rpc.API) -} - -// An implementation of `host.Server` that reuses the Geth `node` package for client communication. -type serverImpl struct { - node *node.Node - logger gethlog.Logger -} - -func NewServer(config *config.HostConfig, logger gethlog.Logger) Server { - rpcConfig := node.Config{ - Logger: logger.New(log.CmpKey, log.HostRPCCmp), - } - if config.HasClientRPCHTTP { - rpcConfig.HTTPHost = config.ClientRPCHost - rpcConfig.HTTPPort = int(config.ClientRPCPortHTTP) - // todo (@pedro) - review if this poses a security issue - rpcConfig.HTTPVirtualHosts = []string{allOrigins} - } - if config.HasClientRPCWebsockets { - rpcConfig.WSHost = config.ClientRPCHost - rpcConfig.WSPort = int(config.ClientRPCPortWS) - // todo (@pedro) - review if this poses a security issue - rpcConfig.WSOrigins = []string{allOrigins} - } - - rpcServerNode, err := node.New(&rpcConfig) - if err != nil { - logger.Crit("could not create new client server.", log.ErrKey, err) - } - - return &serverImpl{node: rpcServerNode, logger: logger} -} - -func (s *serverImpl) RegisterAPIs(apis []rpc.API) { - s.node.RegisterAPIs(apis) -} - -func (s *serverImpl) Start() error { - return s.node.Start() -} - -func (s *serverImpl) Stop() { - err := s.node.Close() - if err != nil { - s.logger.Crit("could not stop node client server.", log.ErrKey, err) - } -} diff --git a/go/host/rpc/enclaverpc/enclave_client.go b/go/host/rpc/enclaverpc/enclave_client.go index d37ad7b85a..7f8ce81bac 100644 --- a/go/host/rpc/enclaverpc/enclave_client.go +++ b/go/host/rpc/enclaverpc/enclave_client.go @@ -29,7 +29,7 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" gethlog "github.com/ethereum/go-ethereum/log" - gethrpc "github.com/ethereum/go-ethereum/rpc" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) // Client implements enclave.Enclave and should be used by the host when communicating with the enclave via RPC. diff --git a/go/obsclient/test_util.go b/go/obsclient/test_util.go index 6ce9d9e28e..ace5d981ad 100644 --- a/go/obsclient/test_util.go +++ b/go/obsclient/test_util.go @@ -3,7 +3,7 @@ package obsclient import ( "context" - "github.com/ethereum/go-ethereum/rpc" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" "github.com/stretchr/testify/mock" ) diff --git a/go/rpc/client.go b/go/rpc/client.go index 81bea5c0bb..426f39e6a6 100644 --- a/go/rpc/client.go +++ b/go/rpc/client.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/ethereum/go-ethereum/rpc" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) const ( diff --git a/go/rpc/encrypted_client.go b/go/rpc/encrypted_client.go index 2412d8af50..aab5337397 100644 --- a/go/rpc/encrypted_client.go +++ b/go/rpc/encrypted_client.go @@ -14,12 +14,12 @@ import ( "github.com/ethereum/go-ethereum/crypto/ecies" "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/rlp" - gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common" "github.com/ten-protocol/go-ten/go/common/errutil" "github.com/ten-protocol/go-ten/go/common/log" "github.com/ten-protocol/go-ten/go/common/viewingkey" "github.com/ten-protocol/go-ten/go/responses" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" gethcommon "github.com/ethereum/go-ethereum/common" gethlog "github.com/ethereum/go-ethereum/log" diff --git a/go/rpc/network_client.go b/go/rpc/network_client.go index 6aaf41faa9..22c9c4e64d 100644 --- a/go/rpc/network_client.go +++ b/go/rpc/network_client.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common/viewingkey" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" gethlog "github.com/ethereum/go-ethereum/log" ) diff --git a/integration/simulation/devnetwork/node.go b/integration/simulation/devnetwork/node.go index 087f99ac72..3fe32fe087 100644 --- a/integration/simulation/devnetwork/node.go +++ b/integration/simulation/devnetwork/node.go @@ -4,6 +4,8 @@ import ( "fmt" "os" + "github.com/ten-protocol/go-ten/lib/gethfork/node" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ten-protocol/go-ten/go/host/l1" @@ -23,7 +25,6 @@ import ( "github.com/ten-protocol/go-ten/go/ethadapter" hostcontainer "github.com/ten-protocol/go-ten/go/host/container" "github.com/ten-protocol/go-ten/go/host/p2p" - "github.com/ten-protocol/go-ten/go/host/rpc/clientrpc" "github.com/ten-protocol/go-ten/go/host/rpc/enclaverpc" "github.com/ten-protocol/go-ten/go/wallet" "github.com/ten-protocol/go-ten/integration" @@ -145,7 +146,15 @@ func (n *InMemNodeOperator) createHostContainer() *hostcontainer.HostContainer { // create an enclave client enclaveClient := enclaverpc.NewClient(hostConfig, testlog.Logger().New(log.NodeIDKey, n.l1Wallet.Address())) - rpcServer := clientrpc.NewServer(hostConfig, n.logger) + rpcConfig := node.RPCConfig{ + Host: hostConfig.ClientRPCHost, + EnableHTTP: hostConfig.HasClientRPCHTTP, + HTTPPort: int(hostConfig.ClientRPCPortHTTP), + EnableWs: hostConfig.HasClientRPCWebsockets, + WsPort: int(hostConfig.ClientRPCPortWS), + ExposedURLParamNames: nil, + } + rpcServer := node.NewServer(&rpcConfig, n.logger) mgmtContractLib := mgmtcontractlib.NewMgmtContractLib(&hostConfig.ManagementContractAddress, n.logger) l1Repo := l1.NewL1Repository(n.l1Client, []gethcommon.Address{hostConfig.ManagementContractAddress, hostConfig.MessageBusAddress}, n.logger) return hostcontainer.NewHostContainer(hostConfig, svcLocator, nodeP2p, n.l1Client, l1Repo, enclaveClient, mgmtContractLib, n.l1Wallet, rpcServer, hostLogger, metrics.New(false, 0, n.logger)) diff --git a/integration/simulation/p2p/in_mem_obscuro_client.go b/integration/simulation/p2p/in_mem_obscuro_client.go index 30e1072115..9026ce671f 100644 --- a/integration/simulation/p2p/in_mem_obscuro_client.go +++ b/integration/simulation/p2p/in_mem_obscuro_client.go @@ -18,8 +18,8 @@ import ( "github.com/ten-protocol/go-ten/integration/common/testlog" gethcommon "github.com/ethereum/go-ethereum/common" - gethrpc "github.com/ethereum/go-ethereum/rpc" hostcommon "github.com/ten-protocol/go-ten/go/common/host" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) const ( diff --git a/lib/gethfork/README.MD b/lib/gethfork/README.MD new file mode 100644 index 0000000000..d03b1f19c0 --- /dev/null +++ b/lib/gethfork/README.MD @@ -0,0 +1,10 @@ +This package contains a fork of the rpc stack from go-ethereum. + +We need to change it, to add a custom authentication mechanism. + +The RPC URL to the Ten Gateway contains a "token" URL parameter kept secret by every user, which we use for authentication. + +1. Create an http service: `extract_params_handler.go` which extracts it and adds it to the Context +2. From the Context, it can be read by the http rpc handler +3. For WS, we need to add it to a couple of more objects to make it available during processing. +4. Removed unnecessary elements from the "node", such as p2p, database, etc diff --git a/lib/gethfork/node/api.go b/lib/gethfork/node/api.go new file mode 100644 index 0000000000..bad3cd6663 --- /dev/null +++ b/lib/gethfork/node/api.go @@ -0,0 +1,209 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package node + +import ( + "strings" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" +) + +// apis returns the collection of built-in RPC APIs. +func (n *Node) apis() []rpc.API { + return []rpc.API{ + { + Namespace: "admin", + Service: &adminAPI{n}, + //}, { + // Namespace: "debug", + // Service: debug.Handler, + }, { + Namespace: "web3", + Service: &web3API{n}, + }, + } +} + +// adminAPI is the collection of administrative API methods exposed over +// both secure and unsecure RPC channels. +type adminAPI struct { + node *Node // Node interfaced by this API +} + +// StartHTTP starts the HTTP RPC API server. +func (api *adminAPI) StartHTTP(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) { + api.node.lock.Lock() + defer api.node.lock.Unlock() + + // Determine host and port. + if host == nil { + h := DefaultHTTPHost + if api.node.config.HTTPHost != "" { + h = api.node.config.HTTPHost + } + host = &h + } + if port == nil { + port = &api.node.config.HTTPPort + } + + // Determine config. + config := httpConfig{ + CorsAllowedOrigins: api.node.config.HTTPCors, + Vhosts: api.node.config.HTTPVirtualHosts, + Modules: api.node.config.HTTPModules, + rpcEndpointConfig: rpcEndpointConfig{ + batchItemLimit: api.node.config.BatchRequestLimit, + batchResponseSizeLimit: api.node.config.BatchResponseMaxSize, + httpBodyLimit: engineAPIBodyLimit, + }, + ExposedParam: "token", + } + if cors != nil { + config.CorsAllowedOrigins = nil + for _, origin := range strings.Split(*cors, ",") { + config.CorsAllowedOrigins = append(config.CorsAllowedOrigins, strings.TrimSpace(origin)) + } + } + if vhosts != nil { + config.Vhosts = nil + for _, vhost := range strings.Split(*host, ",") { + config.Vhosts = append(config.Vhosts, strings.TrimSpace(vhost)) + } + } + if apis != nil { + config.Modules = nil + for _, m := range strings.Split(*apis, ",") { + config.Modules = append(config.Modules, strings.TrimSpace(m)) + } + } + + if err := api.node.http.setListenAddr(*host, *port); err != nil { + return false, err + } + if err := api.node.http.enableRPC(api.node.rpcAPIs, config); err != nil { + return false, err + } + if err := api.node.http.start(); err != nil { + return false, err + } + return true, nil +} + +// StartRPC starts the HTTP RPC API server. +// Deprecated: use StartHTTP instead. +func (api *adminAPI) StartRPC(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) { + log.Warn("Deprecation warning", "method", "admin.StartRPC", "use-instead", "admin.StartHTTP") + return api.StartHTTP(host, port, cors, apis, vhosts) +} + +// StopHTTP shuts down the HTTP server. +func (api *adminAPI) StopHTTP() (bool, error) { + api.node.http.stop() + return true, nil +} + +// StopRPC shuts down the HTTP server. +// Deprecated: use StopHTTP instead. +func (api *adminAPI) StopRPC() (bool, error) { + log.Warn("Deprecation warning", "method", "admin.StopRPC", "use-instead", "admin.StopHTTP") + return api.StopHTTP() +} + +// StartWS starts the websocket RPC API server. +func (api *adminAPI) StartWS(host *string, port *int, allowedOrigins *string, apis *string) (bool, error) { + api.node.lock.Lock() + defer api.node.lock.Unlock() + + // Determine host and port. + if host == nil { + h := DefaultWSHost + if api.node.config.WSHost != "" { + h = api.node.config.WSHost + } + host = &h + } + if port == nil { + port = &api.node.config.WSPort + } + + // Determine config. + config := wsConfig{ + Modules: api.node.config.WSModules, + Origins: api.node.config.WSOrigins, + // ExposeAll: api.node.config.WSExposeAll, + rpcEndpointConfig: rpcEndpointConfig{ + batchItemLimit: api.node.config.BatchRequestLimit, + batchResponseSizeLimit: api.node.config.BatchResponseMaxSize, + httpBodyLimit: engineAPIBodyLimit, + }, + ExposedParam: "token", + } + if apis != nil { + config.Modules = nil + for _, m := range strings.Split(*apis, ",") { + config.Modules = append(config.Modules, strings.TrimSpace(m)) + } + } + if allowedOrigins != nil { + config.Origins = nil + for _, origin := range strings.Split(*allowedOrigins, ",") { + config.Origins = append(config.Origins, strings.TrimSpace(origin)) + } + } + + // Enable WebSocket on the server. + server := api.node.wsServerForPort(*port, false) + if err := server.setListenAddr(*host, *port); err != nil { + return false, err + } + openApis, _ := api.node.getAPIs() + if err := server.enableWS(openApis, config); err != nil { + return false, err + } + if err := server.start(); err != nil { + return false, err + } + api.node.http.log.Info("WebSocket endpoint opened", "url", api.node.WSEndpoint()) + return true, nil +} + +// StopWS terminates all WebSocket servers. +func (api *adminAPI) StopWS() (bool, error) { + api.node.http.stopWS() + api.node.ws.stop() + return true, nil +} + +// Datadir retrieves the current data directory the node is using. +func (api *adminAPI) Datadir() string { + return api.node.DataDir() +} + +// web3API offers helper utils +type web3API struct { + stack *Node +} + +// Sha3 applies the ethereum sha3 implementation on the input. +// It assumes the input is hex encoded. +func (s *web3API) Sha3(input hexutil.Bytes) hexutil.Bytes { + return crypto.Keccak256(input) +} diff --git a/lib/gethfork/node/config.go b/lib/gethfork/node/config.go new file mode 100644 index 0000000000..c275292890 --- /dev/null +++ b/lib/gethfork/node/config.go @@ -0,0 +1,323 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package node + +import ( + "fmt" + "net" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" +) + +const ( + datadirJWTKey = "jwtsecret" // Path within the datadir to the node's jwt secret + datadirDefaultKeyStore = "keystore" // Path within the datadir to the keystore + datadirNodeDatabase = "nodes" // Path within the datadir to store the node infos +) + +// Config represents a small collection of configuration values to fine tune the +// P2P network layer of a protocol stack. These values can be further extended by +// all registered services. +type Config struct { + // Name sets the instance name of the node. It must not contain the / character and is + // used in the devp2p node identifier. The instance name of geth is "geth". If no + // value is specified, the basename of the current executable is used. + Name string `toml:"-"` + + // Version should be set to the version number of the program. It is used + // in the devp2p node identifier. + Version string `toml:"-"` + + // DataDir is the file system folder the node should use for any data storage + // requirements. The configured data directory will not be directly shared with + // registered services, instead those can use utility methods to create/access + // databases or flat files. This enables ephemeral nodes which can fully reside + // in memory. + DataDir string + + // IPCPath is the requested location to place the IPC endpoint. If the path is + // a simple file name, it is placed inside the data directory (or on the root + // pipe path on Windows), whereas if it's a resolvable path name (absolute or + // relative), then that specific path is enforced. An empty path disables IPC. + IPCPath string + + // HTTPHost is the host interface on which to start the HTTP RPC server. If this + // field is empty, no HTTP API endpoint will be started. + HTTPHost string + + // HTTPPort is the TCP port number on which to start the HTTP RPC server. The + // default zero value is/ valid and will pick a port number randomly (useful + // for ephemeral nodes). + HTTPPort int `toml:",omitempty"` + + // HTTPCors is the Cross-Origin Resource Sharing header to send to requesting + // clients. Please be aware that CORS is a browser enforced security, it's fully + // useless for custom HTTP clients. + HTTPCors []string `toml:",omitempty"` + + // HTTPVirtualHosts is the list of virtual hostnames which are allowed on incoming requests. + // This is by default {'localhost'}. Using this prevents attacks like + // DNS rebinding, which bypasses SOP by simply masquerading as being within the same + // origin. These attacks do not utilize CORS, since they are not cross-domain. + // By explicitly checking the Host-header, the server will not allow requests + // made against the server with a malicious host domain. + // Requests using ip address directly are not affected + HTTPVirtualHosts []string `toml:",omitempty"` + + // HTTPModules is a list of API modules to expose via the HTTP RPC interface. + // If the module list is empty, all RPC API endpoints designated public will be + // exposed. + HTTPModules []string + + // HTTPTimeouts allows for customization of the timeout values used by the HTTP RPC + // interface. + HTTPTimeouts rpc.HTTPTimeouts + + // HTTPPathPrefix specifies a path prefix on which http-rpc is to be served. + HTTPPathPrefix string `toml:",omitempty"` + + // AuthAddr is the listening address on which authenticated APIs are provided. + AuthAddr string `toml:",omitempty"` + + // AuthPort is the port number on which authenticated APIs are provided. + AuthPort int `toml:",omitempty"` + + // AuthVirtualHosts is the list of virtual hostnames which are allowed on incoming requests + // for the authenticated api. This is by default {'localhost'}. + AuthVirtualHosts []string `toml:",omitempty"` + + // WSHost is the host interface on which to start the websocket RPC server. If + // this field is empty, no websocket API endpoint will be started. + WSHost string + + // WSPort is the TCP port number on which to start the websocket RPC server. The + // default zero value is/ valid and will pick a port number randomly (useful for + // ephemeral nodes). + WSPort int `toml:",omitempty"` + + // WSPathPrefix specifies a path prefix on which ws-rpc is to be served. + WSPathPrefix string `toml:",omitempty"` + + // WSOrigins is the list of domain to accept websocket requests from. Please be + // aware that the server can only act upon the HTTP request the client sends and + // cannot verify the validity of the request header. + WSOrigins []string `toml:",omitempty"` + + // WSModules is a list of API modules to expose via the websocket RPC interface. + // If the module list is empty, all RPC API endpoints designated public will be + // exposed. + WSModules []string + + // WSExposeAll exposes all API modules via the WebSocket RPC interface rather + // than just the public ones. + // + // *WARNING* Only set this if the node is running in a trusted network, exposing + // private APIs to untrusted users is a major security risk. + WSExposeAll bool `toml:",omitempty"` + + // GraphQLCors is the Cross-Origin Resource Sharing header to send to requesting + // clients. Please be aware that CORS is a browser enforced security, it's fully + // useless for custom HTTP clients. + GraphQLCors []string `toml:",omitempty"` + + // GraphQLVirtualHosts is the list of virtual hostnames which are allowed on incoming requests. + // This is by default {'localhost'}. Using this prevents attacks like + // DNS rebinding, which bypasses SOP by simply masquerading as being within the same + // origin. These attacks do not utilize CORS, since they are not cross-domain. + // By explicitly checking the Host-header, the server will not allow requests + // made against the server with a malicious host domain. + // Requests using ip address directly are not affected + GraphQLVirtualHosts []string `toml:",omitempty"` + + // Logger is a custom logger to use with the p2p.Server. + Logger log.Logger `toml:",omitempty"` + + oldGethResourceWarning bool + + // AllowUnprotectedTxs allows non EIP-155 protected transactions to be send over RPC. + AllowUnprotectedTxs bool `toml:",omitempty"` + + // BatchRequestLimit is the maximum number of requests in a batch. + BatchRequestLimit int `toml:",omitempty"` + + // BatchResponseMaxSize is the maximum number of bytes returned from a batched rpc call. + BatchResponseMaxSize int `toml:",omitempty"` + + // JWTSecret is the path to the hex-encoded jwt secret. + JWTSecret string `toml:",omitempty"` + + // EnablePersonal enables the deprecated personal namespace. + EnablePersonal bool `toml:"-"` + + // TEN + ExposedURLParamNames []string +} + +// IPCEndpoint resolves an IPC endpoint based on a configured value, taking into +// account the set data folders as well as the designated platform we're currently +// running on. +func (c *Config) IPCEndpoint() string { + // Short circuit if IPC has not been enabled + if c.IPCPath == "" { + return "" + } + // On windows we can only use plain top-level pipes + if runtime.GOOS == "windows" { + if strings.HasPrefix(c.IPCPath, `\\.\pipe\`) { + return c.IPCPath + } + return `\\.\pipe\` + c.IPCPath + } + // Resolve names into the data directory full paths otherwise + if filepath.Base(c.IPCPath) == c.IPCPath { + if c.DataDir == "" { + return filepath.Join(os.TempDir(), c.IPCPath) + } + return filepath.Join(c.DataDir, c.IPCPath) + } + return c.IPCPath +} + +// NodeDB returns the path to the discovery node database. +func (c *Config) NodeDB() string { + if c.DataDir == "" { + return "" // ephemeral + } + return c.ResolvePath(datadirNodeDatabase) +} + +// DefaultIPCEndpoint returns the IPC path used by default. +func DefaultIPCEndpoint(clientIdentifier string) string { + if clientIdentifier == "" { + clientIdentifier = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe") + if clientIdentifier == "" { + panic("empty executable name") + } + } + config := &Config{DataDir: DefaultDataDir(), IPCPath: clientIdentifier + ".ipc"} + return config.IPCEndpoint() +} + +// HTTPEndpoint resolves an HTTP endpoint based on the configured host interface +// and port parameters. +func (c *Config) HTTPEndpoint() string { + if c.HTTPHost == "" { + return "" + } + return net.JoinHostPort(c.HTTPHost, fmt.Sprintf("%d", c.HTTPPort)) +} + +// DefaultHTTPEndpoint returns the HTTP endpoint used by default. +func DefaultHTTPEndpoint() string { + config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort, AuthPort: DefaultAuthPort} + return config.HTTPEndpoint() +} + +// WSEndpoint resolves a websocket endpoint based on the configured host interface +// and port parameters. +func (c *Config) WSEndpoint() string { + if c.WSHost == "" { + return "" + } + return net.JoinHostPort(c.WSHost, fmt.Sprintf("%d", c.WSPort)) +} + +// DefaultWSEndpoint returns the websocket endpoint used by default. +func DefaultWSEndpoint() string { + config := &Config{WSHost: DefaultWSHost, WSPort: DefaultWSPort} + return config.WSEndpoint() +} + +// ExtRPCEnabled returns the indicator whether node enables the external +// RPC(http, ws or graphql). +func (c *Config) ExtRPCEnabled() bool { + return c.HTTPHost != "" || c.WSHost != "" +} + +// NodeName returns the devp2p node identifier. +func (c *Config) NodeName() string { + name := c.name() + // Backwards compatibility: previous versions used title-cased "Geth", keep that. + if name == "geth" || name == "geth-testnet" { + name = "Geth" + } + if c.Version != "" { + name += "/v" + c.Version + } + name += "/" + runtime.GOOS + "-" + runtime.GOARCH + name += "/" + runtime.Version() + return name +} + +func (c *Config) name() string { + if c.Name == "" { + progname := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe") + if progname == "" { + panic("empty executable name, set Config.Name") + } + return progname + } + return c.Name +} + +// These resources are resolved differently for "geth" instances. +var isOldGethResource = map[string]bool{ + "chaindata": true, + "nodes": true, + "nodekey": true, + "static-nodes.json": false, // no warning for these because they have their + "trusted-nodes.json": false, // own separate warning. +} + +// ResolvePath resolves path in the instance directory. +func (c *Config) ResolvePath(path string) string { + if filepath.IsAbs(path) { + return path + } + if c.DataDir == "" { + return "" + } + // Backwards-compatibility: ensure that data directory files created + // by geth 1.4 are used if they exist. + if warn, isOld := isOldGethResource[path]; isOld { + oldpath := "" + if c.name() == "geth" { + oldpath = filepath.Join(c.DataDir, path) + } + if oldpath != "" && common.FileExist(oldpath) { + if warn && !c.oldGethResourceWarning { + c.oldGethResourceWarning = true + log.Warn("Using deprecated resource file, please move this file to the 'geth' subdirectory of datadir.", "file", oldpath) + } + return oldpath + } + } + return filepath.Join(c.instanceDir(), path) +} + +func (c *Config) instanceDir() string { + if c.DataDir == "" { + return "" + } + return filepath.Join(c.DataDir, c.name()) +} diff --git a/lib/gethfork/node/defaults.go b/lib/gethfork/node/defaults.go new file mode 100644 index 0000000000..9a1fe8635b --- /dev/null +++ b/lib/gethfork/node/defaults.go @@ -0,0 +1,126 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package node + +import ( + "os" + "os/user" + "path/filepath" + "runtime" + + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" +) + +const ( + DefaultHTTPHost = "localhost" // Default host interface for the HTTP RPC server + DefaultHTTPPort = 8545 // Default TCP port for the HTTP RPC server + DefaultWSHost = "localhost" // Default host interface for the websocket RPC server + DefaultWSPort = 8546 // Default TCP port for the websocket RPC server + DefaultAuthHost = "localhost" // Default host interface for the authenticated apis + DefaultAuthPort = 8551 // Default port for the authenticated apis +) + +const ( + // Engine API batch limits: these are not configurable by users, and should cover the + // needs of all CLs. + engineAPIBatchItemLimit = 2000 + engineAPIBatchResponseSizeLimit = 250 * 1000 * 1000 + engineAPIBodyLimit = 128 * 1024 * 1024 +) + +var ( + DefaultAuthCors = []string{"localhost"} // Default cors domain for the authenticated apis + DefaultAuthVhosts = []string{"localhost"} // Default virtual hosts for the authenticated apis + DefaultAuthOrigins = []string{"localhost"} // Default origins for the authenticated apis + DefaultAuthPrefix = "" // Default prefix for the authenticated apis + DefaultAuthModules = []string{"eth", "engine"} +) + +// DefaultConfig contains reasonable default settings. +var DefaultConfig = Config{ + DataDir: DefaultDataDir(), + HTTPPort: DefaultHTTPPort, + AuthAddr: DefaultAuthHost, + AuthPort: DefaultAuthPort, + AuthVirtualHosts: DefaultAuthVhosts, + HTTPModules: []string{"net", "web3"}, + HTTPVirtualHosts: []string{"localhost"}, + HTTPTimeouts: rpc.DefaultHTTPTimeouts, + WSPort: DefaultWSPort, + WSModules: []string{"net", "web3"}, + BatchRequestLimit: 1000, + BatchResponseMaxSize: 25 * 1000 * 1000, + GraphQLVirtualHosts: []string{"localhost"}, +} + +// DefaultDataDir is the default data directory to use for the databases and other +// persistence requirements. +func DefaultDataDir() string { + // Try to place the data folder in the user's home dir + home := homeDir() + if home != "" { + switch runtime.GOOS { + case "darwin": + return filepath.Join(home, "Library", "Ethereum") + case "windows": + // We used to put everything in %HOME%\AppData\Roaming, but this caused + // problems with non-typical setups. If this fallback location exists and + // is non-empty, use it, otherwise DTRT and check %LOCALAPPDATA%. + fallback := filepath.Join(home, "AppData", "Roaming", "Ethereum") + appdata := windowsAppData() + if appdata == "" || isNonEmptyDir(fallback) { + return fallback + } + return filepath.Join(appdata, "Ethereum") + default: + return filepath.Join(home, ".ethereum") + } + } + // As we cannot guess a stable location, return empty and handle later + return "" +} + +func windowsAppData() string { + v := os.Getenv("LOCALAPPDATA") + if v == "" { + // Windows XP and below don't have LocalAppData. Crash here because + // we don't support Windows XP and undefining the variable will cause + // other issues. + panic("environment variable LocalAppData is undefined") + } + return v +} + +func isNonEmptyDir(dir string) bool { + f, err := os.Open(dir) + if err != nil { + return false + } + names, _ := f.Readdir(1) + f.Close() + return len(names) > 0 +} + +func homeDir() string { + if home := os.Getenv("HOME"); home != "" { + return home + } + if usr, err := user.Current(); err == nil { + return usr.HomeDir + } + return "" +} diff --git a/lib/gethfork/node/doc.go b/lib/gethfork/node/doc.go new file mode 100644 index 0000000000..2ac7085679 --- /dev/null +++ b/lib/gethfork/node/doc.go @@ -0,0 +1,124 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +/* +Package node sets up multi-protocol Ethereum nodes. + +In the model exposed by this package, a node is a collection of services which use shared +resources to provide RPC APIs. Services can also offer devp2p protocols, which are wired +up to the devp2p network when the node instance is started. + +# Node Lifecycle + +The Node object has a lifecycle consisting of three basic states, INITIALIZING, RUNNING +and CLOSED. + + ●───────┐ + New() + │ + ▼ + INITIALIZING ────Start()─┐ + │ │ + │ ▼ + Close() RUNNING + │ │ + ▼ │ + CLOSED ◀──────Close()─┘ + +Creating a Node allocates basic resources such as the data directory and returns the node +in its INITIALIZING state. Lifecycle objects, RPC APIs and peer-to-peer networking +protocols can be registered in this state. Basic operations such as opening a key-value +database are permitted while initializing. + +Once everything is registered, the node can be started, which moves it into the RUNNING +state. Starting the node starts all registered Lifecycle objects and enables RPC and +peer-to-peer networking. Note that no additional Lifecycles, APIs or p2p protocols can be +registered while the node is running. + +Closing the node releases all held resources. The actions performed by Close depend on the +state it was in. When closing a node in INITIALIZING state, resources related to the data +directory are released. If the node was RUNNING, closing it also stops all Lifecycle +objects and shuts down RPC and peer-to-peer networking. + +You must always call Close on Node, even if the node was not started. + +# Resources Managed By Node + +All file-system resources used by a node instance are located in a directory called the +data directory. The location of each resource can be overridden through additional node +configuration. The data directory is optional. If it is not set and the location of a +resource is otherwise unspecified, //nolint +package node + + will create the resource in memory. + +To access to the devp2p network, Node configures and starts p2p.Server. Each host on the +devp2p network has a unique identifier, the node key. The Node instance persists this key +across restarts. Node also loads static and trusted node lists and ensures that knowledge +about other hosts is persisted. + +JSON-RPC servers which run HTTP, WebSocket or IPC can be started on a Node. RPC modules +offered by registered services will be offered on those endpoints. Users can restrict any +endpoint to a subset of RPC modules. Node itself offers the "debug", "admin" and "web3" +modules. + +Service implementations can open LevelDB databases through the service context. Package +node chooses the file system location of each database. If the node is configured to run +without a data directory, databases are opened in memory instead. + +Node also creates the shared store of encrypted Ethereum account keys. Services can access +the account manager through the service context. + +# Sharing Data Directory Among Instances + +Multiple node instances can share a single data directory if they have distinct instance +names (set through the Name config option). Sharing behaviour depends on the type of +resource. + +devp2p-related resources (node key, static/trusted node lists, known hosts database) are +stored in a directory with the same name as the instance. Thus, multiple node instances +using the same data directory will store this information in different subdirectories of +the data directory. + +LevelDB databases are also stored within the instance subdirectory. If multiple node +instances use the same data directory, opening the databases with identical names will +create one database for each instance. + +The account key store is shared among all node instances using the same data directory +unless its location is changed through the KeyStoreDir configuration option. + +# Data Directory Sharing Example + +In this example, two node instances named A and B are started with the same data +directory. Node instance A opens the database "db", node instance B opens the databases +"db" and "db-2". The following files will be created in the data directory: + + data-directory/ + A/ + nodekey -- devp2p node key of instance A + nodes/ -- devp2p discovery knowledge database of instance A + db/ -- LevelDB content for "db" + A.ipc -- JSON-RPC UNIX domain socket endpoint of instance A + B/ + nodekey -- devp2p node key of node B + nodes/ -- devp2p discovery knowledge database of instance B + static-nodes.json -- devp2p static node list of instance B + db/ -- LevelDB content for "db" + db-2/ -- LevelDB content for "db-2" + B.ipc -- JSON-RPC UNIX domain socket endpoint of instance B + keystore/ -- account key store, used by both instances +*/ +package node diff --git a/lib/gethfork/node/endpoints.go b/lib/gethfork/node/endpoints.go new file mode 100644 index 0000000000..f24d115abb --- /dev/null +++ b/lib/gethfork/node/endpoints.go @@ -0,0 +1,92 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package node + +import ( + "net" + "net/http" + "time" + + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" + + "github.com/ethereum/go-ethereum/log" +) + +// StartHTTPEndpoint starts the HTTP RPC endpoint. +func StartHTTPEndpoint(endpoint string, timeouts rpc.HTTPTimeouts, handler http.Handler) (*http.Server, net.Addr, error) { + // start the HTTP listener + var ( + listener net.Listener + err error + ) + if listener, err = net.Listen("tcp", endpoint); err != nil { + return nil, nil, err + } + // make sure timeout values are meaningful + CheckTimeouts(&timeouts) + // Bundle and start the HTTP server + httpSrv := &http.Server{ + Handler: handler, + ReadTimeout: timeouts.ReadTimeout, + ReadHeaderTimeout: timeouts.ReadHeaderTimeout, + WriteTimeout: timeouts.WriteTimeout, + IdleTimeout: timeouts.IdleTimeout, + } + go httpSrv.Serve(listener) + return httpSrv, listener.Addr(), err +} + +// checkModuleAvailability checks that all names given in modules are actually +// available API services. It assumes that the MetadataApi module ("rpc") is always available; +// the registration of this "rpc" module happens in NewServer() and is thus common to all endpoints. +func checkModuleAvailability(modules []string, apis []rpc.API) (bad, available []string) { + availableSet := make(map[string]struct{}) + for _, api := range apis { + if _, ok := availableSet[api.Namespace]; !ok { + availableSet[api.Namespace] = struct{}{} + available = append(available, api.Namespace) + } + } + for _, name := range modules { + if _, ok := availableSet[name]; !ok { + if name != rpc.MetadataApi && name != rpc.EngineApi { + bad = append(bad, name) + } + } + } + return bad, available +} + +// CheckTimeouts ensures that timeout values are meaningful +func CheckTimeouts(timeouts *rpc.HTTPTimeouts) { + if timeouts.ReadTimeout < time.Second { + log.Warn("Sanitizing invalid HTTP read timeout", "provided", timeouts.ReadTimeout, "updated", rpc.DefaultHTTPTimeouts.ReadTimeout) + timeouts.ReadTimeout = rpc.DefaultHTTPTimeouts.ReadTimeout + } + if timeouts.ReadHeaderTimeout < time.Second { + log.Warn("Sanitizing invalid HTTP read header timeout", "provided", timeouts.ReadHeaderTimeout, "updated", rpc.DefaultHTTPTimeouts.ReadHeaderTimeout) + timeouts.ReadHeaderTimeout = rpc.DefaultHTTPTimeouts.ReadHeaderTimeout + } + if timeouts.WriteTimeout < time.Second { + log.Warn("Sanitizing invalid HTTP write timeout", "provided", timeouts.WriteTimeout, "updated", rpc.DefaultHTTPTimeouts.WriteTimeout) + timeouts.WriteTimeout = rpc.DefaultHTTPTimeouts.WriteTimeout + } + if timeouts.IdleTimeout < time.Second { + log.Warn("Sanitizing invalid HTTP idle timeout", "provided", timeouts.IdleTimeout, "updated", rpc.DefaultHTTPTimeouts.IdleTimeout) + timeouts.IdleTimeout = rpc.DefaultHTTPTimeouts.IdleTimeout + } +} diff --git a/lib/gethfork/node/errors.go b/lib/gethfork/node/errors.go new file mode 100644 index 0000000000..cc25f638cb --- /dev/null +++ b/lib/gethfork/node/errors.go @@ -0,0 +1,41 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package node + +import ( + "errors" + "fmt" + "reflect" +) + +var ( + ErrDatadirUsed = errors.New("datadir already used by another process") + ErrNodeStopped = errors.New("node not started") + ErrNodeRunning = errors.New("node already running") +) + +// StopError is returned if a Node fails to stop either any of its registered +// services or itself. +type StopError struct { + Server error + Services map[reflect.Type]error +} + +// Error generates a textual representation of the stop error. +func (e *StopError) Error() string { + return fmt.Sprintf("server: %v, services: %v", e.Server, e.Services) +} diff --git a/lib/gethfork/node/extract_params_handler.go b/lib/gethfork/node/extract_params_handler.go new file mode 100644 index 0000000000..02a92babc0 --- /dev/null +++ b/lib/gethfork/node/extract_params_handler.go @@ -0,0 +1,32 @@ +package node + +import ( + "context" + "net/http" + + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" +) + +type httpParamsHandler struct { + exposedParam string + next http.Handler +} + +// newTenTokenHandler creates a http.Handler that extracts params +func newHTTPParamsHandler(exposedParam string, next http.Handler) http.Handler { + return &httpParamsHandler{ + exposedParam: exposedParam, + next: next, + } +} + +// ServeHTTP implements http.Handler +func (handler *httpParamsHandler) ServeHTTP(out http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + val := q.Get(handler.exposedParam) + if len(val) > 0 { + ctx := context.WithValue(r.Context(), rpc.GWTokenKey{}, val) + handler.next.ServeHTTP(out, r.WithContext(ctx)) + } + handler.next.ServeHTTP(out, r) +} diff --git a/lib/gethfork/node/jwt_auth.go b/lib/gethfork/node/jwt_auth.go new file mode 100644 index 0000000000..5e70193171 --- /dev/null +++ b/lib/gethfork/node/jwt_auth.go @@ -0,0 +1,45 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package node + +import ( + "fmt" + "net/http" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" +) + +// NewJWTAuth creates an rpc client authentication provider that uses JWT. The +// secret MUST be 32 bytes (256 bits) as defined by the Engine-API authentication spec. +// +// See https://github.com/ethereum/execution-apis/blob/main/src/engine/authentication.md +// for more details about this authentication scheme. +func NewJWTAuth(jwtsecret [32]byte) rpc.HTTPAuth { + return func(h http.Header) error { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "iat": &jwt.NumericDate{Time: time.Now()}, + }) + s, err := token.SignedString(jwtsecret[:]) + if err != nil { + return fmt.Errorf("failed to create JWT token: %w", err) + } + h.Set("Authorization", "Bearer "+s) + return nil + } +} diff --git a/lib/gethfork/node/jwt_handler.go b/lib/gethfork/node/jwt_handler.go new file mode 100644 index 0000000000..375a62d618 --- /dev/null +++ b/lib/gethfork/node/jwt_handler.go @@ -0,0 +1,80 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package node + +import ( + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +const jwtExpiryTimeout = 60 * time.Second + +type jwtHandler struct { + keyFunc func(token *jwt.Token) (interface{}, error) + next http.Handler +} + +// newJWTHandler creates a http.Handler with jwt authentication support. +func newJWTHandler(secret []byte, next http.Handler) http.Handler { + return &jwtHandler{ + keyFunc: func(_ *jwt.Token) (interface{}, error) { + return secret, nil + }, + next: next, + } +} + +// ServeHTTP implements http.Handler +func (handler *jwtHandler) ServeHTTP(out http.ResponseWriter, r *http.Request) { + var ( + strToken string + claims jwt.RegisteredClaims + ) + if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") { + strToken = strings.TrimPrefix(auth, "Bearer ") + } + if len(strToken) == 0 { + http.Error(out, "missing token", http.StatusUnauthorized) + return + } + // We explicitly set only HS256 allowed, and also disables the + // claim-check: the RegisteredClaims internally requires 'iat' to + // be no later than 'now', but we allow for a bit of drift. + token, err := jwt.ParseWithClaims(strToken, &claims, handler.keyFunc, + jwt.WithValidMethods([]string{"HS256"}), + jwt.WithoutClaimsValidation()) + + switch { + case err != nil: + http.Error(out, err.Error(), http.StatusUnauthorized) + case !token.Valid: + http.Error(out, "invalid token", http.StatusUnauthorized) + case !claims.VerifyExpiresAt(time.Now(), false): // optional + http.Error(out, "token is expired", http.StatusUnauthorized) + case claims.IssuedAt == nil: + http.Error(out, "missing issued-at", http.StatusUnauthorized) + case time.Since(claims.IssuedAt.Time) > jwtExpiryTimeout: + http.Error(out, "stale token", http.StatusUnauthorized) + case time.Until(claims.IssuedAt.Time) > jwtExpiryTimeout: + http.Error(out, "future token", http.StatusUnauthorized) + default: + handler.next.ServeHTTP(out, r) + } +} diff --git a/lib/gethfork/node/lifecycle.go b/lib/gethfork/node/lifecycle.go new file mode 100644 index 0000000000..0d5f9a0680 --- /dev/null +++ b/lib/gethfork/node/lifecycle.go @@ -0,0 +1,31 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package node + +// Lifecycle encompasses the behavior of services that can be started and stopped +// on the node. Lifecycle management is delegated to the node, but it is the +// responsibility of the service-specific package to configure and register the +// service on the node using the `RegisterLifecycle` method. +type Lifecycle interface { + // Start is called after all services have been constructed and the networking + // layer was also initialized to spawn any goroutines required by the service. + Start() error + + // Stop terminates all goroutines belonging to the service, blocking until they + // are all terminated. + Stop() error +} diff --git a/lib/gethfork/node/node.go b/lib/gethfork/node/node.go new file mode 100644 index 0000000000..66233713ba --- /dev/null +++ b/lib/gethfork/node/node.go @@ -0,0 +1,635 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package node + +import ( + crand "crypto/rand" + "errors" + "fmt" + "hash/crc32" + "net/http" + "os" + "path/filepath" + "reflect" + "strings" + "sync" + + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/gofrs/flock" +) + +// Node is a container on which services can be registered. +type Node struct { + eventmux *event.TypeMux + config *Config + log log.Logger + dirLock *flock.Flock // prevents concurrent use of instance directory + stop chan struct{} // Channel to wait for termination notifications + startStopLock sync.Mutex // Start/Stop are protected by an additional lock + state int // Tracks state of node lifecycle + + lock sync.Mutex + lifecycles []Lifecycle // All registered backends, services, and auxiliary services that have a lifecycle + rpcAPIs []rpc.API // List of APIs currently provided by the node + http *httpServer // + ws *httpServer // + httpAuth *httpServer // + wsAuth *httpServer // + ipc *ipcServer // Stores information about the ipc http server + inprocHandler *rpc.Server // In-process RPC request handler to process the API requests +} + +const ( + initializingState = iota + runningState + closedState +) + +// New creates a new P2P node, ready for protocol registration. +func New(conf *Config) (*Node, error) { + // Copy config and resolve the datadir so future changes to the current + // working directory don't affect the node. + confCopy := *conf + conf = &confCopy + if conf.DataDir != "" { + absdatadir, err := filepath.Abs(conf.DataDir) + if err != nil { + return nil, err + } + conf.DataDir = absdatadir + } + if conf.Logger == nil { + conf.Logger = log.New() + } + + // Ensure that the instance name doesn't cause weird conflicts with + // other files in the data directory. + if strings.ContainsAny(conf.Name, `/\`) { + return nil, errors.New(`Config.Name must not contain '/' or '\'`) + } + if conf.Name == datadirDefaultKeyStore { + return nil, errors.New(`Config.Name cannot be "` + datadirDefaultKeyStore + `"`) + } + if strings.HasSuffix(conf.Name, ".ipc") { + return nil, errors.New(`Config.Name cannot end in ".ipc"`) + } + server := rpc.NewServer() + server.SetBatchLimits(conf.BatchRequestLimit, conf.BatchResponseMaxSize) + node := &Node{ + config: conf, + inprocHandler: server, + eventmux: new(event.TypeMux), + log: conf.Logger, + stop: make(chan struct{}), + } + + // Register built-in APIs. + node.rpcAPIs = append(node.rpcAPIs, node.apis()...) + + // Acquire the instance directory lock. + if err := node.openDataDir(); err != nil { + return nil, err + } + + // Check HTTP/WS prefixes are valid. + if err := validatePrefix("HTTP", conf.HTTPPathPrefix); err != nil { + return nil, err + } + if err := validatePrefix("WebSocket", conf.WSPathPrefix); err != nil { + return nil, err + } + + // Configure RPC servers. + node.http = newHTTPServer(node.log, conf.HTTPTimeouts) + node.httpAuth = newHTTPServer(node.log, conf.HTTPTimeouts) + node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts) + node.wsAuth = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts) + node.ipc = newIPCServer(node.log, conf.IPCEndpoint()) + + return node, nil +} + +// Start starts all registered lifecycles, RPC services and p2p networking. +// Node can only be started once. +func (n *Node) Start() error { + n.startStopLock.Lock() + defer n.startStopLock.Unlock() + + n.lock.Lock() + switch n.state { + case runningState: + n.lock.Unlock() + return ErrNodeRunning + case closedState: + n.lock.Unlock() + return ErrNodeStopped + } + n.state = runningState + // open networking and RPC endpoints + err := n.openEndpoints() + lifecycles := make([]Lifecycle, len(n.lifecycles)) + copy(lifecycles, n.lifecycles) + n.lock.Unlock() + + // Check if endpoint startup failed. + if err != nil { + n.doClose(nil) + return err + } + // Start all registered lifecycles. + var started []Lifecycle //nolint:prealloc + for _, lifecycle := range lifecycles { + if err = lifecycle.Start(); err != nil { + break + } + started = append(started, lifecycle) + } + // Check if any lifecycle failed to start. + if err != nil { + n.stopServices(started) //nolint:errcheck + n.doClose(nil) //nolint:errcheck + } + return err +} + +// Close stops the Node and releases resources acquired in +// Node constructor New. +func (n *Node) Close() error { + n.startStopLock.Lock() + defer n.startStopLock.Unlock() + + n.lock.Lock() + state := n.state + n.lock.Unlock() + switch state { + case initializingState: + // The node was never started. + return n.doClose(nil) + case runningState: + // The node was started, release resources acquired by Start(). + var errs []error + if err := n.stopServices(n.lifecycles); err != nil { + errs = append(errs, err) + } + return n.doClose(errs) + case closedState: + return ErrNodeStopped + default: + panic(fmt.Sprintf("node is in unknown state %d", state)) + } +} + +// doClose releases resources acquired by New(), collecting errors. +func (n *Node) doClose(errs []error) error { + // Release instance directory lock. + n.closeDataDir() + + // Unblock n.Wait. + close(n.stop) + + // Report any errors that might have occurred. + switch len(errs) { + case 0: + return nil + case 1: + return errs[0] + default: + return fmt.Errorf("%v", errs) + } +} + +// openEndpoints starts all network and RPC endpoints. +func (n *Node) openEndpoints() error { + // start RPC endpoints + err := n.startRPC() + if err != nil { + n.stopRPC() + } + return err +} + +// containsLifecycle checks if 'lfs' contains 'l'. +func containsLifecycle(lfs []Lifecycle, l Lifecycle) bool { + for _, obj := range lfs { + if obj == l { + return true + } + } + return false +} + +// stopServices terminates running services, RPC and p2p networking. +// It is the inverse of Start. +func (n *Node) stopServices(running []Lifecycle) error { + n.stopRPC() + + // Stop running lifecycles in reverse order. + failure := &StopError{Services: make(map[reflect.Type]error)} + for i := len(running) - 1; i >= 0; i-- { + if err := running[i].Stop(); err != nil { + failure.Services[reflect.TypeOf(running[i])] = err + } + } + + if len(failure.Services) > 0 { + return failure + } + return nil +} + +func (n *Node) openDataDir() error { + if n.config.DataDir == "" { + return nil // ephemeral + } + + instdir := filepath.Join(n.config.DataDir, n.config.name()) + if err := os.MkdirAll(instdir, 0o700); err != nil { + return err + } + // Lock the instance directory to prevent concurrent use by another instance as well as + // accidental use of the instance directory as a database. + n.dirLock = flock.New(filepath.Join(instdir, "LOCK")) + + if locked, err := n.dirLock.TryLock(); err != nil { + return err + } else if !locked { + return ErrDatadirUsed + } + return nil +} + +func (n *Node) closeDataDir() { + // Release instance directory lock. + if n.dirLock != nil && n.dirLock.Locked() { + n.dirLock.Unlock() + n.dirLock = nil + } +} + +// obtainJWTSecret loads the jwt-secret, either from the provided config, +// or from the default location. If neither of those are present, it generates +// a new secret and stores to the default location. +func (n *Node) obtainJWTSecret(cliParam string) ([]byte, error) { + fileName := cliParam + if len(fileName) == 0 { + // no path provided, use default + fileName = n.ResolvePath(datadirJWTKey) + } + // try reading from file + if data, err := os.ReadFile(fileName); err == nil { + jwtSecret := common.FromHex(strings.TrimSpace(string(data))) + if len(jwtSecret) == 32 { + log.Info("Loaded JWT secret file", "path", fileName, "crc32", fmt.Sprintf("%#x", crc32.ChecksumIEEE(jwtSecret))) + return jwtSecret, nil + } + log.Error("Invalid JWT secret", "path", fileName, "length", len(jwtSecret)) + return nil, errors.New("invalid JWT secret") + } + // Need to generate one + jwtSecret := make([]byte, 32) + crand.Read(jwtSecret) //nolint:errcheck + // if we're in --dev mode, don't bother saving, just show it + if fileName == "" { + log.Info("Generated ephemeral JWT secret", "secret", hexutil.Encode(jwtSecret)) + return jwtSecret, nil + } + if err := os.WriteFile(fileName, []byte(hexutil.Encode(jwtSecret)), 0o600); err != nil { + return nil, err + } + log.Info("Generated JWT secret", "path", fileName) + return jwtSecret, nil +} + +// startRPC is a helper method to configure all the various RPC endpoints during node +// startup. It's not meant to be called at any time afterwards as it makes certain +// assumptions about the state of the node. +// +//nolint:gocognit +func (n *Node) startRPC() error { + // Filter out personal api + var apis []rpc.API //nolint:prealloc + for _, api := range n.rpcAPIs { + if api.Namespace == "personal" { + if n.config.EnablePersonal { + log.Warn("Deprecated personal namespace activated") + } else { + continue + } + } + apis = append(apis, api) + } + if err := n.startInProc(apis); err != nil { + return err + } + + // Configure IPC. + if n.ipc.endpoint != "" { + if err := n.ipc.start(apis); err != nil { + return err + } + } + var ( + servers []*httpServer + openAPIs, allAPIs = n.getAPIs() + ) + + rpcConfig := rpcEndpointConfig{ + batchItemLimit: n.config.BatchRequestLimit, + batchResponseSizeLimit: n.config.BatchResponseMaxSize, + httpBodyLimit: engineAPIBodyLimit, + } + + initHTTP := func(server *httpServer, port int) error { + if err := server.setListenAddr(n.config.HTTPHost, port); err != nil { + return err + } + if err := server.enableRPC(openAPIs, httpConfig{ + CorsAllowedOrigins: n.config.HTTPCors, + Vhosts: n.config.HTTPVirtualHosts, + Modules: n.config.HTTPModules, + prefix: n.config.HTTPPathPrefix, + rpcEndpointConfig: rpcConfig, + ExposedParam: "token", + }); err != nil { + return err + } + servers = append(servers, server) + return nil + } + + initWS := func(port int) error { + server := n.wsServerForPort(port, false) + if err := server.setListenAddr(n.config.WSHost, port); err != nil { + return err + } + if err := server.enableWS(openAPIs, wsConfig{ + Modules: n.config.WSModules, + Origins: n.config.WSOrigins, + prefix: n.config.WSPathPrefix, + rpcEndpointConfig: rpcConfig, + ExposedParam: "token", + }); err != nil { + return err + } + servers = append(servers, server) + return nil + } + + initAuth := func(port int, secret []byte) error { + // Enable auth via HTTP + server := n.httpAuth + if err := server.setListenAddr(n.config.AuthAddr, port); err != nil { + return err + } + sharedConfig := rpcEndpointConfig{ + jwtSecret: secret, + batchItemLimit: engineAPIBatchItemLimit, + batchResponseSizeLimit: engineAPIBatchResponseSizeLimit, + httpBodyLimit: engineAPIBodyLimit, + } + if err := server.enableRPC(allAPIs, httpConfig{ + CorsAllowedOrigins: DefaultAuthCors, + Vhosts: n.config.AuthVirtualHosts, + Modules: DefaultAuthModules, + prefix: DefaultAuthPrefix, + rpcEndpointConfig: sharedConfig, + }); err != nil { + return err + } + servers = append(servers, server) + + // Enable auth via WS + server = n.wsServerForPort(port, true) + if err := server.setListenAddr(n.config.AuthAddr, port); err != nil { + return err + } + if err := server.enableWS(allAPIs, wsConfig{ + Modules: DefaultAuthModules, + Origins: DefaultAuthOrigins, + prefix: DefaultAuthPrefix, + rpcEndpointConfig: sharedConfig, + }); err != nil { + return err + } + servers = append(servers, server) + return nil + } + + // Set up HTTP. + if n.config.HTTPHost != "" { + // Configure legacy unauthenticated HTTP. + if err := initHTTP(n.http, n.config.HTTPPort); err != nil { + return err + } + } + // Configure WebSocket. + if n.config.WSHost != "" { + // legacy unauthenticated + if err := initWS(n.config.WSPort); err != nil { + return err + } + } + // Configure authenticated API + if len(openAPIs) != len(allAPIs) { + jwtSecret, err := n.obtainJWTSecret(n.config.JWTSecret) + if err != nil { + return err + } + if err := initAuth(n.config.AuthPort, jwtSecret); err != nil { + return err + } + } + // Start the servers + for _, server := range servers { + if err := server.start(); err != nil { + return err + } + } + return nil +} + +func (n *Node) wsServerForPort(port int, authenticated bool) *httpServer { + httpServer, wsServer := n.http, n.ws + if authenticated { + httpServer, wsServer = n.httpAuth, n.wsAuth + } + if n.config.HTTPHost == "" || httpServer.port == port { + return httpServer + } + return wsServer +} + +func (n *Node) stopRPC() { + n.http.stop() + n.ws.stop() + n.httpAuth.stop() + n.wsAuth.stop() + n.ipc.stop() //nolint:errcheck + n.stopInProc() +} + +// startInProc registers all RPC APIs on the inproc server. +func (n *Node) startInProc(apis []rpc.API) error { + for _, api := range apis { + if err := n.inprocHandler.RegisterName(api.Namespace, api.Service); err != nil { + return err + } + } + return nil +} + +// stopInProc terminates the in-process RPC endpoint. +func (n *Node) stopInProc() { + n.inprocHandler.Stop() +} + +// Wait blocks until the node is closed. +func (n *Node) Wait() { + <-n.stop +} + +// RegisterLifecycle registers the given Lifecycle on the node. +func (n *Node) RegisterLifecycle(lifecycle Lifecycle) { + n.lock.Lock() + defer n.lock.Unlock() + + if n.state != initializingState { + panic("can't register lifecycle on running/stopped node") + } + if containsLifecycle(n.lifecycles, lifecycle) { + panic(fmt.Sprintf("attempt to register lifecycle %T more than once", lifecycle)) + } + n.lifecycles = append(n.lifecycles, lifecycle) +} + +// RegisterAPIs registers the APIs a service provides on the node. +func (n *Node) RegisterAPIs(apis []rpc.API) { + n.lock.Lock() + defer n.lock.Unlock() + + if n.state != initializingState { + panic("can't register APIs on running/stopped node") + } + n.rpcAPIs = append(n.rpcAPIs, apis...) +} + +// getAPIs return two sets of APIs, both the ones that do not require +// authentication, and the complete set +func (n *Node) getAPIs() (unauthenticated, all []rpc.API) { + for _, api := range n.rpcAPIs { + if !api.Authenticated { + unauthenticated = append(unauthenticated, api) + } + } + return unauthenticated, n.rpcAPIs +} + +// RegisterHandler mounts a handler on the given path on the canonical HTTP server. +// +// The name of the handler is shown in a log message when the HTTP server starts +// and should be a descriptive term for the service provided by the handler. +func (n *Node) RegisterHandler(name, path string, handler http.Handler) { + n.lock.Lock() + defer n.lock.Unlock() + + if n.state != initializingState { + panic("can't register HTTP handler on running/stopped node") + } + + n.http.mux.Handle(path, handler) + n.http.handlerNames[path] = name +} + +// Attach creates an RPC client attached to an in-process API handler. +func (n *Node) Attach() *rpc.Client { + return rpc.DialInProc(n.inprocHandler) +} + +// RPCHandler returns the in-process RPC request handler. +func (n *Node) RPCHandler() (*rpc.Server, error) { + n.lock.Lock() + defer n.lock.Unlock() + + if n.state == closedState { + return nil, ErrNodeStopped + } + return n.inprocHandler, nil +} + +// Config returns the configuration of node. +func (n *Node) Config() *Config { + return n.config +} + +// DataDir retrieves the current datadir used by the protocol stack. +// Deprecated: No files should be stored in this directory, use InstanceDir instead. +func (n *Node) DataDir() string { + return n.config.DataDir +} + +// InstanceDir retrieves the instance directory used by the protocol stack. +func (n *Node) InstanceDir() string { + return n.config.instanceDir() +} + +// IPCEndpoint retrieves the current IPC endpoint used by the protocol stack. +func (n *Node) IPCEndpoint() string { + return n.ipc.endpoint +} + +// HTTPEndpoint returns the URL of the HTTP server. Note that this URL does not +// contain the JSON-RPC path prefix set by HTTPPathPrefix. +func (n *Node) HTTPEndpoint() string { + return "http://" + n.http.listenAddr() +} + +// WSEndpoint returns the current JSON-RPC over WebSocket endpoint. +func (n *Node) WSEndpoint() string { + if n.http.wsAllowed() { + return "ws://" + n.http.listenAddr() + n.http.wsConfig.prefix + } + return "ws://" + n.ws.listenAddr() + n.ws.wsConfig.prefix +} + +// HTTPAuthEndpoint returns the URL of the authenticated HTTP server. +func (n *Node) HTTPAuthEndpoint() string { + return "http://" + n.httpAuth.listenAddr() +} + +// WSAuthEndpoint returns the current authenticated JSON-RPC over WebSocket endpoint. +func (n *Node) WSAuthEndpoint() string { + if n.httpAuth.wsAllowed() { + return "ws://" + n.httpAuth.listenAddr() + n.httpAuth.wsConfig.prefix + } + return "ws://" + n.wsAuth.listenAddr() + n.wsAuth.wsConfig.prefix +} + +// EventMux retrieves the event multiplexer used by all the network services in +// the current protocol stack. +func (n *Node) EventMux() *event.TypeMux { + return n.eventmux +} + +// ResolvePath returns the absolute path of a resource in the instance directory. +func (n *Node) ResolvePath(x string) string { + return n.config.ResolvePath(x) +} diff --git a/lib/gethfork/node/rpc_server.go b/lib/gethfork/node/rpc_server.go new file mode 100644 index 0000000000..c97d9ad17e --- /dev/null +++ b/lib/gethfork/node/rpc_server.go @@ -0,0 +1,91 @@ +package node + +import ( + "net/http" + + "github.com/ten-protocol/go-ten/tools/walletextension/api" + + gethlog "github.com/ethereum/go-ethereum/log" + "github.com/ten-protocol/go-ten/go/common/log" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" +) + +const ( + allOrigins = "*" +) + +type RPCConfig struct { + Host string + EnableHTTP bool + HTTPPort int + EnableWs bool + WsPort int + WsPath string + HTTPPath string + + // ExposedURLParamNames - url prams that are available in the services + ExposedURLParamNames []string +} + +// Server manages the lifeycle of an RPC Server +type Server interface { + Start() error + Stop() + RegisterAPIs(apis []rpc.API) + RegisterRoutes(routes []api.Route) +} + +// An implementation of `host.Server` that reuses the Geth `node` package for client communication. +type serverImpl struct { + node *Node + logger gethlog.Logger +} + +func NewServer(config *RPCConfig, logger gethlog.Logger) Server { + rpcConfig := Config{ + Logger: logger, + ExposedURLParamNames: config.ExposedURLParamNames, + } + if config.EnableHTTP { + rpcConfig.HTTPHost = config.Host + rpcConfig.HTTPPort = config.HTTPPort + // todo (@pedro) - review if this poses a security issue + rpcConfig.HTTPVirtualHosts = []string{allOrigins} + rpcConfig.HTTPPathPrefix = config.HTTPPath + } + if config.EnableWs { + rpcConfig.WSHost = config.Host + rpcConfig.WSPort = config.WsPort + // todo (@pedro) - review if this poses a security issue + rpcConfig.WSOrigins = []string{allOrigins} + rpcConfig.WSPathPrefix = config.WsPath + } + + rpcServerNode, err := New(&rpcConfig) + if err != nil { + logger.Crit("could not create new client server.", log.ErrKey, err) + } + + return &serverImpl{node: rpcServerNode, logger: logger} +} + +func (s *serverImpl) RegisterAPIs(apis []rpc.API) { + s.node.RegisterAPIs(apis) +} + +func (s *serverImpl) RegisterRoutes(routes []api.Route) { + for _, route := range routes { + s.node.RegisterHandler(route.Name, route.Name, http.HandlerFunc(route.Func)) + } +} + +func (s *serverImpl) Start() error { + return s.node.Start() +} + +func (s *serverImpl) Stop() { + err := s.node.Close() + if err != nil { + s.logger.Crit("could not stop node client server.", log.ErrKey, err) + } +} diff --git a/lib/gethfork/node/rpcstack.go b/lib/gethfork/node/rpcstack.go new file mode 100644 index 0000000000..76d58863ba --- /dev/null +++ b/lib/gethfork/node/rpcstack.go @@ -0,0 +1,658 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package node + +import ( + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" + + "github.com/ethereum/go-ethereum/log" + "github.com/rs/cors" +) + +// httpConfig is the JSON-RPC/HTTP configuration. +type httpConfig struct { + Modules []string + CorsAllowedOrigins []string + Vhosts []string + prefix string // path prefix on which to mount http handler + ExposedParam string + rpcEndpointConfig +} + +// wsConfig is the JSON-RPC/Websocket configuration +type wsConfig struct { + Origins []string + Modules []string + prefix string // path prefix on which to mount ws handler + ExposedParam string + rpcEndpointConfig +} + +type rpcEndpointConfig struct { + jwtSecret []byte // optional JWT secret + batchItemLimit int + batchResponseSizeLimit int + httpBodyLimit int +} + +type rpcHandler struct { + http.Handler + server *rpc.Server +} + +type httpServer struct { + log log.Logger + timeouts rpc.HTTPTimeouts + mux http.ServeMux // registered handlers go here + + mu sync.Mutex + server *http.Server + listener net.Listener // non-nil when server is running + + // HTTP RPC handler things. + + httpConfig httpConfig + httpHandler atomic.Value // *rpcHandler + + // WebSocket handler things. + wsConfig wsConfig + wsHandler atomic.Value // *rpcHandler + + // These are set by setListenAddr. + endpoint string + host string + port int + + handlerNames map[string]string +} + +const ( + shutdownTimeout = 5 * time.Second +) + +func newHTTPServer(log log.Logger, timeouts rpc.HTTPTimeouts) *httpServer { + h := &httpServer{log: log, timeouts: timeouts, handlerNames: make(map[string]string)} + + h.httpHandler.Store((*rpcHandler)(nil)) + h.wsHandler.Store((*rpcHandler)(nil)) + return h +} + +// setListenAddr configures the listening address of the server. +// The address can only be set while the server isn't running. +func (h *httpServer) setListenAddr(host string, port int) error { + h.mu.Lock() + defer h.mu.Unlock() + + if h.listener != nil && (host != h.host || port != h.port) { + return fmt.Errorf("HTTP server already running on %s", h.endpoint) + } + + h.host, h.port = host, port + h.endpoint = net.JoinHostPort(host, fmt.Sprintf("%d", port)) + return nil +} + +// listenAddr returns the listening address of the server. +func (h *httpServer) listenAddr() string { + h.mu.Lock() + defer h.mu.Unlock() + + if h.listener != nil { + return h.listener.Addr().String() + } + return h.endpoint +} + +// start starts the HTTP server if it is enabled and not already running. +func (h *httpServer) start() error { + h.mu.Lock() + defer h.mu.Unlock() + + if h.endpoint == "" || h.listener != nil { + return nil // already running or not configured + } + + // Initialize the server. + h.server = &http.Server{Handler: h} + if h.timeouts != (rpc.HTTPTimeouts{}) { + CheckTimeouts(&h.timeouts) + h.server.ReadTimeout = h.timeouts.ReadTimeout + h.server.ReadHeaderTimeout = h.timeouts.ReadHeaderTimeout + h.server.WriteTimeout = h.timeouts.WriteTimeout + h.server.IdleTimeout = h.timeouts.IdleTimeout + } + + // Start the server. + listener, err := net.Listen("tcp", h.endpoint) + if err != nil { + // If the server fails to start, we need to clear out the RPC and WS + // configuration so they can be configured another time. + h.disableRPC() + h.disableWS() + return err + } + h.listener = listener + go h.server.Serve(listener) + + if h.wsAllowed() { + url := fmt.Sprintf("ws://%v", listener.Addr()) + if h.wsConfig.prefix != "" { + url += h.wsConfig.prefix + } + h.log.Info("WebSocket enabled", "url", url) + } + // if server is websocket only, return after logging + if !h.rpcAllowed() { + return nil + } + // Log http endpoint. + h.log.Info("HTTP server started", + "endpoint", listener.Addr(), "auth", (h.httpConfig.jwtSecret != nil), + "prefix", h.httpConfig.prefix, + "cors", strings.Join(h.httpConfig.CorsAllowedOrigins, ","), + "vhosts", strings.Join(h.httpConfig.Vhosts, ","), + ) + + // Log all handlers mounted on server. + var paths []string + for path := range h.handlerNames { + paths = append(paths, path) + } + sort.Strings(paths) + logged := make(map[string]bool, len(paths)) + for _, path := range paths { + name := h.handlerNames[path] + if !logged[name] { + log.Info(name+" enabled", "url", "http://"+listener.Addr().String()+path) + logged[name] = true + } + } + return nil +} + +func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // check if ws request and serve if ws enabled + ws := h.wsHandler.Load().(*rpcHandler) + if ws != nil && isWebsocket(r) { + if checkPath(r, h.wsConfig.prefix) { + ws.ServeHTTP(w, r) + } + return + } + + // if http-rpc is enabled, try to serve request + rpc := h.httpHandler.Load().(*rpcHandler) + if rpc != nil { + // First try to route in the mux. + // Requests to a path below root are handled by the mux, + // which has all the handlers registered via Node.RegisterHandler. + // These are made available when RPC is enabled. + muxHandler, pattern := h.mux.Handler(r) + if pattern != "" { + muxHandler.ServeHTTP(w, r) + return + } + + if checkPath(r, h.httpConfig.prefix) { + rpc.ServeHTTP(w, r) + return + } + } + w.WriteHeader(http.StatusNotFound) +} + +// checkPath checks whether a given request URL matches a given path prefix. +func checkPath(r *http.Request, path string) bool { + // if no prefix has been specified, request URL must be on root + if path == "" { + return r.URL.Path == "/" + } + // otherwise, check to make sure prefix matches + return len(r.URL.Path) >= len(path) && r.URL.Path[:len(path)] == path +} + +// validatePrefix checks if 'path' is a valid configuration value for the RPC prefix option. +func validatePrefix(what, path string) error { + if path == "" { + return nil + } + if path[0] != '/' { + return fmt.Errorf(`%s RPC path prefix %q does not contain leading "/"`, what, path) + } + if strings.ContainsAny(path, "?#") { + // This is just to avoid confusion. While these would match correctly (i.e. they'd + // match if URL-escaped into path), it's not easy to understand for users when + // setting that on the command line. + return fmt.Errorf("%s RPC path prefix %q contains URL meta-characters", what, path) + } + return nil +} + +// stop shuts down the HTTP server. +func (h *httpServer) stop() { + h.mu.Lock() + defer h.mu.Unlock() + h.doStop() +} + +func (h *httpServer) doStop() { + if h.listener == nil { + return // not running + } + + // Shut down the server. + httpHandler := h.httpHandler.Load().(*rpcHandler) + wsHandler := h.wsHandler.Load().(*rpcHandler) + if httpHandler != nil { + h.httpHandler.Store((*rpcHandler)(nil)) + httpHandler.server.Stop() + } + if wsHandler != nil { + h.wsHandler.Store((*rpcHandler)(nil)) + wsHandler.server.Stop() + } + + ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + defer cancel() + err := h.server.Shutdown(ctx) + if err != nil && err == ctx.Err() { + h.log.Warn("HTTP server graceful shutdown timed out") + h.server.Close() + } + + h.listener.Close() + h.log.Info("HTTP server stopped", "endpoint", h.listener.Addr()) + + // Clear out everything to allow re-configuring it later. + h.host, h.port, h.endpoint = "", 0, "" + h.server, h.listener = nil, nil +} + +// enableRPC turns on JSON-RPC over HTTP on the server. +func (h *httpServer) enableRPC(apis []rpc.API, config httpConfig) error { + h.mu.Lock() + defer h.mu.Unlock() + + if h.rpcAllowed() { + return errors.New("JSON-RPC over HTTP is already enabled") + } + + // Create RPC server and handler. + srv := rpc.NewServer() + srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit) + if config.httpBodyLimit > 0 { + srv.SetHTTPBodyLimit(config.httpBodyLimit) + } + if err := RegisterApis(apis, config.Modules, srv); err != nil { + return err + } + h.httpConfig = config + h.httpHandler.Store(&rpcHandler{ + Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts, config.jwtSecret, config.ExposedParam), + server: srv, + }) + return nil +} + +// disableRPC stops the HTTP RPC handler. This is internal, the caller must hold h.mu. +func (h *httpServer) disableRPC() bool { + handler := h.httpHandler.Load().(*rpcHandler) + if handler != nil { + h.httpHandler.Store((*rpcHandler)(nil)) + handler.server.Stop() + } + return handler != nil +} + +// enableWS turns on JSON-RPC over WebSocket on the server. +func (h *httpServer) enableWS(apis []rpc.API, config wsConfig) error { + h.mu.Lock() + defer h.mu.Unlock() + + if h.wsAllowed() { + return errors.New("JSON-RPC over WebSocket is already enabled") + } + // Create RPC server and handler. + srv := rpc.NewServer() + srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit) + if config.httpBodyLimit > 0 { + srv.SetHTTPBodyLimit(config.httpBodyLimit) + } + if err := RegisterApis(apis, config.Modules, srv); err != nil { + return err + } + h.wsConfig = config + h.wsHandler.Store(&rpcHandler{ + Handler: NewWSHandlerStack(srv.WebsocketHandler(config.Origins), config.jwtSecret, config.ExposedParam), + server: srv, + }) + return nil +} + +// stopWS disables JSON-RPC over WebSocket and also stops the server if it only serves WebSocket. +func (h *httpServer) stopWS() { + h.mu.Lock() + defer h.mu.Unlock() + + if h.disableWS() { + if !h.rpcAllowed() { + h.doStop() + } + } +} + +// disableWS disables the WebSocket handler. This is internal, the caller must hold h.mu. +func (h *httpServer) disableWS() bool { + ws := h.wsHandler.Load().(*rpcHandler) + if ws != nil { + h.wsHandler.Store((*rpcHandler)(nil)) + ws.server.Stop() + } + return ws != nil +} + +// rpcAllowed returns true when JSON-RPC over HTTP is enabled. +func (h *httpServer) rpcAllowed() bool { + return h.httpHandler.Load().(*rpcHandler) != nil +} + +// wsAllowed returns true when JSON-RPC over WebSocket is enabled. +func (h *httpServer) wsAllowed() bool { + return h.wsHandler.Load().(*rpcHandler) != nil +} + +// isWebsocket checks the header of an http request for a websocket upgrade request. +func isWebsocket(r *http.Request) bool { + return strings.EqualFold(r.Header.Get("Upgrade"), "websocket") && + strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade") +} + +// NewHTTPHandlerStack returns wrapped http-related handlers +func NewHTTPHandlerStack(srv http.Handler, cors []string, vhosts []string, jwtSecret []byte, exposedParam string) http.Handler { + // Wrap the CORS-handler within a host-handler + handler := newCorsHandler(srv, cors) + handler = newHTTPParamsHandler(exposedParam, handler) + handler = newVHostHandler(vhosts, handler) + if len(jwtSecret) != 0 { + handler = newJWTHandler(jwtSecret, handler) + } + return newGzipHandler(handler) +} + +// NewWSHandlerStack returns a wrapped ws-related handler. +func NewWSHandlerStack(srv http.Handler, jwtSecret []byte, exposedParam string) http.Handler { + handler := srv + handler = newHTTPParamsHandler(exposedParam, handler) + if len(jwtSecret) != 0 { + handler = newJWTHandler(jwtSecret, handler) + } + return handler +} + +func newCorsHandler(srv http.Handler, allowedOrigins []string) http.Handler { + // disable CORS support if user has not specified a custom CORS configuration + if len(allowedOrigins) == 0 { + return srv + } + c := cors.New(cors.Options{ + AllowedOrigins: allowedOrigins, + AllowedMethods: []string{http.MethodPost, http.MethodGet}, + AllowedHeaders: []string{"*"}, + MaxAge: 600, + }) + return c.Handler(srv) +} + +// virtualHostHandler is a handler which validates the Host-header of incoming requests. +// Using virtual hosts can help prevent DNS rebinding attacks, where a 'random' domain name points to +// the service ip address (but without CORS headers). By verifying the targeted virtual host, we can +// ensure that it's a destination that the node operator has defined. +type virtualHostHandler struct { + vhosts map[string]struct{} + next http.Handler +} + +func newVHostHandler(vhosts []string, next http.Handler) http.Handler { + vhostMap := make(map[string]struct{}) + for _, allowedHost := range vhosts { + vhostMap[strings.ToLower(allowedHost)] = struct{}{} + } + return &virtualHostHandler{vhostMap, next} +} + +// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler +func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // if r.Host is not set, we can continue serving since a browser would set the Host header + if r.Host == "" { + h.next.ServeHTTP(w, r) + return + } + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + // Either invalid (too many colons) or no port specified + host = r.Host + } + if ipAddr := net.ParseIP(host); ipAddr != nil { + // It's an IP address, we can serve that + h.next.ServeHTTP(w, r) + return + } + // Not an IP address, but a hostname. Need to validate + if _, exist := h.vhosts["*"]; exist { + h.next.ServeHTTP(w, r) + return + } + if _, exist := h.vhosts[host]; exist { + h.next.ServeHTTP(w, r) + return + } + http.Error(w, "invalid host specified", http.StatusForbidden) +} + +var gzPool = sync.Pool{ + New: func() interface{} { + w := gzip.NewWriter(io.Discard) + return w + }, +} + +type gzipResponseWriter struct { + resp http.ResponseWriter + + gz *gzip.Writer + contentLength uint64 // total length of the uncompressed response + written uint64 // amount of written bytes from the uncompressed response + hasLength bool // true if uncompressed response had Content-Length + inited bool // true after init was called for the first time +} + +// init runs just before response headers are written. Among other things, this function +// also decides whether compression will be applied at all. +func (w *gzipResponseWriter) init() { + if w.inited { + return + } + w.inited = true + + hdr := w.resp.Header() + length := hdr.Get("content-length") + if len(length) > 0 { + if n, err := strconv.ParseUint(length, 10, 64); err != nil { + w.hasLength = true + w.contentLength = n + } + } + + // Setting Transfer-Encoding to "identity" explicitly disables compression. net/http + // also recognizes this header value and uses it to disable "chunked" transfer + // encoding, trimming the header from the response. This means downstream handlers can + // set this without harm, even if they aren't wrapped by newGzipHandler. + // + // In go-ethereum, we use this signal to disable compression for certain error + // responses which are flushed out close to the write deadline of the response. For + // these cases, we want to avoid chunked transfer encoding and compression because + // they require additional output that may not get written in time. + passthrough := hdr.Get("transfer-encoding") == "identity" + if !passthrough { + w.gz = gzPool.Get().(*gzip.Writer) + w.gz.Reset(w.resp) + hdr.Del("content-length") + hdr.Set("content-encoding", "gzip") + } +} + +func (w *gzipResponseWriter) Header() http.Header { + return w.resp.Header() +} + +func (w *gzipResponseWriter) WriteHeader(status int) { + w.init() + w.resp.WriteHeader(status) +} + +func (w *gzipResponseWriter) Write(b []byte) (int, error) { + w.init() + + if w.gz == nil { + // Compression is disabled. + return w.resp.Write(b) + } + + n, err := w.gz.Write(b) + w.written += uint64(n) + if w.hasLength && w.written >= w.contentLength { + // The HTTP handler has finished writing the entire uncompressed response. Close + // the gzip stream to ensure the footer will be seen by the client in case the + // response is flushed after this call to write. + err = w.gz.Close() + } + return n, err +} + +func (w *gzipResponseWriter) Flush() { + if w.gz != nil { + w.gz.Flush() + } + if f, ok := w.resp.(http.Flusher); ok { + f.Flush() + } +} + +func (w *gzipResponseWriter) close() { + if w.gz == nil { + return + } + w.gz.Close() + gzPool.Put(w.gz) + w.gz = nil +} + +func newGzipHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + next.ServeHTTP(w, r) + return + } + + wrapper := &gzipResponseWriter{resp: w} + defer wrapper.close() + + next.ServeHTTP(wrapper, r) + }) +} + +type ipcServer struct { + log log.Logger + endpoint string + + mu sync.Mutex + listener net.Listener + srv *rpc.Server +} + +func newIPCServer(log log.Logger, endpoint string) *ipcServer { + return &ipcServer{log: log, endpoint: endpoint} +} + +// Start starts the httpServer's http.Server +func (is *ipcServer) start(apis []rpc.API) error { + is.mu.Lock() + defer is.mu.Unlock() + + if is.listener != nil { + return nil // already running + } + listener, srv, err := rpc.StartIPCEndpoint(is.endpoint, apis) + if err != nil { + is.log.Warn("IPC opening failed", "url", is.endpoint, "error", err) + return err + } + is.log.Info("IPC endpoint opened", "url", is.endpoint) + is.listener, is.srv = listener, srv + return nil +} + +func (is *ipcServer) stop() error { + is.mu.Lock() + defer is.mu.Unlock() + + if is.listener == nil { + return nil // not running + } + err := is.listener.Close() + is.srv.Stop() + is.listener, is.srv = nil, nil + is.log.Info("IPC endpoint closed", "url", is.endpoint) + return err +} + +// RegisterApis checks the given modules' availability, generates an allowlist based on the allowed modules, +// and then registers all of the APIs exposed by the services. +func RegisterApis(apis []rpc.API, modules []string, srv *rpc.Server) error { + if bad, available := checkModuleAvailability(modules, apis); len(bad) > 0 { + log.Error("Unavailable modules in HTTP API list", "unavailable", bad, "available", available) + } + // Generate the allow list based on the allowed modules + allowList := make(map[string]bool) + for _, module := range modules { + allowList[module] = true + } + // Register all the APIs exposed by the services + for _, api := range apis { + if allowList[api.Namespace] || len(allowList) == 0 { + if err := srv.RegisterName(api.Namespace, api.Service); err != nil { + return err + } + } + } + return nil +} diff --git a/lib/gethfork/rpc/client.go b/lib/gethfork/rpc/client.go new file mode 100644 index 0000000000..de7fd5396a --- /dev/null +++ b/lib/gethfork/rpc/client.go @@ -0,0 +1,727 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "os" + "reflect" + "strconv" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/log" +) + +var ( + ErrBadResult = errors.New("bad result in JSON-RPC response") + ErrClientQuit = errors.New("client is closed") + ErrNoResult = errors.New("JSON-RPC response has no result") + ErrMissingBatchResponse = errors.New("response batch did not contain a response to this call") + ErrSubscriptionQueueOverflow = errors.New("subscription queue overflow") + errClientReconnected = errors.New("client reconnected") + errDead = errors.New("connection lost") +) + +// Timeouts +const ( + defaultDialTimeout = 10 * time.Second // used if context has no deadline + subscribeTimeout = 10 * time.Second // overall timeout eth_subscribe, rpc_modules calls +) + +const ( + // Subscriptions are removed when the subscriber cannot keep up. + // + // This can be worked around by supplying a channel with sufficiently sized buffer, + // but this can be inconvenient and hard to explain in the docs. Another issue with + // buffered channels is that the buffer is static even though it might not be needed + // most of the time. + // + // The approach taken here is to maintain a per-subscription linked list buffer + // shrinks on demand. If the buffer reaches the size below, the subscription is + // dropped. + maxClientSubscriptionBuffer = 20000 +) + +// BatchElem is an element in a batch request. +type BatchElem struct { + Method string + Args []interface{} + // The result is unmarshaled into this field. Result must be set to a + // non-nil pointer value of the desired type, otherwise the response will be + // discarded. + Result interface{} + // Error is set if the server returns an error for this request, or if + // unmarshalling into Result fails. It is not set for I/O errors. + Error error +} + +// Client represents a connection to an RPC server. +type Client struct { + UserID string + idgen func() ID // for subscriptions + isHTTP bool // connection type: http, ws or ipc + services *serviceRegistry + + idCounter atomic.Uint32 + + // This function, if non-nil, is called when the connection is lost. + reconnectFunc reconnectFunc + + // config fields + batchItemLimit int + batchResponseMaxSize int + + // writeConn is used for writing to the connection on the caller's goroutine. It should + // only be accessed outside of dispatch, with the write lock held. The write lock is + // taken by sending on reqInit and released by sending on reqSent. + writeConn jsonWriter + + // for dispatch + close chan struct{} + closing chan struct{} // closed when client is quitting + didClose chan struct{} // closed when client quits + reconnected chan ServerCodec // where write/reconnect sends the new connection + readOp chan readOp // read messages + readErr chan error // errors from read + reqInit chan *requestOp // register response IDs, takes write lock + reqSent chan error // signals write completion, releases write lock + reqTimeout chan *requestOp // removes response IDs when call timeout expires +} + +type reconnectFunc func(context.Context) (ServerCodec, error) + +type clientContextKey struct{} + +type clientConn struct { + codec ServerCodec + handler *handler +} + +func (c *Client) newClientConn(conn ServerCodec) *clientConn { + ctx := context.Background() + ctx = context.WithValue(ctx, clientContextKey{}, c) + ctx = context.WithValue(ctx, peerInfoContextKey{}, conn.peerInfo()) + handler := newHandler(ctx, conn, c.idgen, c.services, c.batchItemLimit, c.batchResponseMaxSize, c.UserID) + return &clientConn{conn, handler} +} + +func (cc *clientConn) close(err error, inflightReq *requestOp) { + cc.handler.close(err, inflightReq) + cc.codec.close() +} + +type readOp struct { + msgs []*jsonrpcMessage + batch bool +} + +// requestOp represents a pending request. This is used for both batch and non-batch +// requests. +type requestOp struct { + ids []json.RawMessage + err error + resp chan []*jsonrpcMessage // the response goes here + sub *ClientSubscription // set for Subscribe requests. + hadResponse bool // true when the request was responded to +} + +func (op *requestOp) wait(ctx context.Context, c *Client) ([]*jsonrpcMessage, error) { + select { + case <-ctx.Done(): + // Send the timeout to dispatch so it can remove the request IDs. + if !c.isHTTP { + select { + case c.reqTimeout <- op: + case <-c.closing: + } + } + return nil, ctx.Err() + case resp := <-op.resp: + return resp, op.err + } +} + +// Dial creates a new client for the given URL. +// +// The currently supported URL schemes are "http", "https", "ws" and "wss". If rawurl is a +// file name with no URL scheme, a local socket connection is established using UNIX +// domain sockets on supported platforms and named pipes on Windows. +// +// If you want to further configure the transport, use DialOptions instead of this +// function. +// +// For websocket connections, the origin is set to the local host name. +// +// The client reconnects automatically when the connection is lost. +func Dial(rawurl string) (*Client, error) { + return DialOptions(context.Background(), rawurl) +} + +// DialContext creates a new RPC client, just like Dial. +// +// The context is used to cancel or time out the initial connection establishment. It does +// not affect subsequent interactions with the client. +func DialContext(ctx context.Context, rawurl string) (*Client, error) { + return DialOptions(ctx, rawurl) +} + +// DialOptions creates a new RPC client for the given URL. You can supply any of the +// pre-defined client options to configure the underlying transport. +// +// The context is used to cancel or time out the initial connection establishment. It does +// not affect subsequent interactions with the client. +// +// The client reconnects automatically when the connection is lost. +func DialOptions(ctx context.Context, rawurl string, options ...ClientOption) (*Client, error) { + u, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + + cfg := new(clientConfig) + for _, opt := range options { + opt.applyOption(cfg) + } + + var reconnect reconnectFunc + switch u.Scheme { + case "http", "https": + reconnect = newClientTransportHTTP(rawurl, cfg) + case "ws", "wss": + rc, err := newClientTransportWS(rawurl, cfg) + if err != nil { + return nil, err + } + reconnect = rc + case "stdio": + reconnect = newClientTransportIO(os.Stdin, os.Stdout) + case "": + reconnect = newClientTransportIPC(rawurl) + default: + return nil, fmt.Errorf("no known transport for URL scheme %q", u.Scheme) + } + + return newClient(ctx, cfg, reconnect) +} + +// ClientFromContext retrieves the client from the context, if any. This can be used to perform +// 'reverse calls' in a handler method. +func ClientFromContext(ctx context.Context) (*Client, bool) { + client, ok := ctx.Value(clientContextKey{}).(*Client) + return client, ok +} + +func newClient(initctx context.Context, cfg *clientConfig, connect reconnectFunc) (*Client, error) { + conn, err := connect(initctx) + if err != nil { + return nil, err + } + c := initClient(conn, new(serviceRegistry), cfg) + c.reconnectFunc = connect + return c, nil +} + +func initClient(conn ServerCodec, services *serviceRegistry, cfg *clientConfig) *Client { + _, isHTTP := conn.(*httpConn) + c := &Client{ + UserID: cfg.UserID, + isHTTP: isHTTP, + services: services, + idgen: cfg.idgen, + batchItemLimit: cfg.batchItemLimit, + batchResponseMaxSize: cfg.batchResponseLimit, + writeConn: conn, + close: make(chan struct{}), + closing: make(chan struct{}), + didClose: make(chan struct{}), + reconnected: make(chan ServerCodec), + readOp: make(chan readOp), + readErr: make(chan error), + reqInit: make(chan *requestOp), + reqSent: make(chan error, 1), + reqTimeout: make(chan *requestOp), + } + + // Set defaults. + if c.idgen == nil { + c.idgen = randomIDGenerator() + } + + // Launch the main loop. + if !isHTTP { + go c.dispatch(conn) + } + return c +} + +// RegisterName creates a service for the given receiver type under the given name. When no +// methods on the given receiver match the criteria to be either a RPC method or a +// subscription an error is returned. Otherwise a new service is created and added to the +// service collection this client provides to the server. +func (c *Client) RegisterName(name string, receiver interface{}) error { + return c.services.registerName(name, receiver) +} + +func (c *Client) nextID() json.RawMessage { + id := c.idCounter.Add(1) + return strconv.AppendUint(nil, uint64(id), 10) +} + +// SupportedModules calls the rpc_modules method, retrieving the list of +// APIs that are available on the server. +func (c *Client) SupportedModules() (map[string]string, error) { + var result map[string]string + ctx, cancel := context.WithTimeout(context.Background(), subscribeTimeout) + defer cancel() + err := c.CallContext(ctx, &result, "rpc_modules") + return result, err +} + +// Close closes the client, aborting any in-flight requests. +func (c *Client) Close() { + if c.isHTTP { + return + } + select { + case c.close <- struct{}{}: + <-c.didClose + case <-c.didClose: + } +} + +// SetHeader adds a custom HTTP header to the client's requests. +// This method only works for clients using HTTP, it doesn't have +// any effect for clients using another transport. +func (c *Client) SetHeader(key, value string) { + if !c.isHTTP { + return + } + conn := c.writeConn.(*httpConn) + conn.mu.Lock() + conn.headers.Set(key, value) + conn.mu.Unlock() +} + +// Call performs a JSON-RPC call with the given arguments and unmarshals into +// result if no error occurred. +// +// The result must be a pointer so that package json can unmarshal into it. You +// can also pass nil, in which case the result is ignored. +func (c *Client) Call(result interface{}, method string, args ...interface{}) error { + ctx := context.Background() + return c.CallContext(ctx, result, method, args...) +} + +// CallContext performs a JSON-RPC call with the given arguments. If the context is +// canceled before the call has successfully returned, CallContext returns immediately. +// +// The result must be a pointer so that package json can unmarshal into it. You +// can also pass nil, in which case the result is ignored. +func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { + if result != nil && reflect.TypeOf(result).Kind() != reflect.Ptr { + return fmt.Errorf("call result parameter must be pointer or nil interface: %v", result) + } + msg, err := c.newMessage(method, args...) + if err != nil { + return err + } + op := &requestOp{ + ids: []json.RawMessage{msg.ID}, + resp: make(chan []*jsonrpcMessage, 1), + } + + if c.isHTTP { + err = c.sendHTTP(ctx, op, msg) + } else { + err = c.send(ctx, op, msg) + } + if err != nil { + return err + } + + // dispatch has accepted the request and will close the channel when it quits. + batchresp, err := op.wait(ctx, c) + if err != nil { + return err + } + resp := batchresp[0] + switch { + case resp.Error != nil: + return resp.Error + case len(resp.Result) == 0: + return ErrNoResult + default: + if result == nil { + return nil + } + return json.Unmarshal(resp.Result, result) + } +} + +// BatchCall sends all given requests as a single batch and waits for the server +// to return a response for all of them. +// +// In contrast to Call, BatchCall only returns I/O errors. Any error specific to +// a request is reported through the Error field of the corresponding BatchElem. +// +// Note that batch calls may not be executed atomically on the server side. +func (c *Client) BatchCall(b []BatchElem) error { + ctx := context.Background() + return c.BatchCallContext(ctx, b) +} + +// BatchCallContext sends all given requests as a single batch and waits for the server +// to return a response for all of them. The wait duration is bounded by the +// context's deadline. +// +// In contrast to CallContext, BatchCallContext only returns errors that have occurred +// while sending the request. Any error specific to a request is reported through the +// Error field of the corresponding BatchElem. +// +// Note that batch calls may not be executed atomically on the server side. +func (c *Client) BatchCallContext(ctx context.Context, b []BatchElem) error { + var ( + msgs = make([]*jsonrpcMessage, len(b)) + byID = make(map[string]int, len(b)) + ) + op := &requestOp{ + ids: make([]json.RawMessage, len(b)), + resp: make(chan []*jsonrpcMessage, 1), + } + for i, elem := range b { + msg, err := c.newMessage(elem.Method, elem.Args...) + if err != nil { + return err + } + msgs[i] = msg + op.ids[i] = msg.ID + byID[string(msg.ID)] = i + } + + var err error + if c.isHTTP { + err = c.sendBatchHTTP(ctx, op, msgs) + } else { + err = c.send(ctx, op, msgs) + } + if err != nil { + return err + } + + batchresp, err := op.wait(ctx, c) + if err != nil { + return err + } + + // Wait for all responses to come back. + for n := 0; n < len(batchresp) && err == nil; n++ { + resp := batchresp[n] + if resp == nil { + // Ignore null responses. These can happen for batches sent via HTTP. + continue + } + + // Find the element corresponding to this response. + index, ok := byID[string(resp.ID)] + if !ok { + continue + } + delete(byID, string(resp.ID)) + + // Assign result and error. + elem := &b[index] + switch { + case resp.Error != nil: + elem.Error = resp.Error + case resp.Result == nil: + elem.Error = ErrNoResult + default: + elem.Error = json.Unmarshal(resp.Result, elem.Result) + } + } + + // Check that all expected responses have been received. + for _, index := range byID { + elem := &b[index] + elem.Error = ErrMissingBatchResponse + } + + return err +} + +// Notify sends a notification, i.e. a method call that doesn't expect a response. +func (c *Client) Notify(ctx context.Context, method string, args ...interface{}) error { + op := new(requestOp) + msg, err := c.newMessage(method, args...) + if err != nil { + return err + } + msg.ID = nil + + if c.isHTTP { + return c.sendHTTP(ctx, op, msg) + } + return c.send(ctx, op, msg) +} + +// EthSubscribe registers a subscription under the "eth" namespace. +func (c *Client) EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (*ClientSubscription, error) { + return c.Subscribe(ctx, "eth", channel, args...) +} + +// ShhSubscribe registers a subscription under the "shh" namespace. +// Deprecated: use Subscribe(ctx, "shh", ...). +func (c *Client) ShhSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (*ClientSubscription, error) { + return c.Subscribe(ctx, "shh", channel, args...) +} + +// Subscribe calls the "_subscribe" method with the given arguments, +// registering a subscription. Server notifications for the subscription are +// sent to the given channel. The element type of the channel must match the +// expected type of content returned by the subscription. +// +// The context argument cancels the RPC request that sets up the subscription but has no +// effect on the subscription after Subscribe has returned. +// +// Slow subscribers will be dropped eventually. Client buffers up to 20000 notifications +// before considering the subscriber dead. The subscription Err channel will receive +// ErrSubscriptionQueueOverflow. Use a sufficiently large buffer on the channel or ensure +// that the channel usually has at least one reader to prevent this issue. +func (c *Client) Subscribe(ctx context.Context, namespace string, channel interface{}, args ...interface{}) (*ClientSubscription, error) { + // Check type of channel first. + chanVal := reflect.ValueOf(channel) + if chanVal.Kind() != reflect.Chan || chanVal.Type().ChanDir()&reflect.SendDir == 0 { + panic(fmt.Sprintf("channel argument of Subscribe has type %T, need writable channel", channel)) + } + if chanVal.IsNil() { + panic("channel given to Subscribe must not be nil") + } + if c.isHTTP { + return nil, ErrNotificationsUnsupported + } + + msg, err := c.newMessage(namespace+subscribeMethodSuffix, args...) + if err != nil { + return nil, err + } + op := &requestOp{ + ids: []json.RawMessage{msg.ID}, + resp: make(chan []*jsonrpcMessage, 1), + sub: newClientSubscription(c, namespace, chanVal), + } + + // Send the subscription request. + // The arrival and validity of the response is signaled on sub.quit. + if err := c.send(ctx, op, msg); err != nil { + return nil, err + } + if _, err := op.wait(ctx, c); err != nil { + return nil, err + } + return op.sub, nil +} + +// SupportsSubscriptions reports whether subscriptions are supported by the client +// transport. When this returns false, Subscribe and related methods will return +// ErrNotificationsUnsupported. +func (c *Client) SupportsSubscriptions() bool { + return !c.isHTTP +} + +func (c *Client) newMessage(method string, paramsIn ...interface{}) (*jsonrpcMessage, error) { + msg := &jsonrpcMessage{Version: vsn, ID: c.nextID(), Method: method} + if paramsIn != nil { // prevent sending "params":null + var err error + if msg.Params, err = json.Marshal(paramsIn); err != nil { + return nil, err + } + } + return msg, nil +} + +// send registers op with the dispatch loop, then sends msg on the connection. +// if sending fails, op is deregistered. +func (c *Client) send(ctx context.Context, op *requestOp, msg interface{}) error { + select { + case c.reqInit <- op: + err := c.write(ctx, msg, false) + c.reqSent <- err + return err + case <-ctx.Done(): + // This can happen if the client is overloaded or unable to keep up with + // subscription notifications. + return ctx.Err() + case <-c.closing: + return ErrClientQuit + } +} + +func (c *Client) write(ctx context.Context, msg interface{}, retry bool) error { + if c.writeConn == nil { + // The previous write failed. Try to establish a new connection. + if err := c.reconnect(ctx); err != nil { + return err + } + } + err := c.writeConn.writeJSON(ctx, msg, false) + if err != nil { + c.writeConn = nil + if !retry { + return c.write(ctx, msg, true) + } + } + return err +} + +func (c *Client) reconnect(ctx context.Context) error { + if c.reconnectFunc == nil { + return errDead + } + + if _, ok := ctx.Deadline(); !ok { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, defaultDialTimeout) + defer cancel() + } + newconn, err := c.reconnectFunc(ctx) + if err != nil { + log.Trace("RPC client reconnect failed", "err", err) + return err + } + select { + case c.reconnected <- newconn: + c.writeConn = newconn + return nil + case <-c.didClose: + newconn.close() + return ErrClientQuit + } +} + +// dispatch is the main loop of the client. +// It sends read messages to waiting calls to Call and BatchCall +// and subscription notifications to registered subscriptions. +func (c *Client) dispatch(codec ServerCodec) { + var ( + lastOp *requestOp // tracks last send operation + reqInitLock = c.reqInit // nil while the send lock is held + conn = c.newClientConn(codec) + reading = true + ) + defer func() { + close(c.closing) + if reading { + conn.close(ErrClientQuit, nil) + c.drainRead() + } + close(c.didClose) + }() + + // Spawn the initial read loop. + go c.read(codec) + + for { + select { + case <-c.close: + return + + // Read path: + case op := <-c.readOp: + if op.batch { + conn.handler.handleBatch(op.msgs) + } else { + conn.handler.handleMsg(op.msgs[0]) + } + + case err := <-c.readErr: + conn.handler.log.Debug("RPC connection read error", "err", err) + conn.close(err, lastOp) + reading = false + + // Reconnect: + case newcodec := <-c.reconnected: + log.Debug("RPC client reconnected", "reading", reading, "conn", newcodec.remoteAddr()) + if reading { + // Wait for the previous read loop to exit. This is a rare case which + // happens if this loop isn't notified in time after the connection breaks. + // In those cases the caller will notice first and reconnect. Closing the + // handler terminates all waiting requests (closing op.resp) except for + // lastOp, which will be transferred to the new handler. + conn.close(errClientReconnected, lastOp) + c.drainRead() + } + go c.read(newcodec) + reading = true + conn = c.newClientConn(newcodec) + // Re-register the in-flight request on the new handler + // because that's where it will be sent. + conn.handler.addRequestOp(lastOp) + + // Send path: + case op := <-reqInitLock: + // Stop listening for further requests until the current one has been sent. + reqInitLock = nil + lastOp = op + conn.handler.addRequestOp(op) + + case err := <-c.reqSent: + if err != nil { + // Remove response handlers for the last send. When the read loop + // goes down, it will signal all other current operations. + conn.handler.removeRequestOp(lastOp) + } + // Let the next request in. + reqInitLock = c.reqInit + lastOp = nil + + case op := <-c.reqTimeout: + conn.handler.removeRequestOp(op) + } + } +} + +// drainRead drops read messages until an error occurs. +func (c *Client) drainRead() { + for { + select { + case <-c.readOp: + case <-c.readErr: + return + } + } +} + +// read decodes RPC messages from a codec, feeding them into dispatch. +func (c *Client) read(codec ServerCodec) { + for { + msgs, batch, err := codec.readBatch() + if _, ok := err.(*json.SyntaxError); ok { + msg := errorMessage(&parseError{err.Error()}) + codec.writeJSON(context.Background(), msg, true) + } + if err != nil { + c.readErr <- err + return + } + c.readOp <- readOp{msgs, batch} + } +} diff --git a/lib/gethfork/rpc/client_opt.go b/lib/gethfork/rpc/client_opt.go new file mode 100644 index 0000000000..0eae2e6134 --- /dev/null +++ b/lib/gethfork/rpc/client_opt.go @@ -0,0 +1,145 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "net/http" + + "github.com/gorilla/websocket" +) + +// ClientOption is a configuration option for the RPC client. +type ClientOption interface { + applyOption(*clientConfig) +} + +type clientConfig struct { + UserID string + // HTTP settings + httpClient *http.Client + httpHeaders http.Header + httpAuth HTTPAuth + + // WebSocket options + wsDialer *websocket.Dialer + wsMessageSizeLimit *int64 // wsMessageSizeLimit nil = default, 0 = no limit + + // RPC handler options + idgen func() ID + batchItemLimit int + batchResponseLimit int +} + +func (cfg *clientConfig) initHeaders() { + if cfg.httpHeaders == nil { + cfg.httpHeaders = make(http.Header) + } +} + +func (cfg *clientConfig) setHeader(key, value string) { + cfg.initHeaders() + cfg.httpHeaders.Set(key, value) +} + +type optionFunc func(*clientConfig) + +func (fn optionFunc) applyOption(opt *clientConfig) { + fn(opt) +} + +// WithWebsocketDialer configures the websocket.Dialer used by the RPC client. +func WithWebsocketDialer(dialer websocket.Dialer) ClientOption { + return optionFunc(func(cfg *clientConfig) { + cfg.wsDialer = &dialer + }) +} + +// WithWebsocketMessageSizeLimit configures the websocket message size limit used by the RPC +// client. Passing a limit of 0 means no limit. +func WithWebsocketMessageSizeLimit(messageSizeLimit int64) ClientOption { + return optionFunc(func(cfg *clientConfig) { + cfg.wsMessageSizeLimit = &messageSizeLimit + }) +} + +// WithHeader configures HTTP headers set by the RPC client. Headers set using this option +// will be used for both HTTP and WebSocket connections. +func WithHeader(key, value string) ClientOption { + return optionFunc(func(cfg *clientConfig) { + cfg.initHeaders() + cfg.httpHeaders.Set(key, value) + }) +} + +// WithHeaders configures HTTP headers set by the RPC client. Headers set using this +// option will be used for both HTTP and WebSocket connections. +func WithHeaders(headers http.Header) ClientOption { + return optionFunc(func(cfg *clientConfig) { + cfg.initHeaders() + for k, vs := range headers { + cfg.httpHeaders[k] = vs + } + }) +} + +// WithHTTPClient configures the http.Client used by the RPC client. +func WithHTTPClient(c *http.Client) ClientOption { + return optionFunc(func(cfg *clientConfig) { + cfg.httpClient = c + }) +} + +// WithHTTPAuth configures HTTP request authentication. The given provider will be called +// whenever a request is made. Note that only one authentication provider can be active at +// any time. +func WithHTTPAuth(a HTTPAuth) ClientOption { + if a == nil { + panic("nil auth") + } + return optionFunc(func(cfg *clientConfig) { + cfg.httpAuth = a + }) +} + +// A HTTPAuth function is called by the client whenever a HTTP request is sent. +// The function must be safe for concurrent use. +// +// Usually, HTTPAuth functions will call h.Set("authorization", "...") to add +// auth information to the request. +type HTTPAuth func(h http.Header) error + +// WithBatchItemLimit changes the maximum number of items allowed in batch requests. +// +// Note: this option applies when processing incoming batch requests. It does not affect +// batch requests sent by the client. +func WithBatchItemLimit(limit int) ClientOption { + return optionFunc(func(cfg *clientConfig) { + cfg.batchItemLimit = limit + }) +} + +// WithBatchResponseSizeLimit changes the maximum number of response bytes that can be +// generated for batch requests. When this limit is reached, further calls in the batch +// will not be processed. +// +// Note: this option applies when processing incoming batch requests. It does not affect +// batch requests sent by the client. +func WithBatchResponseSizeLimit(sizeLimit int) ClientOption { + return optionFunc(func(cfg *clientConfig) { + cfg.batchResponseLimit = sizeLimit + }) +} diff --git a/lib/gethfork/rpc/context_headers.go b/lib/gethfork/rpc/context_headers.go new file mode 100644 index 0000000000..29a58150e3 --- /dev/null +++ b/lib/gethfork/rpc/context_headers.go @@ -0,0 +1,56 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "net/http" +) + +type mdHeaderKey struct{} + +// NewContextWithHeaders wraps the given context, adding HTTP headers. These headers will +// be applied by Client when making a request using the returned context. +func NewContextWithHeaders(ctx context.Context, h http.Header) context.Context { + if len(h) == 0 { + // This check ensures the header map set in context will never be nil. + return ctx + } + + var ctxh http.Header + prev, ok := ctx.Value(mdHeaderKey{}).(http.Header) + if ok { + ctxh = setHeaders(prev.Clone(), h) + } else { + ctxh = h.Clone() + } + return context.WithValue(ctx, mdHeaderKey{}, ctxh) +} + +// headersFromContext is used to extract http.Header from context. +func headersFromContext(ctx context.Context) http.Header { + source, _ := ctx.Value(mdHeaderKey{}).(http.Header) + return source +} + +// setHeaders sets all headers from src in dst. +func setHeaders(dst http.Header, src http.Header) http.Header { + for key, values := range src { + dst[http.CanonicalHeaderKey(key)] = values + } + return dst +} diff --git a/lib/gethfork/rpc/doc.go b/lib/gethfork/rpc/doc.go new file mode 100644 index 0000000000..7c87793dca --- /dev/null +++ b/lib/gethfork/rpc/doc.go @@ -0,0 +1,109 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +/* +Package rpc implements bi-directional JSON-RPC 2.0 on multiple transports. + +It provides access to the exported methods of an object across a network or other I/O +connection. After creating a server or client instance, objects can be registered to make +them visible as 'services'. Exported methods that follow specific conventions can be +called remotely. It also has support for the publish/subscribe pattern. + +# RPC Methods + +Methods that satisfy the following criteria are made available for remote access: + + - method must be exported + - method returns 0, 1 (response or error) or 2 (response and error) values + +An example method: + + func (s *CalcService) Add(a, b int) (int, error) + +When the returned error isn't nil the returned integer is ignored and the error is sent +back to the client. Otherwise the returned integer is sent back to the client. + +Optional arguments are supported by accepting pointer values as arguments. E.g. if we want +to do the addition in an optional finite field we can accept a mod argument as pointer +value. + + func (s *CalcService) Add(a, b int, mod *int) (int, error) + +This RPC method can be called with 2 integers and a null value as third argument. In that +case the mod argument will be nil. Or it can be called with 3 integers, in that case mod +will be pointing to the given third argument. Since the optional argument is the last +argument the RPC package will also accept 2 integers as arguments. It will pass the mod +argument as nil to the RPC method. + +The server offers the ServeCodec method which accepts a ServerCodec instance. It will read +requests from the codec, process the request and sends the response back to the client +using the codec. The server can execute requests concurrently. Responses can be sent back +to the client out of order. + +An example server which uses the JSON codec: + + type CalculatorService struct {} + + func (s *CalculatorService) Add(a, b int) int { + return a + b + } + + func (s *CalculatorService) Div(a, b int) (int, error) { + if b == 0 { + return 0, errors.New("divide by zero") + } + return a/b, nil + } + + calculator := new(CalculatorService) + server := NewServer() + server.RegisterName("calculator", calculator) + l, _ := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: "/tmp/calculator.sock"}) + server.ServeListener(l) + +# Subscriptions + +The package also supports the publish subscribe pattern through the use of subscriptions. +A method that is considered eligible for notifications must satisfy the following +criteria: + + - method must be exported + - first method argument type must be context.Context + - method must have return types (rpc.Subscription, error) + +An example method: + + func (s *BlockChainService) NewBlocks(ctx context.Context) (rpc.Subscription, error) { + ... + } + +When the service containing the subscription method is registered to the server, for +example under the "blockchain" namespace, a subscription is created by calling the +"blockchain_subscribe" method. + +Subscriptions are deleted when the user sends an unsubscribe request or when the +connection which was used to create the subscription is closed. This can be initiated by +the client and server. The server will close the connection for any write error. + +For more information about subscriptions, see https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB. + +# Reverse Calls + +In any method handler, an instance of rpc.Client can be accessed through the +ClientFromContext method. Using this client instance, server-to-client method calls can be +performed on the RPC connection. +*/ +package rpc diff --git a/lib/gethfork/rpc/endpoints.go b/lib/gethfork/rpc/endpoints.go new file mode 100644 index 0000000000..69ea3e99d4 --- /dev/null +++ b/lib/gethfork/rpc/endpoints.go @@ -0,0 +1,52 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "net" + "strings" + + "github.com/ethereum/go-ethereum/log" +) + +// StartIPCEndpoint starts an IPC endpoint. +func StartIPCEndpoint(ipcEndpoint string, apis []API) (net.Listener, *Server, error) { + // Register all the APIs exposed by the services. + var ( + handler = NewServer() + regMap = make(map[string]struct{}) + registered []string + ) + for _, api := range apis { + if err := handler.RegisterName(api.Namespace, api.Service); err != nil { + log.Info("IPC registration failed", "namespace", api.Namespace, "error", err) + return nil, nil, err + } + if _, ok := regMap[api.Namespace]; !ok { + registered = append(registered, api.Namespace) + regMap[api.Namespace] = struct{}{} + } + } + log.Debug("IPCs registered", "namespaces", strings.Join(registered, ",")) + // All APIs registered, start the IPC listener. + listener, err := ipcListen(ipcEndpoint) + if err != nil { + return nil, nil, err + } + go handler.ServeListener(listener) //nolint:errcheck + return listener, handler, nil +} diff --git a/lib/gethfork/rpc/errors.go b/lib/gethfork/rpc/errors.go new file mode 100644 index 0000000000..438aff218c --- /dev/null +++ b/lib/gethfork/rpc/errors.go @@ -0,0 +1,156 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import "fmt" + +// HTTPError is returned by client operations when the HTTP status code of the +// response is not a 2xx status. +type HTTPError struct { + StatusCode int + Status string + Body []byte +} + +func (err HTTPError) Error() string { + if len(err.Body) == 0 { + return err.Status + } + return fmt.Sprintf("%v: %s", err.Status, err.Body) +} + +// Error wraps RPC errors, which contain an error code in addition to the message. +type Error interface { + Error() string // returns the message + ErrorCode() int // returns the code +} + +// A DataError contains some data in addition to the error message. +type DataError interface { + Error() string // returns the message + ErrorData() interface{} // returns the error data +} + +// Error types defined below are the built-in JSON-RPC errors. + +var ( + _ Error = new(methodNotFoundError) + _ Error = new(subscriptionNotFoundError) + _ Error = new(parseError) + _ Error = new(invalidRequestError) + _ Error = new(invalidMessageError) + _ Error = new(invalidParamsError) + _ Error = new(internalServerError) +) + +const ( + errcodeDefault = -32000 + errcodeTimeout = -32002 + errcodeResponseTooLarge = -32003 + errcodePanic = -32603 + errcodeMarshalError = -32603 + + legacyErrcodeNotificationsUnsupported = -32001 +) + +const ( + errMsgTimeout = "request timed out" + errMsgResponseTooLarge = "response too large" + errMsgBatchTooLarge = "batch too large" +) + +type methodNotFoundError struct{ method string } + +func (e *methodNotFoundError) ErrorCode() int { return -32601 } + +func (e *methodNotFoundError) Error() string { + return fmt.Sprintf("the method %s does not exist/is not available", e.method) +} + +type notificationsUnsupportedError struct{} + +func (e notificationsUnsupportedError) Error() string { + return "notifications not supported" +} + +func (e notificationsUnsupportedError) ErrorCode() int { return -32601 } + +// Is checks for equivalence to another error. Here we define that all errors with code +// -32601 (method not found) are equivalent to notificationsUnsupportedError. This is +// done to enable the following pattern: +// +// sub, err := client.Subscribe(...) +// if errors.Is(err, rpc.ErrNotificationsUnsupported) { +// // server doesn't support subscriptions +// } +func (e notificationsUnsupportedError) Is(other error) bool { + if other == (notificationsUnsupportedError{}) { + return true + } + rpcErr, ok := other.(Error) + if ok { + code := rpcErr.ErrorCode() + return code == -32601 || code == legacyErrcodeNotificationsUnsupported + } + return false +} + +type subscriptionNotFoundError struct{ namespace, subscription string } + +func (e *subscriptionNotFoundError) ErrorCode() int { return -32601 } + +func (e *subscriptionNotFoundError) Error() string { + return fmt.Sprintf("no %q subscription in %s namespace", e.subscription, e.namespace) +} + +// Invalid JSON was received by the server. +type parseError struct{ message string } + +func (e *parseError) ErrorCode() int { return -32700 } + +func (e *parseError) Error() string { return e.message } + +// received message isn't a valid request +type invalidRequestError struct{ message string } + +func (e *invalidRequestError) ErrorCode() int { return -32600 } + +func (e *invalidRequestError) Error() string { return e.message } + +// received message is invalid +type invalidMessageError struct{ message string } + +func (e *invalidMessageError) ErrorCode() int { return -32700 } + +func (e *invalidMessageError) Error() string { return e.message } + +// unable to decode supplied params, or an invalid number of parameters +type invalidParamsError struct{ message string } + +func (e *invalidParamsError) ErrorCode() int { return -32602 } + +func (e *invalidParamsError) Error() string { return e.message } + +// internalServerError is used for server errors during request processing. +type internalServerError struct { + code int + message string +} + +func (e *internalServerError) ErrorCode() int { return e.code } + +func (e *internalServerError) Error() string { return e.message } diff --git a/lib/gethfork/rpc/gw_auth.go b/lib/gethfork/rpc/gw_auth.go new file mode 100644 index 0000000000..0f578c5dfa --- /dev/null +++ b/lib/gethfork/rpc/gw_auth.go @@ -0,0 +1,3 @@ +package rpc + +type GWTokenKey struct{} diff --git a/lib/gethfork/rpc/handler.go b/lib/gethfork/rpc/handler.go new file mode 100644 index 0000000000..856ef8b2c4 --- /dev/null +++ b/lib/gethfork/rpc/handler.go @@ -0,0 +1,595 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "encoding/json" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/log" +) + +// handler handles JSON-RPC messages. There is one handler per connection. Note that +// handler is not safe for concurrent use. Message handling never blocks indefinitely +// because RPCs are processed on background goroutines launched by handler. +// +// The entry points for incoming messages are: +// +// h.handleMsg(message) +// h.handleBatch(message) +// +// Outgoing calls use the requestOp struct. Register the request before sending it +// on the connection: +// +// op := &requestOp{ids: ...} +// h.addRequestOp(op) +// +// Now send the request, then wait for the reply to be delivered through handleMsg: +// +// if err := op.wait(...); err != nil { +// h.removeRequestOp(op) // timeout, etc. +// } +type handler struct { + reg *serviceRegistry + unsubscribeCb *callback + idgen func() ID // subscription ID generator + respWait map[string]*requestOp // active client requests + clientSubs map[string]*ClientSubscription // active client subscriptions + callWG sync.WaitGroup // pending call goroutines + rootCtx context.Context // canceled by close() + cancelRoot func() // cancel function for rootCtx + conn jsonWriter // where responses will be sent + log log.Logger + allowSubscribe bool + batchRequestLimit int + batchResponseMaxSize int + + subLock sync.Mutex + serverSubs map[ID]*Subscription + UserID string +} + +type callProc struct { + ctx context.Context + notifiers []*Notifier +} + +func newHandler(connCtx context.Context, conn jsonWriter, idgen func() ID, reg *serviceRegistry, batchRequestLimit, batchResponseMaxSize int, userID string) *handler { + rootCtx, cancelRoot := context.WithCancel(connCtx) + h := &handler{ + reg: reg, + idgen: idgen, + conn: conn, + respWait: make(map[string]*requestOp), + clientSubs: make(map[string]*ClientSubscription), + rootCtx: rootCtx, + cancelRoot: cancelRoot, + allowSubscribe: true, + serverSubs: make(map[ID]*Subscription), + log: log.Root(), + batchRequestLimit: batchRequestLimit, + batchResponseMaxSize: batchResponseMaxSize, + UserID: userID, + } + if conn.remoteAddr() != "" { + h.log = h.log.New("conn", conn.remoteAddr()) + } + h.unsubscribeCb = newCallback(reflect.Value{}, reflect.ValueOf(h.unsubscribe)) + return h +} + +// batchCallBuffer manages in progress call messages and their responses during a batch +// call. Calls need to be synchronized between the processing and timeout-triggering +// goroutines. +type batchCallBuffer struct { + mutex sync.Mutex + calls []*jsonrpcMessage + resp []*jsonrpcMessage + wrote bool +} + +// nextCall returns the next unprocessed message. +func (b *batchCallBuffer) nextCall() *jsonrpcMessage { + b.mutex.Lock() + defer b.mutex.Unlock() + + if len(b.calls) == 0 { + return nil + } + // The popping happens in `pushAnswer`. The in progress call is kept + // so we can return an error for it in case of timeout. + msg := b.calls[0] + return msg +} + +// pushResponse adds the response to last call returned by nextCall. +func (b *batchCallBuffer) pushResponse(answer *jsonrpcMessage) { + b.mutex.Lock() + defer b.mutex.Unlock() + + if answer != nil { + b.resp = append(b.resp, answer) + } + b.calls = b.calls[1:] +} + +// write sends the responses. +func (b *batchCallBuffer) write(ctx context.Context, conn jsonWriter) { + b.mutex.Lock() + defer b.mutex.Unlock() + + b.doWrite(ctx, conn, false) +} + +// respondWithError sends the responses added so far. For the remaining unanswered call +// messages, it responds with the given error. +func (b *batchCallBuffer) respondWithError(ctx context.Context, conn jsonWriter, err error) { + b.mutex.Lock() + defer b.mutex.Unlock() + + for _, msg := range b.calls { + if !msg.isNotification() { + b.resp = append(b.resp, msg.errorResponse(err)) + } + } + b.doWrite(ctx, conn, true) +} + +// doWrite actually writes the response. +// This assumes b.mutex is held. +func (b *batchCallBuffer) doWrite(ctx context.Context, conn jsonWriter, isErrorResponse bool) { + if b.wrote { + return + } + b.wrote = true // can only write once + if len(b.resp) > 0 { + conn.writeJSON(ctx, b.resp, isErrorResponse) + } +} + +// handleBatch executes all messages in a batch and returns the responses. +func (h *handler) handleBatch(msgs []*jsonrpcMessage) { + // Emit error response for empty batches: + if len(msgs) == 0 { + h.startCallProc(func(cp *callProc) { + resp := errorMessage(&invalidRequestError{"empty batch"}) + h.conn.writeJSON(cp.ctx, resp, true) + }) + return + } + // Apply limit on total number of requests. + if h.batchRequestLimit != 0 && len(msgs) > h.batchRequestLimit { + h.startCallProc(func(cp *callProc) { + h.respondWithBatchTooLarge(cp, msgs) + }) + return + } + + // Handle non-call messages first. + // Here we need to find the requestOp that sent the request batch. + calls := make([]*jsonrpcMessage, 0, len(msgs)) + h.handleResponses(msgs, func(msg *jsonrpcMessage) { + calls = append(calls, msg) + }) + if len(calls) == 0 { + return + } + + // Process calls on a goroutine because they may block indefinitely: + h.startCallProc(func(cp *callProc) { + var ( + timer *time.Timer + cancel context.CancelFunc + callBuffer = &batchCallBuffer{calls: calls, resp: make([]*jsonrpcMessage, 0, len(calls))} + ) + + cp.ctx, cancel = context.WithCancel(cp.ctx) + defer cancel() + + // Cancel the request context after timeout and send an error response. Since the + // currently-running method might not return immediately on timeout, we must wait + // for the timeout concurrently with processing the request. + if timeout, ok := ContextRequestTimeout(cp.ctx); ok { + timer = time.AfterFunc(timeout, func() { + cancel() + err := &internalServerError{errcodeTimeout, errMsgTimeout} + callBuffer.respondWithError(cp.ctx, h.conn, err) + }) + } + + responseBytes := 0 + for { + // No need to handle rest of calls if timed out. + if cp.ctx.Err() != nil { + break + } + msg := callBuffer.nextCall() + if msg == nil { + break + } + resp := h.handleCallMsg(cp, msg) + callBuffer.pushResponse(resp) + if resp != nil && h.batchResponseMaxSize != 0 { + responseBytes += len(resp.Result) + if responseBytes > h.batchResponseMaxSize { + err := &internalServerError{errcodeResponseTooLarge, errMsgResponseTooLarge} + callBuffer.respondWithError(cp.ctx, h.conn, err) + break + } + } + } + if timer != nil { + timer.Stop() + } + + h.addSubscriptions(cp.notifiers) + callBuffer.write(cp.ctx, h.conn) + for _, n := range cp.notifiers { + n.activate() + } + }) +} + +func (h *handler) respondWithBatchTooLarge(cp *callProc, batch []*jsonrpcMessage) { + resp := errorMessage(&invalidRequestError{errMsgBatchTooLarge}) + // Find the first call and add its "id" field to the error. + // This is the best we can do, given that the protocol doesn't have a way + // of reporting an error for the entire batch. + for _, msg := range batch { + if msg.isCall() { + resp.ID = msg.ID + break + } + } + h.conn.writeJSON(cp.ctx, []*jsonrpcMessage{resp}, true) +} + +// handleMsg handles a single non-batch message. +func (h *handler) handleMsg(msg *jsonrpcMessage) { + msgs := []*jsonrpcMessage{msg} + h.handleResponses(msgs, func(msg *jsonrpcMessage) { + h.startCallProc(func(cp *callProc) { + h.handleNonBatchCall(cp, msg) + }) + }) +} + +func (h *handler) handleNonBatchCall(cp *callProc, msg *jsonrpcMessage) { + var ( + responded sync.Once + timer *time.Timer + cancel context.CancelFunc + ) + cp.ctx, cancel = context.WithCancel(cp.ctx) + defer cancel() + + // Cancel the request context after timeout and send an error response. Since the + // running method might not return immediately on timeout, we must wait for the + // timeout concurrently with processing the request. + if timeout, ok := ContextRequestTimeout(cp.ctx); ok { + timer = time.AfterFunc(timeout, func() { + cancel() + responded.Do(func() { + resp := msg.errorResponse(&internalServerError{errcodeTimeout, errMsgTimeout}) + h.conn.writeJSON(cp.ctx, resp, true) + }) + }) + } + + answer := h.handleCallMsg(cp, msg) + if timer != nil { + timer.Stop() + } + h.addSubscriptions(cp.notifiers) + if answer != nil { + responded.Do(func() { + h.conn.writeJSON(cp.ctx, answer, false) + }) + } + for _, n := range cp.notifiers { + n.activate() + } +} + +// close cancels all requests except for inflightReq and waits for +// call goroutines to shut down. +func (h *handler) close(err error, inflightReq *requestOp) { + h.cancelAllRequests(err, inflightReq) + h.callWG.Wait() + h.cancelRoot() + h.cancelServerSubscriptions(err) +} + +// addRequestOp registers a request operation. +func (h *handler) addRequestOp(op *requestOp) { + for _, id := range op.ids { + h.respWait[string(id)] = op + } +} + +// removeRequestOp stops waiting for the given request IDs. +func (h *handler) removeRequestOp(op *requestOp) { + for _, id := range op.ids { + delete(h.respWait, string(id)) + } +} + +// cancelAllRequests unblocks and removes pending requests and active subscriptions. +func (h *handler) cancelAllRequests(err error, inflightReq *requestOp) { + didClose := make(map[*requestOp]bool) + if inflightReq != nil { + didClose[inflightReq] = true + } + + for id, op := range h.respWait { + // Remove the op so that later calls will not close op.resp again. + delete(h.respWait, id) + + if !didClose[op] { + op.err = err + close(op.resp) + didClose[op] = true + } + } + for id, sub := range h.clientSubs { + delete(h.clientSubs, id) + sub.close(err) + } +} + +func (h *handler) addSubscriptions(nn []*Notifier) { + h.subLock.Lock() + defer h.subLock.Unlock() + + for _, n := range nn { + if sub := n.takeSubscription(); sub != nil { + h.serverSubs[sub.ID] = sub + } + } +} + +// cancelServerSubscriptions removes all subscriptions and closes their error channels. +func (h *handler) cancelServerSubscriptions(err error) { + h.subLock.Lock() + defer h.subLock.Unlock() + + for id, s := range h.serverSubs { + s.err <- err + close(s.err) + delete(h.serverSubs, id) + } +} + +// startCallProc runs fn in a new goroutine and starts tracking it in the h.calls wait group. +func (h *handler) startCallProc(fn func(*callProc)) { + h.callWG.Add(1) + go func() { + ctx, cancel := context.WithCancel(h.rootCtx) + defer h.callWG.Done() + defer cancel() + fn(&callProc{ctx: ctx}) + }() +} + +// handleResponse processes method call responses. +func (h *handler) handleResponses(batch []*jsonrpcMessage, handleCall func(*jsonrpcMessage)) { + var resolvedops []*requestOp + handleResp := func(msg *jsonrpcMessage) { + op := h.respWait[string(msg.ID)] + if op == nil { + h.log.Debug("Unsolicited RPC response", "reqid", idForLog{msg.ID}) + return + } + resolvedops = append(resolvedops, op) + delete(h.respWait, string(msg.ID)) + + // For subscription responses, start the subscription if the server + // indicates success. EthSubscribe gets unblocked in either case through + // the op.resp channel. + if op.sub != nil { + if msg.Error != nil { + op.err = msg.Error + } else { + op.err = json.Unmarshal(msg.Result, &op.sub.subid) + if op.err == nil { + go op.sub.run() + h.clientSubs[op.sub.subid] = op.sub + } + } + } + + if !op.hadResponse { + op.hadResponse = true + op.resp <- batch + } + } + + for _, msg := range batch { + start := time.Now() + switch { + case msg.isResponse(): + handleResp(msg) + h.log.Trace("Handled RPC response", "reqid", idForLog{msg.ID}, "duration", time.Since(start)) + + case msg.isNotification(): + if strings.HasSuffix(msg.Method, notificationMethodSuffix) { + h.handleSubscriptionResult(msg) + continue + } + handleCall(msg) + + default: + handleCall(msg) + } + } + + for _, op := range resolvedops { + h.removeRequestOp(op) + } +} + +// handleSubscriptionResult processes subscription notifications. +func (h *handler) handleSubscriptionResult(msg *jsonrpcMessage) { + var result subscriptionResult + if err := json.Unmarshal(msg.Params, &result); err != nil { + h.log.Debug("Dropping invalid subscription message") + return + } + if h.clientSubs[result.ID] != nil { + h.clientSubs[result.ID].deliver(result.Result) + } +} + +// handleCallMsg executes a call message and returns the answer. +func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMessage { + start := time.Now() + switch { + case msg.isNotification(): + h.handleCall(ctx, msg) + h.log.Debug("Served "+msg.Method, "duration", time.Since(start)) + return nil + + case msg.isCall(): + resp := h.handleCall(ctx, msg) + var ctx []interface{} + ctx = append(ctx, "reqid", idForLog{msg.ID}, "duration", time.Since(start)) + if resp.Error != nil { + ctx = append(ctx, "err", resp.Error.Message) + if resp.Error.Data != nil { + ctx = append(ctx, "errdata", resp.Error.Data) + } + h.log.Warn("Served "+msg.Method, ctx...) + } else { + h.log.Debug("Served "+msg.Method, ctx...) + } + return resp + + case msg.hasValidID(): + return msg.errorResponse(&invalidRequestError{"invalid request"}) + + default: + return errorMessage(&invalidRequestError{"invalid request"}) + } +} + +// handleCall processes method calls. +func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage { + if msg.isSubscribe() { + return h.handleSubscribe(cp, msg) + } + var callb *callback + if msg.isUnsubscribe() { + callb = h.unsubscribeCb + } else { + callb = h.reg.callback(msg.Method) + } + if callb == nil { + return msg.errorResponse(&methodNotFoundError{method: msg.Method}) + } + + args, err := parsePositionalArguments(msg.Params, callb.argTypes) + if err != nil { + return msg.errorResponse(&invalidParamsError{err.Error()}) + } + start := time.Now() + answer := h.runMethod(cp.ctx, msg, callb, args) + + // Collect the statistics for RPC calls if metrics is enabled. + // We only care about pure rpc call. Filter out subscription. + if callb != h.unsubscribeCb { + rpcRequestGauge.Inc(1) + if answer.Error != nil { + failedRequestGauge.Inc(1) + } else { + successfulRequestGauge.Inc(1) + } + rpcServingTimer.UpdateSince(start) + updateServeTimeHistogram(msg.Method, answer.Error == nil, time.Since(start)) + } + + return answer +} + +// handleSubscribe processes *_subscribe method calls. +func (h *handler) handleSubscribe(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage { + if !h.allowSubscribe { + return msg.errorResponse(ErrNotificationsUnsupported) + } + + // Subscription method name is first argument. + name, err := parseSubscriptionName(msg.Params) + if err != nil { + return msg.errorResponse(&invalidParamsError{err.Error()}) + } + namespace := msg.namespace() + callb := h.reg.subscription(namespace, name) + if callb == nil { + return msg.errorResponse(&subscriptionNotFoundError{namespace, name}) + } + + // Parse subscription name arg too, but remove it before calling the callback. + argTypes := append([]reflect.Type{stringType}, callb.argTypes...) + args, err := parsePositionalArguments(msg.Params, argTypes) + if err != nil { + return msg.errorResponse(&invalidParamsError{err.Error()}) + } + args = args[1:] + + // Install notifier in context so the subscription handler can find it. + n := &Notifier{h: h, namespace: namespace, UserID: h.UserID} + cp.notifiers = append(cp.notifiers, n) + ctx := context.WithValue(cp.ctx, notifierKey{}, n) + + return h.runMethod(ctx, msg, callb, args) +} + +// runMethod runs the Go callback for an RPC method. +func (h *handler) runMethod(ctx context.Context, msg *jsonrpcMessage, callb *callback, args []reflect.Value) *jsonrpcMessage { + result, err := callb.call(ctx, msg.Method, args) + if err != nil { + return msg.errorResponse(err) + } + return msg.response(result) +} + +// unsubscribe is the callback function for all *_unsubscribe calls. +func (h *handler) unsubscribe(ctx context.Context, id ID) (bool, error) { + h.subLock.Lock() + defer h.subLock.Unlock() + + s := h.serverSubs[id] + if s == nil { + return false, ErrSubscriptionNotFound + } + close(s.err) + delete(h.serverSubs, id) + return true, nil +} + +type idForLog struct{ json.RawMessage } + +func (id idForLog) String() string { + if s, err := strconv.Unquote(string(id.RawMessage)); err == nil { + return s + } + return string(id.RawMessage) +} diff --git a/lib/gethfork/rpc/http.go b/lib/gethfork/rpc/http.go new file mode 100644 index 0000000000..ac8fee6a3c --- /dev/null +++ b/lib/gethfork/rpc/http.go @@ -0,0 +1,397 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "mime" + "net/http" + "net/url" + "strconv" + "sync" + "time" +) + +const ( + defaultBodyLimit = 5 * 1024 * 1024 + contentType = "application/json" +) + +// https://www.jsonrpc.org/historical/json-rpc-over-http.html#id13 +var acceptedContentTypes = []string{contentType, "application/json-rpc", "application/jsonrequest"} + +type httpConn struct { + client *http.Client + url string + closeOnce sync.Once + closeCh chan interface{} + mu sync.Mutex // protects headers + headers http.Header + auth HTTPAuth +} + +// httpConn implements ServerCodec, but it is treated specially by Client +// and some methods don't work. The panic() stubs here exist to ensure +// this special treatment is correct. + +func (hc *httpConn) writeJSON(context.Context, interface{}, bool) error { + panic("writeJSON called on httpConn") +} + +func (hc *httpConn) peerInfo() PeerInfo { + panic("peerInfo called on httpConn") +} + +func (hc *httpConn) remoteAddr() string { + return hc.url +} + +func (hc *httpConn) readBatch() ([]*jsonrpcMessage, bool, error) { + <-hc.closeCh + return nil, false, io.EOF +} + +func (hc *httpConn) close() { + hc.closeOnce.Do(func() { close(hc.closeCh) }) +} + +func (hc *httpConn) closed() <-chan interface{} { + return hc.closeCh +} + +// HTTPTimeouts represents the configuration params for the HTTP RPC server. +type HTTPTimeouts struct { + // ReadTimeout is the maximum duration for reading the entire + // request, including the body. + // + // Because ReadTimeout does not let Handlers make per-request + // decisions on each request body's acceptable deadline or + // upload rate, most users will prefer to use + // ReadHeaderTimeout. It is valid to use them both. + ReadTimeout time.Duration + + // ReadHeaderTimeout is the amount of time allowed to read + // request headers. The connection's read deadline is reset + // after reading the headers and the Handler can decide what + // is considered too slow for the body. If ReadHeaderTimeout + // is zero, the value of ReadTimeout is used. If both are + // zero, there is no timeout. + ReadHeaderTimeout time.Duration + + // WriteTimeout is the maximum duration before timing out + // writes of the response. It is reset whenever a new + // request's header is read. Like ReadTimeout, it does not + // let Handlers make decisions on a per-request basis. + WriteTimeout time.Duration + + // IdleTimeout is the maximum amount of time to wait for the + // next request when keep-alives are enabled. If IdleTimeout + // is zero, the value of ReadTimeout is used. If both are + // zero, ReadHeaderTimeout is used. + IdleTimeout time.Duration +} + +// DefaultHTTPTimeouts represents the default timeout values used if further +// configuration is not provided. +var DefaultHTTPTimeouts = HTTPTimeouts{ + ReadTimeout: 30 * time.Second, + ReadHeaderTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, +} + +// DialHTTP creates a new RPC client that connects to an RPC server over HTTP. +func DialHTTP(endpoint string) (*Client, error) { + return DialHTTPWithClient(endpoint, new(http.Client)) +} + +// DialHTTPWithClient creates a new RPC client that connects to an RPC server over HTTP +// using the provided HTTP Client. +// +// Deprecated: use DialOptions and the WithHTTPClient option. +func DialHTTPWithClient(endpoint string, client *http.Client) (*Client, error) { + // Sanity check URL so we don't end up with a client that will fail every request. + _, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + + var cfg clientConfig + cfg.httpClient = client + fn := newClientTransportHTTP(endpoint, &cfg) + return newClient(context.Background(), &cfg, fn) +} + +func newClientTransportHTTP(endpoint string, cfg *clientConfig) reconnectFunc { + headers := make(http.Header, 2+len(cfg.httpHeaders)) + headers.Set("accept", contentType) + headers.Set("content-type", contentType) + for key, values := range cfg.httpHeaders { + headers[key] = values + } + + client := cfg.httpClient + if client == nil { + client = new(http.Client) + } + + hc := &httpConn{ + client: client, + headers: headers, + url: endpoint, + auth: cfg.httpAuth, + closeCh: make(chan interface{}), + } + + return func(ctx context.Context) (ServerCodec, error) { + return hc, nil + } +} + +func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg interface{}) error { + hc := c.writeConn.(*httpConn) + respBody, err := hc.doRequest(ctx, msg) + if err != nil { + return err + } + defer respBody.Close() + + var resp jsonrpcMessage + batch := [1]*jsonrpcMessage{&resp} + if err := json.NewDecoder(respBody).Decode(&resp); err != nil { + return err + } + op.resp <- batch[:] + return nil +} + +func (c *Client) sendBatchHTTP(ctx context.Context, op *requestOp, msgs []*jsonrpcMessage) error { + hc := c.writeConn.(*httpConn) + respBody, err := hc.doRequest(ctx, msgs) + if err != nil { + return err + } + defer respBody.Close() + + var respmsgs []*jsonrpcMessage + if err := json.NewDecoder(respBody).Decode(&respmsgs); err != nil { + return err + } + op.resp <- respmsgs + return nil +} + +func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadCloser, error) { + body, err := json.Marshal(msg) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, hc.url, io.NopCloser(bytes.NewReader(body))) + if err != nil { + return nil, err + } + req.ContentLength = int64(len(body)) + req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(body)), nil } + + // set headers + hc.mu.Lock() + req.Header = hc.headers.Clone() + hc.mu.Unlock() + setHeaders(req.Header, headersFromContext(ctx)) + + if hc.auth != nil { + if err := hc.auth(req.Header); err != nil { + return nil, err + } + } + + // do request + resp, err := hc.client.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + var buf bytes.Buffer + var body []byte + if _, err := buf.ReadFrom(resp.Body); err == nil { + body = buf.Bytes() + } + resp.Body.Close() + return nil, HTTPError{ + Status: resp.Status, + StatusCode: resp.StatusCode, + Body: body, + } + } + return resp.Body, nil +} + +// httpServerConn turns a HTTP connection into a Conn. +type httpServerConn struct { + io.Reader + io.Writer + r *http.Request +} + +func (s *Server) newHTTPServerConn(r *http.Request, w http.ResponseWriter) ServerCodec { + body := io.LimitReader(r.Body, int64(s.httpBodyLimit)) + conn := &httpServerConn{Reader: body, Writer: w, r: r} + + encoder := func(v any, isErrorResponse bool) error { + if !isErrorResponse { + return json.NewEncoder(conn).Encode(v) + } + + // It's an error response and requires special treatment. + // + // In case of a timeout error, the response must be written before the HTTP + // server's write timeout occurs. So we need to flush the response. The + // Content-Length header also needs to be set to ensure the client knows + // when it has the full response. + encdata, err := json.Marshal(v) + if err != nil { + return err + } + w.Header().Set("content-length", strconv.Itoa(len(encdata))) + + // If this request is wrapped in a handler that might remove Content-Length (such + // as the automatic gzip we do in package node), we need to ensure the HTTP server + // doesn't perform chunked encoding. In case WriteTimeout is reached, the chunked + // encoding might not be finished correctly, and some clients do not like it when + // the final chunk is missing. + w.Header().Set("transfer-encoding", "identity") + + _, err = w.Write(encdata) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + return err + } + + dec := json.NewDecoder(conn) + dec.UseNumber() + + return NewFuncCodec(conn, encoder, dec.Decode) +} + +// Close does nothing and always returns nil. +func (t *httpServerConn) Close() error { return nil } + +// RemoteAddr returns the peer address of the underlying connection. +func (t *httpServerConn) RemoteAddr() string { + return t.r.RemoteAddr +} + +// SetWriteDeadline does nothing and always returns nil. +func (t *httpServerConn) SetWriteDeadline(time.Time) error { return nil } + +// ServeHTTP serves JSON-RPC requests over HTTP. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Permit dumb empty requests for remote health-checks (AWS) + if r.Method == http.MethodGet && r.ContentLength == 0 && r.URL.RawQuery == "" { + w.WriteHeader(http.StatusOK) + return + } + if code, err := s.validateRequest(r); err != nil { + http.Error(w, err.Error(), code) + return + } + + // Create request-scoped context. + connInfo := PeerInfo{Transport: "http", RemoteAddr: r.RemoteAddr} + connInfo.HTTP.Version = r.Proto + connInfo.HTTP.Host = r.Host + connInfo.HTTP.Origin = r.Header.Get("Origin") + connInfo.HTTP.UserAgent = r.Header.Get("User-Agent") + ctx := r.Context() + ctx = context.WithValue(ctx, peerInfoContextKey{}, connInfo) + + // All checks passed, create a codec that reads directly from the request body + // until EOF, writes the response to w, and orders the server to process a + // single request. + w.Header().Set("content-type", contentType) + codec := s.newHTTPServerConn(r, w) + defer codec.close() + rc := http.NewResponseController(w) + _ = rc.EnableFullDuplex() + s.serveSingleRequest(ctx, codec) +} + +// validateRequest returns a non-zero response code and error message if the +// request is invalid. +func (s *Server) validateRequest(r *http.Request) (int, error) { + if r.Method == http.MethodPut || r.Method == http.MethodDelete { + return http.StatusMethodNotAllowed, errors.New("method not allowed") + } + if r.ContentLength > int64(s.httpBodyLimit) { + err := fmt.Errorf("content length too large (%d>%d)", r.ContentLength, s.httpBodyLimit) + return http.StatusRequestEntityTooLarge, err + } + // Allow OPTIONS (regardless of content-type) + if r.Method == http.MethodOptions { + return 0, nil + } + // Check content-type + if mt, _, err := mime.ParseMediaType(r.Header.Get("content-type")); err == nil { + for _, accepted := range acceptedContentTypes { + if accepted == mt { + return 0, nil + } + } + } + // Invalid content-type + err := fmt.Errorf("invalid content type, only %s is supported", contentType) + return http.StatusUnsupportedMediaType, err +} + +// ContextRequestTimeout returns the request timeout derived from the given context. +func ContextRequestTimeout(ctx context.Context) (time.Duration, bool) { + timeout := time.Duration(math.MaxInt64) + hasTimeout := false + setTimeout := func(d time.Duration) { + if d < timeout { + timeout = d + hasTimeout = true + } + } + + if deadline, ok := ctx.Deadline(); ok { + setTimeout(time.Until(deadline)) + } + + // If the context is an HTTP request context, use the server's WriteTimeout. + httpSrv, ok := ctx.Value(http.ServerContextKey).(*http.Server) + if ok && httpSrv.WriteTimeout > 0 { + wt := httpSrv.WriteTimeout + // When a write timeout is configured, we need to send the response message before + // the HTTP server cuts connection. So our internal timeout must be earlier than + // the server's true timeout. + // + // Note: Timeouts are sanitized to be a minimum of 1 second. + // Also see issue: https://github.com/golang/go/issues/47229 + wt -= 100 * time.Millisecond + setTimeout(wt) + } + + return timeout, hasTimeout +} diff --git a/lib/gethfork/rpc/inproc.go b/lib/gethfork/rpc/inproc.go new file mode 100644 index 0000000000..2a5d400b19 --- /dev/null +++ b/lib/gethfork/rpc/inproc.go @@ -0,0 +1,34 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "net" +) + +// DialInProc attaches an in-process connection to the given RPC server. +func DialInProc(handler *Server) *Client { + initctx := context.Background() + cfg := new(clientConfig) + c, _ := newClient(initctx, cfg, func(context.Context) (ServerCodec, error) { + p1, p2 := net.Pipe() + go handler.ServeCodec(NewCodec(p1), 0, "") + return NewCodec(p2), nil + }) + return c +} diff --git a/lib/gethfork/rpc/ipc.go b/lib/gethfork/rpc/ipc.go new file mode 100644 index 0000000000..9db95dc467 --- /dev/null +++ b/lib/gethfork/rpc/ipc.go @@ -0,0 +1,61 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "net" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p/netutil" +) + +// ServeListener accepts connections on l, serving JSON-RPC on them. +func (s *Server) ServeListener(l net.Listener) error { + for { + conn, err := l.Accept() + if netutil.IsTemporaryError(err) { + log.Warn("RPC accept error", "err", err) + continue + } else if err != nil { + return err + } + log.Trace("Accepted RPC connection", "conn", conn.RemoteAddr()) + go s.ServeCodec(NewCodec(conn), 0, "") + } +} + +// DialIPC create a new IPC client that connects to the given endpoint. On Unix it assumes +// the endpoint is the full path to a unix socket, and Windows the endpoint is an +// identifier for a named pipe. +// +// The context is used for the initial connection establishment. It does not +// affect subsequent interactions with the client. +func DialIPC(ctx context.Context, endpoint string) (*Client, error) { + cfg := new(clientConfig) + return newClient(ctx, cfg, newClientTransportIPC(endpoint)) +} + +func newClientTransportIPC(endpoint string) reconnectFunc { + return func(ctx context.Context) (ServerCodec, error) { + conn, err := newIPCConnection(ctx, endpoint) + if err != nil { + return nil, err + } + return NewCodec(conn), err + } +} diff --git a/lib/gethfork/rpc/ipc_js.go b/lib/gethfork/rpc/ipc_js.go new file mode 100644 index 0000000000..453a20bc1a --- /dev/null +++ b/lib/gethfork/rpc/ipc_js.go @@ -0,0 +1,38 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +//go:build js +// +build js + +package rpc + +import ( + "context" + "errors" + "net" +) + +var errNotSupported = errors.New("rpc: not supported") + +// ipcListen will create a named pipe on the given endpoint. +func ipcListen(endpoint string) (net.Listener, error) { + return nil, errNotSupported +} + +// newIPCConnection will connect to a named pipe with the given endpoint as name. +func newIPCConnection(ctx context.Context, endpoint string) (net.Conn, error) { + return nil, errNotSupported +} diff --git a/lib/gethfork/rpc/ipc_unix.go b/lib/gethfork/rpc/ipc_unix.go new file mode 100644 index 0000000000..2b87fe83a0 --- /dev/null +++ b/lib/gethfork/rpc/ipc_unix.go @@ -0,0 +1,62 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +//go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris +// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris + +package rpc + +import ( + "context" + "fmt" + "net" + "os" + "path/filepath" + + "github.com/ethereum/go-ethereum/log" +) + +const ( + // On Linux, sun_path is 108 bytes in size + // see http://man7.org/linux/man-pages/man7/unix.7.html + maxPathSize = int(108) +) + +// ipcListen will create a Unix socket on the given endpoint. +func ipcListen(endpoint string) (net.Listener, error) { + // account for null-terminator too + if len(endpoint)+1 > maxPathSize { + log.Warn(fmt.Sprintf("The ipc endpoint is longer than %d characters. ", maxPathSize-1), + "endpoint", endpoint) + } + + // Ensure the IPC path exists and remove any previous leftover + if err := os.MkdirAll(filepath.Dir(endpoint), 0o751); err != nil { + return nil, err + } + os.Remove(endpoint) + l, err := net.Listen("unix", endpoint) + if err != nil { + return nil, err + } + os.Chmod(endpoint, 0o600) //nolint:errcheck + return l, nil +} + +// newIPCConnection will connect to a Unix socket on the given endpoint. +func newIPCConnection(ctx context.Context, endpoint string) (net.Conn, error) { + return new(net.Dialer).DialContext(ctx, "unix", endpoint) +} diff --git a/lib/gethfork/rpc/ipc_windows.go b/lib/gethfork/rpc/ipc_windows.go new file mode 100644 index 0000000000..efec38cf37 --- /dev/null +++ b/lib/gethfork/rpc/ipc_windows.go @@ -0,0 +1,44 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +//go:build windows +// +build windows + +package rpc + +import ( + "context" + "net" + "time" + + "github.com/Microsoft/go-winio" +) + +// This is used if the dialing context has no deadline. It is much smaller than the +// defaultDialTimeout because named pipes are local and there is no need to wait so long. +const defaultPipeDialTimeout = 2 * time.Second + +// ipcListen will create a named pipe on the given endpoint. +func ipcListen(endpoint string) (net.Listener, error) { + return winio.ListenPipe(endpoint, nil) +} + +// newIPCConnection will connect to a named pipe with the given endpoint as name. +func newIPCConnection(ctx context.Context, endpoint string) (net.Conn, error) { + ctx, cancel := context.WithTimeout(ctx, defaultPipeDialTimeout) + defer cancel() + return winio.DialPipeContext(ctx, endpoint) +} diff --git a/lib/gethfork/rpc/json.go b/lib/gethfork/rpc/json.go new file mode 100644 index 0000000000..8c1668623a --- /dev/null +++ b/lib/gethfork/rpc/json.go @@ -0,0 +1,370 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "reflect" + "strings" + "sync" + "time" +) + +const ( + vsn = "2.0" + serviceMethodSeparator = "_" + subscribeMethodSuffix = "_subscribe" + unsubscribeMethodSuffix = "_unsubscribe" + notificationMethodSuffix = "_subscription" + + defaultWriteTimeout = 10 * time.Second // used if context has no deadline +) + +var null = json.RawMessage("null") + +type subscriptionResult struct { + ID string `json:"subscription"` + Result json.RawMessage `json:"result,omitempty"` +} + +type subscriptionResultEnc struct { + ID string `json:"subscription"` + Result any `json:"result"` +} + +type jsonrpcSubscriptionNotification struct { + Version string `json:"jsonrpc"` + Method string `json:"method"` + Params subscriptionResultEnc `json:"params"` +} + +// A value of this type can a JSON-RPC request, notification, successful response or +// error response. Which one it is depends on the fields. +type jsonrpcMessage struct { + Version string `json:"jsonrpc,omitempty"` + ID json.RawMessage `json:"id,omitempty"` + Method string `json:"method,omitempty"` + Params json.RawMessage `json:"params,omitempty"` + Error *jsonError `json:"error,omitempty"` + Result json.RawMessage `json:"result,omitempty"` +} + +func (msg *jsonrpcMessage) isNotification() bool { + return msg.hasValidVersion() && msg.ID == nil && msg.Method != "" +} + +func (msg *jsonrpcMessage) isCall() bool { + return msg.hasValidVersion() && msg.hasValidID() && msg.Method != "" +} + +func (msg *jsonrpcMessage) isResponse() bool { + return msg.hasValidVersion() && msg.hasValidID() && msg.Method == "" && msg.Params == nil && (msg.Result != nil || msg.Error != nil) +} + +func (msg *jsonrpcMessage) hasValidID() bool { + return len(msg.ID) > 0 && msg.ID[0] != '{' && msg.ID[0] != '[' +} + +func (msg *jsonrpcMessage) hasValidVersion() bool { + return msg.Version == vsn +} + +func (msg *jsonrpcMessage) isSubscribe() bool { + return strings.HasSuffix(msg.Method, subscribeMethodSuffix) +} + +func (msg *jsonrpcMessage) isUnsubscribe() bool { + return strings.HasSuffix(msg.Method, unsubscribeMethodSuffix) +} + +func (msg *jsonrpcMessage) namespace() string { + before, _, _ := strings.Cut(msg.Method, serviceMethodSeparator) + return before +} + +func (msg *jsonrpcMessage) String() string { + b, _ := json.Marshal(msg) + return string(b) +} + +func (msg *jsonrpcMessage) errorResponse(err error) *jsonrpcMessage { + resp := errorMessage(err) + resp.ID = msg.ID + return resp +} + +func (msg *jsonrpcMessage) response(result interface{}) *jsonrpcMessage { + enc, err := json.Marshal(result) + if err != nil { + return msg.errorResponse(&internalServerError{errcodeMarshalError, err.Error()}) + } + return &jsonrpcMessage{Version: vsn, ID: msg.ID, Result: enc} +} + +func errorMessage(err error) *jsonrpcMessage { + msg := &jsonrpcMessage{Version: vsn, ID: null, Error: &jsonError{ + Code: errcodeDefault, + Message: err.Error(), + }} + ec, ok := err.(Error) + if ok { + msg.Error.Code = ec.ErrorCode() + } + de, ok := err.(DataError) + if ok { + msg.Error.Data = de.ErrorData() + } + return msg +} + +type jsonError struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +func (err *jsonError) Error() string { + if err.Message == "" { + return fmt.Sprintf("json-rpc error %d", err.Code) + } + return err.Message +} + +func (err *jsonError) ErrorCode() int { + return err.Code +} + +func (err *jsonError) ErrorData() interface{} { + return err.Data +} + +// Conn is a subset of the methods of net.Conn which are sufficient for ServerCodec. +type Conn interface { + io.ReadWriteCloser + SetWriteDeadline(time.Time) error +} + +type deadlineCloser interface { + io.Closer + SetWriteDeadline(time.Time) error +} + +// ConnRemoteAddr wraps the RemoteAddr operation, which returns a description +// of the peer address of a connection. If a Conn also implements ConnRemoteAddr, this +// description is used in log messages. +type ConnRemoteAddr interface { + RemoteAddr() string +} + +// jsonCodec reads and writes JSON-RPC messages to the underlying connection. It also has +// support for parsing arguments and serializing (result) objects. +type jsonCodec struct { + remote string + closer sync.Once // close closed channel once + closeCh chan interface{} // closed on Close + decode decodeFunc // decoder to allow multiple transports + encMu sync.Mutex // guards the encoder + encode encodeFunc // encoder to allow multiple transports + conn deadlineCloser +} + +type encodeFunc = func(v interface{}, isErrorResponse bool) error + +type decodeFunc = func(v interface{}) error + +// NewFuncCodec creates a codec which uses the given functions to read and write. If conn +// implements ConnRemoteAddr, log messages will use it to include the remote address of +// the connection. +func NewFuncCodec(conn deadlineCloser, encode encodeFunc, decode decodeFunc) ServerCodec { + codec := &jsonCodec{ + closeCh: make(chan interface{}), + encode: encode, + decode: decode, + conn: conn, + } + if ra, ok := conn.(ConnRemoteAddr); ok { + codec.remote = ra.RemoteAddr() + } + return codec +} + +// NewCodec creates a codec on the given connection. If conn implements ConnRemoteAddr, log +// messages will use it to include the remote address of the connection. +func NewCodec(conn Conn) ServerCodec { + enc := json.NewEncoder(conn) + dec := json.NewDecoder(conn) + dec.UseNumber() + + encode := func(v interface{}, isErrorResponse bool) error { + return enc.Encode(v) + } + return NewFuncCodec(conn, encode, dec.Decode) +} + +func (c *jsonCodec) peerInfo() PeerInfo { + // This returns "ipc" because all other built-in transports have a separate codec type. + return PeerInfo{Transport: "ipc", RemoteAddr: c.remote} +} + +func (c *jsonCodec) remoteAddr() string { + return c.remote +} + +func (c *jsonCodec) readBatch() (messages []*jsonrpcMessage, batch bool, err error) { + // Decode the next JSON object in the input stream. + // This verifies basic syntax, etc. + var rawmsg json.RawMessage + if err := c.decode(&rawmsg); err != nil { + return nil, false, err + } + // fmt.Printf("MSG: %s\n", rawmsg) + messages, batch = parseMessage(rawmsg) + for i, msg := range messages { + if msg == nil { + // Message is JSON 'null'. Replace with zero value so it + // will be treated like any other invalid message. + messages[i] = new(jsonrpcMessage) + } + } + return messages, batch, nil +} + +func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}, isErrorResponse bool) error { + c.encMu.Lock() + defer c.encMu.Unlock() + + deadline, ok := ctx.Deadline() + if !ok { + deadline = time.Now().Add(defaultWriteTimeout) + } + c.conn.SetWriteDeadline(deadline) + return c.encode(v, isErrorResponse) +} + +func (c *jsonCodec) close() { + c.closer.Do(func() { + close(c.closeCh) + c.conn.Close() + }) +} + +// Closed returns a channel which will be closed when Close is called +func (c *jsonCodec) closed() <-chan interface{} { + return c.closeCh +} + +// parseMessage parses raw bytes as a (batch of) JSON-RPC message(s). There are no error +// checks in this function because the raw message has already been syntax-checked when it +// is called. Any non-JSON-RPC messages in the input return the zero value of +// jsonrpcMessage. +func parseMessage(raw json.RawMessage) ([]*jsonrpcMessage, bool) { + if !isBatch(raw) { + msgs := []*jsonrpcMessage{{}} + json.Unmarshal(raw, &msgs[0]) + return msgs, false + } + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.Token() // skip '[' + var msgs []*jsonrpcMessage + for dec.More() { + msgs = append(msgs, new(jsonrpcMessage)) + dec.Decode(&msgs[len(msgs)-1]) + } + return msgs, true +} + +// isBatch returns true when the first non-whitespace characters is '[' +func isBatch(raw json.RawMessage) bool { + for _, c := range raw { + // skip insignificant whitespace (http://www.ietf.org/rfc/rfc4627.txt) + if c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d { + continue + } + return c == '[' + } + return false +} + +// parsePositionalArguments tries to parse the given args to an array of values with the +// given types. It returns the parsed values or an error when the args could not be +// parsed. Missing optional arguments are returned as reflect.Zero values. +func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([]reflect.Value, error) { + dec := json.NewDecoder(bytes.NewReader(rawArgs)) + var args []reflect.Value + tok, err := dec.Token() + switch { + case err == io.EOF || tok == nil && err == nil: + // "params" is optional and may be empty. Also allow "params":null even though it's + // not in the spec because our own client used to send it. + case err != nil: + return nil, err + case tok == json.Delim('['): + // Read argument array. + if args, err = parseArgumentArray(dec, types); err != nil { + return nil, err + } + default: + return nil, errors.New("non-array args") + } + // Set any missing args to nil. + for i := len(args); i < len(types); i++ { + if types[i].Kind() != reflect.Ptr { + return nil, fmt.Errorf("missing value for required argument %d", i) + } + args = append(args, reflect.Zero(types[i])) + } + return args, nil +} + +func parseArgumentArray(dec *json.Decoder, types []reflect.Type) ([]reflect.Value, error) { + args := make([]reflect.Value, 0, len(types)) + for i := 0; dec.More(); i++ { + if i >= len(types) { + return args, fmt.Errorf("too many arguments, want at most %d", len(types)) + } + argval := reflect.New(types[i]) + if err := dec.Decode(argval.Interface()); err != nil { + return args, fmt.Errorf("invalid argument %d: %v", i, err) + } + if argval.IsNil() && types[i].Kind() != reflect.Ptr { + return args, fmt.Errorf("missing value for required argument %d", i) + } + args = append(args, argval.Elem()) + } + // Read end of args array. + _, err := dec.Token() + return args, err +} + +// parseSubscriptionName extracts the subscription name from an encoded argument array. +func parseSubscriptionName(rawArgs json.RawMessage) (string, error) { + dec := json.NewDecoder(bytes.NewReader(rawArgs)) + if tok, _ := dec.Token(); tok != json.Delim('[') { + return "", errors.New("non-array args") + } + v, _ := dec.Token() + method, ok := v.(string) + if !ok { + return "", errors.New("expected subscription name as first argument") + } + return method, nil +} diff --git a/lib/gethfork/rpc/metrics.go b/lib/gethfork/rpc/metrics.go new file mode 100644 index 0000000000..b1f1284535 --- /dev/null +++ b/lib/gethfork/rpc/metrics.go @@ -0,0 +1,50 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "fmt" + "time" + + "github.com/ethereum/go-ethereum/metrics" +) + +var ( + rpcRequestGauge = metrics.NewRegisteredGauge("rpc/requests", nil) + successfulRequestGauge = metrics.NewRegisteredGauge("rpc/success", nil) + failedRequestGauge = metrics.NewRegisteredGauge("rpc/failure", nil) + + // serveTimeHistName is the prefix of the per-request serving time histograms. + serveTimeHistName = "rpc/duration" + + rpcServingTimer = metrics.NewRegisteredTimer("rpc/duration/all", nil) +) + +// updateServeTimeHistogram tracks the serving time of a remote RPC call. +func updateServeTimeHistogram(method string, success bool, elapsed time.Duration) { + note := "success" + if !success { + note = "failure" + } + h := fmt.Sprintf("%s/%s/%s", serveTimeHistName, method, note) + sampler := func() metrics.Sample { + return metrics.ResettingSample( + metrics.NewExpDecaySample(1028, 0.015), + ) + } + metrics.GetOrRegisterHistogramLazy(h, nil, sampler).Update(elapsed.Microseconds()) +} diff --git a/lib/gethfork/rpc/server.go b/lib/gethfork/rpc/server.go new file mode 100644 index 0000000000..e0b96ad53f --- /dev/null +++ b/lib/gethfork/rpc/server.go @@ -0,0 +1,238 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "fmt" + "io" + "sync" + "sync/atomic" + + "github.com/ethereum/go-ethereum/log" +) + +const ( + MetadataApi = "rpc" + EngineApi = "engine" +) + +// CodecOption specifies which type of messages a codec supports. +// +// Deprecated: this option is no longer honored by Server. +type CodecOption int + +const ( + // OptionMethodInvocation is an indication that the codec supports RPC method calls + OptionMethodInvocation CodecOption = 1 << iota + + // OptionSubscriptions is an indication that the codec supports RPC notifications + OptionSubscriptions = 1 << iota // support pub sub +) + +// Server is an RPC server. +type Server struct { + services serviceRegistry + idgen func() ID + + mutex sync.Mutex + codecs map[ServerCodec]struct{} + run atomic.Bool + batchItemLimit int + batchResponseLimit int + httpBodyLimit int +} + +// NewServer creates a new server instance with no registered handlers. +func NewServer() *Server { + server := &Server{ + idgen: randomIDGenerator(), + codecs: make(map[ServerCodec]struct{}), + httpBodyLimit: defaultBodyLimit, + } + server.run.Store(true) + // Register the default service providing meta information about the RPC service such + // as the services and methods it offers. + rpcService := &RPCService{server} + server.RegisterName(MetadataApi, rpcService) + return server +} + +// SetBatchLimits sets limits applied to batch requests. There are two limits: 'itemLimit' +// is the maximum number of items in a batch. 'maxResponseSize' is the maximum number of +// response bytes across all requests in a batch. +// +// This method should be called before processing any requests via ServeCodec, ServeHTTP, +// ServeListener etc. +func (s *Server) SetBatchLimits(itemLimit, maxResponseSize int) { + s.batchItemLimit = itemLimit + s.batchResponseLimit = maxResponseSize +} + +// SetHTTPBodyLimit sets the size limit for HTTP requests. +// +// This method should be called before processing any requests via ServeHTTP. +func (s *Server) SetHTTPBodyLimit(limit int) { + s.httpBodyLimit = limit +} + +// RegisterName creates a service for the given receiver type under the given name. When no +// methods on the given receiver match the criteria to be either a RPC method or a +// subscription an error is returned. Otherwise a new service is created and added to the +// service collection this server provides to clients. +func (s *Server) RegisterName(name string, receiver interface{}) error { + return s.services.registerName(name, receiver) +} + +// ServeCodec reads incoming requests from codec, calls the appropriate callback and writes +// the response back using the given codec. It will block until the codec is closed or the +// server is stopped. In either case the codec is closed. +// +// Note that codec options are no longer supported. +func (s *Server) ServeCodec(codec ServerCodec, _ CodecOption, userID string) { + defer codec.close() + + if !s.trackCodec(codec) { + return + } + defer s.untrackCodec(codec) + + cfg := &clientConfig{ + idgen: s.idgen, + batchItemLimit: s.batchItemLimit, + batchResponseLimit: s.batchResponseLimit, + UserID: userID, + } + c := initClient(codec, &s.services, cfg) + <-codec.closed() + c.Close() +} + +func (s *Server) trackCodec(codec ServerCodec) bool { + s.mutex.Lock() + defer s.mutex.Unlock() + + if !s.run.Load() { + return false // Don't serve if server is stopped. + } + s.codecs[codec] = struct{}{} + return true +} + +func (s *Server) untrackCodec(codec ServerCodec) { + s.mutex.Lock() + defer s.mutex.Unlock() + + delete(s.codecs, codec) +} + +// serveSingleRequest reads and processes a single RPC request from the given codec. This +// is used to serve HTTP connections. Subscriptions and reverse calls are not allowed in +// this mode. +func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) { + // Don't serve if server is stopped. + if !s.run.Load() { + return + } + + h := newHandler(ctx, codec, s.idgen, &s.services, s.batchItemLimit, s.batchResponseLimit, "") + h.allowSubscribe = false + defer h.close(io.EOF, nil) + + reqs, batch, err := codec.readBatch() + if err != nil { + if err != io.EOF { + resp := errorMessage(&invalidMessageError{"parse error"}) + fmt.Printf(">> Parse error %s. requests: %v\n", err, reqs) + codec.writeJSON(ctx, resp, true) + } + return + } + if batch { + h.handleBatch(reqs) + } else { + h.handleMsg(reqs[0]) + } +} + +// Stop stops reading new requests, waits for stopPendingRequestTimeout to allow pending +// requests to finish, then closes all codecs which will cancel pending requests and +// subscriptions. +func (s *Server) Stop() { + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.run.CompareAndSwap(true, false) { + log.Debug("RPC server shutting down") + for codec := range s.codecs { + codec.close() + } + } +} + +// RPCService gives meta information about the server. +// e.g. gives information about the loaded modules. +type RPCService struct { + server *Server +} + +// Modules returns the list of RPC services with their version number +func (s *RPCService) Modules() map[string]string { + s.server.services.mu.Lock() + defer s.server.services.mu.Unlock() + + modules := make(map[string]string) + for name := range s.server.services.services { + modules[name] = "1.0" + } + return modules +} + +// PeerInfo contains information about the remote end of the network connection. +// +// This is available within RPC method handlers through the context. Call +// PeerInfoFromContext to get information about the client connection related to +// the current method call. +type PeerInfo struct { + // Transport is name of the protocol used by the client. + // This can be "http", "ws" or "ipc". + Transport string + + // Address of client. This will usually contain the IP address and port. + RemoteAddr string + + // Additional information for HTTP and WebSocket connections. + HTTP struct { + // Protocol version, i.e. "HTTP/1.1". This is not set for WebSocket. + Version string + // Header values sent by the client. + UserAgent string + Origin string + Host string + } +} + +type peerInfoContextKey struct{} + +// PeerInfoFromContext returns information about the client's network connection. +// Use this with the context passed to RPC method handler functions. +// +// The zero value is returned if no connection info is present in ctx. +func PeerInfoFromContext(ctx context.Context) PeerInfo { + info, _ := ctx.Value(peerInfoContextKey{}).(PeerInfo) + return info +} diff --git a/lib/gethfork/rpc/service.go b/lib/gethfork/rpc/service.go new file mode 100644 index 0000000000..38f16bb26f --- /dev/null +++ b/lib/gethfork/rpc/service.go @@ -0,0 +1,249 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "fmt" + "reflect" + "runtime" + "strings" + "sync" + "unicode" + + "github.com/ethereum/go-ethereum/log" +) + +var ( + contextType = reflect.TypeOf((*context.Context)(nil)).Elem() + errorType = reflect.TypeOf((*error)(nil)).Elem() + subscriptionType = reflect.TypeOf(Subscription{}) + stringType = reflect.TypeOf("") +) + +type serviceRegistry struct { + mu sync.Mutex + services map[string]service +} + +// service represents a registered object. +type service struct { + name string // name for service + callbacks map[string]*callback // registered handlers + subscriptions map[string]*callback // available subscriptions/notifications +} + +// callback is a method callback which was registered in the server +type callback struct { + fn reflect.Value // the function + rcvr reflect.Value // receiver object of method, set if fn is method + argTypes []reflect.Type // input argument types + hasCtx bool // method's first argument is a context (not included in argTypes) + errPos int // err return idx, of -1 when method cannot return error + isSubscribe bool // true if this is a subscription callback +} + +func (r *serviceRegistry) registerName(name string, rcvr interface{}) error { + rcvrVal := reflect.ValueOf(rcvr) + if name == "" { + return fmt.Errorf("no service name for type %s", rcvrVal.Type().String()) + } + callbacks := suitableCallbacks(rcvrVal) + if len(callbacks) == 0 { + return fmt.Errorf("service %T doesn't have any suitable methods/subscriptions to expose", rcvr) + } + + r.mu.Lock() + defer r.mu.Unlock() + if r.services == nil { + r.services = make(map[string]service) + } + svc, ok := r.services[name] + if !ok { + svc = service{ + name: name, + callbacks: make(map[string]*callback), + subscriptions: make(map[string]*callback), + } + r.services[name] = svc + } + for name, cb := range callbacks { + if cb.isSubscribe { + svc.subscriptions[name] = cb + } else { + svc.callbacks[name] = cb + } + } + return nil +} + +// callback returns the callback corresponding to the given RPC method name. +func (r *serviceRegistry) callback(method string) *callback { + elem := strings.SplitN(method, serviceMethodSeparator, 2) + if len(elem) != 2 { + return nil + } + r.mu.Lock() + defer r.mu.Unlock() + return r.services[elem[0]].callbacks[elem[1]] +} + +// subscription returns a subscription callback in the given service. +func (r *serviceRegistry) subscription(service, name string) *callback { + r.mu.Lock() + defer r.mu.Unlock() + return r.services[service].subscriptions[name] +} + +// suitableCallbacks iterates over the methods of the given type. It determines if a method +// satisfies the criteria for a RPC callback or a subscription callback and adds it to the +// collection of callbacks. See server documentation for a summary of these criteria. +func suitableCallbacks(receiver reflect.Value) map[string]*callback { + typ := receiver.Type() + callbacks := make(map[string]*callback) + for m := 0; m < typ.NumMethod(); m++ { + method := typ.Method(m) + if method.PkgPath != "" { + continue // method not exported + } + cb := newCallback(receiver, method.Func) + if cb == nil { + continue // function invalid + } + name := formatName(method.Name) + callbacks[name] = cb + } + return callbacks +} + +// newCallback turns fn (a function) into a callback object. It returns nil if the function +// is unsuitable as an RPC callback. +func newCallback(receiver, fn reflect.Value) *callback { + fntype := fn.Type() + c := &callback{fn: fn, rcvr: receiver, errPos: -1, isSubscribe: isPubSub(fntype)} + // Determine parameter types. They must all be exported or builtin types. + c.makeArgTypes() + + // Verify return types. The function must return at most one error + // and/or one other non-error value. + outs := make([]reflect.Type, fntype.NumOut()) + for i := 0; i < fntype.NumOut(); i++ { + outs[i] = fntype.Out(i) + } + if len(outs) > 2 { + return nil + } + // If an error is returned, it must be the last returned value. + switch { + case len(outs) == 1 && isErrorType(outs[0]): + c.errPos = 0 + case len(outs) == 2: + if isErrorType(outs[0]) || !isErrorType(outs[1]) { + return nil + } + c.errPos = 1 + } + return c +} + +// makeArgTypes composes the argTypes list. +func (c *callback) makeArgTypes() { + fntype := c.fn.Type() + // Skip receiver and context.Context parameter (if present). + firstArg := 0 + if c.rcvr.IsValid() { + firstArg++ + } + if fntype.NumIn() > firstArg && fntype.In(firstArg) == contextType { + c.hasCtx = true + firstArg++ + } + // Add all remaining parameters. + c.argTypes = make([]reflect.Type, fntype.NumIn()-firstArg) + for i := firstArg; i < fntype.NumIn(); i++ { + c.argTypes[i-firstArg] = fntype.In(i) + } +} + +// call invokes the callback. +func (c *callback) call(ctx context.Context, method string, args []reflect.Value) (res interface{}, errRes error) { + // Create the argument slice. + fullargs := make([]reflect.Value, 0, 2+len(args)) + if c.rcvr.IsValid() { + fullargs = append(fullargs, c.rcvr) + } + if c.hasCtx { + fullargs = append(fullargs, reflect.ValueOf(ctx)) + } + fullargs = append(fullargs, args...) + + // Catch panic while running the callback. + defer func() { + if err := recover(); err != nil { + const size = 64 << 10 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + log.Error("RPC method " + method + " crashed: " + fmt.Sprintf("%v\n%s", err, buf)) + errRes = &internalServerError{errcodePanic, "method handler crashed"} + } + }() + // Run the callback. + results := c.fn.Call(fullargs) + if len(results) == 0 { + return nil, nil //nolint:nilnil + } + if c.errPos >= 0 && !results[c.errPos].IsNil() { + // Method has returned non-nil error value. + err := results[c.errPos].Interface().(error) + return reflect.Value{}, err + } + return results[0].Interface(), nil +} + +// Does t satisfy the error interface? +func isErrorType(t reflect.Type) bool { + return t.Implements(errorType) +} + +// Is t Subscription or *Subscription? +func isSubscriptionType(t reflect.Type) bool { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + return t == subscriptionType +} + +// isPubSub tests whether the given method has as as first argument a context.Context and +// returns the pair (Subscription, error). +func isPubSub(methodType reflect.Type) bool { + // numIn(0) is the receiver type + if methodType.NumIn() < 2 || methodType.NumOut() != 2 { + return false + } + return methodType.In(1) == contextType && + isSubscriptionType(methodType.Out(0)) && + isErrorType(methodType.Out(1)) +} + +// formatName converts to first character of name to lowercase. +func formatName(name string) string { + ret := []rune(name) + if len(ret) > 0 { + ret[0] = unicode.ToLower(ret[0]) + } + return string(ret) +} diff --git a/lib/gethfork/rpc/stdio.go b/lib/gethfork/rpc/stdio.go new file mode 100644 index 0000000000..f84452a977 --- /dev/null +++ b/lib/gethfork/rpc/stdio.go @@ -0,0 +1,71 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "errors" + "io" + "net" + "os" + "time" +) + +// DialStdIO creates a client on stdin/stdout. +func DialStdIO(ctx context.Context) (*Client, error) { + return DialIO(ctx, os.Stdin, os.Stdout) +} + +// DialIO creates a client which uses the given IO channels +func DialIO(ctx context.Context, in io.Reader, out io.Writer) (*Client, error) { + cfg := new(clientConfig) + return newClient(ctx, cfg, newClientTransportIO(in, out)) +} + +func newClientTransportIO(in io.Reader, out io.Writer) reconnectFunc { + return func(context.Context) (ServerCodec, error) { + return NewCodec(stdioConn{ + in: in, + out: out, + }), nil + } +} + +type stdioConn struct { + in io.Reader + out io.Writer +} + +func (io stdioConn) Read(b []byte) (n int, err error) { + return io.in.Read(b) +} + +func (io stdioConn) Write(b []byte) (n int, err error) { + return io.out.Write(b) +} + +func (io stdioConn) Close() error { + return nil +} + +func (io stdioConn) RemoteAddr() string { + return "/dev/stdin" +} + +func (io stdioConn) SetWriteDeadline(_ time.Time) error { + return &net.OpError{Op: "set", Net: "stdio", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} +} diff --git a/lib/gethfork/rpc/subscription.go b/lib/gethfork/rpc/subscription.go new file mode 100644 index 0000000000..c7fbc6c8c2 --- /dev/null +++ b/lib/gethfork/rpc/subscription.go @@ -0,0 +1,387 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "container/list" + "context" + crand "crypto/rand" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "math/rand" + "reflect" + "strings" + "sync" + "time" +) + +var ( + // ErrNotificationsUnsupported is returned by the client when the connection doesn't + // support notifications. You can use this error value to check for subscription + // support like this: + // + // sub, err := client.EthSubscribe(ctx, channel, "newHeads", true) + // if errors.Is(err, rpc.ErrNotificationsUnsupported) { + // // Server does not support subscriptions, fall back to polling. + // } + // + ErrNotificationsUnsupported = notificationsUnsupportedError{} + + // ErrSubscriptionNotFound is returned when the notification for the given id is not found + ErrSubscriptionNotFound = errors.New("subscription not found") +) + +var globalGen = randomIDGenerator() + +// ID defines a pseudo random number that is used to identify RPC subscriptions. +type ID string + +// NewID returns a new, random ID. +func NewID() ID { + return globalGen() +} + +// randomIDGenerator returns a function generates a random IDs. +func randomIDGenerator() func() ID { + buf := make([]byte, 8) + var seed int64 + if _, err := crand.Read(buf); err == nil { + seed = int64(binary.BigEndian.Uint64(buf)) + } else { + seed = int64(time.Now().Nanosecond()) + } + + var ( + mu sync.Mutex + rng = rand.New(rand.NewSource(seed)) + ) + return func() ID { + mu.Lock() + defer mu.Unlock() + id := make([]byte, 16) + rng.Read(id) + return encodeID(id) + } +} + +func encodeID(b []byte) ID { + id := hex.EncodeToString(b) + id = strings.TrimLeft(id, "0") + if id == "" { + id = "0" // ID's are RPC quantities, no leading zero's and 0 is 0x0. + } + return ID("0x" + id) +} + +type notifierKey struct{} + +// NotifierFromContext returns the Notifier value stored in ctx, if any. +func NotifierFromContext(ctx context.Context) (*Notifier, bool) { + n, ok := ctx.Value(notifierKey{}).(*Notifier) + return n, ok +} + +// Notifier is tied to a RPC connection that supports subscriptions. +// Server callbacks use the notifier to send notifications. +type Notifier struct { + h *handler + UserID string // added by TEN + namespace string + + mu sync.Mutex + sub *Subscription + buffer []json.RawMessage + callReturned bool + activated bool +} + +// CreateSubscription returns a new subscription that is coupled to the +// RPC connection. By default subscriptions are inactive and notifications +// are dropped until the subscription is marked as active. This is done +// by the RPC server after the subscription ID is send to the client. +func (n *Notifier) CreateSubscription() *Subscription { + n.mu.Lock() + defer n.mu.Unlock() + + if n.sub != nil { + panic("can't create multiple subscriptions with Notifier") + } else if n.callReturned { + panic("can't create subscription after subscribe call has returned") + } + n.sub = &Subscription{ID: n.h.idgen(), namespace: n.namespace, err: make(chan error, 1)} + return n.sub +} + +// Notify sends a notification to the client with the given data as payload. +// If an error occurs the RPC connection is closed and the error is returned. +func (n *Notifier) Notify(id ID, data interface{}) error { + enc, err := json.Marshal(data) + if err != nil { + return err + } + + n.mu.Lock() + defer n.mu.Unlock() + + if n.sub == nil { + panic("can't Notify before subscription is created") + } else if n.sub.ID != id { + panic("Notify with wrong ID") + } + if n.activated { + return n.send(n.sub, enc) + } + n.buffer = append(n.buffer, enc) + return nil +} + +// Closed returns a channel that is closed when the RPC connection is closed. +// Deprecated: use subscription error channel +func (n *Notifier) Closed() <-chan interface{} { + return n.h.conn.closed() +} + +// takeSubscription returns the subscription (if one has been created). No subscription can +// be created after this call. +func (n *Notifier) takeSubscription() *Subscription { + n.mu.Lock() + defer n.mu.Unlock() + n.callReturned = true + return n.sub +} + +// activate is called after the subscription ID was sent to client. Notifications are +// buffered before activation. This prevents notifications being sent to the client before +// the subscription ID is sent to the client. +func (n *Notifier) activate() error { + n.mu.Lock() + defer n.mu.Unlock() + + for _, data := range n.buffer { + if err := n.send(n.sub, data); err != nil { + return err + } + } + n.activated = true + return nil +} + +func (n *Notifier) send(sub *Subscription, data json.RawMessage) error { + params, _ := json.Marshal(&subscriptionResult{ID: string(sub.ID), Result: data}) + ctx := context.Background() + + msg := &jsonrpcMessage{ + Version: vsn, + Method: n.namespace + notificationMethodSuffix, + Params: params, + } + return n.h.conn.writeJSON(ctx, msg, false) +} + +// A Subscription is created by a notifier and tied to that notifier. The client can use +// this subscription to wait for an unsubscribe request for the client, see Err(). +type Subscription struct { + ID ID + namespace string + err chan error // closed on unsubscribe +} + +// Err returns a channel that is closed when the client send an unsubscribe request. +func (s *Subscription) Err() <-chan error { + return s.err +} + +// MarshalJSON marshals a subscription as its ID. +func (s *Subscription) MarshalJSON() ([]byte, error) { + return json.Marshal(s.ID) +} + +// ClientSubscription is a subscription established through the Client's Subscribe or +// EthSubscribe methods. +type ClientSubscription struct { + client *Client + etype reflect.Type + channel reflect.Value + namespace string + subid string + + // The in channel receives notification values from client dispatcher. + in chan json.RawMessage + + // The error channel receives the error from the forwarding loop. + // It is closed by Unsubscribe. + err chan error + errOnce sync.Once + + // Closing of the subscription is requested by sending on 'quit'. This is handled by + // the forwarding loop, which closes 'forwardDone' when it has stopped sending to + // sub.channel. Finally, 'unsubDone' is closed after unsubscribing on the server side. + quit chan error + forwardDone chan struct{} + unsubDone chan struct{} +} + +// This is the sentinel value sent on sub.quit when Unsubscribe is called. +var errUnsubscribed = errors.New("unsubscribed") + +func newClientSubscription(c *Client, namespace string, channel reflect.Value) *ClientSubscription { + sub := &ClientSubscription{ + client: c, + namespace: namespace, + etype: channel.Type().Elem(), + channel: channel, + in: make(chan json.RawMessage), + quit: make(chan error), + forwardDone: make(chan struct{}), + unsubDone: make(chan struct{}), + err: make(chan error, 1), + } + return sub +} + +// Err returns the subscription error channel. The intended use of Err is to schedule +// resubscription when the client connection is closed unexpectedly. +// +// The error channel receives a value when the subscription has ended due to an error. The +// received error is nil if Close has been called on the underlying client and no other +// error has occurred. +// +// The error channel is closed when Unsubscribe is called on the subscription. +func (sub *ClientSubscription) Err() <-chan error { + return sub.err +} + +// Unsubscribe unsubscribes the notification and closes the error channel. +// It can safely be called more than once. +func (sub *ClientSubscription) Unsubscribe() { + sub.errOnce.Do(func() { + select { + case sub.quit <- errUnsubscribed: + <-sub.unsubDone + case <-sub.unsubDone: + } + close(sub.err) + }) +} + +// deliver is called by the client's message dispatcher to send a notification value. +func (sub *ClientSubscription) deliver(result json.RawMessage) (ok bool) { + select { + case sub.in <- result: + return true + case <-sub.forwardDone: + return false + } +} + +// close is called by the client's message dispatcher when the connection is closed. +func (sub *ClientSubscription) close(err error) { + select { + case sub.quit <- err: + case <-sub.forwardDone: + } +} + +// run is the forwarding loop of the subscription. It runs in its own goroutine and +// is launched by the client's handler after the subscription has been created. +func (sub *ClientSubscription) run() { + defer close(sub.unsubDone) + + unsubscribe, err := sub.forward() + + // The client's dispatch loop won't be able to execute the unsubscribe call if it is + // blocked in sub.deliver() or sub.close(). Closing forwardDone unblocks them. + close(sub.forwardDone) + + // Call the unsubscribe method on the server. + if unsubscribe { + _ = sub.requestUnsubscribe() + } + + // Send the error. + if err != nil { + if errors.Is(err, ErrClientQuit) { + // ErrClientQuit gets here when Client.Close is called. This is reported as a + // nil error because it's not an error, but we can't close sub.err here. + err = nil + } + sub.err <- err + } +} + +// forward is the forwarding loop. It takes in RPC notifications and sends them +// on the subscription channel. +func (sub *ClientSubscription) forward() (unsubscribeServer bool, err error) { + cases := []reflect.SelectCase{ + {Dir: reflect.SelectRecv, Chan: reflect.ValueOf(sub.quit)}, + {Dir: reflect.SelectRecv, Chan: reflect.ValueOf(sub.in)}, + {Dir: reflect.SelectSend, Chan: sub.channel}, + } + buffer := list.New() + + for { + var chosen int + var recv reflect.Value + if buffer.Len() == 0 { + // Idle, omit send case. + chosen, recv, _ = reflect.Select(cases[:2]) + } else { + // Non-empty buffer, send the first queued item. + cases[2].Send = reflect.ValueOf(buffer.Front().Value) + chosen, recv, _ = reflect.Select(cases) + } + + switch chosen { + case 0: // <-sub.quit + if !recv.IsNil() { + err = recv.Interface().(error) + } + if errors.Is(err, errUnsubscribed) { + // Exiting because Unsubscribe was called, unsubscribe on server. + return true, nil + } + return false, err + + case 1: // <-sub.in + val, err := sub.unmarshal(recv.Interface().(json.RawMessage)) + if err != nil { + return true, err + } + if buffer.Len() == maxClientSubscriptionBuffer { + return true, ErrSubscriptionQueueOverflow + } + buffer.PushBack(val) + + case 2: // sub.channel<- + cases[2].Send = reflect.Value{} // Don't hold onto the value. + buffer.Remove(buffer.Front()) + } + } +} + +func (sub *ClientSubscription) unmarshal(result json.RawMessage) (interface{}, error) { + val := reflect.New(sub.etype) + err := json.Unmarshal(result, val.Interface()) + return val.Elem().Interface(), err +} + +func (sub *ClientSubscription) requestUnsubscribe() error { + var result interface{} + return sub.client.Call(&result, sub.namespace+unsubscribeMethodSuffix, sub.subid) +} diff --git a/lib/gethfork/rpc/types.go b/lib/gethfork/rpc/types.go new file mode 100644 index 0000000000..34a1451dea --- /dev/null +++ b/lib/gethfork/rpc/types.go @@ -0,0 +1,253 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "encoding/json" + "fmt" + "math" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// API describes the set of methods offered over the RPC interface +type API struct { + Namespace string // namespace under which the rpc methods of Service are exposed + Version string // deprecated - this field is no longer used, but retained for compatibility + Service interface{} // receiver instance which holds the methods + Public bool // deprecated - this field is no longer used, but retained for compatibility + Authenticated bool // whether the api should only be available behind authentication. +} + +// ServerCodec implements reading, parsing and writing RPC messages for the server side of +// a RPC session. Implementations must be go-routine safe since the codec can be called in +// multiple go-routines concurrently. +type ServerCodec interface { + peerInfo() PeerInfo + readBatch() (msgs []*jsonrpcMessage, isBatch bool, err error) + close() + + jsonWriter +} + +// jsonWriter can write JSON messages to its underlying connection. +// Implementations must be safe for concurrent use. +type jsonWriter interface { + // writeJSON writes a message to the connection. + writeJSON(ctx context.Context, msg interface{}, isError bool) error + + // Closed returns a channel which is closed when the connection is closed. + closed() <-chan interface{} + // RemoteAddr returns the peer address of the connection. + remoteAddr() string +} + +type BlockNumber int64 + +const ( + SafeBlockNumber = BlockNumber(-4) + FinalizedBlockNumber = BlockNumber(-3) + LatestBlockNumber = BlockNumber(-2) + PendingBlockNumber = BlockNumber(-1) + EarliestBlockNumber = BlockNumber(0) +) + +// UnmarshalJSON parses the given JSON fragment into a BlockNumber. It supports: +// - "safe", "finalized", "latest", "earliest" or "pending" as string arguments +// - the block number +// Returned errors: +// - an invalid block number error when the given argument isn't a known strings +// - an out of range error when the given block number is either too little or too large +func (bn *BlockNumber) UnmarshalJSON(data []byte) error { + input := strings.TrimSpace(string(data)) + if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' { + input = input[1 : len(input)-1] + } + + switch input { + case "earliest": + *bn = EarliestBlockNumber + return nil + case "latest": + *bn = LatestBlockNumber + return nil + case "pending": + *bn = PendingBlockNumber + return nil + case "finalized": + *bn = FinalizedBlockNumber + return nil + case "safe": + *bn = SafeBlockNumber + return nil + } + + blckNum, err := hexutil.DecodeUint64(input) + if err != nil { + return err + } + if blckNum > math.MaxInt64 { + return fmt.Errorf("block number larger than int64") + } + *bn = BlockNumber(blckNum) + return nil +} + +// Int64 returns the block number as int64. +func (bn BlockNumber) Int64() int64 { + return (int64)(bn) +} + +// MarshalText implements encoding.TextMarshaler. It marshals: +// - "safe", "finalized", "latest", "earliest" or "pending" as strings +// - other numbers as hex +func (bn BlockNumber) MarshalText() ([]byte, error) { + return []byte(bn.String()), nil +} + +func (bn BlockNumber) String() string { + switch bn { + case EarliestBlockNumber: + return "earliest" + case LatestBlockNumber: + return "latest" + case PendingBlockNumber: + return "pending" + case FinalizedBlockNumber: + return "finalized" + case SafeBlockNumber: + return "safe" + default: + if bn < 0 { + return fmt.Sprintf("", bn) + } + return hexutil.Uint64(bn).String() + } +} + +type BlockNumberOrHash struct { + BlockNumber *BlockNumber `json:"blockNumber,omitempty"` + BlockHash *common.Hash `json:"blockHash,omitempty"` + RequireCanonical bool `json:"requireCanonical,omitempty"` +} + +func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error { + type erased BlockNumberOrHash + e := erased{} + err := json.Unmarshal(data, &e) + if err == nil { + if e.BlockNumber != nil && e.BlockHash != nil { + return fmt.Errorf("cannot specify both BlockHash and BlockNumber, choose one or the other") + } + bnh.BlockNumber = e.BlockNumber + bnh.BlockHash = e.BlockHash + bnh.RequireCanonical = e.RequireCanonical + return nil + } + var input string + err = json.Unmarshal(data, &input) + if err != nil { + return err + } + switch input { + case "earliest": + bn := EarliestBlockNumber + bnh.BlockNumber = &bn + return nil + case "latest": + bn := LatestBlockNumber + bnh.BlockNumber = &bn + return nil + case "pending": + bn := PendingBlockNumber + bnh.BlockNumber = &bn + return nil + case "finalized": + bn := FinalizedBlockNumber + bnh.BlockNumber = &bn + return nil + case "safe": + bn := SafeBlockNumber + bnh.BlockNumber = &bn + return nil + default: + if len(input) == 66 { + hash := common.Hash{} + err := hash.UnmarshalText([]byte(input)) + if err != nil { + return err + } + bnh.BlockHash = &hash + return nil + } else { + blckNum, err := hexutil.DecodeUint64(input) + if err != nil { + return err + } + if blckNum > math.MaxInt64 { + return fmt.Errorf("blocknumber too high") + } + bn := BlockNumber(blckNum) + bnh.BlockNumber = &bn + return nil + } + } +} + +func (bnh *BlockNumberOrHash) Number() (BlockNumber, bool) { + if bnh.BlockNumber != nil { + return *bnh.BlockNumber, true + } + return BlockNumber(0), false +} + +func (bnh *BlockNumberOrHash) String() string { + if bnh.BlockNumber != nil { + return strconv.Itoa(int(*bnh.BlockNumber)) + } + if bnh.BlockHash != nil { + return bnh.BlockHash.String() + } + return "nil" +} + +func (bnh *BlockNumberOrHash) Hash() (common.Hash, bool) { + if bnh.BlockHash != nil { + return *bnh.BlockHash, true + } + return common.Hash{}, false +} + +func BlockNumberOrHashWithNumber(blockNr BlockNumber) BlockNumberOrHash { + return BlockNumberOrHash{ + BlockNumber: &blockNr, + BlockHash: nil, + RequireCanonical: false, + } +} + +func BlockNumberOrHashWithHash(hash common.Hash, canonical bool) BlockNumberOrHash { + return BlockNumberOrHash{ + BlockNumber: nil, + BlockHash: &hash, + RequireCanonical: canonical, + } +} diff --git a/lib/gethfork/rpc/websocket.go b/lib/gethfork/rpc/websocket.go new file mode 100644 index 0000000000..605931c47d --- /dev/null +++ b/lib/gethfork/rpc/websocket.go @@ -0,0 +1,380 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "os" + "strings" + "sync" + "time" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/ethereum/go-ethereum/log" + "github.com/gorilla/websocket" +) + +const ( + wsReadBuffer = 1024 + wsWriteBuffer = 1024 + wsPingInterval = 30 * time.Second + wsPingWriteTimeout = 5 * time.Second + wsPongTimeout = 30 * time.Second + wsDefaultReadLimit = 32 * 1024 * 1024 +) + +var wsBufferPool = new(sync.Pool) + +// WebsocketHandler returns a handler that serves JSON-RPC to WebSocket connections. +// +// allowedOrigins should be a comma-separated list of allowed origin URLs. +// To allow connections with any origin, pass "*". +func (s *Server) WebsocketHandler(allowedOrigins []string) http.Handler { + upgrader := websocket.Upgrader{ + ReadBufferSize: wsReadBuffer, + WriteBufferSize: wsWriteBuffer, + WriteBufferPool: wsBufferPool, + CheckOrigin: wsHandshakeValidator(allowedOrigins), + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Debug("WebSocket upgrade failed", "err", err) + return + } + codec := newWebsocketCodec(conn, r.Host, r.Header, wsDefaultReadLimit) + s.ServeCodec(codec, 0, extractUserID(r.Context())) + }) +} + +func extractUserID(ctx context.Context) string { + token, ok := ctx.Value(GWTokenKey{}).(string) + if !ok { + return "" + } + return token +} + +// wsHandshakeValidator returns a handler that verifies the origin during the +// websocket upgrade process. When a '*' is specified as an allowed origins all +// connections are accepted. +func wsHandshakeValidator(allowedOrigins []string) func(*http.Request) bool { + origins := mapset.NewSet[string]() + allowAllOrigins := false + + for _, origin := range allowedOrigins { + if origin == "*" { + allowAllOrigins = true + } + if origin != "" { + origins.Add(origin) + } + } + // allow localhost if no allowedOrigins are specified. + if len(origins.ToSlice()) == 0 { + origins.Add("http://localhost") + if hostname, err := os.Hostname(); err == nil { + origins.Add("http://" + hostname) + } + } + log.Debug(fmt.Sprintf("Allowed origin(s) for WS RPC interface %v", origins.ToSlice())) + + f := func(req *http.Request) bool { + // Skip origin verification if no Origin header is present. The origin check + // is supposed to protect against browser based attacks. Browsers always set + // Origin. Non-browser software can put anything in origin and checking it doesn't + // provide additional security. + if _, ok := req.Header["Origin"]; !ok { + return true + } + // Verify origin against allow list. + origin := strings.ToLower(req.Header.Get("Origin")) + if allowAllOrigins || originIsAllowed(origins, origin) { + return true + } + log.Warn("Rejected WebSocket connection", "origin", origin) + return false + } + + return f +} + +type wsHandshakeError struct { + err error + status string +} + +func (e wsHandshakeError) Error() string { + s := e.err.Error() + if e.status != "" { + s += " (HTTP status " + e.status + ")" + } + return s +} + +func originIsAllowed(allowedOrigins mapset.Set[string], browserOrigin string) bool { + it := allowedOrigins.Iterator() + for origin := range it.C { + if ruleAllowsOrigin(origin, browserOrigin) { + return true + } + } + return false +} + +func ruleAllowsOrigin(allowedOrigin string, browserOrigin string) bool { + var ( + allowedScheme, allowedHostname, allowedPort string + browserScheme, browserHostname, browserPort string + err error + ) + allowedScheme, allowedHostname, allowedPort, err = parseOriginURL(allowedOrigin) + if err != nil { + log.Warn("Error parsing allowed origin specification", "spec", allowedOrigin, "error", err) + return false + } + browserScheme, browserHostname, browserPort, err = parseOriginURL(browserOrigin) + if err != nil { + log.Warn("Error parsing browser 'Origin' field", "Origin", browserOrigin, "error", err) + return false + } + if allowedScheme != "" && allowedScheme != browserScheme { + return false + } + if allowedHostname != "" && allowedHostname != browserHostname { + return false + } + if allowedPort != "" && allowedPort != browserPort { + return false + } + return true +} + +func parseOriginURL(origin string) (string, string, string, error) { + parsedURL, err := url.Parse(strings.ToLower(origin)) + if err != nil { + return "", "", "", err + } + var scheme, hostname, port string + if strings.Contains(origin, "://") { + scheme = parsedURL.Scheme + hostname = parsedURL.Hostname() + port = parsedURL.Port() + } else { + scheme = "" + hostname = parsedURL.Scheme + port = parsedURL.Opaque + if hostname == "" { + hostname = origin + } + } + return scheme, hostname, port, nil +} + +// DialWebsocketWithDialer creates a new RPC client using WebSocket. +// +// The context is used for the initial connection establishment. It does not +// affect subsequent interactions with the client. +// +// Deprecated: use DialOptions and the WithWebsocketDialer option. +func DialWebsocketWithDialer(ctx context.Context, endpoint, origin string, dialer websocket.Dialer) (*Client, error) { + cfg := new(clientConfig) + cfg.wsDialer = &dialer + if origin != "" { + cfg.setHeader("origin", origin) + } + connect, err := newClientTransportWS(endpoint, cfg) + if err != nil { + return nil, err + } + return newClient(ctx, cfg, connect) +} + +// DialWebsocket creates a new RPC client that communicates with a JSON-RPC server +// that is listening on the given endpoint. +// +// The context is used for the initial connection establishment. It does not +// affect subsequent interactions with the client. +func DialWebsocket(ctx context.Context, endpoint, origin string) (*Client, error) { + cfg := new(clientConfig) + if origin != "" { + cfg.setHeader("origin", origin) + } + connect, err := newClientTransportWS(endpoint, cfg) + if err != nil { + return nil, err + } + return newClient(ctx, cfg, connect) +} + +func newClientTransportWS(endpoint string, cfg *clientConfig) (reconnectFunc, error) { + dialer := cfg.wsDialer + if dialer == nil { + dialer = &websocket.Dialer{ + ReadBufferSize: wsReadBuffer, + WriteBufferSize: wsWriteBuffer, + WriteBufferPool: wsBufferPool, + Proxy: http.ProxyFromEnvironment, + } + } + + dialURL, header, err := wsClientHeaders(endpoint, "") + if err != nil { + return nil, err + } + for key, values := range cfg.httpHeaders { + header[key] = values + } + + connect := func(ctx context.Context) (ServerCodec, error) { + header := header.Clone() + if cfg.httpAuth != nil { + if err := cfg.httpAuth(header); err != nil { + return nil, err + } + } + conn, resp, err := dialer.DialContext(ctx, dialURL, header) + if err != nil { + hErr := wsHandshakeError{err: err} + if resp != nil { + hErr.status = resp.Status + } + return nil, hErr + } + messageSizeLimit := int64(wsDefaultReadLimit) + if cfg.wsMessageSizeLimit != nil && *cfg.wsMessageSizeLimit >= 0 { + messageSizeLimit = *cfg.wsMessageSizeLimit + } + return newWebsocketCodec(conn, dialURL, header, messageSizeLimit), nil + } + return connect, nil +} + +func wsClientHeaders(endpoint, origin string) (string, http.Header, error) { + endpointURL, err := url.Parse(endpoint) + if err != nil { + return endpoint, nil, err + } + header := make(http.Header) + if origin != "" { + header.Add("origin", origin) + } + if endpointURL.User != nil { + b64auth := base64.StdEncoding.EncodeToString([]byte(endpointURL.User.String())) + header.Add("authorization", "Basic "+b64auth) + endpointURL.User = nil + } + return endpointURL.String(), header, nil +} + +type websocketCodec struct { + *jsonCodec + conn *websocket.Conn + info PeerInfo + + wg sync.WaitGroup + pingReset chan struct{} + pongReceived chan struct{} +} + +func newWebsocketCodec(conn *websocket.Conn, host string, req http.Header, readLimit int64) ServerCodec { + conn.SetReadLimit(readLimit) + encode := func(v interface{}, isErrorResponse bool) error { + return conn.WriteJSON(v) + } + wc := &websocketCodec{ + jsonCodec: NewFuncCodec(conn, encode, conn.ReadJSON).(*jsonCodec), + conn: conn, + pingReset: make(chan struct{}, 1), + pongReceived: make(chan struct{}), + info: PeerInfo{ + Transport: "ws", + RemoteAddr: conn.RemoteAddr().String(), + }, + } + // Fill in connection details. + wc.info.HTTP.Host = host + wc.info.HTTP.Origin = req.Get("Origin") + wc.info.HTTP.UserAgent = req.Get("User-Agent") + // Start pinger. + conn.SetPongHandler(func(appData string) error { + select { + case wc.pongReceived <- struct{}{}: + case <-wc.closed(): + } + return nil + }) + wc.wg.Add(1) + go wc.pingLoop() + return wc +} + +func (wc *websocketCodec) close() { + wc.jsonCodec.close() + wc.wg.Wait() +} + +func (wc *websocketCodec) peerInfo() PeerInfo { + return wc.info +} + +func (wc *websocketCodec) writeJSON(ctx context.Context, v interface{}, isError bool) error { + err := wc.jsonCodec.writeJSON(ctx, v, isError) + if err == nil { + // Notify pingLoop to delay the next idle ping. + select { + case wc.pingReset <- struct{}{}: + default: + } + } + return err +} + +// pingLoop sends periodic ping frames when the connection is idle. +func (wc *websocketCodec) pingLoop() { + pingTimer := time.NewTimer(wsPingInterval) + defer wc.wg.Done() + defer pingTimer.Stop() + + for { + select { + case <-wc.closed(): + return + + case <-wc.pingReset: + if !pingTimer.Stop() { + <-pingTimer.C + } + pingTimer.Reset(wsPingInterval) + + case <-pingTimer.C: + wc.jsonCodec.encMu.Lock() + wc.conn.SetWriteDeadline(time.Now().Add(wsPingWriteTimeout)) + wc.conn.WriteMessage(websocket.PingMessage, nil) + wc.conn.SetReadDeadline(time.Now().Add(wsPongTimeout)) + wc.jsonCodec.encMu.Unlock() + pingTimer.Reset(wsPingInterval) + + case <-wc.pongReceived: + wc.conn.SetReadDeadline(time.Time{}) + } + } +} diff --git a/tools/walletextension/common/responses.go b/tools/walletextension/common/responses.go index 9b0afeaed4..65b6d77c2f 100644 --- a/tools/walletextension/common/responses.go +++ b/tools/walletextension/common/responses.go @@ -3,7 +3,7 @@ package common import ( "errors" - gethrpc "github.com/ethereum/go-ethereum/rpc" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) func CraftErrorResponse(err error) map[string]interface{} { diff --git a/tools/walletextension/subscriptions/subscriptions.go b/tools/walletextension/subscriptions/subscriptions.go index 74bbe1de95..7d3fe3f7a4 100644 --- a/tools/walletextension/subscriptions/subscriptions.go +++ b/tools/walletextension/subscriptions/subscriptions.go @@ -10,10 +10,10 @@ import ( "github.com/go-kit/kit/transport/http/jsonrpc" gethlog "github.com/ethereum/go-ethereum/log" - gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common" "github.com/ten-protocol/go-ten/go/common/log" "github.com/ten-protocol/go-ten/go/rpc" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" wecommon "github.com/ten-protocol/go-ten/tools/walletextension/common" "github.com/ten-protocol/go-ten/tools/walletextension/userconn" ) diff --git a/tools/walletextension/test/apis.go b/tools/walletextension/test/apis.go index 8d0a0a5bb4..50f1cfe4b5 100644 --- a/tools/walletextension/test/apis.go +++ b/tools/walletextension/test/apis.go @@ -15,10 +15,10 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/ecies" "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/rpc" "github.com/ten-protocol/go-ten/go/common" "github.com/ten-protocol/go-ten/go/enclave/vkhandler" "github.com/ten-protocol/go-ten/go/responses" + "github.com/ten-protocol/go-ten/lib/gethfork/rpc" gethcommon "github.com/ethereum/go-ethereum/common" ) diff --git a/tools/walletextension/test/utils.go b/tools/walletextension/test/utils.go index 7c5d2420cd..25f8a7444b 100644 --- a/tools/walletextension/test/utils.go +++ b/tools/walletextension/test/utils.go @@ -25,8 +25,8 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" gethlog "github.com/ethereum/go-ethereum/log" gethnode "github.com/ethereum/go-ethereum/node" - gethrpc "github.com/ethereum/go-ethereum/rpc" hostcontainer "github.com/ten-protocol/go-ten/go/host/container" + gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) const jsonID = "1" From bb3410d31ed4df0a5e3dbab66b3ee5a5e508000a Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Tue, 19 Mar 2024 12:15:37 +0000 Subject: [PATCH 2/5] clarifications --- lib/gethfork/README.MD | 1 + lib/gethfork/rpc/http.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/lib/gethfork/README.MD b/lib/gethfork/README.MD index d03b1f19c0..3c4193ff5a 100644 --- a/lib/gethfork/README.MD +++ b/lib/gethfork/README.MD @@ -8,3 +8,4 @@ The RPC URL to the Ten Gateway contains a "token" URL parameter kept secret by e 2. From the Context, it can be read by the http rpc handler 3. For WS, we need to add it to a couple of more objects to make it available during processing. 4. Removed unnecessary elements from the "node", such as p2p, database, etc +5. Enable full duplex to enable keep-alive connections \ No newline at end of file diff --git a/lib/gethfork/rpc/http.go b/lib/gethfork/rpc/http.go index ac8fee6a3c..0bab94be5f 100644 --- a/lib/gethfork/rpc/http.go +++ b/lib/gethfork/rpc/http.go @@ -332,8 +332,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", contentType) codec := s.newHTTPServerConn(r, w) defer codec.close() + + // added by TEN to support keep-alive rc := http.NewResponseController(w) _ = rc.EnableFullDuplex() + s.serveSingleRequest(ctx, codec) } From 8e6b236df7c8f4656058f47912bbe84663994cb0 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Tue, 19 Mar 2024 12:19:53 +0000 Subject: [PATCH 3/5] lint --- lib/gethfork/rpc/json.go | 11 ----------- tools/walletextension/test/apis.go | 2 +- tools/walletextension/test/utils.go | 5 +++-- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/gethfork/rpc/json.go b/lib/gethfork/rpc/json.go index 8c1668623a..f815adc313 100644 --- a/lib/gethfork/rpc/json.go +++ b/lib/gethfork/rpc/json.go @@ -46,17 +46,6 @@ type subscriptionResult struct { Result json.RawMessage `json:"result,omitempty"` } -type subscriptionResultEnc struct { - ID string `json:"subscription"` - Result any `json:"result"` -} - -type jsonrpcSubscriptionNotification struct { - Version string `json:"jsonrpc"` - Method string `json:"method"` - Params subscriptionResultEnc `json:"params"` -} - // A value of this type can a JSON-RPC request, notification, successful response or // error response. Which one it is depends on the fields. type jsonrpcMessage struct { diff --git a/tools/walletextension/test/apis.go b/tools/walletextension/test/apis.go index 50f1cfe4b5..b21fa499a9 100644 --- a/tools/walletextension/test/apis.go +++ b/tools/walletextension/test/apis.go @@ -1,4 +1,4 @@ -package test +package test //nolint:typecheck import ( "context" diff --git a/tools/walletextension/test/utils.go b/tools/walletextension/test/utils.go index 25f8a7444b..8e0f3001c6 100644 --- a/tools/walletextension/test/utils.go +++ b/tools/walletextension/test/utils.go @@ -12,6 +12,8 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/crypto" "github.com/go-kit/kit/transport/http/jsonrpc" @@ -26,7 +28,6 @@ import ( gethlog "github.com/ethereum/go-ethereum/log" gethnode "github.com/ethereum/go-ethereum/node" hostcontainer "github.com/ten-protocol/go-ten/go/host/container" - gethrpc "github.com/ten-protocol/go-ten/lib/gethfork/rpc" ) const jsonID = "1" @@ -69,7 +70,7 @@ func createDummyHost(t *testing.T, wsRPCPort int) (*DummyAPI, func() error) { // WSOrigins: []string{"*"}, } rpcServerNode, err := gethnode.New(&cfg) - rpcServerNode.RegisterAPIs([]gethrpc.API{ + rpcServerNode.RegisterAPIs([]rpc.API{ { Namespace: hostcontainer.APINamespaceObscuro, Version: hostcontainer.APIVersion1, From 52e320a264bb08a158f2126235500cc3b5221eb6 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Tue, 19 Mar 2024 13:07:29 +0000 Subject: [PATCH 4/5] lint --- lib/gethfork/node/rpc_server.go | 12 ++++++++---- tools/walletextension/api/routes.go | 16 ++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/gethfork/node/rpc_server.go b/lib/gethfork/node/rpc_server.go index c97d9ad17e..a9b3d30b21 100644 --- a/lib/gethfork/node/rpc_server.go +++ b/lib/gethfork/node/rpc_server.go @@ -3,8 +3,6 @@ package node import ( "net/http" - "github.com/ten-protocol/go-ten/tools/walletextension/api" - gethlog "github.com/ethereum/go-ethereum/log" "github.com/ten-protocol/go-ten/go/common/log" "github.com/ten-protocol/go-ten/lib/gethfork/rpc" @@ -27,12 +25,18 @@ type RPCConfig struct { ExposedURLParamNames []string } +// Route defines the path plus handler for a given path +type Route struct { + Name string + Func func(resp http.ResponseWriter, req *http.Request) +} + // Server manages the lifeycle of an RPC Server type Server interface { Start() error Stop() RegisterAPIs(apis []rpc.API) - RegisterRoutes(routes []api.Route) + RegisterRoutes(routes []Route) } // An implementation of `host.Server` that reuses the Geth `node` package for client communication. @@ -73,7 +77,7 @@ func (s *serverImpl) RegisterAPIs(apis []rpc.API) { s.node.RegisterAPIs(apis) } -func (s *serverImpl) RegisterRoutes(routes []api.Route) { +func (s *serverImpl) RegisterRoutes(routes []Route) { for _, route := range routes { s.node.RegisterHandler(route.Name, route.Name, http.HandlerFunc(route.Func)) } diff --git a/tools/walletextension/api/routes.go b/tools/walletextension/api/routes.go index da8aea31f5..829dc69a38 100644 --- a/tools/walletextension/api/routes.go +++ b/tools/walletextension/api/routes.go @@ -6,6 +6,8 @@ import ( "fmt" "net/http" + "github.com/ten-protocol/go-ten/lib/gethfork/node" + "github.com/ten-protocol/go-ten/go/common/log" "github.com/ten-protocol/go-ten/go/common/httputil" @@ -17,15 +19,9 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" ) -// Route defines the path plus handler for a given path -type Route struct { - Name string - Func func(resp http.ResponseWriter, req *http.Request) -} - // NewHTTPRoutes returns the http specific routes -func NewHTTPRoutes(walletExt *walletextension.WalletExtension) []Route { - return []Route{ +func NewHTTPRoutes(walletExt *walletextension.WalletExtension) []node.Route { + return []node.Route{ { Name: common.APIVersion1 + common.PathRoot, Func: httpHandler(walletExt, ethRequestHandler), @@ -95,8 +91,8 @@ func httpRequestHandler(walletExt *walletextension.WalletExtension, resp http.Re } // NewWSRoutes returns the WS specific routes -func NewWSRoutes(walletExt *walletextension.WalletExtension) []Route { - return []Route{ +func NewWSRoutes(walletExt *walletextension.WalletExtension) []node.Route { + return []node.Route{ { Name: common.PathRoot, Func: wsHandler(walletExt, ethRequestHandler), From b4b0064d94806143d05edcad36853c193174dcfd Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Tue, 19 Mar 2024 13:10:38 +0000 Subject: [PATCH 5/5] fix --- tools/walletextension/api/server.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tools/walletextension/api/server.go b/tools/walletextension/api/server.go index dc6b512e25..ae1fac8252 100644 --- a/tools/walletextension/api/server.go +++ b/tools/walletextension/api/server.go @@ -8,6 +8,8 @@ import ( "net/http" "time" + "github.com/ten-protocol/go-ten/lib/gethfork/node" + "github.com/ten-protocol/go-ten/tools/walletextension/common" ) @@ -39,20 +41,20 @@ func (s *Server) Stop() error { } // NewHTTPServer returns the HTTP server for the WE -func NewHTTPServer(address string, routes []Route) *Server { +func NewHTTPServer(address string, routes []node.Route) *Server { return &Server{ server: createHTTPServer(address, routes), } } // NewWSServer returns the WS server for the WE -func NewWSServer(address string, routes []Route) *Server { +func NewWSServer(address string, routes []node.Route) *Server { return &Server{ server: createWSServer(address, routes), } } -func createHTTPServer(address string, routes []Route) *http.Server { +func createHTTPServer(address string, routes []node.Route) *http.Server { serveMux := http.NewServeMux() // Handles Ethereum JSON-RPC requests received over HTTP. @@ -72,7 +74,7 @@ func createHTTPServer(address string, routes []Route) *http.Server { return server } -func createWSServer(address string, routes []Route) *http.Server { +func createWSServer(address string, routes []node.Route) *http.Server { serveMux := http.NewServeMux() // Handles Ethereum JSON-RPC requests received over HTTP.