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:
183
node/api.go
183
node/api.go
@ -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
350
node/api_test.go
Normal 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 "
|
||||
}
|
@ -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.
|
||||
|
@ -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",
|
||||
|
37
node/doc.go
37
node/doc.go
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
31
node/lifecycle.go
Normal 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
|
||||
}
|
870
node/node.go
870
node/node.go
File diff suppressed because it is too large
Load Diff
@ -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...
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
369
node/rpcstack.go
369
node/rpcstack.go
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
122
node/service.go
122
node/service.go
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user