node: refactor package node (#21105)

This PR significantly changes the APIs for instantiating Ethereum nodes in
a Go program. The new APIs are not backwards-compatible, but we feel that
this is made up for by the much simpler way of registering services on
node.Node. You can find more information and rationale in the design
document: https://gist.github.com/renaynay/5bec2de19fde66f4d04c535fd24f0775.

There is also a new feature in Node's Go API: it is now possible to
register arbitrary handlers on the user-facing HTTP server. In geth, this
facility is used to enable GraphQL.

There is a single minor change relevant for geth users in this PR: The
GraphQL API is no longer available separately from the JSON-RPC HTTP
server. If you want GraphQL, you need to enable it using the
./geth --http --graphql flag combination.

The --graphql.port and --graphql.addr flags are no longer available.
This commit is contained in:
rene
2020-08-03 19:40:46 +02:00
committed by GitHub
parent b2b14e6ce3
commit c0c01612e9
63 changed files with 2606 additions and 2887 deletions

View File

@ -23,26 +23,46 @@ import (
"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/p2p"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/rpc"
)
// PrivateAdminAPI is the collection of administrative API methods exposed only
// over a secure RPC channel.
type PrivateAdminAPI struct {
node *Node // Node interfaced by this API
// apis returns the collection of built-in RPC APIs.
func (n *Node) apis() []rpc.API {
return []rpc.API{
{
Namespace: "admin",
Version: "1.0",
Service: &privateAdminAPI{n},
}, {
Namespace: "admin",
Version: "1.0",
Service: &publicAdminAPI{n},
Public: true,
}, {
Namespace: "debug",
Version: "1.0",
Service: debug.Handler,
}, {
Namespace: "web3",
Version: "1.0",
Service: &publicWeb3API{n},
Public: true,
},
}
}
// NewPrivateAdminAPI creates a new API definition for the private admin methods
// of the node itself.
func NewPrivateAdminAPI(node *Node) *PrivateAdminAPI {
return &PrivateAdminAPI{node: node}
// privateAdminAPI is the collection of administrative API methods exposed only
// over a secure RPC channel.
type privateAdminAPI 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 *PrivateAdminAPI) AddPeer(url string) (bool, error) {
func (api *privateAdminAPI) AddPeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
@ -58,7 +78,7 @@ func (api *PrivateAdminAPI) AddPeer(url string) (bool, error) {
}
// RemovePeer disconnects from a remote node if the connection exists
func (api *PrivateAdminAPI) RemovePeer(url string) (bool, error) {
func (api *privateAdminAPI) RemovePeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
@ -74,7 +94,7 @@ func (api *PrivateAdminAPI) RemovePeer(url string) (bool, error) {
}
// AddTrustedPeer allows a remote node to always connect, even if slots are full
func (api *PrivateAdminAPI) AddTrustedPeer(url string) (bool, error) {
func (api *privateAdminAPI) AddTrustedPeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
@ -90,7 +110,7 @@ func (api *PrivateAdminAPI) AddTrustedPeer(url string) (bool, error) {
// RemoveTrustedPeer removes a remote node from the trusted peer set, but it
// does not disconnect it automatically.
func (api *PrivateAdminAPI) RemoveTrustedPeer(url string) (bool, error) {
func (api *privateAdminAPI) RemoveTrustedPeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
@ -106,7 +126,7 @@ func (api *PrivateAdminAPI) RemoveTrustedPeer(url string) (bool, error) {
// PeerEvents creates an RPC subscription which receives peer events from the
// node's p2p.Server
func (api *PrivateAdminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription, error) {
func (api *privateAdminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
@ -143,14 +163,11 @@ func (api *PrivateAdminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription,
}
// StartRPC starts the HTTP RPC API server.
func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) {
func (api *privateAdminAPI) StartRPC(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) {
api.node.lock.Lock()
defer api.node.lock.Unlock()
if api.node.httpHandler != nil {
return false, fmt.Errorf("HTTP RPC already running on %s", api.node.httpEndpoint)
}
// Determine host and port.
if host == nil {
h := DefaultHTTPHost
if api.node.config.HTTPHost != "" {
@ -162,57 +179,55 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis
port = &api.node.config.HTTPPort
}
allowedOrigins := api.node.config.HTTPCors
// Determine config.
config := httpConfig{
CorsAllowedOrigins: api.node.config.HTTPCors,
Vhosts: api.node.config.HTTPVirtualHosts,
Modules: api.node.config.HTTPModules,
}
if cors != nil {
allowedOrigins = nil
config.CorsAllowedOrigins = nil
for _, origin := range strings.Split(*cors, ",") {
allowedOrigins = append(allowedOrigins, strings.TrimSpace(origin))
config.CorsAllowedOrigins = append(config.CorsAllowedOrigins, strings.TrimSpace(origin))
}
}
allowedVHosts := api.node.config.HTTPVirtualHosts
if vhosts != nil {
allowedVHosts = nil
config.Vhosts = nil
for _, vhost := range strings.Split(*host, ",") {
allowedVHosts = append(allowedVHosts, strings.TrimSpace(vhost))
config.Vhosts = append(config.Vhosts, strings.TrimSpace(vhost))
}
}
modules := api.node.httpWhitelist
if apis != nil {
modules = nil
config.Modules = nil
for _, m := range strings.Split(*apis, ",") {
modules = append(modules, strings.TrimSpace(m))
config.Modules = append(config.Modules, strings.TrimSpace(m))
}
}
if err := api.node.startHTTP(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, allowedOrigins, allowedVHosts, api.node.config.HTTPTimeouts, api.node.config.WSOrigins); err != nil {
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
}
// StopRPC terminates an already running HTTP RPC API endpoint.
func (api *PrivateAdminAPI) StopRPC() (bool, error) {
api.node.lock.Lock()
defer api.node.lock.Unlock()
if api.node.httpHandler == nil {
return false, fmt.Errorf("HTTP RPC not running")
}
api.node.stopHTTP()
// StopRPC shuts down the HTTP server.
func (api *privateAdminAPI) StopRPC() (bool, error) {
api.node.http.stop()
return true, nil
}
// StartWS starts the websocket RPC API server.
func (api *PrivateAdminAPI) StartWS(host *string, port *int, allowedOrigins *string, apis *string) (bool, error) {
func (api *privateAdminAPI) StartWS(host *string, port *int, allowedOrigins *string, apis *string) (bool, error) {
api.node.lock.Lock()
defer api.node.lock.Unlock()
if api.node.wsHandler != nil {
return false, fmt.Errorf("WebSocket RPC already running on %s", api.node.wsEndpoint)
}
// Determine host and port.
if host == nil {
h := DefaultWSHost
if api.node.config.WSHost != "" {
@ -224,55 +239,56 @@ func (api *PrivateAdminAPI) StartWS(host *string, port *int, allowedOrigins *str
port = &api.node.config.WSPort
}
origins := api.node.config.WSOrigins
if allowedOrigins != nil {
origins = nil
for _, origin := range strings.Split(*allowedOrigins, ",") {
origins = append(origins, strings.TrimSpace(origin))
}
// Determine config.
config := wsConfig{
Modules: api.node.config.WSModules,
Origins: api.node.config.WSOrigins,
// ExposeAll: api.node.config.WSExposeAll,
}
modules := api.node.config.WSModules
if apis != nil {
modules = nil
config.Modules = nil
for _, m := range strings.Split(*apis, ",") {
modules = append(modules, strings.TrimSpace(m))
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))
}
}
if err := api.node.startWS(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, origins, api.node.config.WSExposeAll); err != nil {
// Enable WebSocket on the server.
server := api.node.wsServerForPort(*port)
if err := server.setListenAddr(*host, *port); err != nil {
return false, err
}
return true, nil
}
// StopWS terminates an already running websocket RPC API endpoint.
func (api *PrivateAdminAPI) StopWS() (bool, error) {
api.node.lock.Lock()
defer api.node.lock.Unlock()
if api.node.wsHandler == nil {
return false, fmt.Errorf("WebSocket RPC not running")
if err := server.enableWS(api.node.rpcAPIs, config); err != nil {
return false, err
}
api.node.stopWS()
if err := server.start(); err != nil {
return false, err
}
api.node.http.log.Info("WebSocket endpoint opened", "url", api.node.WSEndpoint())
return true, nil
}
// PublicAdminAPI is the collection of administrative API methods exposed over
// both secure and unsecure RPC channels.
type PublicAdminAPI struct {
node *Node // Node interfaced by this API
// StopWS terminates all WebSocket servers.
func (api *privateAdminAPI) StopWS() (bool, error) {
api.node.http.stopWS()
api.node.ws.stop()
return true, nil
}
// NewPublicAdminAPI creates a new API definition for the public admin methods
// of the node itself.
func NewPublicAdminAPI(node *Node) *PublicAdminAPI {
return &PublicAdminAPI{node: node}
// publicAdminAPI is the collection of administrative API methods exposed over
// both secure and unsecure RPC channels.
type publicAdminAPI struct {
node *Node // Node interfaced by this API
}
// Peers retrieves all the information we know about each individual peer at the
// protocol granularity.
func (api *PublicAdminAPI) Peers() ([]*p2p.PeerInfo, error) {
func (api *publicAdminAPI) Peers() ([]*p2p.PeerInfo, error) {
server := api.node.Server()
if server == nil {
return nil, ErrNodeStopped
@ -282,7 +298,7 @@ func (api *PublicAdminAPI) Peers() ([]*p2p.PeerInfo, error) {
// NodeInfo retrieves all the information we know about the host node at the
// protocol granularity.
func (api *PublicAdminAPI) NodeInfo() (*p2p.NodeInfo, error) {
func (api *publicAdminAPI) NodeInfo() (*p2p.NodeInfo, error) {
server := api.node.Server()
if server == nil {
return nil, ErrNodeStopped
@ -291,27 +307,22 @@ func (api *PublicAdminAPI) NodeInfo() (*p2p.NodeInfo, error) {
}
// Datadir retrieves the current data directory the node is using.
func (api *PublicAdminAPI) Datadir() string {
func (api *publicAdminAPI) Datadir() string {
return api.node.DataDir()
}
// PublicWeb3API offers helper utils
type PublicWeb3API struct {
// publicWeb3API offers helper utils
type publicWeb3API struct {
stack *Node
}
// NewPublicWeb3API creates a new Web3Service instance
func NewPublicWeb3API(stack *Node) *PublicWeb3API {
return &PublicWeb3API{stack}
}
// ClientVersion returns the node name
func (s *PublicWeb3API) ClientVersion() string {
func (s *publicWeb3API) 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 *PublicWeb3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
func (s *publicWeb3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
return crypto.Keccak256(input)
}

350
node/api_test.go Normal file
View File

@ -0,0 +1,350 @@
// 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 <http://www.gnu.org/licenses/>.
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, *privateAdminAPI)
// 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 *privateAdminAPI) {
},
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 *privateAdminAPI) {
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: false,
},
{
name: "rpc enabled through API",
cfg: Config{},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
_, err := api.StartRPC(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 *privateAdminAPI) {
// 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.StartRPC(sp("127.0.0.1"), ip(port), nil, nil, nil)
if err == nil {
t.Fatal("StartRPC should have failed on port", port)
}
// Try again after unblocking the port. It should work this time.
listener.Close()
_, err = api.StartRPC(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 *privateAdminAPI) {
_, err := api.StopRPC()
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 *privateAdminAPI) {
_, err := api.StopRPC()
assert.NoError(t, err)
_, err = api.StopRPC()
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 *privateAdminAPI) {
_, 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 *privateAdminAPI) {
_, 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 *privateAdminAPI) {
_, 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 *privateAdminAPI) {
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 *privateAdminAPI) {
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 *privateAdminAPI) {
_, err := api.StartRPC(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.StopRPC()
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 *privateAdminAPI) {
_, err := api.StartWS(sp("127.0.0.1"), ip(0), nil, nil)
assert.NoError(t, err)
wsport := n.http.port
_, err = api.StartRPC(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 {
t.Run(test.name, func(t *testing.T) {
// Apply some sane defaults.
config := test.cfg
// config.Logger = testlog.Logger(t, log.LvlDebug)
config.NoUSB = true
config.P2P.NoDiscovery = true
// 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, &privateAdminAPI{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 "
}

View File

@ -162,15 +162,6 @@ type Config struct {
// private APIs to untrusted users is a major security risk.
WSExposeAll bool `toml:",omitempty"`
// GraphQLHost is the host interface on which to start the GraphQL server. If this
// field is empty, no GraphQL API endpoint will be started.
GraphQLHost string
// GraphQLPort is the TCP port number on which to start the GraphQL server. The
// default zero value is/ valid and will pick a port number randomly (useful
// for ephemeral nodes).
GraphQLPort int `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.
@ -247,15 +238,6 @@ func (c *Config) HTTPEndpoint() string {
return fmt.Sprintf("%s:%d", c.HTTPHost, c.HTTPPort)
}
// GraphQLEndpoint resolves a GraphQL endpoint based on the configured host interface
// and port parameters.
func (c *Config) GraphQLEndpoint() string {
if c.GraphQLHost == "" {
return ""
}
return fmt.Sprintf("%s:%d", c.GraphQLHost, c.GraphQLPort)
}
// DefaultHTTPEndpoint returns the HTTP endpoint used by default.
func DefaultHTTPEndpoint() string {
config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort}
@ -280,7 +262,7 @@ func DefaultWSEndpoint() string {
// ExtRPCEnabled returns the indicator whether node enables the external
// RPC(http, ws or graphql).
func (c *Config) ExtRPCEnabled() bool {
return c.HTTPHost != "" || c.WSHost != "" || c.GraphQLHost != ""
return c.HTTPHost != "" || c.WSHost != ""
}
// NodeName returns the devp2p node identifier.

View File

@ -45,7 +45,6 @@ var DefaultConfig = Config{
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
WSPort: DefaultWSPort,
WSModules: []string{"net", "web3"},
GraphQLPort: DefaultGraphQLPort,
GraphQLVirtualHosts: []string{"localhost"},
P2P: p2p.Config{
ListenAddr: ":30303",

View File

@ -22,6 +22,43 @@ resources to provide RPC APIs. Services can also offer devp2p protocols, which a
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

View File

@ -48,21 +48,6 @@ func StartHTTPEndpoint(endpoint string, timeouts rpc.HTTPTimeouts, handler http.
return httpSrv, listener.Addr(), err
}
// startWSEndpoint starts a websocket endpoint.
func startWSEndpoint(endpoint string, 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
}
wsSrv := &http.Server{Handler: handler}
go wsSrv.Serve(listener)
return wsSrv, 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.

View File

@ -39,17 +39,6 @@ func convertFileLockError(err error) error {
return err
}
// DuplicateServiceError is returned during Node startup if a registered service
// constructor returns a service of the same type that was already started.
type DuplicateServiceError struct {
Kind reflect.Type
}
// Error generates a textual representation of the duplicate service error.
func (e *DuplicateServiceError) Error() string {
return fmt.Sprintf("duplicate service: %v", e.Kind)
}
// StopError is returned if a Node fails to stop either any of its registered
// services or itself.
type StopError struct {

31
node/lifecycle.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
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
}

File diff suppressed because it is too large Load Diff

View File

@ -21,26 +21,20 @@ import (
"log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// SampleService is a trivial network service that can be attached to a node for
// 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.Service:
// - Protocols() []p2p.Protocol - devp2p protocols the service can communicate on
// - APIs() []rpc.API - api methods the service wants to expose on rpc channels
// 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 SampleService struct{}
type SampleLifecycle struct{}
func (s *SampleService) Protocols() []p2p.Protocol { return nil }
func (s *SampleService) APIs() []rpc.API { return nil }
func (s *SampleService) Start(*p2p.Server) error { fmt.Println("Service starting..."); return nil }
func (s *SampleService) Stop() error { fmt.Println("Service stopping..."); return nil }
func (s *SampleLifecycle) Start() error { fmt.Println("Service starting..."); return nil }
func (s *SampleLifecycle) Stop() error { fmt.Println("Service stopping..."); return nil }
func ExampleService() {
func ExampleLifecycle() {
// Create a network node to run protocols with the default values.
stack, err := node.New(&node.Config{})
if err != nil {
@ -48,29 +42,18 @@ func ExampleService() {
}
defer stack.Close()
// Create and register a simple network service. This is done through the definition
// of a node.ServiceConstructor that will instantiate a node.Service. The reason for
// the factory method approach is to support service restarts without relying on the
// individual implementations' support for such operations.
constructor := func(context *node.ServiceContext) (node.Service, error) {
return new(SampleService), nil
}
if err := stack.Register(constructor); err != nil {
log.Fatalf("Failed to register service: %v", err)
}
// 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.Restart(); err != nil {
log.Fatalf("Failed to restart the protocol stack: %v", err)
}
if err := stack.Stop(); err != nil {
if err := stack.Close(); err != nil {
log.Fatalf("Failed to stop the protocol stack: %v", err)
}
// Output:
// Service starting...
// Service stopping...
// Service starting...
// Service stopping...
}

View File

@ -18,14 +18,18 @@ package node
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
@ -43,20 +47,28 @@ func testNodeConfig() *Config {
}
}
// Tests that an empty protocol stack can be started, restarted and stopped.
func TestNodeLifeCycle(t *testing.T) {
// 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)
}
defer stack.Close()
stack.Close()
// Ensure that a stopped node can be stopped again
for i := 0; i < 3; i++ {
if err := stack.Stop(); err != ErrNodeStopped {
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)
@ -64,17 +76,11 @@ func TestNodeLifeCycle(t *testing.T) {
if err := stack.Start(); err != ErrNodeRunning {
t.Fatalf("start failure mismatch: have %v, want %v ", err, ErrNodeRunning)
}
// Ensure that a node can be restarted arbitrarily many times
for i := 0; i < 3; i++ {
if err := stack.Restart(); err != nil {
t.Fatalf("iter %d: failed to restart node: %v", i, err)
}
}
// Ensure that a node can be stopped, but only once
if err := stack.Stop(); err != nil {
if err := stack.Close(); err != nil {
t.Fatalf("failed to stop node: %v", err)
}
if err := stack.Stop(); err != ErrNodeStopped {
if err := stack.Close(); err != ErrNodeStopped {
t.Fatalf("stop failure mismatch: have %v, want %v ", err, ErrNodeStopped)
}
}
@ -94,92 +100,152 @@ func TestNodeUsedDataDir(t *testing.T) {
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)
}
defer original.Stop()
// Create a second node based on the same data directory and ensure failure
duplicate, err := New(&Config{DataDir: dir})
if err != nil {
t.Fatalf("failed to create duplicate protocol stack: %v", err)
}
defer duplicate.Close()
if err := duplicate.Start(); err != ErrDatadirUsed {
_, err = New(&Config{DataDir: dir})
if err != ErrDatadirUsed {
t.Fatalf("duplicate datadir failure mismatch: have %v, want %v", err, ErrDatadirUsed)
}
}
// Tests whether services can be registered and duplicates caught.
func TestServiceRegistry(t *testing.T) {
// 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()
// Register a batch of unique services and ensure they start successfully
services := []ServiceConstructor{NewNoopServiceA, NewNoopServiceB, NewNoopServiceC}
for i, constructor := range services {
if err := stack.Register(constructor); err != nil {
t.Fatalf("service #%d: registration failed: %v", i, err)
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)
}
}
if err := stack.Start(); err != nil {
t.Fatalf("failed to start original service stack: %v", err)
}
if err := stack.Stop(); err != nil {
t.Fatalf("failed to stop original service stack: %v", err)
}
// Duplicate one of the services and retry starting the node
if err := stack.Register(NewNoopServiceB); err != nil {
t.Fatalf("duplicate registration failed: %v", err)
}
if err := stack.Start(); err == nil {
t.Fatalf("duplicate service started")
} else {
if _, ok := err.(*DuplicateServiceError); !ok {
t.Fatalf("duplicate error mismatch: have %v, want %v", err, DuplicateServiceError{})
for _, api := range fs.APIs() {
if !containsAPI(stack.rpcAPIs, api) {
t.Fatalf("api %v was not successfully registered", api)
}
}
}
// Tests that registered services get started and stopped correctly.
func TestServiceLifeCycle(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
// This test checks that open databases are closed with node.
func TestNodeCloseClosesDB(t *testing.T) {
stack, _ := New(testNodeConfig())
defer stack.Close()
// Register a batch of life-cycle instrumented services
services := map[string]InstrumentingWrapper{
"A": InstrumentedServiceMakerA,
"B": InstrumentedServiceMakerB,
"C": InstrumentedServiceMakerC,
db, err := stack.OpenDatabase("mydb", 0, 0, "")
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, "")
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, "")
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)
for id, maker := range services {
id := id // Closure for the constructor
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
startHook: func(*p2p.Server) { started[id] = true },
stopHook: func() { stopped[id] = true },
}, nil
}
if err := stack.Register(maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
}
// 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 services {
for id := range lifecycles {
if !started[id] {
t.Fatalf("service %s: freshly started service not running", id)
}
@ -188,470 +254,286 @@ func TestServiceLifeCycle(t *testing.T) {
}
}
// Stop the node and check that all services have been stopped
if err := stack.Stop(); err != nil {
if err := stack.Close(); err != nil {
t.Fatalf("failed to stop protocol stack: %v", err)
}
for id := range services {
for id := range lifecycles {
if !stopped[id] {
t.Fatalf("service %s: freshly terminated service still running", id)
}
}
}
// Tests that services are restarted cleanly as new instances.
func TestServiceRestarts(t *testing.T) {
// 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()
// Define a service that does not support restarts
var (
running bool
started int
)
constructor := func(*ServiceContext) (Service, error) {
running = false
return &InstrumentedService{
startHook: func(*p2p.Server) {
if running {
panic("already running")
}
running = true
started++
},
}, nil
}
// Register the service and start the protocol stack
if err := stack.Register(constructor); err != nil {
t.Fatalf("failed to register the service: %v", err)
}
if err := stack.Start(); err != nil {
t.Fatalf("failed to start protocol stack: %v", err)
}
defer stack.Stop()
if !running || started != 1 {
t.Fatalf("running/started mismatch: have %v/%d, want true/1", running, started)
}
// Restart the stack a few times and check successful service restarts
for i := 0; i < 3; i++ {
if err := stack.Restart(); err != nil {
t.Fatalf("iter %d: failed to restart stack: %v", i, err)
}
}
if !running || started != 4 {
t.Fatalf("running/started mismatch: have %v/%d, want true/4", running, started)
}
}
// Tests that if a service fails to initialize itself, none of the other services
// will be allowed to even start.
func TestServiceConstructionAbortion(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Define a batch of good services
services := map[string]InstrumentingWrapper{
"A": InstrumentedServiceMakerA,
"B": InstrumentedServiceMakerB,
"C": InstrumentedServiceMakerC,
}
started := make(map[string]bool)
for id, maker := range services {
id := id // Closure for the constructor
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
startHook: func(*p2p.Server) { started[id] = true },
}, nil
}
if err := stack.Register(maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
}
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 := func(*ServiceContext) (Service, error) {
return nil, failure
}
if err := stack.Register(failer); err != nil {
t.Fatalf("failer registration failed: %v", err)
}
// Start the protocol stack and ensure none of the services get started
for i := 0; i < 100; i++ {
if err := stack.Start(); err != failure {
t.Fatalf("iter %d: stack startup failure mismatch: have %v, want %v", i, err, failure)
}
for id := range services {
if started[id] {
t.Fatalf("service %s: started should not have", id)
}
delete(started, id)
}
}
}
failer := &InstrumentedService{start: failure}
stack.RegisterLifecycle(failer)
// Tests that if a service fails to start, all others started before it will be
// shut down.
func TestServiceStartupAbortion(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Register a batch of good services
services := map[string]InstrumentingWrapper{
"A": InstrumentedServiceMakerA,
"B": InstrumentedServiceMakerB,
"C": InstrumentedServiceMakerC,
}
started := make(map[string]bool)
stopped := make(map[string]bool)
for id, maker := range services {
id := id // Closure for the constructor
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
startHook: func(*p2p.Server) { started[id] = true },
stopHook: func() { stopped[id] = true },
}, nil
}
if err := stack.Register(maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
}
}
// Register a service that fails to start
failure := errors.New("fail")
failer := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
start: failure,
}, nil
}
if err := stack.Register(failer); err != nil {
t.Fatalf("failer registration failed: %v", err)
}
// Start the protocol stack and ensure all started services stop
for i := 0; i < 100; i++ {
if err := stack.Start(); err != failure {
t.Fatalf("iter %d: stack startup failure mismatch: have %v, want %v", i, err, failure)
}
for id := range services {
if started[id] && !stopped[id] {
t.Fatalf("service %s: started but not stopped", id)
}
delete(started, id)
delete(stopped, id)
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 service fails to shut down cleanly, it does
// Tests that even if a registered Lifecycle fails to shut down cleanly, it does
// not influence the rest of the shutdown invocations.
func TestServiceTerminationGuarantee(t *testing.T) {
func TestLifecycleTerminationGuarantee(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Register a batch of good services
services := map[string]InstrumentingWrapper{
"A": InstrumentedServiceMakerA,
"B": InstrumentedServiceMakerB,
"C": InstrumentedServiceMakerC,
}
started := make(map[string]bool)
stopped := make(map[string]bool)
for id, maker := range services {
id := id // Closure for the constructor
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
startHook: func(*p2p.Server) { started[id] = true },
stopHook: func() { stopped[id] = true },
}, nil
}
if err := stack.Register(maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
}
// 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 := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
stop: failure,
}, nil
}
if err := stack.Register(failer); err != nil {
t.Fatalf("failer registration failed: %v", err)
}
failer := &InstrumentedService{stop: failure}
stack.RegisterLifecycle(failer)
// Start the protocol stack, and ensure that a failing shut down terminates all
for i := 0; i < 100; i++ {
// Start the stack and make sure all is online
if err := stack.Start(); err != nil {
t.Fatalf("iter %d: failed to start protocol stack: %v", i, err)
}
for id := range services {
if !started[id] {
t.Fatalf("iter %d, service %s: service not running", i, id)
}
if stopped[id] {
t.Fatalf("iter %d, service %s: service already stopped", i, id)
}
}
// Stop the stack, verify failure and check all terminations
err := stack.Stop()
if err, ok := err.(*StopError); !ok {
t.Fatalf("iter %d: termination failure mismatch: have %v, want StopError", i, err)
} else {
failer := reflect.TypeOf(&InstrumentedService{})
if err.Services[failer] != failure {
t.Fatalf("iter %d: failer termination failure mismatch: have %v, want %v", i, err.Services[failer], failure)
}
if len(err.Services) != 1 {
t.Fatalf("iter %d: failure count mismatch: have %d, want %d", i, len(err.Services), 1)
}
}
for id := range services {
if !stopped[id] {
t.Fatalf("iter %d, service %s: service not terminated", i, id)
}
delete(started, id)
delete(stopped, id)
}
}
}
// TestServiceRetrieval tests that individual services can be retrieved.
func TestServiceRetrieval(t *testing.T) {
// Create a simple stack and register two service types
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
if err := stack.Register(NewNoopService); err != nil {
t.Fatalf("noop service registration failed: %v", err)
}
if err := stack.Register(NewInstrumentedService); err != nil {
t.Fatalf("instrumented service registration failed: %v", err)
}
// Make sure none of the services can be retrieved until started
var noopServ *NoopService
if err := stack.Service(&noopServ); err != ErrNodeStopped {
t.Fatalf("noop service retrieval mismatch: have %v, want %v", err, ErrNodeStopped)
}
var instServ *InstrumentedService
if err := stack.Service(&instServ); err != ErrNodeStopped {
t.Fatalf("instrumented service retrieval mismatch: have %v, want %v", err, ErrNodeStopped)
}
// Start the stack and ensure everything is retrievable now
if err := stack.Start(); err != nil {
t.Fatalf("failed to start stack: %v", err)
}
defer stack.Stop()
if err := stack.Service(&noopServ); err != nil {
t.Fatalf("noop service retrieval mismatch: have %v, want %v", err, nil)
}
if err := stack.Service(&instServ); err != nil {
t.Fatalf("instrumented service retrieval mismatch: have %v, want %v", err, nil)
}
}
// Tests that all protocols defined by individual services get launched.
func TestProtocolGather(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Register a batch of services with some configured number of protocols
services := map[string]struct {
Count int
Maker InstrumentingWrapper
}{
"zero": {0, InstrumentedServiceMakerA},
"one": {1, InstrumentedServiceMakerB},
"many": {10, InstrumentedServiceMakerC},
}
for id, config := range services {
protocols := make([]p2p.Protocol, config.Count)
for i := 0; i < len(protocols); i++ {
protocols[i].Name = id
protocols[i].Version = uint(i)
}
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
protocols: protocols,
}, nil
}
if err := stack.Register(config.Maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
}
}
// Start the services and ensure all protocols start successfully
// Start the stack and make sure all is online
if err := stack.Start(); err != nil {
t.Fatalf("failed to start protocol stack: %v", err)
}
defer stack.Stop()
protocols := stack.Server().Protocols
if len(protocols) != 11 {
t.Fatalf("mismatching number of protocols launched: have %d, want %d", len(protocols), 26)
}
for id, config := range services {
for ver := 0; ver < config.Count; ver++ {
launched := false
for i := 0; i < len(protocols); i++ {
if protocols[i].Name == id && protocols[i].Version == uint(ver) {
launched = true
break
}
}
if !launched {
t.Errorf("configured protocol not launched: %s v%d", id, ver)
}
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 that all APIs defined by individual services get exposed.
func TestAPIGather(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Tests whether a handler can be successfully mounted on the canonical HTTP server
// on the given path
func TestRegisterHandler_Successful(t *testing.T) {
node := createNode(t, 7878, 7979)
// Register a batch of services with some configured APIs
calls := make(chan string, 1)
makeAPI := func(result string) *OneMethodAPI {
return &OneMethodAPI{fun: func() { calls <- result }}
}
services := map[string]struct {
APIs []rpc.API
Maker InstrumentingWrapper
}{
"Zero APIs": {
[]rpc.API{}, InstrumentedServiceMakerA},
"Single API": {
[]rpc.API{
{Namespace: "single", Version: "1", Service: makeAPI("single.v1"), Public: true},
}, InstrumentedServiceMakerB},
"Many APIs": {
[]rpc.API{
{Namespace: "multi", Version: "1", Service: makeAPI("multi.v1"), Public: true},
{Namespace: "multi.v2", Version: "2", Service: makeAPI("multi.v2"), Public: true},
{Namespace: "multi.v2.nested", Version: "2", Service: makeAPI("multi.v2.nested"), Public: true},
}, InstrumentedServiceMakerC},
// 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)
}
for id, config := range services {
config := config
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{apis: config.APIs}, nil
}
if err := stack.Register(config.Maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
}
}
// Start the services and ensure all API start successfully
if err := stack.Start(); err != nil {
t.Fatalf("failed to start protocol stack: %v", err)
}
defer stack.Stop()
// Connect to the RPC server and verify the various registered endpoints
client, err := stack.Attach()
if err != nil {
t.Fatalf("failed to connect to the inproc API server: %v", err)
}
defer client.Close()
tests := []struct {
Method string
Result string
}{
{"single_theOneMethod", "single.v1"},
{"multi_theOneMethod", "multi.v1"},
{"multi.v2_theOneMethod", "multi.v2"},
{"multi.v2.nested_theOneMethod", "multi.v2.nested"},
}
for i, test := range tests {
if err := client.Call(nil, test.Method); err != nil {
t.Errorf("test %d: API request failed: %v", i, err)
}
select {
case result := <-calls:
if result != test.Result {
t.Errorf("test %d: result mismatch: have %s, want %s", i, result, test.Result)
}
case <-time.After(time.Second):
t.Fatalf("test %d: rpc execution timeout", i)
}
}
}
func TestWebsocketHTTPOnSamePort_WebsocketRequest(t *testing.T) {
node := startHTTP(t)
defer node.stopHTTP()
wsReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:7453", nil)
// 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)
}
wsReq.Header.Set("Connection", "upgrade")
wsReq.Header.Set("Upgrade", "websocket")
wsReq.Header.Set("Sec-WebSocket-Version", "13")
wsReq.Header.Set("Sec-Websocket-Key", "SGVsbG8sIHdvcmxkIQ==")
resp := doHTTPRequest(t, wsReq)
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
}
func TestWebsocketHTTPOnSamePort_HTTPRequest(t *testing.T) {
node := startHTTP(t)
defer node.stopHTTP()
httpReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:7453", nil)
if err != nil {
t.Error("could not issue new http request ", err)
}
httpReq.Header.Set("Accept-Encoding", "gzip")
// check response
resp := doHTTPRequest(t, httpReq)
assert.Equal(t, "gzip", resp.Header.Get("Content-Encoding"))
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))
}
func startHTTP(t *testing.T) *Node {
conf := &Config{HTTPPort: 7453, WSPort: 7453}
// 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")
}
}
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,
}
node, err := New(conf)
if err != nil {
t.Error("could not create a new node ", err)
t.Fatalf("could not create a new node: %v", err)
}
return node
}
err = node.startHTTP("127.0.0.1:7453", []rpc.API{}, []string{}, []string{}, []string{}, rpc.HTTPTimeouts{}, []string{})
func startHTTP(t *testing.T, httpPort, wsPort int) *Node {
node := createNode(t, httpPort, wsPort)
err := node.Start()
if err != nil {
t.Error("could not start http service on node ", err)
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.Client{}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
t.Error("could not issue a GET request to the given endpoint", err)
t.Fatalf("could not issue a GET request to the given endpoint: %v", err)
}
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
}

View File

@ -18,17 +18,304 @@ package node
import (
"compress/gzip"
"context"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"sort"
"strings"
"sync"
"sync/atomic"
"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
}
// wsConfig is the JSON-RPC/Websocket configuration
type wsConfig struct {
Origins []string
Modules []string
}
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
}
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 = fmt.Sprintf("%s:%d", host, 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.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 server is websocket only, return after logging
if h.wsAllowed() && !h.rpcAllowed() {
h.log.Info("WebSocket enabled", "url", fmt.Sprintf("ws://%v", listener.Addr()))
return nil
}
// Log http endpoint.
h.log.Info("HTTP server started",
"endpoint", listener.Addr(),
"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) {
rpc := h.httpHandler.Load().(*rpcHandler)
if r.RequestURI == "/" {
// Serve JSON-RPC on the root path.
ws := h.wsHandler.Load().(*rpcHandler)
if ws != nil && isWebsocket(r) {
ws.ServeHTTP(w, r)
return
}
if rpc != nil {
rpc.ServeHTTP(w, r)
return
}
} else if rpc != nil {
// 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.
h.mux.ServeHTTP(w, r)
return
}
w.WriteHeader(404)
}
// 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.httpHandler.Load().(*rpcHandler)
if httpHandler != nil {
h.httpHandler.Store((*rpcHandler)(nil))
httpHandler.server.Stop()
}
if wsHandler != nil {
h.wsHandler.Store((*rpcHandler)(nil))
wsHandler.server.Stop()
}
h.server.Shutdown(context.Background())
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()
if err := RegisterApisFromWhitelist(apis, config.Modules, srv, false); err != nil {
return err
}
h.httpConfig = config
h.httpHandler.Store(&rpcHandler{
Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts),
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()
if err := RegisterApisFromWhitelist(apis, config.Modules, srv, false); err != nil {
return err
}
h.wsConfig = config
h.wsHandler.Store(&rpcHandler{
Handler: srv.WebsocketHandler(config.Origins),
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.ToLower(r.Header.Get("Upgrade")) == "websocket" &&
strings.ToLower(r.Header.Get("Connection")) == "upgrade"
}
// NewHTTPHandlerStack returns wrapped http-related handlers
func NewHTTPHandlerStack(srv http.Handler, cors []string, vhosts []string) http.Handler {
// Wrap the CORS-handler within a host-handler
@ -45,8 +332,8 @@ func newCorsHandler(srv http.Handler, allowedOrigins []string) http.Handler {
c := cors.New(cors.Options{
AllowedOrigins: allowedOrigins,
AllowedMethods: []string{http.MethodPost, http.MethodGet},
MaxAge: 600,
AllowedHeaders: []string{"*"},
MaxAge: 600,
})
return c.Handler(srv)
}
@ -138,22 +425,68 @@ func newGzipHandler(next http.Handler) http.Handler {
})
}
// NewWebsocketUpgradeHandler returns a websocket handler that serves an incoming request only if it contains an upgrade
// request to the websocket protocol. If not, serves the the request with the http handler.
func NewWebsocketUpgradeHandler(h http.Handler, ws http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isWebsocket(r) {
ws.ServeHTTP(w, r)
log.Debug("serving websocket request")
return
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 {
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
}
// RegisterApisFromWhitelist checks the given modules' availability, generates a whitelist based on the allowed modules,
// and then registers all of the APIs exposed by the services.
func RegisterApisFromWhitelist(apis []rpc.API, modules []string, srv *rpc.Server, exposeAll bool) error {
if bad, available := checkModuleAvailability(modules, apis); len(bad) > 0 {
log.Error("Unavailable modules in HTTP API list", "unavailable", bad, "available", available)
}
// Generate the whitelist based on the allowed modules
whitelist := make(map[string]bool)
for _, module := range modules {
whitelist[module] = true
}
// Register all the APIs exposed by the services
for _, api := range apis {
if exposeAll || whitelist[api.Namespace] || (len(whitelist) == 0 && api.Public) {
if err := srv.RegisterName(api.Namespace, api.Service); err != nil {
return err
}
}
h.ServeHTTP(w, r)
})
}
// isWebsocket checks the header of an http request for a websocket upgrade request.
func isWebsocket(r *http.Request) bool {
return strings.ToLower(r.Header.Get("Upgrade")) == "websocket" &&
strings.ToLower(r.Header.Get("Connection")) == "upgrade"
}
return nil
}

View File

@ -1,38 +1,110 @@
// 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 <http://www.gnu.org/licenses/>.
package node
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/ethereum/go-ethereum/internal/testlog"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
)
func TestNewWebsocketUpgradeHandler_websocket(t *testing.T) {
srv := rpc.NewServer()
// 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{})
defer srv.stop()
handler := NewWebsocketUpgradeHandler(nil, srv.WebsocketHandler([]string{}))
ts := httptest.NewServer(handler)
defer ts.Close()
resp := testRequest(t, "origin", "test.com", "", srv)
assert.Equal(t, "test.com", resp.Header.Get("Access-Control-Allow-Origin"))
responses := make(chan *http.Response)
go func(responses chan *http.Response) {
client := &http.Client{}
req, _ := http.NewRequest(http.MethodGet, ts.URL, nil)
req.Header.Set("Connection", "upgrade")
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Sec-WebSocket-Version", "13")
req.Header.Set("Sec-Websocket-Key", "SGVsbG8sIHdvcmxkIQ==")
resp, err := client.Do(req)
if err != nil {
t.Error("could not issue a GET request to the test http server", err)
}
responses <- resp
}(responses)
response := <-responses
assert.Equal(t, "websocket", response.Header.Get("Upgrade"))
resp2 := testRequest(t, "origin", "bad", "", srv)
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{})
defer srv.stop()
resp := testRequest(t, "", "", "test", srv)
assert.Equal(t, resp.StatusCode, http.StatusOK)
resp2 := testRequest(t, "", "", "bad", srv)
assert.Equal(t, resp2.StatusCode, http.StatusForbidden)
}
// TestWebsocketOrigins makes sure the websocket origins are properly handled on the websocket server.
func TestWebsocketOrigins(t *testing.T) {
srv := createAndStartServer(t, httpConfig{}, true, wsConfig{Origins: []string{"test"}})
defer srv.stop()
dialer := websocket.DefaultDialer
_, _, err := dialer.Dial("ws://"+srv.listenAddr(), http.Header{
"Content-type": []string{"application/json"},
"Sec-WebSocket-Version": []string{"13"},
"Origin": []string{"test"},
})
assert.NoError(t, err)
_, _, err = dialer.Dial("ws://"+srv.listenAddr(), http.Header{
"Content-type": []string{"application/json"},
"Sec-WebSocket-Version": []string{"13"},
"Origin": []string{"bad"},
})
assert.Error(t, err)
}
func createAndStartServer(t *testing.T, conf httpConfig, ws bool, wsConf wsConfig) *httpServer {
t.Helper()
srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), rpc.DefaultHTTPTimeouts)
assert.NoError(t, srv.enableRPC(nil, conf))
if ws {
assert.NoError(t, srv.enableWS(nil, wsConf))
}
assert.NoError(t, srv.setListenAddr("localhost", 0))
assert.NoError(t, srv.start())
return srv
}
func testRequest(t *testing.T, key, value, host string, srv *httpServer) *http.Response {
t.Helper()
body := bytes.NewReader([]byte(`{"jsonrpc":"2.0","id":1,method":"rpc_modules"}`))
req, _ := http.NewRequest("POST", "http://"+srv.listenAddr(), body)
req.Header.Set("content-type", "application/json")
if key != "" && value != "" {
req.Header.Set(key, value)
}
if host != "" {
req.Host = host
}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
return resp
}

View File

@ -1,122 +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 <http://www.gnu.org/licenses/>.
package node
import (
"path/filepath"
"reflect"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// ServiceContext is a collection of service independent options inherited from
// the protocol stack, that is passed to all constructors to be optionally used;
// as well as utility methods to operate on the service environment.
type ServiceContext struct {
services map[reflect.Type]Service // Index of the already constructed services
Config Config
EventMux *event.TypeMux // Event multiplexer used for decoupled notifications
AccountManager *accounts.Manager // Account manager created by the node.
}
// OpenDatabase opens an existing database with the given name (or creates one
// if no previous can be found) from within the node's data directory. If the
// node is an ephemeral one, a memory database is returned.
func (ctx *ServiceContext) OpenDatabase(name string, cache int, handles int, namespace string) (ethdb.Database, error) {
if ctx.Config.DataDir == "" {
return rawdb.NewMemoryDatabase(), nil
}
return rawdb.NewLevelDBDatabase(ctx.Config.ResolvePath(name), cache, handles, namespace)
}
// 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 (ctx *ServiceContext) OpenDatabaseWithFreezer(name string, cache int, handles int, freezer string, namespace string) (ethdb.Database, error) {
if ctx.Config.DataDir == "" {
return rawdb.NewMemoryDatabase(), nil
}
root := ctx.Config.ResolvePath(name)
switch {
case freezer == "":
freezer = filepath.Join(root, "ancient")
case !filepath.IsAbs(freezer):
freezer = ctx.Config.ResolvePath(freezer)
}
return rawdb.NewLevelDBDatabaseWithFreezer(root, cache, handles, freezer, namespace)
}
// ResolvePath resolves a user path into the data directory if that was relative
// and if the user actually uses persistent storage. It will return an empty string
// for emphemeral storage and the user's own input for absolute paths.
func (ctx *ServiceContext) ResolvePath(path string) string {
return ctx.Config.ResolvePath(path)
}
// Service retrieves a currently running service registered of a specific type.
func (ctx *ServiceContext) Service(service interface{}) error {
element := reflect.ValueOf(service).Elem()
if running, ok := ctx.services[element.Type()]; ok {
element.Set(reflect.ValueOf(running))
return nil
}
return ErrServiceUnknown
}
// ExtRPCEnabled returns the indicator whether node enables the external
// RPC(http, ws or graphql).
func (ctx *ServiceContext) ExtRPCEnabled() bool {
return ctx.Config.ExtRPCEnabled()
}
// ServiceConstructor is the function signature of the constructors needed to be
// registered for service instantiation.
type ServiceConstructor func(ctx *ServiceContext) (Service, error)
// Service is an individual protocol that can be registered into a node.
//
// Notes:
//
// • Service life-cycle management is delegated to the node. The service is allowed to
// initialize itself upon creation, but no goroutines should be spun up outside of the
// Start method.
//
// • Restart logic is not required as the node will create a fresh instance
// every time a service is started.
type Service interface {
// Protocols retrieves the P2P protocols the service wishes to start.
Protocols() []p2p.Protocol
// APIs retrieves the list of RPC descriptors the service provides
APIs() []rpc.API
// 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(server *p2p.Server) error
// Stop terminates all goroutines belonging to the service, blocking until they
// are all terminated.
Stop() error
}

View File

@ -1,98 +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 <http://www.gnu.org/licenses/>.
package node
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
// Tests that databases are correctly created persistent or ephemeral based on
// the configured service context.
func TestContextDatabases(t *testing.T) {
// Create a temporary folder and ensure no database is contained within
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("failed to create temporary data directory: %v", err)
}
defer os.RemoveAll(dir)
if _, err := os.Stat(filepath.Join(dir, "database")); err == nil {
t.Fatalf("non-created database already exists")
}
// Request the opening/creation of a database and ensure it persists to disk
ctx := &ServiceContext{Config: Config{Name: "unit-test", DataDir: dir}}
db, err := ctx.OpenDatabase("persistent", 0, 0, "")
if err != nil {
t.Fatalf("failed to open persistent database: %v", err)
}
db.Close()
if _, err := os.Stat(filepath.Join(dir, "unit-test", "persistent")); err != nil {
t.Fatalf("persistent database doesn't exists: %v", err)
}
// Request th opening/creation of an ephemeral database and ensure it's not persisted
ctx = &ServiceContext{Config: Config{DataDir: ""}}
db, err = ctx.OpenDatabase("ephemeral", 0, 0, "")
if err != nil {
t.Fatalf("failed to open ephemeral database: %v", err)
}
db.Close()
if _, err := os.Stat(filepath.Join(dir, "ephemeral")); err == nil {
t.Fatalf("ephemeral database exists")
}
}
// Tests that already constructed services can be retrieves by later ones.
func TestContextServices(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Define a verifier that ensures a NoopA is before it and NoopB after
verifier := func(ctx *ServiceContext) (Service, error) {
var objA *NoopServiceA
if ctx.Service(&objA) != nil {
return nil, fmt.Errorf("former service not found")
}
var objB *NoopServiceB
if err := ctx.Service(&objB); err != ErrServiceUnknown {
return nil, fmt.Errorf("latters lookup error mismatch: have %v, want %v", err, ErrServiceUnknown)
}
return new(NoopService), nil
}
// Register the collection of services
if err := stack.Register(NewNoopServiceA); err != nil {
t.Fatalf("former failed to register service: %v", err)
}
if err := stack.Register(verifier); err != nil {
t.Fatalf("failed to register service verifier: %v", err)
}
if err := stack.Register(NewNoopServiceB); err != nil {
t.Fatalf("latter failed to register service: %v", err)
}
// Start the protocol stack and ensure services are constructed in order
if err := stack.Start(); err != nil {
t.Fatalf("failed to start stack: %v", err)
}
defer stack.Stop()
}

View File

@ -20,61 +20,40 @@
package node
import (
"reflect"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// NoopService is a trivial implementation of the Service interface.
type NoopService struct{}
// NoopLifecycle is a trivial implementation of the Service interface.
type NoopLifecycle struct{}
func (s *NoopService) Protocols() []p2p.Protocol { return nil }
func (s *NoopService) APIs() []rpc.API { return nil }
func (s *NoopService) Start(*p2p.Server) error { return nil }
func (s *NoopService) Stop() error { return nil }
func (s *NoopLifecycle) Start() error { return nil }
func (s *NoopLifecycle) Stop() error { return nil }
func NewNoopService(*ServiceContext) (Service, error) { return new(NoopService), nil }
func NewNoop() *Noop {
noop := new(Noop)
return noop
}
// Set of services all wrapping the base NoopService resulting in the same method
// Set of services all wrapping the base NoopLifecycle resulting in the same method
// signatures but different outer types.
type NoopServiceA struct{ NoopService }
type NoopServiceB struct{ NoopService }
type NoopServiceC struct{ NoopService }
type Noop struct{ NoopLifecycle }
func NewNoopServiceA(*ServiceContext) (Service, error) { return new(NoopServiceA), nil }
func NewNoopServiceB(*ServiceContext) (Service, error) { return new(NoopServiceB), nil }
func NewNoopServiceC(*ServiceContext) (Service, error) { return new(NoopServiceC), nil }
// InstrumentedService is an implementation of Service for which all interface
// 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()
protocols []p2p.Protocol
apis []rpc.API
start error
stop error
protocolsHook func()
startHook func(*p2p.Server)
stopHook func()
}
func NewInstrumentedService(*ServiceContext) (Service, error) { return new(InstrumentedService), nil }
func (s *InstrumentedService) Protocols() []p2p.Protocol {
if s.protocolsHook != nil {
s.protocolsHook()
}
return s.protocols
}
func (s *InstrumentedService) APIs() []rpc.API {
return s.apis
}
func (s *InstrumentedService) Start(server *p2p.Server) error {
func (s *InstrumentedService) Start() error {
if s.startHook != nil {
s.startHook(server)
s.startHook()
}
return s.start
}
@ -86,48 +65,49 @@ func (s *InstrumentedService) Stop() error {
return s.stop
}
// InstrumentingWrapper is a method to specialize a service constructor returning
// a generic InstrumentedService into one returning a wrapping specific one.
type InstrumentingWrapper func(base ServiceConstructor) ServiceConstructor
type FullService struct{}
func InstrumentingWrapperMaker(base ServiceConstructor, kind reflect.Type) ServiceConstructor {
return func(ctx *ServiceContext) (Service, error) {
obj, err := base(ctx)
if err != nil {
return nil, err
}
wrapper := reflect.New(kind)
wrapper.Elem().Field(0).Set(reflect.ValueOf(obj).Elem())
func NewFullService(stack *Node) (*FullService, error) {
fs := new(FullService)
return wrapper.Interface().(Service), nil
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{
p2p.Protocol{
Name: "test1",
Version: uint(1),
},
p2p.Protocol{
Name: "test2",
Version: uint(2),
},
}
}
// Set of services all wrapping the base InstrumentedService resulting in the
// same method signatures but different outer types.
type InstrumentedServiceA struct{ InstrumentedService }
type InstrumentedServiceB struct{ InstrumentedService }
type InstrumentedServiceC struct{ InstrumentedService }
func InstrumentedServiceMakerA(base ServiceConstructor) ServiceConstructor {
return InstrumentingWrapperMaker(base, reflect.TypeOf(InstrumentedServiceA{}))
}
func InstrumentedServiceMakerB(base ServiceConstructor) ServiceConstructor {
return InstrumentingWrapperMaker(base, reflect.TypeOf(InstrumentedServiceB{}))
}
func InstrumentedServiceMakerC(base ServiceConstructor) ServiceConstructor {
return InstrumentingWrapperMaker(base, reflect.TypeOf(InstrumentedServiceC{}))
}
// OneMethodAPI is a single-method API handler to be returned by test services.
type OneMethodAPI struct {
fun func()
}
func (api *OneMethodAPI) TheOneMethod() {
if api.fun != nil {
api.fun()
func (f *FullService) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "admin",
Version: "1.0",
},
{
Namespace: "debug",
Version: "1.0",
Public: true,
},
{
Namespace: "net",
Version: "1.0",
Public: true,
},
}
}