From ba5ec1dc582fa93ca42f5c66567ede69bf23d50e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Sch=C3=B6ps=20Thiel=20=40IsabelSchoepd?= <155141998+IST-Github@users.noreply.github.com> Date: Thu, 4 Jan 2024 04:32:57 +0100 Subject: [PATCH] Delete node directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Isabel Schöps Thiel @IsabelSchoepd <155141998+IST-Github@users.noreply.github.com> --- node/api.go | 337 ---------------- node/api_test.go | 355 ---------------- node/config.go | 480 ---------------------- node/config_test.go | 153 ------- node/defaults.go | 133 ------ node/doc.go | 121 ------ node/endpoints.go | 91 ----- node/errors.go | 52 --- node/jwt_auth.go | 45 --- node/jwt_handler.go | 80 ---- node/lifecycle.go | 31 -- node/node.go | 822 -------------------------------------- node/node_auth_test.go | 237 ----------- node/node_example_test.go | 59 --- node/node_test.go | 638 ----------------------------- node/rpcstack.go | 644 ----------------------------- node/rpcstack_test.go | 616 ---------------------------- node/utils_test.go | 106 ----- 18 files changed, 5000 deletions(-) delete mode 100644 node/api.go delete mode 100644 node/api_test.go delete mode 100644 node/config.go delete mode 100644 node/config_test.go delete mode 100644 node/defaults.go delete mode 100644 node/doc.go delete mode 100644 node/endpoints.go delete mode 100644 node/errors.go delete mode 100644 node/jwt_auth.go delete mode 100644 node/jwt_handler.go delete mode 100644 node/lifecycle.go delete mode 100644 node/node.go delete mode 100644 node/node_auth_test.go delete mode 100644 node/node_example_test.go delete mode 100644 node/node_test.go delete mode 100644 node/rpcstack.go delete mode 100644 node/rpcstack_test.go delete mode 100644 node/utils_test.go diff --git a/node/api.go b/node/api.go deleted file mode 100644 index f81f394beb..0000000000 --- a/node/api.go +++ /dev/null @@ -1,337 +0,0 @@ -// 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 ( - "context" - "fmt" - "strings" - - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/internal/debug" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/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 -} - -// AddPeer requests connecting to a remote node, and also maintaining the new -// connection at all times, even reconnecting if it is lost. -func (api *adminAPI) AddPeer(url string) (bool, error) { - // Make sure the server is running, fail otherwise - server := api.node.Server() - if server == nil { - return false, ErrNodeStopped - } - // Try to add the url as a static peer and return - node, err := enode.Parse(enode.ValidSchemes, url) - if err != nil { - return false, fmt.Errorf("invalid enode: %v", err) - } - server.AddPeer(node) - return true, nil -} - -// RemovePeer disconnects from a remote node if the connection exists -func (api *adminAPI) RemovePeer(url string) (bool, error) { - // Make sure the server is running, fail otherwise - server := api.node.Server() - if server == nil { - return false, ErrNodeStopped - } - // Try to remove the url as a static peer and return - node, err := enode.Parse(enode.ValidSchemes, url) - if err != nil { - return false, fmt.Errorf("invalid enode: %v", err) - } - server.RemovePeer(node) - return true, nil -} - -// AddTrustedPeer allows a remote node to always connect, even if slots are full -func (api *adminAPI) AddTrustedPeer(url string) (bool, error) { - // Make sure the server is running, fail otherwise - server := api.node.Server() - if server == nil { - return false, ErrNodeStopped - } - node, err := enode.Parse(enode.ValidSchemes, url) - if err != nil { - return false, fmt.Errorf("invalid enode: %v", err) - } - server.AddTrustedPeer(node) - return true, nil -} - -// RemoveTrustedPeer removes a remote node from the trusted peer set, but it -// does not disconnect it automatically. -func (api *adminAPI) RemoveTrustedPeer(url string) (bool, error) { - // Make sure the server is running, fail otherwise - server := api.node.Server() - if server == nil { - return false, ErrNodeStopped - } - node, err := enode.Parse(enode.ValidSchemes, url) - if err != nil { - return false, fmt.Errorf("invalid enode: %v", err) - } - server.RemoveTrustedPeer(node) - return true, nil -} - -// PeerEvents creates an RPC subscription which receives peer events from the -// node's p2p.Server -func (api *adminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription, error) { - // Make sure the server is running, fail otherwise - server := api.node.Server() - if server == nil { - return nil, ErrNodeStopped - } - - // Create the subscription - notifier, supported := rpc.NotifierFromContext(ctx) - if !supported { - return nil, rpc.ErrNotificationsUnsupported - } - rpcSub := notifier.CreateSubscription() - - go func() { - events := make(chan *p2p.PeerEvent) - sub := server.SubscribeEvents(events) - defer sub.Unsubscribe() - - for { - select { - case event := <-events: - notifier.Notify(rpcSub.ID, event) - case <-sub.Err(): - return - case <-rpcSub.Err(): - return - case <-notifier.Closed(): - return - } - } - }() - - return rpcSub, nil -} - -// 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, - }, - } - 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, - }, - } - 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 -} - -// Peers retrieves all the information we know about each individual peer at the -// protocol granularity. -func (api *adminAPI) Peers() ([]*p2p.PeerInfo, error) { - server := api.node.Server() - if server == nil { - return nil, ErrNodeStopped - } - return server.PeersInfo(), nil -} - -// NodeInfo retrieves all the information we know about the host node at the -// protocol granularity. -func (api *adminAPI) NodeInfo() (*p2p.NodeInfo, error) { - server := api.node.Server() - if server == nil { - return nil, ErrNodeStopped - } - return server.NodeInfo(), 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 -} - -// ClientVersion returns the node name -func (s *web3API) ClientVersion() string { - return s.stack.Server().Name -} - -// 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/node/api_test.go b/node/api_test.go deleted file mode 100644 index 8761c4883e..0000000000 --- a/node/api_test.go +++ /dev/null @@ -1,355 +0,0 @@ -// 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 ( - "bytes" - "io" - "net" - "net/http" - "net/url" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/rpc" - "github.com/stretchr/testify/assert" -) - -// This test uses the admin_startRPC and admin_startWS APIs, -// checking whether the HTTP server is started correctly. -func TestStartRPC(t *testing.T) { - type test struct { - name string - cfg Config - fn func(*testing.T, *Node, *adminAPI) - - // Checks. These run after the node is configured and all API calls have been made. - wantReachable bool // whether the HTTP server should be reachable at all - wantHandlers bool // whether RegisterHandler handlers should be accessible - wantRPC bool // whether JSON-RPC/HTTP should be accessible - wantWS bool // whether JSON-RPC/WS should be accessible - } - - tests := []test{ - { - name: "all off", - cfg: Config{}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - }, - wantReachable: false, - wantHandlers: false, - wantRPC: false, - wantWS: false, - }, - { - name: "rpc enabled through config", - cfg: Config{HTTPHost: "127.0.0.1"}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - }, - wantReachable: true, - wantHandlers: true, - wantRPC: true, - wantWS: false, - }, - { - name: "rpc enabled through API", - cfg: Config{}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - _, err := api.StartHTTP(sp("127.0.0.1"), ip(0), nil, nil, nil) - assert.NoError(t, err) - }, - wantReachable: true, - wantHandlers: true, - wantRPC: true, - wantWS: false, - }, - { - name: "rpc start again after failure", - cfg: Config{}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - // Listen on a random port. - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal("can't listen:", err) - } - defer listener.Close() - port := listener.Addr().(*net.TCPAddr).Port - - // Now try to start RPC on that port. This should fail. - _, err = api.StartHTTP(sp("127.0.0.1"), ip(port), nil, nil, nil) - if err == nil { - t.Fatal("StartHTTP should have failed on port", port) - } - - // Try again after unblocking the port. It should work this time. - listener.Close() - _, err = api.StartHTTP(sp("127.0.0.1"), ip(port), nil, nil, nil) - assert.NoError(t, err) - }, - wantReachable: true, - wantHandlers: true, - wantRPC: true, - wantWS: false, - }, - { - name: "rpc stopped through API", - cfg: Config{HTTPHost: "127.0.0.1"}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - _, err := api.StopHTTP() - assert.NoError(t, err) - }, - wantReachable: false, - wantHandlers: false, - wantRPC: false, - wantWS: false, - }, - { - name: "rpc stopped twice", - cfg: Config{HTTPHost: "127.0.0.1"}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - _, err := api.StopHTTP() - assert.NoError(t, err) - - _, err = api.StopHTTP() - assert.NoError(t, err) - }, - wantReachable: false, - wantHandlers: false, - wantRPC: false, - wantWS: false, - }, - { - name: "ws enabled through config", - cfg: Config{WSHost: "127.0.0.1"}, - wantReachable: true, - wantHandlers: false, - wantRPC: false, - wantWS: true, - }, - { - name: "ws enabled through API", - cfg: Config{}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - _, err := api.StartWS(sp("127.0.0.1"), ip(0), nil, nil) - assert.NoError(t, err) - }, - wantReachable: true, - wantHandlers: false, - wantRPC: false, - wantWS: true, - }, - { - name: "ws stopped through API", - cfg: Config{WSHost: "127.0.0.1"}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - _, err := api.StopWS() - assert.NoError(t, err) - }, - wantReachable: false, - wantHandlers: false, - wantRPC: false, - wantWS: false, - }, - { - name: "ws stopped twice", - cfg: Config{WSHost: "127.0.0.1"}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - _, err := api.StopWS() - assert.NoError(t, err) - - _, err = api.StopWS() - assert.NoError(t, err) - }, - wantReachable: false, - wantHandlers: false, - wantRPC: false, - wantWS: false, - }, - { - name: "ws enabled after RPC", - cfg: Config{HTTPHost: "127.0.0.1"}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - wsport := n.http.port - _, err := api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil) - assert.NoError(t, err) - }, - wantReachable: true, - wantHandlers: true, - wantRPC: true, - wantWS: true, - }, - { - name: "ws enabled after RPC then stopped", - cfg: Config{HTTPHost: "127.0.0.1"}, - fn: func(t *testing.T, n *Node, api *adminAPI) { - wsport := n.http.port - _, err := api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil) - assert.NoError(t, err) - - _, err = api.StopWS() - assert.NoError(t, err) - }, - wantReachable: true, - wantHandlers: true, - wantRPC: true, - wantWS: false, - }, - { - name: "rpc stopped with ws enabled", - fn: func(t *testing.T, n *Node, api *adminAPI) { - _, err := api.StartHTTP(sp("127.0.0.1"), ip(0), nil, nil, nil) - assert.NoError(t, err) - - wsport := n.http.port - _, err = api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil) - assert.NoError(t, err) - - _, err = api.StopHTTP() - assert.NoError(t, err) - }, - wantReachable: false, - wantHandlers: false, - wantRPC: false, - wantWS: false, - }, - { - name: "rpc enabled after ws", - fn: func(t *testing.T, n *Node, api *adminAPI) { - _, err := api.StartWS(sp("127.0.0.1"), ip(0), nil, nil) - assert.NoError(t, err) - - wsport := n.http.port - _, err = api.StartHTTP(sp("127.0.0.1"), ip(wsport), nil, nil, nil) - assert.NoError(t, err) - }, - wantReachable: true, - wantHandlers: true, - wantRPC: true, - wantWS: true, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - // Apply some sane defaults. - config := test.cfg - // config.Logger = testlog.Logger(t, log.LvlDebug) - config.P2P.NoDiscovery = true - if config.HTTPTimeouts == (rpc.HTTPTimeouts{}) { - config.HTTPTimeouts = rpc.DefaultHTTPTimeouts - } - - // Create Node. - stack, err := New(&config) - if err != nil { - t.Fatal("can't create node:", err) - } - defer stack.Close() - - // Register the test handler. - stack.RegisterHandler("test", "/test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("OK")) - })) - - if err := stack.Start(); err != nil { - t.Fatal("can't start node:", err) - } - - // Run the API call hook. - if test.fn != nil { - test.fn(t, stack, &adminAPI{stack}) - } - - // Check if the HTTP endpoints are available. - baseURL := stack.HTTPEndpoint() - reachable := checkReachable(baseURL) - handlersAvailable := checkBodyOK(baseURL + "/test") - rpcAvailable := checkRPC(baseURL) - wsAvailable := checkRPC(strings.Replace(baseURL, "http://", "ws://", 1)) - if reachable != test.wantReachable { - t.Errorf("HTTP server is %sreachable, want it %sreachable", not(reachable), not(test.wantReachable)) - } - if handlersAvailable != test.wantHandlers { - t.Errorf("RegisterHandler handlers %savailable, want them %savailable", not(handlersAvailable), not(test.wantHandlers)) - } - if rpcAvailable != test.wantRPC { - t.Errorf("HTTP RPC %savailable, want it %savailable", not(rpcAvailable), not(test.wantRPC)) - } - if wsAvailable != test.wantWS { - t.Errorf("WS RPC %savailable, want it %savailable", not(wsAvailable), not(test.wantWS)) - } - }) - } -} - -// checkReachable checks if the TCP endpoint in rawurl is open. -func checkReachable(rawurl string) bool { - u, err := url.Parse(rawurl) - if err != nil { - panic(err) - } - conn, err := net.Dial("tcp", u.Host) - if err != nil { - return false - } - conn.Close() - return true -} - -// checkBodyOK checks whether the given HTTP URL responds with 200 OK and body "OK". -func checkBodyOK(url string) bool { - resp, err := http.Get(url) - if err != nil { - return false - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return false - } - buf := make([]byte, 2) - if _, err = io.ReadFull(resp.Body, buf); err != nil { - return false - } - return bytes.Equal(buf, []byte("OK")) -} - -// checkRPC checks whether JSON-RPC works against the given URL. -func checkRPC(url string) bool { - c, err := rpc.Dial(url) - if err != nil { - return false - } - defer c.Close() - - _, err = c.SupportedModules() - return err == nil -} - -// string/int pointer helpers. -func sp(s string) *string { return &s } -func ip(i int) *int { return &i } - -func not(ok bool) string { - if ok { - return "" - } - return "not " -} diff --git a/node/config.go b/node/config.go deleted file mode 100644 index 949db887e4..0000000000 --- a/node/config.go +++ /dev/null @@ -1,480 +0,0 @@ -// 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 ( - "crypto/ecdsa" - "fmt" - "net" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rpc" -) - -const ( - datadirPrivateKey = "nodekey" // Path within the datadir to the node's private key - datadirJWTKey = "jwtsecret" // Path within the datadir to the node's jwt secret - datadirDefaultKeyStore = "keystore" // Path within the datadir to the keystore - datadirStaticNodes = "static-nodes.json" // Path within the datadir to the static node list - datadirTrustedNodes = "trusted-nodes.json" // Path within the datadir to the trusted node list - 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:"-"` - - // UserIdent, if set, is used as an additional component in the devp2p node identifier. - UserIdent string `toml:",omitempty"` - - // 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 - - // Configuration of peer-to-peer networking. - P2P p2p.Config - - // KeyStoreDir is the file system folder that contains private keys. The directory can - // be specified as a relative path, in which case it is resolved relative to the - // current directory. - // - // If KeyStoreDir is empty, the default location is the "keystore" subdirectory of - // DataDir. If DataDir is unspecified and KeyStoreDir is empty, an ephemeral directory - // is created by New and destroyed when the node is stopped. - KeyStoreDir string `toml:",omitempty"` - - // ExternalSigner specifies an external URI for a clef-type signer. - ExternalSigner string `toml:",omitempty"` - - // UseLightweightKDF lowers the memory and CPU requirements of the key store - // scrypt KDF at the expense of security. - UseLightweightKDF bool `toml:",omitempty"` - - // InsecureUnlockAllowed allows user to unlock accounts in unsafe http environment. - InsecureUnlockAllowed bool `toml:",omitempty"` - - // NoUSB disables hardware wallet monitoring and connectivity. - // Deprecated: USB monitoring is disabled by default and must be enabled explicitly. - NoUSB bool `toml:",omitempty"` - - // USB enables hardware wallet monitoring and connectivity. - USB bool `toml:",omitempty"` - - // SmartCardDaemonPath is the path to the smartcard daemon's socket. - SmartCardDaemonPath string `toml:",omitempty"` - - // 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:"-"` - - DBEngine string `toml:",omitempty"` -} - -// 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.UserIdent != "" { - name += "/" + c.UserIdent - } - 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()) -} - -// NodeKey retrieves the currently configured private key of the node, checking -// first any manually set key, falling back to the one found in the configured -// data folder. If no key can be found, a new one is generated. -func (c *Config) NodeKey() *ecdsa.PrivateKey { - // Use any specifically configured key. - if c.P2P.PrivateKey != nil { - return c.P2P.PrivateKey - } - // Generate ephemeral key if no datadir is being used. - if c.DataDir == "" { - key, err := crypto.GenerateKey() - if err != nil { - log.Crit(fmt.Sprintf("Failed to generate ephemeral node key: %v", err)) - } - return key - } - - keyfile := c.ResolvePath(datadirPrivateKey) - if key, err := crypto.LoadECDSA(keyfile); err == nil { - return key - } - // No persistent key found, generate and store a new one. - key, err := crypto.GenerateKey() - if err != nil { - log.Crit(fmt.Sprintf("Failed to generate node key: %v", err)) - } - instanceDir := filepath.Join(c.DataDir, c.name()) - if err := os.MkdirAll(instanceDir, 0700); err != nil { - log.Error(fmt.Sprintf("Failed to persist node key: %v", err)) - return key - } - keyfile = filepath.Join(instanceDir, datadirPrivateKey) - if err := crypto.SaveECDSA(keyfile, key); err != nil { - log.Error(fmt.Sprintf("Failed to persist node key: %v", err)) - } - return key -} - -// checkLegacyFiles inspects the datadir for signs of legacy static-nodes -// and trusted-nodes files. If they exist it raises an error. -func (c *Config) checkLegacyFiles() { - c.checkLegacyFile(c.ResolvePath(datadirStaticNodes)) - c.checkLegacyFile(c.ResolvePath(datadirTrustedNodes)) -} - -// checkLegacyFile will only raise an error if a file at the given path exists. -func (c *Config) checkLegacyFile(path string) { - // Short circuit if no node config is present - if c.DataDir == "" { - return - } - if _, err := os.Stat(path); err != nil { - return - } - logger := c.Logger - if logger == nil { - logger = log.Root() - } - switch fname := filepath.Base(path); fname { - case "static-nodes.json": - logger.Error("The static-nodes.json file is deprecated and ignored. Use P2P.StaticNodes in config.toml instead.") - case "trusted-nodes.json": - logger.Error("The trusted-nodes.json file is deprecated and ignored. Use P2P.TrustedNodes in config.toml instead.") - default: - // We shouldn't wind up here, but better print something just in case. - logger.Error("Ignoring deprecated file.", "file", path) - } -} - -// KeyDirConfig determines the settings for keydirectory -func (c *Config) KeyDirConfig() (string, error) { - var ( - keydir string - err error - ) - switch { - case filepath.IsAbs(c.KeyStoreDir): - keydir = c.KeyStoreDir - case c.DataDir != "": - if c.KeyStoreDir == "" { - keydir = filepath.Join(c.DataDir, datadirDefaultKeyStore) - } else { - keydir, err = filepath.Abs(c.KeyStoreDir) - } - case c.KeyStoreDir != "": - keydir, err = filepath.Abs(c.KeyStoreDir) - } - return keydir, err -} - -// GetKeyStoreDir retrieves the key directory and will create -// and ephemeral one if necessary. -func (c *Config) GetKeyStoreDir() (string, bool, error) { - keydir, err := c.KeyDirConfig() - if err != nil { - return "", false, err - } - isEphemeral := false - if keydir == "" { - // There is no datadir. - keydir, err = os.MkdirTemp("", "go-ethereum-keystore") - isEphemeral = true - } - - if err != nil { - return "", false, err - } - if err := os.MkdirAll(keydir, 0700); err != nil { - return "", false, err - } - - return keydir, isEphemeral, nil -} diff --git a/node/config_test.go b/node/config_test.go deleted file mode 100644 index e8af8ddcd8..0000000000 --- a/node/config_test.go +++ /dev/null @@ -1,153 +0,0 @@ -// 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 ( - "bytes" - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/p2p" -) - -// Tests that datadirs can be successfully created, be them manually configured -// ones or automatically generated temporary ones. -func TestDatadirCreation(t *testing.T) { - // Create a temporary data dir and check that it can be used by a node - dir := t.TempDir() - - node, err := New(&Config{DataDir: dir}) - if err != nil { - t.Fatalf("failed to create stack with existing datadir: %v", err) - } - if err := node.Close(); err != nil { - t.Fatalf("failed to close node: %v", err) - } - // Generate a long non-existing datadir path and check that it gets created by a node - dir = filepath.Join(dir, "a", "b", "c", "d", "e", "f") - node, err = New(&Config{DataDir: dir}) - if err != nil { - t.Fatalf("failed to create stack with creatable datadir: %v", err) - } - if err := node.Close(); err != nil { - t.Fatalf("failed to close node: %v", err) - } - if _, err := os.Stat(dir); err != nil { - t.Fatalf("freshly created datadir not accessible: %v", err) - } - // Verify that an impossible datadir fails creation - file, err := os.CreateTemp("", "") - if err != nil { - t.Fatalf("failed to create temporary file: %v", err) - } - defer func() { - file.Close() - os.Remove(file.Name()) - }() - - dir = filepath.Join(file.Name(), "invalid/path") - _, err = New(&Config{DataDir: dir}) - if err == nil { - t.Fatalf("protocol stack created with an invalid datadir") - } -} - -// Tests that IPC paths are correctly resolved to valid endpoints of different -// platforms. -func TestIPCPathResolution(t *testing.T) { - var tests = []struct { - DataDir string - IPCPath string - Windows bool - Endpoint string - }{ - {"", "", false, ""}, - {"data", "", false, ""}, - {"", "geth.ipc", false, filepath.Join(os.TempDir(), "geth.ipc")}, - {"data", "geth.ipc", false, "data/geth.ipc"}, - {"data", "./geth.ipc", false, "./geth.ipc"}, - {"data", "/geth.ipc", false, "/geth.ipc"}, - {"", "", true, ``}, - {"data", "", true, ``}, - {"", "geth.ipc", true, `\\.\pipe\geth.ipc`}, - {"data", "geth.ipc", true, `\\.\pipe\geth.ipc`}, - {"data", `\\.\pipe\geth.ipc`, true, `\\.\pipe\geth.ipc`}, - } - for i, test := range tests { - // Only run when platform/test match - if (runtime.GOOS == "windows") == test.Windows { - if endpoint := (&Config{DataDir: test.DataDir, IPCPath: test.IPCPath}).IPCEndpoint(); endpoint != test.Endpoint { - t.Errorf("test %d: IPC endpoint mismatch: have %s, want %s", i, endpoint, test.Endpoint) - } - } - } -} - -// Tests that node keys can be correctly created, persisted, loaded and/or made -// ephemeral. -func TestNodeKeyPersistency(t *testing.T) { - // Create a temporary folder and make sure no key is present - dir := t.TempDir() - - keyfile := filepath.Join(dir, "unit-test", datadirPrivateKey) - - // Configure a node with a preset key and ensure it's not persisted - key, err := crypto.GenerateKey() - if err != nil { - t.Fatalf("failed to generate one-shot node key: %v", err) - } - config := &Config{Name: "unit-test", DataDir: dir, P2P: p2p.Config{PrivateKey: key}} - config.NodeKey() - if _, err := os.Stat(keyfile); err == nil { - t.Fatalf("one-shot node key persisted to data directory") - } - - // Configure a node with no preset key and ensure it is persisted this time - config = &Config{Name: "unit-test", DataDir: dir} - config.NodeKey() - if _, err := os.Stat(keyfile); err != nil { - t.Fatalf("node key not persisted to data directory: %v", err) - } - if _, err = crypto.LoadECDSA(keyfile); err != nil { - t.Fatalf("failed to load freshly persisted node key: %v", err) - } - blob1, err := os.ReadFile(keyfile) - if err != nil { - t.Fatalf("failed to read freshly persisted node key: %v", err) - } - - // Configure a new node and ensure the previously persisted key is loaded - config = &Config{Name: "unit-test", DataDir: dir} - config.NodeKey() - blob2, err := os.ReadFile(keyfile) - if err != nil { - t.Fatalf("failed to read previously persisted node key: %v", err) - } - if !bytes.Equal(blob1, blob2) { - t.Fatalf("persisted node key mismatch: have %x, want %x", blob2, blob1) - } - - // Configure ephemeral node and ensure no key is dumped locally - config = &Config{Name: "unit-test", DataDir: ""} - config.NodeKey() - if _, err := os.Stat(filepath.Join(".", "unit-test", datadirPrivateKey)); err == nil { - t.Fatalf("ephemeral node key persisted to disk") - } -} diff --git a/node/defaults.go b/node/defaults.go deleted file mode 100644 index 42d9d4cde0..0000000000 --- a/node/defaults.go +++ /dev/null @@ -1,133 +0,0 @@ -// 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/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/nat" - "github.com/ethereum/go-ethereum/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 -) - -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"}, - P2P: p2p.Config{ - ListenAddr: ":30303", - MaxPeers: 50, - NAT: nat.Any(), - }, - DBEngine: "", // Use whatever exists, will default to Pebble if non-existent and supported -} - -// 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/node/doc.go b/node/doc.go deleted file mode 100644 index 4474e43660..0000000000 --- a/node/doc.go +++ /dev/null @@ -1,121 +0,0 @@ -// 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, 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/node/endpoints.go b/node/endpoints.go deleted file mode 100644 index 14c12fd1f1..0000000000 --- a/node/endpoints.go +++ /dev/null @@ -1,91 +0,0 @@ -// 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/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" -) - -// 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/node/errors.go b/node/errors.go deleted file mode 100644 index 67547bf691..0000000000 --- a/node/errors.go +++ /dev/null @@ -1,52 +0,0 @@ -// 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" - "syscall" -) - -var ( - ErrDatadirUsed = errors.New("datadir already used by another process") - ErrNodeStopped = errors.New("node not started") - ErrNodeRunning = errors.New("node already running") - ErrServiceUnknown = errors.New("unknown service") - - datadirInUseErrnos = map[uint]bool{11: true, 32: true, 35: true} -) - -func convertFileLockError(err error) error { - if errno, ok := err.(syscall.Errno); ok && datadirInUseErrnos[uint(errno)] { - return ErrDatadirUsed - } - return err -} - -// 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/node/jwt_auth.go b/node/jwt_auth.go deleted file mode 100644 index d4f8193ca7..0000000000 --- a/node/jwt_auth.go +++ /dev/null @@ -1,45 +0,0 @@ -// 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/ethereum/go-ethereum/rpc" - "github.com/golang-jwt/jwt/v4" -) - -// 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/node/jwt_handler.go b/node/jwt_handler.go deleted file mode 100644 index 637ae19686..0000000000 --- a/node/jwt_handler.go +++ /dev/null @@ -1,80 +0,0 @@ -// 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(token *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/node/lifecycle.go b/node/lifecycle.go deleted file mode 100644 index 0d5f9a0680..0000000000 --- a/node/lifecycle.go +++ /dev/null @@ -1,31 +0,0 @@ -// 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/node/node.go b/node/node.go deleted file mode 100644 index 41c9971fe8..0000000000 --- a/node/node.go +++ /dev/null @@ -1,822 +0,0 @@ -// 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/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rpc" - "github.com/gofrs/flock" -) - -// Node is a container on which services can be registered. -type Node struct { - eventmux *event.TypeMux - config *Config - accman *accounts.Manager - log log.Logger - keyDir string // key store directory - keyDirTemp bool // If true, key directory will be removed by Stop - dirLock *flock.Flock // prevents concurrent use of instance directory - stop chan struct{} // Channel to wait for termination notifications - server *p2p.Server // Currently running P2P networking layer - 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 - - databases map[*closeTrackingDB]struct{} // All open databases -} - -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{}), - server: &p2p.Server{Config: conf.P2P}, - databases: make(map[*closeTrackingDB]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 - } - keyDir, isEphem, err := conf.GetKeyStoreDir() - if err != nil { - return nil, err - } - node.keyDir = keyDir - node.keyDirTemp = isEphem - // Creates an empty AccountManager with no backends. Callers (e.g. cmd/geth) - // are required to add the backends later on. - node.accman = accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: conf.InsecureUnlockAllowed}) - - // Initialize the p2p server. This creates the node key and discovery databases. - node.server.Config.PrivateKey = node.config.NodeKey() - node.server.Config.Name = node.config.NodeName() - node.server.Config.Logger = node.log - node.config.checkLegacyFiles() - if node.server.Config.NodeDatabase == "" { - node.server.Config.NodeDatabase = node.config.NodeDB() - } - - // 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 - 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) - n.doClose(nil) - } - 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 { - // Close databases. This needs the lock because it needs to - // synchronize with OpenDatabase*. - n.lock.Lock() - n.state = closedState - errs = append(errs, n.closeDatabases()...) - n.lock.Unlock() - - if err := n.accman.Close(); err != nil { - errs = append(errs, err) - } - if n.keyDirTemp { - if err := os.RemoveAll(n.keyDir); err != nil { - errs = append(errs, err) - } - } - - // 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 networking endpoints - n.log.Info("Starting peer-to-peer node", "instance", n.server.Name) - if err := n.server.Start(); err != nil { - return convertFileLockError(err) - } - // start RPC endpoints - err := n.startRPC() - if err != nil { - n.stopRPC() - n.server.Stop() - } - 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 - } - } - - // Stop p2p networking. - n.server.Stop() - - 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, 0700); 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) - // 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)), 0600); 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. -func (n *Node) startRPC() error { - // Filter out personal api - var apis []rpc.API - 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, - } - - 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, - }); 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, - }); 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, - } - 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() - 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) -} - -// RegisterProtocols adds backend's protocols to the node's p2p server. -func (n *Node) RegisterProtocols(protocols []p2p.Protocol) { - n.lock.Lock() - defer n.lock.Unlock() - - if n.state != initializingState { - panic("can't register protocols on running/stopped node") - } - n.server.Protocols = append(n.server.Protocols, protocols...) -} - -// 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 -} - -// Server retrieves the currently running P2P network layer. This method is meant -// only to inspect fields of the currently running server. Callers should not -// start or stop the returned server. -func (n *Node) Server() *p2p.Server { - n.lock.Lock() - defer n.lock.Unlock() - - return n.server -} - -// 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() -} - -// KeyStoreDir retrieves the key directory -func (n *Node) KeyStoreDir() string { - return n.keyDir -} - -// AccountManager retrieves the account manager used by the protocol stack. -func (n *Node) AccountManager() *accounts.Manager { - return n.accman -} - -// 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 -} - -// OpenDatabase opens an existing database with the given name (or creates one if no -// previous can be found) from within the node's instance directory. If the node is -// ephemeral, a memory database is returned. -func (n *Node) OpenDatabase(name string, cache, handles int, namespace string, readonly bool) (ethdb.Database, error) { - n.lock.Lock() - defer n.lock.Unlock() - if n.state == closedState { - return nil, ErrNodeStopped - } - - var db ethdb.Database - var err error - if n.config.DataDir == "" { - db = rawdb.NewMemoryDatabase() - } else { - db, err = rawdb.Open(rawdb.OpenOptions{ - Type: n.config.DBEngine, - Directory: n.ResolvePath(name), - Namespace: namespace, - Cache: cache, - Handles: handles, - ReadOnly: readonly, - }) - } - - if err == nil { - db = n.wrapDatabase(db) - } - return db, err -} - -// OpenDatabaseWithFreezer opens an existing database with the given name (or -// creates one if no previous can be found) from within the node's data directory, -// also attaching a chain freezer to it that moves ancient chain data from the -// database to immutable append-only files. If the node is an ephemeral one, a -// memory database is returned. -func (n *Node) OpenDatabaseWithFreezer(name string, cache, handles int, ancient string, namespace string, readonly bool) (ethdb.Database, error) { - n.lock.Lock() - defer n.lock.Unlock() - if n.state == closedState { - return nil, ErrNodeStopped - } - var db ethdb.Database - var err error - if n.config.DataDir == "" { - db = rawdb.NewMemoryDatabase() - } else { - db, err = rawdb.Open(rawdb.OpenOptions{ - Type: n.config.DBEngine, - Directory: n.ResolvePath(name), - AncientsDirectory: n.ResolveAncient(name, ancient), - Namespace: namespace, - Cache: cache, - Handles: handles, - ReadOnly: readonly, - }) - } - - if err == nil { - db = n.wrapDatabase(db) - } - return db, err -} - -// ResolvePath returns the absolute path of a resource in the instance directory. -func (n *Node) ResolvePath(x string) string { - return n.config.ResolvePath(x) -} - -// ResolveAncient returns the absolute path of the root ancient directory. -func (n *Node) ResolveAncient(name string, ancient string) string { - switch { - case ancient == "": - ancient = filepath.Join(n.ResolvePath(name), "ancient") - case !filepath.IsAbs(ancient): - ancient = n.ResolvePath(ancient) - } - return ancient -} - -// closeTrackingDB wraps the Close method of a database. When the database is closed by the -// service, the wrapper removes it from the node's database map. This ensures that Node -// won't auto-close the database if it is closed by the service that opened it. -type closeTrackingDB struct { - ethdb.Database - n *Node -} - -func (db *closeTrackingDB) Close() error { - db.n.lock.Lock() - delete(db.n.databases, db) - db.n.lock.Unlock() - return db.Database.Close() -} - -// wrapDatabase ensures the database will be auto-closed when Node is closed. -func (n *Node) wrapDatabase(db ethdb.Database) ethdb.Database { - wrapper := &closeTrackingDB{db, n} - n.databases[wrapper] = struct{}{} - return wrapper -} - -// closeDatabases closes all open databases. -func (n *Node) closeDatabases() (errors []error) { - for db := range n.databases { - delete(n.databases, db) - if err := db.Database.Close(); err != nil { - errors = append(errors, err) - } - } - return errors -} diff --git a/node/node_auth_test.go b/node/node_auth_test.go deleted file mode 100644 index 597cd8531f..0000000000 --- a/node/node_auth_test.go +++ /dev/null @@ -1,237 +0,0 @@ -// 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 ( - "context" - crand "crypto/rand" - "fmt" - "net/http" - "os" - "path" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/rpc" - "github.com/golang-jwt/jwt/v4" -) - -type helloRPC string - -func (ta helloRPC) HelloWorld() (string, error) { - return string(ta), nil -} - -type authTest struct { - name string - endpoint string - prov rpc.HTTPAuth - expectDialFail bool - expectCall1Fail bool - expectCall2Fail bool -} - -func (at *authTest) Run(t *testing.T) { - ctx := context.Background() - cl, err := rpc.DialOptions(ctx, at.endpoint, rpc.WithHTTPAuth(at.prov)) - if at.expectDialFail { - if err == nil { - t.Fatal("expected initial dial to fail") - } else { - return - } - } - if err != nil { - t.Fatalf("failed to dial rpc endpoint: %v", err) - } - - var x string - err = cl.CallContext(ctx, &x, "engine_helloWorld") - if at.expectCall1Fail { - if err == nil { - t.Fatal("expected call 1 to fail") - } else { - return - } - } - if err != nil { - t.Fatalf("failed to call rpc endpoint: %v", err) - } - if x != "hello engine" { - t.Fatalf("method was silent but did not return expected value: %q", x) - } - - err = cl.CallContext(ctx, &x, "eth_helloWorld") - if at.expectCall2Fail { - if err == nil { - t.Fatal("expected call 2 to fail") - } else { - return - } - } - if err != nil { - t.Fatalf("failed to call rpc endpoint: %v", err) - } - if x != "hello eth" { - t.Fatalf("method was silent but did not return expected value: %q", x) - } -} - -func TestAuthEndpoints(t *testing.T) { - var secret [32]byte - if _, err := crand.Read(secret[:]); err != nil { - t.Fatalf("failed to create jwt secret: %v", err) - } - // Geth must read it from a file, and does not support in-memory JWT secrets, so we create a temporary file. - jwtPath := path.Join(t.TempDir(), "jwt_secret") - if err := os.WriteFile(jwtPath, []byte(hexutil.Encode(secret[:])), 0600); err != nil { - t.Fatalf("failed to prepare jwt secret file: %v", err) - } - // We get ports assigned by the node automatically - conf := &Config{ - HTTPHost: "127.0.0.1", - HTTPPort: 0, - WSHost: "127.0.0.1", - WSPort: 0, - AuthAddr: "127.0.0.1", - AuthPort: 0, - JWTSecret: jwtPath, - - WSModules: []string{"eth", "engine"}, - HTTPModules: []string{"eth", "engine"}, - } - node, err := New(conf) - if err != nil { - t.Fatalf("could not create a new node: %v", err) - } - // register dummy apis so we can test the modules are available and reachable with authentication - node.RegisterAPIs([]rpc.API{ - { - Namespace: "engine", - Version: "1.0", - Service: helloRPC("hello engine"), - Public: true, - Authenticated: true, - }, - { - Namespace: "eth", - Version: "1.0", - Service: helloRPC("hello eth"), - Public: true, - Authenticated: true, - }, - }) - if err := node.Start(); err != nil { - t.Fatalf("failed to start test node: %v", err) - } - defer node.Close() - - // sanity check we are running different endpoints - if a, b := node.WSEndpoint(), node.WSAuthEndpoint(); a == b { - t.Fatalf("expected ws and auth-ws endpoints to be different, got: %q and %q", a, b) - } - if a, b := node.HTTPEndpoint(), node.HTTPAuthEndpoint(); a == b { - t.Fatalf("expected http and auth-http endpoints to be different, got: %q and %q", a, b) - } - - goodAuth := NewJWTAuth(secret) - var otherSecret [32]byte - if _, err := crand.Read(otherSecret[:]); err != nil { - t.Fatalf("failed to create jwt secret: %v", err) - } - badAuth := NewJWTAuth(otherSecret) - - notTooLong := time.Second * 57 - tooLong := time.Second * 60 - requestDelay := time.Second - - testCases := []authTest{ - // Auth works - {name: "ws good", endpoint: node.WSAuthEndpoint(), prov: goodAuth, expectCall1Fail: false}, - {name: "http good", endpoint: node.HTTPAuthEndpoint(), prov: goodAuth, expectCall1Fail: false}, - - // Try a bad auth - {name: "ws bad", endpoint: node.WSAuthEndpoint(), prov: badAuth, expectDialFail: true}, // ws auth is immediate - {name: "http bad", endpoint: node.HTTPAuthEndpoint(), prov: badAuth, expectCall1Fail: true}, // http auth is on first call - - // A common mistake with JWT is to allow the "none" algorithm, which is a valid JWT but not secure. - {name: "ws none", endpoint: node.WSAuthEndpoint(), prov: noneAuth(secret), expectDialFail: true}, - {name: "http none", endpoint: node.HTTPAuthEndpoint(), prov: noneAuth(secret), expectCall1Fail: true}, - - // claims of 5 seconds or more, older or newer, are not allowed - {name: "ws too old", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, -tooLong), expectDialFail: true}, - {name: "http too old", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, -tooLong), expectCall1Fail: true}, - // note: for it to be too long we need to add a delay, so that once we receive the request, the difference has not dipped below the "tooLong" - {name: "ws too new", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, tooLong+requestDelay), expectDialFail: true}, - {name: "http too new", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, tooLong+requestDelay), expectCall1Fail: true}, - - // Try offset the time, but stay just within bounds - {name: "ws old", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, -notTooLong)}, - {name: "http old", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, -notTooLong)}, - {name: "ws new", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, notTooLong)}, - {name: "http new", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, notTooLong)}, - - // ws only authenticates on initial dial, then continues communication - {name: "ws single auth", endpoint: node.WSAuthEndpoint(), prov: changingAuth(goodAuth, badAuth)}, - {name: "http call fail auth", endpoint: node.HTTPAuthEndpoint(), prov: changingAuth(goodAuth, badAuth), expectCall2Fail: true}, - {name: "http call fail time", endpoint: node.HTTPAuthEndpoint(), prov: changingAuth(goodAuth, offsetTimeAuth(secret, tooLong+requestDelay)), expectCall2Fail: true}, - } - - for _, testCase := range testCases { - t.Run(testCase.name, testCase.Run) - } -} - -func noneAuth(secret [32]byte) rpc.HTTPAuth { - return func(header http.Header) error { - token := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{ - "iat": &jwt.NumericDate{Time: time.Now()}, - }) - s, err := token.SignedString(secret[:]) - if err != nil { - return fmt.Errorf("failed to create JWT token: %w", err) - } - header.Set("Authorization", "Bearer "+s) - return nil - } -} - -func changingAuth(provs ...rpc.HTTPAuth) rpc.HTTPAuth { - i := 0 - return func(header http.Header) error { - i += 1 - if i > len(provs) { - i = len(provs) - } - return provs[i-1](header) - } -} - -func offsetTimeAuth(secret [32]byte, offset time.Duration) rpc.HTTPAuth { - return func(header http.Header) error { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "iat": &jwt.NumericDate{Time: time.Now().Add(offset)}, - }) - s, err := token.SignedString(secret[:]) - if err != nil { - return fmt.Errorf("failed to create JWT token: %w", err) - } - header.Set("Authorization", "Bearer "+s) - return nil - } -} diff --git a/node/node_example_test.go b/node/node_example_test.go deleted file mode 100644 index e45ee49a25..0000000000 --- a/node/node_example_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// 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_test - -import ( - "fmt" - "log" - - "github.com/ethereum/go-ethereum/node" -) - -// SampleLifecycle is a trivial network service that can be attached to a node for -// life cycle management. -// -// The following methods are needed to implement a node.Lifecycle: -// - Start() error - method invoked when the node is ready to start the service -// - Stop() error - method invoked when the node terminates the service -type SampleLifecycle struct{} - -func (s *SampleLifecycle) Start() error { fmt.Println("Service starting..."); return nil } -func (s *SampleLifecycle) Stop() error { fmt.Println("Service stopping..."); return nil } - -func ExampleLifecycle() { - // Create a network node to run protocols with the default values. - stack, err := node.New(&node.Config{}) - if err != nil { - log.Fatalf("Failed to create network node: %v", err) - } - defer stack.Close() - - // Create and register a simple network Lifecycle. - service := new(SampleLifecycle) - stack.RegisterLifecycle(service) - - // Boot up the entire protocol stack, do a restart and terminate - if err := stack.Start(); err != nil { - log.Fatalf("Failed to start the protocol stack: %v", err) - } - if err := stack.Close(); err != nil { - log.Fatalf("Failed to stop the protocol stack: %v", err) - } - // Output: - // Service starting... - // Service stopping... -} diff --git a/node/node_test.go b/node/node_test.go deleted file mode 100644 index 04810a815b..0000000000 --- a/node/node_test.go +++ /dev/null @@ -1,638 +0,0 @@ -// 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" - "io" - "net" - "net/http" - "reflect" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rpc" - - "github.com/stretchr/testify/assert" -) - -var ( - testNodeKey, _ = crypto.GenerateKey() -) - -func testNodeConfig() *Config { - return &Config{ - Name: "test node", - P2P: p2p.Config{PrivateKey: testNodeKey}, - } -} - -// Tests that an empty protocol stack can be closed more than once. -func TestNodeCloseMultipleTimes(t *testing.T) { - stack, err := New(testNodeConfig()) - if err != nil { - t.Fatalf("failed to create protocol stack: %v", err) - } - stack.Close() - - // Ensure that a stopped node can be stopped again - for i := 0; i < 3; i++ { - if err := stack.Close(); err != ErrNodeStopped { - t.Fatalf("iter %d: stop failure mismatch: have %v, want %v", i, err, ErrNodeStopped) - } - } -} - -func TestNodeStartMultipleTimes(t *testing.T) { - stack, err := New(testNodeConfig()) - if err != nil { - t.Fatalf("failed to create protocol stack: %v", err) - } - - // Ensure that a node can be successfully started, but only once - if err := stack.Start(); err != nil { - t.Fatalf("failed to start node: %v", err) - } - if err := stack.Start(); err != ErrNodeRunning { - t.Fatalf("start failure mismatch: have %v, want %v ", err, ErrNodeRunning) - } - // Ensure that a node can be stopped, but only once - if err := stack.Close(); err != nil { - t.Fatalf("failed to stop node: %v", err) - } - if err := stack.Close(); err != ErrNodeStopped { - t.Fatalf("stop failure mismatch: have %v, want %v ", err, ErrNodeStopped) - } -} - -// Tests that if the data dir is already in use, an appropriate error is returned. -func TestNodeUsedDataDir(t *testing.T) { - // Create a temporary folder to use as the data directory - dir := t.TempDir() - - // Create a new node based on the data directory - original, err := New(&Config{DataDir: dir}) - if err != nil { - t.Fatalf("failed to create original protocol stack: %v", err) - } - defer original.Close() - if err := original.Start(); err != nil { - t.Fatalf("failed to start original protocol stack: %v", err) - } - - // Create a second node based on the same data directory and ensure failure - _, err = New(&Config{DataDir: dir}) - if err != ErrDatadirUsed { - t.Fatalf("duplicate datadir failure mismatch: have %v, want %v", err, ErrDatadirUsed) - } -} - -// Tests whether a Lifecycle can be registered. -func TestLifecycleRegistry_Successful(t *testing.T) { - stack, err := New(testNodeConfig()) - if err != nil { - t.Fatalf("failed to create protocol stack: %v", err) - } - defer stack.Close() - - noop := NewNoop() - stack.RegisterLifecycle(noop) - - if !containsLifecycle(stack.lifecycles, noop) { - t.Fatalf("lifecycle was not properly registered on the node, %v", err) - } -} - -// Tests whether a service's protocols can be registered properly on the node's p2p server. -func TestRegisterProtocols(t *testing.T) { - stack, err := New(testNodeConfig()) - if err != nil { - t.Fatalf("failed to create protocol stack: %v", err) - } - defer stack.Close() - - fs, err := NewFullService(stack) - if err != nil { - t.Fatalf("could not create full service: %v", err) - } - - for _, protocol := range fs.Protocols() { - if !containsProtocol(stack.server.Protocols, protocol) { - t.Fatalf("protocol %v was not successfully registered", protocol) - } - } - - for _, api := range fs.APIs() { - if !containsAPI(stack.rpcAPIs, api) { - t.Fatalf("api %v was not successfully registered", api) - } - } -} - -// This test checks that open databases are closed with node. -func TestNodeCloseClosesDB(t *testing.T) { - stack, _ := New(testNodeConfig()) - defer stack.Close() - - db, err := stack.OpenDatabase("mydb", 0, 0, "", false) - if err != nil { - t.Fatal("can't open DB:", err) - } - if err = db.Put([]byte{}, []byte{}); err != nil { - t.Fatal("can't Put on open DB:", err) - } - - stack.Close() - if err = db.Put([]byte{}, []byte{}); err == nil { - t.Fatal("Put succeeded after node is closed") - } -} - -// This test checks that OpenDatabase can be used from within a Lifecycle Start method. -func TestNodeOpenDatabaseFromLifecycleStart(t *testing.T) { - stack, _ := New(testNodeConfig()) - defer stack.Close() - - var db ethdb.Database - var err error - stack.RegisterLifecycle(&InstrumentedService{ - startHook: func() { - db, err = stack.OpenDatabase("mydb", 0, 0, "", false) - if err != nil { - t.Fatal("can't open DB:", err) - } - }, - stopHook: func() { - db.Close() - }, - }) - - stack.Start() - stack.Close() -} - -// This test checks that OpenDatabase can be used from within a Lifecycle Stop method. -func TestNodeOpenDatabaseFromLifecycleStop(t *testing.T) { - stack, _ := New(testNodeConfig()) - defer stack.Close() - - stack.RegisterLifecycle(&InstrumentedService{ - stopHook: func() { - db, err := stack.OpenDatabase("mydb", 0, 0, "", false) - if err != nil { - t.Fatal("can't open DB:", err) - } - db.Close() - }, - }) - - stack.Start() - stack.Close() -} - -// Tests that registered Lifecycles get started and stopped correctly. -func TestLifecycleLifeCycle(t *testing.T) { - stack, _ := New(testNodeConfig()) - defer stack.Close() - - started := make(map[string]bool) - stopped := make(map[string]bool) - - // Create a batch of instrumented services - lifecycles := map[string]Lifecycle{ - "A": &InstrumentedService{ - startHook: func() { started["A"] = true }, - stopHook: func() { stopped["A"] = true }, - }, - "B": &InstrumentedService{ - startHook: func() { started["B"] = true }, - stopHook: func() { stopped["B"] = true }, - }, - "C": &InstrumentedService{ - startHook: func() { started["C"] = true }, - stopHook: func() { stopped["C"] = true }, - }, - } - // register lifecycles on node - for _, lifecycle := range lifecycles { - stack.RegisterLifecycle(lifecycle) - } - // Start the node and check that all services are running - if err := stack.Start(); err != nil { - t.Fatalf("failed to start protocol stack: %v", err) - } - for id := range lifecycles { - if !started[id] { - t.Fatalf("service %s: freshly started service not running", id) - } - if stopped[id] { - t.Fatalf("service %s: freshly started service already stopped", id) - } - } - // Stop the node and check that all services have been stopped - if err := stack.Close(); err != nil { - t.Fatalf("failed to stop protocol stack: %v", err) - } - for id := range lifecycles { - if !stopped[id] { - t.Fatalf("service %s: freshly terminated service still running", id) - } - } -} - -// Tests that if a Lifecycle fails to start, all others started before it will be -// shut down. -func TestLifecycleStartupError(t *testing.T) { - stack, err := New(testNodeConfig()) - if err != nil { - t.Fatalf("failed to create protocol stack: %v", err) - } - defer stack.Close() - - started := make(map[string]bool) - stopped := make(map[string]bool) - - // Create a batch of instrumented services - lifecycles := map[string]Lifecycle{ - "A": &InstrumentedService{ - startHook: func() { started["A"] = true }, - stopHook: func() { stopped["A"] = true }, - }, - "B": &InstrumentedService{ - startHook: func() { started["B"] = true }, - stopHook: func() { stopped["B"] = true }, - }, - "C": &InstrumentedService{ - startHook: func() { started["C"] = true }, - stopHook: func() { stopped["C"] = true }, - }, - } - // register lifecycles on node - for _, lifecycle := range lifecycles { - stack.RegisterLifecycle(lifecycle) - } - - // Register a service that fails to construct itself - failure := errors.New("fail") - failer := &InstrumentedService{start: failure} - stack.RegisterLifecycle(failer) - - // Start the protocol stack and ensure all started services stop - if err := stack.Start(); err != failure { - t.Fatalf("stack startup failure mismatch: have %v, want %v", err, failure) - } - for id := range lifecycles { - if started[id] && !stopped[id] { - t.Fatalf("service %s: started but not stopped", id) - } - delete(started, id) - delete(stopped, id) - } -} - -// Tests that even if a registered Lifecycle fails to shut down cleanly, it does -// not influence the rest of the shutdown invocations. -func TestLifecycleTerminationGuarantee(t *testing.T) { - stack, err := New(testNodeConfig()) - if err != nil { - t.Fatalf("failed to create protocol stack: %v", err) - } - defer stack.Close() - - started := make(map[string]bool) - stopped := make(map[string]bool) - - // Create a batch of instrumented services - lifecycles := map[string]Lifecycle{ - "A": &InstrumentedService{ - startHook: func() { started["A"] = true }, - stopHook: func() { stopped["A"] = true }, - }, - "B": &InstrumentedService{ - startHook: func() { started["B"] = true }, - stopHook: func() { stopped["B"] = true }, - }, - "C": &InstrumentedService{ - startHook: func() { started["C"] = true }, - stopHook: func() { stopped["C"] = true }, - }, - } - // register lifecycles on node - for _, lifecycle := range lifecycles { - stack.RegisterLifecycle(lifecycle) - } - - // Register a service that fails to shot down cleanly - failure := errors.New("fail") - failer := &InstrumentedService{stop: failure} - stack.RegisterLifecycle(failer) - - // Start the protocol stack, and ensure that a failing shut down terminates all - // Start the stack and make sure all is online - if err := stack.Start(); err != nil { - t.Fatalf("failed to start protocol stack: %v", err) - } - for id := range lifecycles { - if !started[id] { - t.Fatalf("service %s: service not running", id) - } - if stopped[id] { - t.Fatalf("service %s: service already stopped", id) - } - } - // Stop the stack, verify failure and check all terminations - err = stack.Close() - if err, ok := err.(*StopError); !ok { - t.Fatalf("termination failure mismatch: have %v, want StopError", err) - } else { - failer := reflect.TypeOf(&InstrumentedService{}) - if err.Services[failer] != failure { - t.Fatalf("failer termination failure mismatch: have %v, want %v", err.Services[failer], failure) - } - if len(err.Services) != 1 { - t.Fatalf("failure count mismatch: have %d, want %d", len(err.Services), 1) - } - } - for id := range lifecycles { - if !stopped[id] { - t.Fatalf("service %s: service not terminated", id) - } - delete(started, id) - delete(stopped, id) - } - - stack.server = &p2p.Server{} - stack.server.PrivateKey = testNodeKey -} - -// Tests whether a handler can be successfully mounted on the canonical HTTP server -// on the given prefix -func TestRegisterHandler_Successful(t *testing.T) { - node := createNode(t, 7878, 7979) - defer node.Close() - // create and mount handler - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("success")) - }) - node.RegisterHandler("test", "/test", handler) - - // start node - if err := node.Start(); err != nil { - t.Fatalf("could not start node: %v", err) - } - - // create HTTP request - httpReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:7878/test", nil) - if err != nil { - t.Error("could not issue new http request ", err) - } - - // check response - resp := doHTTPRequest(t, httpReq) - buf := make([]byte, 7) - _, err = io.ReadFull(resp.Body, buf) - if err != nil { - t.Fatalf("could not read response: %v", err) - } - assert.Equal(t, "success", string(buf)) -} - -// Tests that the given handler will not be successfully mounted since no HTTP server -// is enabled for RPC -func TestRegisterHandler_Unsuccessful(t *testing.T) { - node, err := New(&DefaultConfig) - if err != nil { - t.Fatalf("could not create new node: %v", err) - } - - // create and mount handler - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("success")) - }) - node.RegisterHandler("test", "/test", handler) -} - -// Tests whether websocket requests can be handled on the same port as a regular http server. -func TestWebsocketHTTPOnSamePort_WebsocketRequest(t *testing.T) { - node := startHTTP(t, 0, 0) - defer node.Close() - - ws := strings.Replace(node.HTTPEndpoint(), "http://", "ws://", 1) - - if node.WSEndpoint() != ws { - t.Fatalf("endpoints should be the same") - } - if !checkRPC(ws) { - t.Fatalf("ws request failed") - } - if !checkRPC(node.HTTPEndpoint()) { - t.Fatalf("http request failed") - } -} - -func TestWebsocketHTTPOnSeparatePort_WSRequest(t *testing.T) { - // try and get a free port - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal("can't listen:", err) - } - port := listener.Addr().(*net.TCPAddr).Port - listener.Close() - - node := startHTTP(t, 0, port) - defer node.Close() - - wsOnHTTP := strings.Replace(node.HTTPEndpoint(), "http://", "ws://", 1) - ws := fmt.Sprintf("ws://127.0.0.1:%d", port) - - if node.WSEndpoint() == wsOnHTTP { - t.Fatalf("endpoints should not be the same") - } - // ensure ws endpoint matches the expected endpoint - if node.WSEndpoint() != ws { - t.Fatalf("ws endpoint is incorrect: expected %s, got %s", ws, node.WSEndpoint()) - } - - if !checkRPC(ws) { - t.Fatalf("ws request failed") - } - if !checkRPC(node.HTTPEndpoint()) { - t.Fatalf("http request failed") - } -} - -type rpcPrefixTest struct { - httpPrefix, wsPrefix string - // These lists paths on which JSON-RPC should be served / not served. - wantHTTP []string - wantNoHTTP []string - wantWS []string - wantNoWS []string -} - -func TestNodeRPCPrefix(t *testing.T) { - t.Parallel() - - tests := []rpcPrefixTest{ - // both off - { - httpPrefix: "", wsPrefix: "", - wantHTTP: []string{"/", "/?p=1"}, - wantNoHTTP: []string{"/test", "/test?p=1"}, - wantWS: []string{"/", "/?p=1"}, - wantNoWS: []string{"/test", "/test?p=1"}, - }, - // only http prefix - { - httpPrefix: "/testprefix", wsPrefix: "", - wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"}, - wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"}, - wantWS: []string{"/", "/?p=1"}, - wantNoWS: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"}, - }, - // only ws prefix - { - httpPrefix: "", wsPrefix: "/testprefix", - wantHTTP: []string{"/", "/?p=1"}, - wantNoHTTP: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"}, - wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"}, - wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"}, - }, - // both set - { - httpPrefix: "/testprefix", wsPrefix: "/testprefix", - wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"}, - wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"}, - wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"}, - wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"}, - }, - } - - for _, test := range tests { - test := test - name := fmt.Sprintf("http=%s ws=%s", test.httpPrefix, test.wsPrefix) - t.Run(name, func(t *testing.T) { - cfg := &Config{ - HTTPHost: "127.0.0.1", - HTTPPathPrefix: test.httpPrefix, - WSHost: "127.0.0.1", - WSPathPrefix: test.wsPrefix, - } - node, err := New(cfg) - if err != nil { - t.Fatal("can't create node:", err) - } - defer node.Close() - if err := node.Start(); err != nil { - t.Fatal("can't start node:", err) - } - test.check(t, node) - }) - } -} - -func (test rpcPrefixTest) check(t *testing.T, node *Node) { - t.Helper() - httpBase := "http://" + node.http.listenAddr() - wsBase := "ws://" + node.http.listenAddr() - - if node.WSEndpoint() != wsBase+test.wsPrefix { - t.Errorf("Error: node has wrong WSEndpoint %q", node.WSEndpoint()) - } - - for _, path := range test.wantHTTP { - resp := rpcRequest(t, httpBase+path, testMethod) - if resp.StatusCode != 200 { - t.Errorf("Error: %s: bad status code %d, want 200", path, resp.StatusCode) - } - } - for _, path := range test.wantNoHTTP { - resp := rpcRequest(t, httpBase+path, testMethod) - if resp.StatusCode != 404 { - t.Errorf("Error: %s: bad status code %d, want 404", path, resp.StatusCode) - } - } - for _, path := range test.wantWS { - err := wsRequest(t, wsBase+path) - if err != nil { - t.Errorf("Error: %s: WebSocket connection failed: %v", path, err) - } - } - for _, path := range test.wantNoWS { - err := wsRequest(t, wsBase+path) - if err == nil { - t.Errorf("Error: %s: WebSocket connection succeeded for path in wantNoWS", path) - } - } -} - -func createNode(t *testing.T, httpPort, wsPort int) *Node { - conf := &Config{ - HTTPHost: "127.0.0.1", - HTTPPort: httpPort, - WSHost: "127.0.0.1", - WSPort: wsPort, - HTTPTimeouts: rpc.DefaultHTTPTimeouts, - } - node, err := New(conf) - if err != nil { - t.Fatalf("could not create a new node: %v", err) - } - return node -} - -func startHTTP(t *testing.T, httpPort, wsPort int) *Node { - node := createNode(t, httpPort, wsPort) - err := node.Start() - if err != nil { - t.Fatalf("could not start http service on node: %v", err) - } - - return node -} - -func doHTTPRequest(t *testing.T, req *http.Request) *http.Response { - client := http.DefaultClient - resp, err := client.Do(req) - if err != nil { - t.Fatalf("could not issue a GET request to the given endpoint: %v", err) - } - t.Cleanup(func() { resp.Body.Close() }) - return resp -} - -func containsProtocol(stackProtocols []p2p.Protocol, protocol p2p.Protocol) bool { - for _, a := range stackProtocols { - if reflect.DeepEqual(a, protocol) { - return true - } - } - return false -} - -func containsAPI(stackAPIs []rpc.API, api rpc.API) bool { - for _, a := range stackAPIs { - if reflect.DeepEqual(a, api) { - return true - } - } - return false -} diff --git a/node/rpcstack.go b/node/rpcstack.go deleted file mode 100644 index b33c238051..0000000000 --- a/node/rpcstack.go +++ /dev/null @@ -1,644 +0,0 @@ -// 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" - "fmt" - "io" - "net" - "net/http" - "sort" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" - "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 - 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 - rpcEndpointConfig -} - -type rpcEndpointConfig struct { - jwtSecret []byte // optional JWT secret - batchItemLimit int - batchResponseSizeLimit 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 fmt.Errorf("JSON-RPC over HTTP is already enabled") - } - - // Create RPC server and handler. - srv := rpc.NewServer() - srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit) - 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), - 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 fmt.Errorf("JSON-RPC over WebSocket is already enabled") - } - // Create RPC server and handler. - srv := rpc.NewServer() - srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit) - 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), - 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) http.Handler { - // Wrap the CORS-handler within a host-handler - handler := newCorsHandler(srv, cors) - 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) http.Handler { - if len(jwtSecret) != 0 { - return newJWTHandler(jwtSecret, srv) - } - return srv -} - -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/node/rpcstack_test.go b/node/rpcstack_test.go deleted file mode 100644 index e41cc51ad3..0000000000 --- a/node/rpcstack_test.go +++ /dev/null @@ -1,616 +0,0 @@ -// 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 ( - "bytes" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "strconv" - "strings" - "testing" - "time" - - "github.com/ethereum/go-ethereum/internal/testlog" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" - "github.com/golang-jwt/jwt/v4" - "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" -) - -const testMethod = "rpc_modules" - -// TestCorsHandler makes sure CORS are properly handled on the http server. -func TestCorsHandler(t *testing.T) { - srv := createAndStartServer(t, &httpConfig{CorsAllowedOrigins: []string{"test", "test.com"}}, false, &wsConfig{}, nil) - defer srv.stop() - url := "http://" + srv.listenAddr() - - resp := rpcRequest(t, url, testMethod, "origin", "test.com") - assert.Equal(t, "test.com", resp.Header.Get("Access-Control-Allow-Origin")) - - resp2 := rpcRequest(t, url, testMethod, "origin", "bad") - assert.Equal(t, "", resp2.Header.Get("Access-Control-Allow-Origin")) -} - -// TestVhosts makes sure vhosts are properly handled on the http server. -func TestVhosts(t *testing.T) { - srv := createAndStartServer(t, &httpConfig{Vhosts: []string{"test"}}, false, &wsConfig{}, nil) - defer srv.stop() - url := "http://" + srv.listenAddr() - - resp := rpcRequest(t, url, testMethod, "host", "test") - assert.Equal(t, resp.StatusCode, http.StatusOK) - - resp2 := rpcRequest(t, url, testMethod, "host", "bad") - assert.Equal(t, resp2.StatusCode, http.StatusForbidden) -} - -type originTest struct { - spec string - expOk []string - expFail []string -} - -// splitAndTrim splits input separated by a comma -// and trims excessive white space from the substrings. -// Copied over from flags.go -func splitAndTrim(input string) (ret []string) { - l := strings.Split(input, ",") - for _, r := range l { - r = strings.TrimSpace(r) - if len(r) > 0 { - ret = append(ret, r) - } - } - return ret -} - -// TestWebsocketOrigins makes sure the websocket origins are properly handled on the websocket server. -func TestWebsocketOrigins(t *testing.T) { - tests := []originTest{ - { - spec: "*", // allow all - expOk: []string{"", "http://test", "https://test", "http://test:8540", "https://test:8540", - "http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"}, - }, - { - spec: "test", - expOk: []string{"http://test", "https://test", "http://test:8540", "https://test:8540"}, - expFail: []string{"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"}, - }, - // scheme tests - { - spec: "https://test", - expOk: []string{"https://test", "https://test:9999"}, - expFail: []string{ - "test", // no scheme, required by spec - "http://test", // wrong scheme - "http://test.foo", "https://a.test.x", // subdomain variations - "http://testx:8540", "https://xtest:8540"}, - }, - // ip tests - { - spec: "https://12.34.56.78", - expOk: []string{"https://12.34.56.78", "https://12.34.56.78:8540"}, - expFail: []string{ - "http://12.34.56.78", // wrong scheme - "http://12.34.56.78:443", // wrong scheme - "http://1.12.34.56.78", // wrong 'domain name' - "http://12.34.56.78.a", // wrong 'domain name' - "https://87.65.43.21", "http://87.65.43.21:8540", "https://87.65.43.21:8540"}, - }, - // port tests - { - spec: "test:8540", - expOk: []string{"http://test:8540", "https://test:8540"}, - expFail: []string{ - "http://test", "https://test", // spec says port required - "http://test:8541", "https://test:8541", // wrong port - "http://bad", "https://bad", "http://bad:8540", "https://bad:8540"}, - }, - // scheme and port - { - spec: "https://test:8540", - expOk: []string{"https://test:8540"}, - expFail: []string{ - "https://test", // missing port - "http://test", // missing port, + wrong scheme - "http://test:8540", // wrong scheme - "http://test:8541", "https://test:8541", // wrong port - "http://bad", "https://bad", "http://bad:8540", "https://bad:8540"}, - }, - // several allowed origins - { - spec: "localhost,http://127.0.0.1", - expOk: []string{"localhost", "http://localhost", "https://localhost:8443", - "http://127.0.0.1", "http://127.0.0.1:8080"}, - expFail: []string{ - "https://127.0.0.1", // wrong scheme - "http://bad", "https://bad", "http://bad:8540", "https://bad:8540"}, - }, - } - for _, tc := range tests { - srv := createAndStartServer(t, &httpConfig{}, true, &wsConfig{Origins: splitAndTrim(tc.spec)}, nil) - url := fmt.Sprintf("ws://%v", srv.listenAddr()) - for _, origin := range tc.expOk { - if err := wsRequest(t, url, "Origin", origin); err != nil { - t.Errorf("spec '%v', origin '%v': expected ok, got %v", tc.spec, origin, err) - } - } - for _, origin := range tc.expFail { - if err := wsRequest(t, url, "Origin", origin); err == nil { - t.Errorf("spec '%v', origin '%v': expected not to allow, got ok", tc.spec, origin) - } - } - srv.stop() - } -} - -// TestIsWebsocket tests if an incoming websocket upgrade request is handled properly. -func TestIsWebsocket(t *testing.T) { - r, _ := http.NewRequest(http.MethodGet, "/", nil) - - assert.False(t, isWebsocket(r)) - r.Header.Set("upgrade", "websocket") - assert.False(t, isWebsocket(r)) - r.Header.Set("connection", "upgrade") - assert.True(t, isWebsocket(r)) - r.Header.Set("connection", "upgrade,keep-alive") - assert.True(t, isWebsocket(r)) - r.Header.Set("connection", " UPGRADE,keep-alive") - assert.True(t, isWebsocket(r)) -} - -func Test_checkPath(t *testing.T) { - tests := []struct { - req *http.Request - prefix string - expected bool - }{ - { - req: &http.Request{URL: &url.URL{Path: "/test"}}, - prefix: "/test", - expected: true, - }, - { - req: &http.Request{URL: &url.URL{Path: "/testing"}}, - prefix: "/test", - expected: true, - }, - { - req: &http.Request{URL: &url.URL{Path: "/"}}, - prefix: "/test", - expected: false, - }, - { - req: &http.Request{URL: &url.URL{Path: "/fail"}}, - prefix: "/test", - expected: false, - }, - { - req: &http.Request{URL: &url.URL{Path: "/"}}, - prefix: "", - expected: true, - }, - { - req: &http.Request{URL: &url.URL{Path: "/fail"}}, - prefix: "", - expected: false, - }, - { - req: &http.Request{URL: &url.URL{Path: "/"}}, - prefix: "/", - expected: true, - }, - { - req: &http.Request{URL: &url.URL{Path: "/testing"}}, - prefix: "/", - expected: true, - }, - } - - for i, tt := range tests { - t.Run(strconv.Itoa(i), func(t *testing.T) { - assert.Equal(t, tt.expected, checkPath(tt.req, tt.prefix)) - }) - } -} - -func createAndStartServer(t *testing.T, conf *httpConfig, ws bool, wsConf *wsConfig, timeouts *rpc.HTTPTimeouts) *httpServer { - t.Helper() - - if timeouts == nil { - timeouts = &rpc.DefaultHTTPTimeouts - } - srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), *timeouts) - assert.NoError(t, srv.enableRPC(apis(), *conf)) - if ws { - assert.NoError(t, srv.enableWS(nil, *wsConf)) - } - assert.NoError(t, srv.setListenAddr("localhost", 0)) - assert.NoError(t, srv.start()) - return srv -} - -// wsRequest attempts to open a WebSocket connection to the given URL. -func wsRequest(t *testing.T, url string, extraHeaders ...string) error { - t.Helper() - //t.Logf("checking WebSocket on %s (origin %q)", url, browserOrigin) - - headers := make(http.Header) - // Apply extra headers. - if len(extraHeaders)%2 != 0 { - panic("odd extraHeaders length") - } - for i := 0; i < len(extraHeaders); i += 2 { - key, value := extraHeaders[i], extraHeaders[i+1] - headers.Set(key, value) - } - conn, _, err := websocket.DefaultDialer.Dial(url, headers) - if conn != nil { - conn.Close() - } - return err -} - -// rpcRequest performs a JSON-RPC request to the given URL. -func rpcRequest(t *testing.T, url, method string, extraHeaders ...string) *http.Response { - t.Helper() - - body := fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"%s","params":[]}`, method) - return baseRpcRequest(t, url, body, extraHeaders...) -} - -func batchRpcRequest(t *testing.T, url string, methods []string, extraHeaders ...string) *http.Response { - reqs := make([]string, len(methods)) - for i, m := range methods { - reqs[i] = fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"%s","params":[]}`, m) - } - body := fmt.Sprintf(`[%s]`, strings.Join(reqs, ",")) - return baseRpcRequest(t, url, body, extraHeaders...) -} - -func baseRpcRequest(t *testing.T, url, bodyStr string, extraHeaders ...string) *http.Response { - t.Helper() - - // Create the request. - body := bytes.NewReader([]byte(bodyStr)) - req, err := http.NewRequest(http.MethodPost, url, body) - if err != nil { - t.Fatal("could not create http request:", err) - } - req.Header.Set("content-type", "application/json") - req.Header.Set("accept-encoding", "identity") - - // Apply extra headers. - if len(extraHeaders)%2 != 0 { - panic("odd extraHeaders length") - } - for i := 0; i < len(extraHeaders); i += 2 { - key, value := extraHeaders[i], extraHeaders[i+1] - if strings.EqualFold(key, "host") { - req.Host = value - } else { - req.Header.Set(key, value) - } - } - - // Perform the request. - t.Logf("checking RPC/HTTP on %s %v", url, extraHeaders) - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { resp.Body.Close() }) - return resp -} - -type testClaim map[string]interface{} - -func (testClaim) Valid() error { - return nil -} - -func TestJWT(t *testing.T) { - var secret = []byte("secret") - issueToken := func(secret []byte, method jwt.SigningMethod, input map[string]interface{}) string { - if method == nil { - method = jwt.SigningMethodHS256 - } - ss, _ := jwt.NewWithClaims(method, testClaim(input)).SignedString(secret) - return ss - } - cfg := rpcEndpointConfig{jwtSecret: []byte("secret")} - httpcfg := &httpConfig{rpcEndpointConfig: cfg} - wscfg := &wsConfig{Origins: []string{"*"}, rpcEndpointConfig: cfg} - srv := createAndStartServer(t, httpcfg, true, wscfg, nil) - wsUrl := fmt.Sprintf("ws://%v", srv.listenAddr()) - htUrl := fmt.Sprintf("http://%v", srv.listenAddr()) - - expOk := []func() string{ - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})) - }, - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + 4})) - }, - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - 4})) - }, - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{ - "iat": time.Now().Unix(), - "exp": time.Now().Unix() + 2, - })) - }, - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{ - "iat": time.Now().Unix(), - "bar": "baz", - })) - }, - } - for i, tokenFn := range expOk { - token := tokenFn() - if err := wsRequest(t, wsUrl, "Authorization", token); err != nil { - t.Errorf("test %d-ws, token '%v': expected ok, got %v", i, token, err) - } - token = tokenFn() - if resp := rpcRequest(t, htUrl, testMethod, "Authorization", token); resp.StatusCode != 200 { - t.Errorf("test %d-http, token '%v': expected ok, got %v", i, token, resp.StatusCode) - } - } - - expFail := []func() string{ - // future - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + int64(jwtExpiryTimeout.Seconds()) + 1})) - }, - // stale - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - int64(jwtExpiryTimeout.Seconds()) - 1})) - }, - // wrong algo - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, jwt.SigningMethodHS512, testClaim{"iat": time.Now().Unix() + 4})) - }, - // expired - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix(), "exp": time.Now().Unix()})) - }, - // missing mandatory iat - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{})) - }, - // wrong secret - func() string { - return fmt.Sprintf("Bearer %v", issueToken([]byte("wrong"), nil, testClaim{"iat": time.Now().Unix()})) - }, - func() string { - return fmt.Sprintf("Bearer %v", issueToken([]byte{}, nil, testClaim{"iat": time.Now().Unix()})) - }, - func() string { - return fmt.Sprintf("Bearer %v", issueToken(nil, nil, testClaim{"iat": time.Now().Unix()})) - }, - // Various malformed syntax - func() string { - return fmt.Sprintf("%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})) - }, - func() string { - return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})) - }, - func() string { - return fmt.Sprintf("bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})) - }, - func() string { - return fmt.Sprintf("Bearer: %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})) - }, - func() string { - return fmt.Sprintf("Bearer:%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})) - }, - func() string { - return fmt.Sprintf("Bearer\t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})) - }, - func() string { - return fmt.Sprintf("Bearer \t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})) - }, - } - for i, tokenFn := range expFail { - token := tokenFn() - if err := wsRequest(t, wsUrl, "Authorization", token); err == nil { - t.Errorf("tc %d-ws, token '%v': expected not to allow, got ok", i, token) - } - - token = tokenFn() - resp := rpcRequest(t, htUrl, testMethod, "Authorization", token) - if resp.StatusCode != http.StatusUnauthorized { - t.Errorf("tc %d-http, token '%v': expected not to allow, got %v", i, token, resp.StatusCode) - } - } - srv.stop() -} - -func TestGzipHandler(t *testing.T) { - type gzipTest struct { - name string - handler http.HandlerFunc - status int - isGzip bool - header map[string]string - } - tests := []gzipTest{ - { - name: "Write", - handler: func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("response")) - }, - isGzip: true, - status: 200, - }, - { - name: "WriteHeader", - handler: func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("x-foo", "bar") - w.WriteHeader(205) - w.Write([]byte("response")) - }, - isGzip: true, - status: 205, - header: map[string]string{"x-foo": "bar"}, - }, - { - name: "WriteContentLength", - handler: func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("content-length", "8") - w.Write([]byte("response")) - }, - isGzip: true, - status: 200, - }, - { - name: "Flush", - handler: func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("res")) - w.(http.Flusher).Flush() - w.Write([]byte("ponse")) - }, - isGzip: true, - status: 200, - }, - { - name: "disable", - handler: func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("transfer-encoding", "identity") - w.Header().Set("x-foo", "bar") - w.Write([]byte("response")) - }, - isGzip: false, - status: 200, - header: map[string]string{"x-foo": "bar"}, - }, - { - name: "disable-WriteHeader", - handler: func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("transfer-encoding", "identity") - w.Header().Set("x-foo", "bar") - w.WriteHeader(205) - w.Write([]byte("response")) - }, - isGzip: false, - status: 205, - header: map[string]string{"x-foo": "bar"}, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - srv := httptest.NewServer(newGzipHandler(test.handler)) - defer srv.Close() - - resp, err := http.Get(srv.URL) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - - content, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - wasGzip := resp.Uncompressed - - if string(content) != "response" { - t.Fatalf("wrong response content %q", content) - } - if wasGzip != test.isGzip { - t.Fatalf("response gzipped == %t, want %t", wasGzip, test.isGzip) - } - if resp.StatusCode != test.status { - t.Fatalf("response status == %d, want %d", resp.StatusCode, test.status) - } - for name, expectedValue := range test.header { - if v := resp.Header.Get(name); v != expectedValue { - t.Fatalf("response header %s == %s, want %s", name, v, expectedValue) - } - } - }) - } -} - -func TestHTTPWriteTimeout(t *testing.T) { - const ( - timeoutRes = `{"jsonrpc":"2.0","id":1,"error":{"code":-32002,"message":"request timed out"}}` - greetRes = `{"jsonrpc":"2.0","id":1,"result":"Hello"}` - ) - // Set-up server - timeouts := rpc.DefaultHTTPTimeouts - timeouts.WriteTimeout = time.Second - srv := createAndStartServer(t, &httpConfig{Modules: []string{"test"}}, false, &wsConfig{}, &timeouts) - url := fmt.Sprintf("http://%v", srv.listenAddr()) - - // Send normal request - t.Run("message", func(t *testing.T) { - resp := rpcRequest(t, url, "test_sleep") - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - if string(body) != timeoutRes { - t.Errorf("wrong response. have %s, want %s", string(body), timeoutRes) - } - }) - - // Batch request - t.Run("batch", func(t *testing.T) { - want := fmt.Sprintf("[%s,%s,%s]", greetRes, timeoutRes, timeoutRes) - resp := batchRpcRequest(t, url, []string{"test_greet", "test_sleep", "test_greet"}) - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - if string(body) != want { - t.Errorf("wrong response. have %s, want %s", string(body), want) - } - }) -} - -func apis() []rpc.API { - return []rpc.API{ - { - Namespace: "test", - Service: &testService{}, - }, - } -} - -type testService struct{} - -func (s *testService) Greet() string { - return "Hello" -} - -func (s *testService) Sleep() { - time.Sleep(1500 * time.Millisecond) -} diff --git a/node/utils_test.go b/node/utils_test.go deleted file mode 100644 index 681f3a8b28..0000000000 --- a/node/utils_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// 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 . - -// Contains a batch of utility type declarations used by the tests. As the node -// operates on unique types, a lot of them are needed to check various features. - -package node - -import ( - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rpc" -) - -// NoopLifecycle is a trivial implementation of the Service interface. -type NoopLifecycle struct{} - -func (s *NoopLifecycle) Start() error { return nil } -func (s *NoopLifecycle) Stop() error { return nil } - -func NewNoop() *Noop { - noop := new(Noop) - return noop -} - -// Set of services all wrapping the base NoopLifecycle resulting in the same method -// signatures but different outer types. -type Noop struct{ NoopLifecycle } - -// InstrumentedService is an implementation of Lifecycle for which all interface -// methods can be instrumented both return value as well as event hook wise. -type InstrumentedService struct { - start error - stop error - - startHook func() - stopHook func() -} - -func (s *InstrumentedService) Start() error { - if s.startHook != nil { - s.startHook() - } - return s.start -} - -func (s *InstrumentedService) Stop() error { - if s.stopHook != nil { - s.stopHook() - } - return s.stop -} - -type FullService struct{} - -func NewFullService(stack *Node) (*FullService, error) { - fs := new(FullService) - - stack.RegisterProtocols(fs.Protocols()) - stack.RegisterAPIs(fs.APIs()) - stack.RegisterLifecycle(fs) - return fs, nil -} - -func (f *FullService) Start() error { return nil } - -func (f *FullService) Stop() error { return nil } - -func (f *FullService) Protocols() []p2p.Protocol { - return []p2p.Protocol{ - { - Name: "test1", - Version: uint(1), - }, - { - Name: "test2", - Version: uint(2), - }, - } -} - -func (f *FullService) APIs() []rpc.API { - return []rpc.API{ - { - Namespace: "admin", - }, - { - Namespace: "debug", - }, - { - Namespace: "net", - }, - } -}