mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
Delete node directory
Signed-off-by: Isabel Schöps Thiel @IsabelSchoepd <155141998+IST-Github@users.noreply.github.com>
This commit is contained in:
parent
626905af04
commit
ba5ec1dc58
18 changed files with 0 additions and 5000 deletions
337
node/api.go
337
node/api.go
|
|
@ -1,337 +0,0 @@
|
|||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/internal/debug"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
// apis returns the collection of built-in RPC APIs.
|
||||
func (n *Node) apis() []rpc.API {
|
||||
return []rpc.API{
|
||||
{
|
||||
Namespace: "admin",
|
||||
Service: &adminAPI{n},
|
||||
}, {
|
||||
Namespace: "debug",
|
||||
Service: debug.Handler,
|
||||
}, {
|
||||
Namespace: "web3",
|
||||
Service: &web3API{n},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// adminAPI is the collection of administrative API methods exposed over
|
||||
// both secure and unsecure RPC channels.
|
||||
type adminAPI struct {
|
||||
node *Node // Node interfaced by this API
|
||||
}
|
||||
|
||||
// AddPeer requests connecting to a remote node, and also maintaining the new
|
||||
// connection at all times, even reconnecting if it is lost.
|
||||
func (api *adminAPI) AddPeer(url string) (bool, error) {
|
||||
// Make sure the server is running, fail otherwise
|
||||
server := api.node.Server()
|
||||
if server == nil {
|
||||
return false, ErrNodeStopped
|
||||
}
|
||||
// Try to add the url as a static peer and return
|
||||
node, err := enode.Parse(enode.ValidSchemes, url)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid enode: %v", err)
|
||||
}
|
||||
server.AddPeer(node)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RemovePeer disconnects from a remote node if the connection exists
|
||||
func (api *adminAPI) RemovePeer(url string) (bool, error) {
|
||||
// Make sure the server is running, fail otherwise
|
||||
server := api.node.Server()
|
||||
if server == nil {
|
||||
return false, ErrNodeStopped
|
||||
}
|
||||
// Try to remove the url as a static peer and return
|
||||
node, err := enode.Parse(enode.ValidSchemes, url)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid enode: %v", err)
|
||||
}
|
||||
server.RemovePeer(node)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// AddTrustedPeer allows a remote node to always connect, even if slots are full
|
||||
func (api *adminAPI) AddTrustedPeer(url string) (bool, error) {
|
||||
// Make sure the server is running, fail otherwise
|
||||
server := api.node.Server()
|
||||
if server == nil {
|
||||
return false, ErrNodeStopped
|
||||
}
|
||||
node, err := enode.Parse(enode.ValidSchemes, url)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid enode: %v", err)
|
||||
}
|
||||
server.AddTrustedPeer(node)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RemoveTrustedPeer removes a remote node from the trusted peer set, but it
|
||||
// does not disconnect it automatically.
|
||||
func (api *adminAPI) RemoveTrustedPeer(url string) (bool, error) {
|
||||
// Make sure the server is running, fail otherwise
|
||||
server := api.node.Server()
|
||||
if server == nil {
|
||||
return false, ErrNodeStopped
|
||||
}
|
||||
node, err := enode.Parse(enode.ValidSchemes, url)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid enode: %v", err)
|
||||
}
|
||||
server.RemoveTrustedPeer(node)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// PeerEvents creates an RPC subscription which receives peer events from the
|
||||
// node's p2p.Server
|
||||
func (api *adminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription, error) {
|
||||
// Make sure the server is running, fail otherwise
|
||||
server := api.node.Server()
|
||||
if server == nil {
|
||||
return nil, ErrNodeStopped
|
||||
}
|
||||
|
||||
// Create the subscription
|
||||
notifier, supported := rpc.NotifierFromContext(ctx)
|
||||
if !supported {
|
||||
return nil, rpc.ErrNotificationsUnsupported
|
||||
}
|
||||
rpcSub := notifier.CreateSubscription()
|
||||
|
||||
go func() {
|
||||
events := make(chan *p2p.PeerEvent)
|
||||
sub := server.SubscribeEvents(events)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-events:
|
||||
notifier.Notify(rpcSub.ID, event)
|
||||
case <-sub.Err():
|
||||
return
|
||||
case <-rpcSub.Err():
|
||||
return
|
||||
case <-notifier.Closed():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return rpcSub, nil
|
||||
}
|
||||
|
||||
// StartHTTP starts the HTTP RPC API server.
|
||||
func (api *adminAPI) StartHTTP(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) {
|
||||
api.node.lock.Lock()
|
||||
defer api.node.lock.Unlock()
|
||||
|
||||
// Determine host and port.
|
||||
if host == nil {
|
||||
h := DefaultHTTPHost
|
||||
if api.node.config.HTTPHost != "" {
|
||||
h = api.node.config.HTTPHost
|
||||
}
|
||||
host = &h
|
||||
}
|
||||
if port == nil {
|
||||
port = &api.node.config.HTTPPort
|
||||
}
|
||||
|
||||
// Determine config.
|
||||
config := httpConfig{
|
||||
CorsAllowedOrigins: api.node.config.HTTPCors,
|
||||
Vhosts: api.node.config.HTTPVirtualHosts,
|
||||
Modules: api.node.config.HTTPModules,
|
||||
rpcEndpointConfig: rpcEndpointConfig{
|
||||
batchItemLimit: api.node.config.BatchRequestLimit,
|
||||
batchResponseSizeLimit: api.node.config.BatchResponseMaxSize,
|
||||
},
|
||||
}
|
||||
if cors != nil {
|
||||
config.CorsAllowedOrigins = nil
|
||||
for _, origin := range strings.Split(*cors, ",") {
|
||||
config.CorsAllowedOrigins = append(config.CorsAllowedOrigins, strings.TrimSpace(origin))
|
||||
}
|
||||
}
|
||||
if vhosts != nil {
|
||||
config.Vhosts = nil
|
||||
for _, vhost := range strings.Split(*host, ",") {
|
||||
config.Vhosts = append(config.Vhosts, strings.TrimSpace(vhost))
|
||||
}
|
||||
}
|
||||
if apis != nil {
|
||||
config.Modules = nil
|
||||
for _, m := range strings.Split(*apis, ",") {
|
||||
config.Modules = append(config.Modules, strings.TrimSpace(m))
|
||||
}
|
||||
}
|
||||
|
||||
if err := api.node.http.setListenAddr(*host, *port); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := api.node.http.enableRPC(api.node.rpcAPIs, config); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := api.node.http.start(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// StartRPC starts the HTTP RPC API server.
|
||||
// Deprecated: use StartHTTP instead.
|
||||
func (api *adminAPI) StartRPC(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) {
|
||||
log.Warn("Deprecation warning", "method", "admin.StartRPC", "use-instead", "admin.StartHTTP")
|
||||
return api.StartHTTP(host, port, cors, apis, vhosts)
|
||||
}
|
||||
|
||||
// StopHTTP shuts down the HTTP server.
|
||||
func (api *adminAPI) StopHTTP() (bool, error) {
|
||||
api.node.http.stop()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// StopRPC shuts down the HTTP server.
|
||||
// Deprecated: use StopHTTP instead.
|
||||
func (api *adminAPI) StopRPC() (bool, error) {
|
||||
log.Warn("Deprecation warning", "method", "admin.StopRPC", "use-instead", "admin.StopHTTP")
|
||||
return api.StopHTTP()
|
||||
}
|
||||
|
||||
// StartWS starts the websocket RPC API server.
|
||||
func (api *adminAPI) StartWS(host *string, port *int, allowedOrigins *string, apis *string) (bool, error) {
|
||||
api.node.lock.Lock()
|
||||
defer api.node.lock.Unlock()
|
||||
|
||||
// Determine host and port.
|
||||
if host == nil {
|
||||
h := DefaultWSHost
|
||||
if api.node.config.WSHost != "" {
|
||||
h = api.node.config.WSHost
|
||||
}
|
||||
host = &h
|
||||
}
|
||||
if port == nil {
|
||||
port = &api.node.config.WSPort
|
||||
}
|
||||
|
||||
// Determine config.
|
||||
config := wsConfig{
|
||||
Modules: api.node.config.WSModules,
|
||||
Origins: api.node.config.WSOrigins,
|
||||
// ExposeAll: api.node.config.WSExposeAll,
|
||||
rpcEndpointConfig: rpcEndpointConfig{
|
||||
batchItemLimit: api.node.config.BatchRequestLimit,
|
||||
batchResponseSizeLimit: api.node.config.BatchResponseMaxSize,
|
||||
},
|
||||
}
|
||||
if apis != nil {
|
||||
config.Modules = nil
|
||||
for _, m := range strings.Split(*apis, ",") {
|
||||
config.Modules = append(config.Modules, strings.TrimSpace(m))
|
||||
}
|
||||
}
|
||||
if allowedOrigins != nil {
|
||||
config.Origins = nil
|
||||
for _, origin := range strings.Split(*allowedOrigins, ",") {
|
||||
config.Origins = append(config.Origins, strings.TrimSpace(origin))
|
||||
}
|
||||
}
|
||||
|
||||
// Enable WebSocket on the server.
|
||||
server := api.node.wsServerForPort(*port, false)
|
||||
if err := server.setListenAddr(*host, *port); err != nil {
|
||||
return false, err
|
||||
}
|
||||
openApis, _ := api.node.getAPIs()
|
||||
if err := server.enableWS(openApis, config); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := server.start(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
api.node.http.log.Info("WebSocket endpoint opened", "url", api.node.WSEndpoint())
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// StopWS terminates all WebSocket servers.
|
||||
func (api *adminAPI) StopWS() (bool, error) {
|
||||
api.node.http.stopWS()
|
||||
api.node.ws.stop()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Peers retrieves all the information we know about each individual peer at the
|
||||
// protocol granularity.
|
||||
func (api *adminAPI) Peers() ([]*p2p.PeerInfo, error) {
|
||||
server := api.node.Server()
|
||||
if server == nil {
|
||||
return nil, ErrNodeStopped
|
||||
}
|
||||
return server.PeersInfo(), nil
|
||||
}
|
||||
|
||||
// NodeInfo retrieves all the information we know about the host node at the
|
||||
// protocol granularity.
|
||||
func (api *adminAPI) NodeInfo() (*p2p.NodeInfo, error) {
|
||||
server := api.node.Server()
|
||||
if server == nil {
|
||||
return nil, ErrNodeStopped
|
||||
}
|
||||
return server.NodeInfo(), nil
|
||||
}
|
||||
|
||||
// Datadir retrieves the current data directory the node is using.
|
||||
func (api *adminAPI) Datadir() string {
|
||||
return api.node.DataDir()
|
||||
}
|
||||
|
||||
// web3API offers helper utils
|
||||
type web3API struct {
|
||||
stack *Node
|
||||
}
|
||||
|
||||
// ClientVersion returns the node name
|
||||
func (s *web3API) ClientVersion() string {
|
||||
return s.stack.Server().Name
|
||||
}
|
||||
|
||||
// Sha3 applies the ethereum sha3 implementation on the input.
|
||||
// It assumes the input is hex encoded.
|
||||
func (s *web3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
|
||||
return crypto.Keccak256(input)
|
||||
}
|
||||
355
node/api_test.go
355
node/api_test.go
|
|
@ -1,355 +0,0 @@
|
|||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <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, *adminAPI)
|
||||
|
||||
// Checks. These run after the node is configured and all API calls have been made.
|
||||
wantReachable bool // whether the HTTP server should be reachable at all
|
||||
wantHandlers bool // whether RegisterHandler handlers should be accessible
|
||||
wantRPC bool // whether JSON-RPC/HTTP should be accessible
|
||||
wantWS bool // whether JSON-RPC/WS should be accessible
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
{
|
||||
name: "all off",
|
||||
cfg: Config{},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
},
|
||||
wantReachable: false,
|
||||
wantHandlers: false,
|
||||
wantRPC: false,
|
||||
wantWS: false,
|
||||
},
|
||||
{
|
||||
name: "rpc enabled through config",
|
||||
cfg: Config{HTTPHost: "127.0.0.1"},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
},
|
||||
wantReachable: true,
|
||||
wantHandlers: true,
|
||||
wantRPC: true,
|
||||
wantWS: false,
|
||||
},
|
||||
{
|
||||
name: "rpc enabled through API",
|
||||
cfg: Config{},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
_, err := api.StartHTTP(sp("127.0.0.1"), ip(0), nil, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: true,
|
||||
wantHandlers: true,
|
||||
wantRPC: true,
|
||||
wantWS: false,
|
||||
},
|
||||
{
|
||||
name: "rpc start again after failure",
|
||||
cfg: Config{},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
// Listen on a random port.
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal("can't listen:", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
// Now try to start RPC on that port. This should fail.
|
||||
_, err = api.StartHTTP(sp("127.0.0.1"), ip(port), nil, nil, nil)
|
||||
if err == nil {
|
||||
t.Fatal("StartHTTP should have failed on port", port)
|
||||
}
|
||||
|
||||
// Try again after unblocking the port. It should work this time.
|
||||
listener.Close()
|
||||
_, err = api.StartHTTP(sp("127.0.0.1"), ip(port), nil, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: true,
|
||||
wantHandlers: true,
|
||||
wantRPC: true,
|
||||
wantWS: false,
|
||||
},
|
||||
{
|
||||
name: "rpc stopped through API",
|
||||
cfg: Config{HTTPHost: "127.0.0.1"},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
_, err := api.StopHTTP()
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: false,
|
||||
wantHandlers: false,
|
||||
wantRPC: false,
|
||||
wantWS: false,
|
||||
},
|
||||
{
|
||||
name: "rpc stopped twice",
|
||||
cfg: Config{HTTPHost: "127.0.0.1"},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
_, err := api.StopHTTP()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = api.StopHTTP()
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: false,
|
||||
wantHandlers: false,
|
||||
wantRPC: false,
|
||||
wantWS: false,
|
||||
},
|
||||
{
|
||||
name: "ws enabled through config",
|
||||
cfg: Config{WSHost: "127.0.0.1"},
|
||||
wantReachable: true,
|
||||
wantHandlers: false,
|
||||
wantRPC: false,
|
||||
wantWS: true,
|
||||
},
|
||||
{
|
||||
name: "ws enabled through API",
|
||||
cfg: Config{},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
_, err := api.StartWS(sp("127.0.0.1"), ip(0), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: true,
|
||||
wantHandlers: false,
|
||||
wantRPC: false,
|
||||
wantWS: true,
|
||||
},
|
||||
{
|
||||
name: "ws stopped through API",
|
||||
cfg: Config{WSHost: "127.0.0.1"},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
_, err := api.StopWS()
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: false,
|
||||
wantHandlers: false,
|
||||
wantRPC: false,
|
||||
wantWS: false,
|
||||
},
|
||||
{
|
||||
name: "ws stopped twice",
|
||||
cfg: Config{WSHost: "127.0.0.1"},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
_, err := api.StopWS()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = api.StopWS()
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: false,
|
||||
wantHandlers: false,
|
||||
wantRPC: false,
|
||||
wantWS: false,
|
||||
},
|
||||
{
|
||||
name: "ws enabled after RPC",
|
||||
cfg: Config{HTTPHost: "127.0.0.1"},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
wsport := n.http.port
|
||||
_, err := api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: true,
|
||||
wantHandlers: true,
|
||||
wantRPC: true,
|
||||
wantWS: true,
|
||||
},
|
||||
{
|
||||
name: "ws enabled after RPC then stopped",
|
||||
cfg: Config{HTTPHost: "127.0.0.1"},
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
wsport := n.http.port
|
||||
_, err := api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = api.StopWS()
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: true,
|
||||
wantHandlers: true,
|
||||
wantRPC: true,
|
||||
wantWS: false,
|
||||
},
|
||||
{
|
||||
name: "rpc stopped with ws enabled",
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
_, err := api.StartHTTP(sp("127.0.0.1"), ip(0), nil, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
wsport := n.http.port
|
||||
_, err = api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = api.StopHTTP()
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: false,
|
||||
wantHandlers: false,
|
||||
wantRPC: false,
|
||||
wantWS: false,
|
||||
},
|
||||
{
|
||||
name: "rpc enabled after ws",
|
||||
fn: func(t *testing.T, n *Node, api *adminAPI) {
|
||||
_, err := api.StartWS(sp("127.0.0.1"), ip(0), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
wsport := n.http.port
|
||||
_, err = api.StartHTTP(sp("127.0.0.1"), ip(wsport), nil, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
wantReachable: true,
|
||||
wantHandlers: true,
|
||||
wantRPC: true,
|
||||
wantWS: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Apply some sane defaults.
|
||||
config := test.cfg
|
||||
// config.Logger = testlog.Logger(t, log.LvlDebug)
|
||||
config.P2P.NoDiscovery = true
|
||||
if config.HTTPTimeouts == (rpc.HTTPTimeouts{}) {
|
||||
config.HTTPTimeouts = rpc.DefaultHTTPTimeouts
|
||||
}
|
||||
|
||||
// Create Node.
|
||||
stack, err := New(&config)
|
||||
if err != nil {
|
||||
t.Fatal("can't create node:", err)
|
||||
}
|
||||
defer stack.Close()
|
||||
|
||||
// Register the test handler.
|
||||
stack.RegisterHandler("test", "/test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("OK"))
|
||||
}))
|
||||
|
||||
if err := stack.Start(); err != nil {
|
||||
t.Fatal("can't start node:", err)
|
||||
}
|
||||
|
||||
// Run the API call hook.
|
||||
if test.fn != nil {
|
||||
test.fn(t, stack, &adminAPI{stack})
|
||||
}
|
||||
|
||||
// Check if the HTTP endpoints are available.
|
||||
baseURL := stack.HTTPEndpoint()
|
||||
reachable := checkReachable(baseURL)
|
||||
handlersAvailable := checkBodyOK(baseURL + "/test")
|
||||
rpcAvailable := checkRPC(baseURL)
|
||||
wsAvailable := checkRPC(strings.Replace(baseURL, "http://", "ws://", 1))
|
||||
if reachable != test.wantReachable {
|
||||
t.Errorf("HTTP server is %sreachable, want it %sreachable", not(reachable), not(test.wantReachable))
|
||||
}
|
||||
if handlersAvailable != test.wantHandlers {
|
||||
t.Errorf("RegisterHandler handlers %savailable, want them %savailable", not(handlersAvailable), not(test.wantHandlers))
|
||||
}
|
||||
if rpcAvailable != test.wantRPC {
|
||||
t.Errorf("HTTP RPC %savailable, want it %savailable", not(rpcAvailable), not(test.wantRPC))
|
||||
}
|
||||
if wsAvailable != test.wantWS {
|
||||
t.Errorf("WS RPC %savailable, want it %savailable", not(wsAvailable), not(test.wantWS))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// checkReachable checks if the TCP endpoint in rawurl is open.
|
||||
func checkReachable(rawurl string) bool {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conn, err := net.Dial("tcp", u.Host)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// checkBodyOK checks whether the given HTTP URL responds with 200 OK and body "OK".
|
||||
func checkBodyOK(url string) bool {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return false
|
||||
}
|
||||
buf := make([]byte, 2)
|
||||
if _, err = io.ReadFull(resp.Body, buf); err != nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(buf, []byte("OK"))
|
||||
}
|
||||
|
||||
// checkRPC checks whether JSON-RPC works against the given URL.
|
||||
func checkRPC(url string) bool {
|
||||
c, err := rpc.Dial(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
_, err = c.SupportedModules()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// string/int pointer helpers.
|
||||
func sp(s string) *string { return &s }
|
||||
func ip(i int) *int { return &i }
|
||||
|
||||
func not(ok bool) string {
|
||||
if ok {
|
||||
return ""
|
||||
}
|
||||
return "not "
|
||||
}
|
||||
480
node/config.go
480
node/config.go
|
|
@ -1,480 +0,0 @@
|
|||
// Copyright 2014 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
const (
|
||||
datadirPrivateKey = "nodekey" // Path within the datadir to the node's private key
|
||||
datadirJWTKey = "jwtsecret" // Path within the datadir to the node's jwt secret
|
||||
datadirDefaultKeyStore = "keystore" // Path within the datadir to the keystore
|
||||
datadirStaticNodes = "static-nodes.json" // Path within the datadir to the static node list
|
||||
datadirTrustedNodes = "trusted-nodes.json" // Path within the datadir to the trusted node list
|
||||
datadirNodeDatabase = "nodes" // Path within the datadir to store the node infos
|
||||
)
|
||||
|
||||
// Config represents a small collection of configuration values to fine tune the
|
||||
// P2P network layer of a protocol stack. These values can be further extended by
|
||||
// all registered services.
|
||||
type Config struct {
|
||||
// Name sets the instance name of the node. It must not contain the / character and is
|
||||
// used in the devp2p node identifier. The instance name of geth is "geth". If no
|
||||
// value is specified, the basename of the current executable is used.
|
||||
Name string `toml:"-"`
|
||||
|
||||
// UserIdent, if set, is used as an additional component in the devp2p node identifier.
|
||||
UserIdent string `toml:",omitempty"`
|
||||
|
||||
// Version should be set to the version number of the program. It is used
|
||||
// in the devp2p node identifier.
|
||||
Version string `toml:"-"`
|
||||
|
||||
// DataDir is the file system folder the node should use for any data storage
|
||||
// requirements. The configured data directory will not be directly shared with
|
||||
// registered services, instead those can use utility methods to create/access
|
||||
// databases or flat files. This enables ephemeral nodes which can fully reside
|
||||
// in memory.
|
||||
DataDir string
|
||||
|
||||
// Configuration of peer-to-peer networking.
|
||||
P2P p2p.Config
|
||||
|
||||
// KeyStoreDir is the file system folder that contains private keys. The directory can
|
||||
// be specified as a relative path, in which case it is resolved relative to the
|
||||
// current directory.
|
||||
//
|
||||
// If KeyStoreDir is empty, the default location is the "keystore" subdirectory of
|
||||
// DataDir. If DataDir is unspecified and KeyStoreDir is empty, an ephemeral directory
|
||||
// is created by New and destroyed when the node is stopped.
|
||||
KeyStoreDir string `toml:",omitempty"`
|
||||
|
||||
// ExternalSigner specifies an external URI for a clef-type signer.
|
||||
ExternalSigner string `toml:",omitempty"`
|
||||
|
||||
// UseLightweightKDF lowers the memory and CPU requirements of the key store
|
||||
// scrypt KDF at the expense of security.
|
||||
UseLightweightKDF bool `toml:",omitempty"`
|
||||
|
||||
// InsecureUnlockAllowed allows user to unlock accounts in unsafe http environment.
|
||||
InsecureUnlockAllowed bool `toml:",omitempty"`
|
||||
|
||||
// NoUSB disables hardware wallet monitoring and connectivity.
|
||||
// Deprecated: USB monitoring is disabled by default and must be enabled explicitly.
|
||||
NoUSB bool `toml:",omitempty"`
|
||||
|
||||
// USB enables hardware wallet monitoring and connectivity.
|
||||
USB bool `toml:",omitempty"`
|
||||
|
||||
// SmartCardDaemonPath is the path to the smartcard daemon's socket.
|
||||
SmartCardDaemonPath string `toml:",omitempty"`
|
||||
|
||||
// IPCPath is the requested location to place the IPC endpoint. If the path is
|
||||
// a simple file name, it is placed inside the data directory (or on the root
|
||||
// pipe path on Windows), whereas if it's a resolvable path name (absolute or
|
||||
// relative), then that specific path is enforced. An empty path disables IPC.
|
||||
IPCPath string
|
||||
|
||||
// HTTPHost is the host interface on which to start the HTTP RPC server. If this
|
||||
// field is empty, no HTTP API endpoint will be started.
|
||||
HTTPHost string
|
||||
|
||||
// HTTPPort is the TCP port number on which to start the HTTP RPC server. The
|
||||
// default zero value is/ valid and will pick a port number randomly (useful
|
||||
// for ephemeral nodes).
|
||||
HTTPPort int `toml:",omitempty"`
|
||||
|
||||
// HTTPCors is the Cross-Origin Resource Sharing header to send to requesting
|
||||
// clients. Please be aware that CORS is a browser enforced security, it's fully
|
||||
// useless for custom HTTP clients.
|
||||
HTTPCors []string `toml:",omitempty"`
|
||||
|
||||
// HTTPVirtualHosts is the list of virtual hostnames which are allowed on incoming requests.
|
||||
// This is by default {'localhost'}. Using this prevents attacks like
|
||||
// DNS rebinding, which bypasses SOP by simply masquerading as being within the same
|
||||
// origin. These attacks do not utilize CORS, since they are not cross-domain.
|
||||
// By explicitly checking the Host-header, the server will not allow requests
|
||||
// made against the server with a malicious host domain.
|
||||
// Requests using ip address directly are not affected
|
||||
HTTPVirtualHosts []string `toml:",omitempty"`
|
||||
|
||||
// HTTPModules is a list of API modules to expose via the HTTP RPC interface.
|
||||
// If the module list is empty, all RPC API endpoints designated public will be
|
||||
// exposed.
|
||||
HTTPModules []string
|
||||
|
||||
// HTTPTimeouts allows for customization of the timeout values used by the HTTP RPC
|
||||
// interface.
|
||||
HTTPTimeouts rpc.HTTPTimeouts
|
||||
|
||||
// HTTPPathPrefix specifies a path prefix on which http-rpc is to be served.
|
||||
HTTPPathPrefix string `toml:",omitempty"`
|
||||
|
||||
// AuthAddr is the listening address on which authenticated APIs are provided.
|
||||
AuthAddr string `toml:",omitempty"`
|
||||
|
||||
// AuthPort is the port number on which authenticated APIs are provided.
|
||||
AuthPort int `toml:",omitempty"`
|
||||
|
||||
// AuthVirtualHosts is the list of virtual hostnames which are allowed on incoming requests
|
||||
// for the authenticated api. This is by default {'localhost'}.
|
||||
AuthVirtualHosts []string `toml:",omitempty"`
|
||||
|
||||
// WSHost is the host interface on which to start the websocket RPC server. If
|
||||
// this field is empty, no websocket API endpoint will be started.
|
||||
WSHost string
|
||||
|
||||
// WSPort is the TCP port number on which to start the websocket RPC server. The
|
||||
// default zero value is/ valid and will pick a port number randomly (useful for
|
||||
// ephemeral nodes).
|
||||
WSPort int `toml:",omitempty"`
|
||||
|
||||
// WSPathPrefix specifies a path prefix on which ws-rpc is to be served.
|
||||
WSPathPrefix string `toml:",omitempty"`
|
||||
|
||||
// WSOrigins is the list of domain to accept websocket requests from. Please be
|
||||
// aware that the server can only act upon the HTTP request the client sends and
|
||||
// cannot verify the validity of the request header.
|
||||
WSOrigins []string `toml:",omitempty"`
|
||||
|
||||
// WSModules is a list of API modules to expose via the websocket RPC interface.
|
||||
// If the module list is empty, all RPC API endpoints designated public will be
|
||||
// exposed.
|
||||
WSModules []string
|
||||
|
||||
// WSExposeAll exposes all API modules via the WebSocket RPC interface rather
|
||||
// than just the public ones.
|
||||
//
|
||||
// *WARNING* Only set this if the node is running in a trusted network, exposing
|
||||
// private APIs to untrusted users is a major security risk.
|
||||
WSExposeAll bool `toml:",omitempty"`
|
||||
|
||||
// GraphQLCors is the Cross-Origin Resource Sharing header to send to requesting
|
||||
// clients. Please be aware that CORS is a browser enforced security, it's fully
|
||||
// useless for custom HTTP clients.
|
||||
GraphQLCors []string `toml:",omitempty"`
|
||||
|
||||
// GraphQLVirtualHosts is the list of virtual hostnames which are allowed on incoming requests.
|
||||
// This is by default {'localhost'}. Using this prevents attacks like
|
||||
// DNS rebinding, which bypasses SOP by simply masquerading as being within the same
|
||||
// origin. These attacks do not utilize CORS, since they are not cross-domain.
|
||||
// By explicitly checking the Host-header, the server will not allow requests
|
||||
// made against the server with a malicious host domain.
|
||||
// Requests using ip address directly are not affected
|
||||
GraphQLVirtualHosts []string `toml:",omitempty"`
|
||||
|
||||
// Logger is a custom logger to use with the p2p.Server.
|
||||
Logger log.Logger `toml:",omitempty"`
|
||||
|
||||
oldGethResourceWarning bool
|
||||
|
||||
// AllowUnprotectedTxs allows non EIP-155 protected transactions to be send over RPC.
|
||||
AllowUnprotectedTxs bool `toml:",omitempty"`
|
||||
|
||||
// BatchRequestLimit is the maximum number of requests in a batch.
|
||||
BatchRequestLimit int `toml:",omitempty"`
|
||||
|
||||
// BatchResponseMaxSize is the maximum number of bytes returned from a batched rpc call.
|
||||
BatchResponseMaxSize int `toml:",omitempty"`
|
||||
|
||||
// JWTSecret is the path to the hex-encoded jwt secret.
|
||||
JWTSecret string `toml:",omitempty"`
|
||||
|
||||
// EnablePersonal enables the deprecated personal namespace.
|
||||
EnablePersonal bool `toml:"-"`
|
||||
|
||||
DBEngine string `toml:",omitempty"`
|
||||
}
|
||||
|
||||
// IPCEndpoint resolves an IPC endpoint based on a configured value, taking into
|
||||
// account the set data folders as well as the designated platform we're currently
|
||||
// running on.
|
||||
func (c *Config) IPCEndpoint() string {
|
||||
// Short circuit if IPC has not been enabled
|
||||
if c.IPCPath == "" {
|
||||
return ""
|
||||
}
|
||||
// On windows we can only use plain top-level pipes
|
||||
if runtime.GOOS == "windows" {
|
||||
if strings.HasPrefix(c.IPCPath, `\\.\pipe\`) {
|
||||
return c.IPCPath
|
||||
}
|
||||
return `\\.\pipe\` + c.IPCPath
|
||||
}
|
||||
// Resolve names into the data directory full paths otherwise
|
||||
if filepath.Base(c.IPCPath) == c.IPCPath {
|
||||
if c.DataDir == "" {
|
||||
return filepath.Join(os.TempDir(), c.IPCPath)
|
||||
}
|
||||
return filepath.Join(c.DataDir, c.IPCPath)
|
||||
}
|
||||
return c.IPCPath
|
||||
}
|
||||
|
||||
// NodeDB returns the path to the discovery node database.
|
||||
func (c *Config) NodeDB() string {
|
||||
if c.DataDir == "" {
|
||||
return "" // ephemeral
|
||||
}
|
||||
return c.ResolvePath(datadirNodeDatabase)
|
||||
}
|
||||
|
||||
// DefaultIPCEndpoint returns the IPC path used by default.
|
||||
func DefaultIPCEndpoint(clientIdentifier string) string {
|
||||
if clientIdentifier == "" {
|
||||
clientIdentifier = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
|
||||
if clientIdentifier == "" {
|
||||
panic("empty executable name")
|
||||
}
|
||||
}
|
||||
config := &Config{DataDir: DefaultDataDir(), IPCPath: clientIdentifier + ".ipc"}
|
||||
return config.IPCEndpoint()
|
||||
}
|
||||
|
||||
// HTTPEndpoint resolves an HTTP endpoint based on the configured host interface
|
||||
// and port parameters.
|
||||
func (c *Config) HTTPEndpoint() string {
|
||||
if c.HTTPHost == "" {
|
||||
return ""
|
||||
}
|
||||
return net.JoinHostPort(c.HTTPHost, fmt.Sprintf("%d", c.HTTPPort))
|
||||
}
|
||||
|
||||
// DefaultHTTPEndpoint returns the HTTP endpoint used by default.
|
||||
func DefaultHTTPEndpoint() string {
|
||||
config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort, AuthPort: DefaultAuthPort}
|
||||
return config.HTTPEndpoint()
|
||||
}
|
||||
|
||||
// WSEndpoint resolves a websocket endpoint based on the configured host interface
|
||||
// and port parameters.
|
||||
func (c *Config) WSEndpoint() string {
|
||||
if c.WSHost == "" {
|
||||
return ""
|
||||
}
|
||||
return net.JoinHostPort(c.WSHost, fmt.Sprintf("%d", c.WSPort))
|
||||
}
|
||||
|
||||
// DefaultWSEndpoint returns the websocket endpoint used by default.
|
||||
func DefaultWSEndpoint() string {
|
||||
config := &Config{WSHost: DefaultWSHost, WSPort: DefaultWSPort}
|
||||
return config.WSEndpoint()
|
||||
}
|
||||
|
||||
// ExtRPCEnabled returns the indicator whether node enables the external
|
||||
// RPC(http, ws or graphql).
|
||||
func (c *Config) ExtRPCEnabled() bool {
|
||||
return c.HTTPHost != "" || c.WSHost != ""
|
||||
}
|
||||
|
||||
// NodeName returns the devp2p node identifier.
|
||||
func (c *Config) NodeName() string {
|
||||
name := c.name()
|
||||
// Backwards compatibility: previous versions used title-cased "Geth", keep that.
|
||||
if name == "geth" || name == "geth-testnet" {
|
||||
name = "Geth"
|
||||
}
|
||||
if c.UserIdent != "" {
|
||||
name += "/" + c.UserIdent
|
||||
}
|
||||
if c.Version != "" {
|
||||
name += "/v" + c.Version
|
||||
}
|
||||
name += "/" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
name += "/" + runtime.Version()
|
||||
return name
|
||||
}
|
||||
|
||||
func (c *Config) name() string {
|
||||
if c.Name == "" {
|
||||
progname := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
|
||||
if progname == "" {
|
||||
panic("empty executable name, set Config.Name")
|
||||
}
|
||||
return progname
|
||||
}
|
||||
return c.Name
|
||||
}
|
||||
|
||||
// These resources are resolved differently for "geth" instances.
|
||||
var isOldGethResource = map[string]bool{
|
||||
"chaindata": true,
|
||||
"nodes": true,
|
||||
"nodekey": true,
|
||||
"static-nodes.json": false, // no warning for these because they have their
|
||||
"trusted-nodes.json": false, // own separate warning.
|
||||
}
|
||||
|
||||
// ResolvePath resolves path in the instance directory.
|
||||
func (c *Config) ResolvePath(path string) string {
|
||||
if filepath.IsAbs(path) {
|
||||
return path
|
||||
}
|
||||
if c.DataDir == "" {
|
||||
return ""
|
||||
}
|
||||
// Backwards-compatibility: ensure that data directory files created
|
||||
// by geth 1.4 are used if they exist.
|
||||
if warn, isOld := isOldGethResource[path]; isOld {
|
||||
oldpath := ""
|
||||
if c.name() == "geth" {
|
||||
oldpath = filepath.Join(c.DataDir, path)
|
||||
}
|
||||
if oldpath != "" && common.FileExist(oldpath) {
|
||||
if warn && !c.oldGethResourceWarning {
|
||||
c.oldGethResourceWarning = true
|
||||
log.Warn("Using deprecated resource file, please move this file to the 'geth' subdirectory of datadir.", "file", oldpath)
|
||||
}
|
||||
return oldpath
|
||||
}
|
||||
}
|
||||
return filepath.Join(c.instanceDir(), path)
|
||||
}
|
||||
|
||||
func (c *Config) instanceDir() string {
|
||||
if c.DataDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(c.DataDir, c.name())
|
||||
}
|
||||
|
||||
// NodeKey retrieves the currently configured private key of the node, checking
|
||||
// first any manually set key, falling back to the one found in the configured
|
||||
// data folder. If no key can be found, a new one is generated.
|
||||
func (c *Config) NodeKey() *ecdsa.PrivateKey {
|
||||
// Use any specifically configured key.
|
||||
if c.P2P.PrivateKey != nil {
|
||||
return c.P2P.PrivateKey
|
||||
}
|
||||
// Generate ephemeral key if no datadir is being used.
|
||||
if c.DataDir == "" {
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
log.Crit(fmt.Sprintf("Failed to generate ephemeral node key: %v", err))
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
keyfile := c.ResolvePath(datadirPrivateKey)
|
||||
if key, err := crypto.LoadECDSA(keyfile); err == nil {
|
||||
return key
|
||||
}
|
||||
// No persistent key found, generate and store a new one.
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
log.Crit(fmt.Sprintf("Failed to generate node key: %v", err))
|
||||
}
|
||||
instanceDir := filepath.Join(c.DataDir, c.name())
|
||||
if err := os.MkdirAll(instanceDir, 0700); err != nil {
|
||||
log.Error(fmt.Sprintf("Failed to persist node key: %v", err))
|
||||
return key
|
||||
}
|
||||
keyfile = filepath.Join(instanceDir, datadirPrivateKey)
|
||||
if err := crypto.SaveECDSA(keyfile, key); err != nil {
|
||||
log.Error(fmt.Sprintf("Failed to persist node key: %v", err))
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// checkLegacyFiles inspects the datadir for signs of legacy static-nodes
|
||||
// and trusted-nodes files. If they exist it raises an error.
|
||||
func (c *Config) checkLegacyFiles() {
|
||||
c.checkLegacyFile(c.ResolvePath(datadirStaticNodes))
|
||||
c.checkLegacyFile(c.ResolvePath(datadirTrustedNodes))
|
||||
}
|
||||
|
||||
// checkLegacyFile will only raise an error if a file at the given path exists.
|
||||
func (c *Config) checkLegacyFile(path string) {
|
||||
// Short circuit if no node config is present
|
||||
if c.DataDir == "" {
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return
|
||||
}
|
||||
logger := c.Logger
|
||||
if logger == nil {
|
||||
logger = log.Root()
|
||||
}
|
||||
switch fname := filepath.Base(path); fname {
|
||||
case "static-nodes.json":
|
||||
logger.Error("The static-nodes.json file is deprecated and ignored. Use P2P.StaticNodes in config.toml instead.")
|
||||
case "trusted-nodes.json":
|
||||
logger.Error("The trusted-nodes.json file is deprecated and ignored. Use P2P.TrustedNodes in config.toml instead.")
|
||||
default:
|
||||
// We shouldn't wind up here, but better print something just in case.
|
||||
logger.Error("Ignoring deprecated file.", "file", path)
|
||||
}
|
||||
}
|
||||
|
||||
// KeyDirConfig determines the settings for keydirectory
|
||||
func (c *Config) KeyDirConfig() (string, error) {
|
||||
var (
|
||||
keydir string
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
case filepath.IsAbs(c.KeyStoreDir):
|
||||
keydir = c.KeyStoreDir
|
||||
case c.DataDir != "":
|
||||
if c.KeyStoreDir == "" {
|
||||
keydir = filepath.Join(c.DataDir, datadirDefaultKeyStore)
|
||||
} else {
|
||||
keydir, err = filepath.Abs(c.KeyStoreDir)
|
||||
}
|
||||
case c.KeyStoreDir != "":
|
||||
keydir, err = filepath.Abs(c.KeyStoreDir)
|
||||
}
|
||||
return keydir, err
|
||||
}
|
||||
|
||||
// GetKeyStoreDir retrieves the key directory and will create
|
||||
// and ephemeral one if necessary.
|
||||
func (c *Config) GetKeyStoreDir() (string, bool, error) {
|
||||
keydir, err := c.KeyDirConfig()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
isEphemeral := false
|
||||
if keydir == "" {
|
||||
// There is no datadir.
|
||||
keydir, err = os.MkdirTemp("", "go-ethereum-keystore")
|
||||
isEphemeral = true
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if err := os.MkdirAll(keydir, 0700); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return keydir, isEphemeral, nil
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
)
|
||||
|
||||
// Tests that datadirs can be successfully created, be them manually configured
|
||||
// ones or automatically generated temporary ones.
|
||||
func TestDatadirCreation(t *testing.T) {
|
||||
// Create a temporary data dir and check that it can be used by a node
|
||||
dir := t.TempDir()
|
||||
|
||||
node, err := New(&Config{DataDir: dir})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create stack with existing datadir: %v", err)
|
||||
}
|
||||
if err := node.Close(); err != nil {
|
||||
t.Fatalf("failed to close node: %v", err)
|
||||
}
|
||||
// Generate a long non-existing datadir path and check that it gets created by a node
|
||||
dir = filepath.Join(dir, "a", "b", "c", "d", "e", "f")
|
||||
node, err = New(&Config{DataDir: dir})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create stack with creatable datadir: %v", err)
|
||||
}
|
||||
if err := node.Close(); err != nil {
|
||||
t.Fatalf("failed to close node: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
t.Fatalf("freshly created datadir not accessible: %v", err)
|
||||
}
|
||||
// Verify that an impossible datadir fails creation
|
||||
file, err := os.CreateTemp("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temporary file: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
file.Close()
|
||||
os.Remove(file.Name())
|
||||
}()
|
||||
|
||||
dir = filepath.Join(file.Name(), "invalid/path")
|
||||
_, err = New(&Config{DataDir: dir})
|
||||
if err == nil {
|
||||
t.Fatalf("protocol stack created with an invalid datadir")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that IPC paths are correctly resolved to valid endpoints of different
|
||||
// platforms.
|
||||
func TestIPCPathResolution(t *testing.T) {
|
||||
var tests = []struct {
|
||||
DataDir string
|
||||
IPCPath string
|
||||
Windows bool
|
||||
Endpoint string
|
||||
}{
|
||||
{"", "", false, ""},
|
||||
{"data", "", false, ""},
|
||||
{"", "geth.ipc", false, filepath.Join(os.TempDir(), "geth.ipc")},
|
||||
{"data", "geth.ipc", false, "data/geth.ipc"},
|
||||
{"data", "./geth.ipc", false, "./geth.ipc"},
|
||||
{"data", "/geth.ipc", false, "/geth.ipc"},
|
||||
{"", "", true, ``},
|
||||
{"data", "", true, ``},
|
||||
{"", "geth.ipc", true, `\\.\pipe\geth.ipc`},
|
||||
{"data", "geth.ipc", true, `\\.\pipe\geth.ipc`},
|
||||
{"data", `\\.\pipe\geth.ipc`, true, `\\.\pipe\geth.ipc`},
|
||||
}
|
||||
for i, test := range tests {
|
||||
// Only run when platform/test match
|
||||
if (runtime.GOOS == "windows") == test.Windows {
|
||||
if endpoint := (&Config{DataDir: test.DataDir, IPCPath: test.IPCPath}).IPCEndpoint(); endpoint != test.Endpoint {
|
||||
t.Errorf("test %d: IPC endpoint mismatch: have %s, want %s", i, endpoint, test.Endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that node keys can be correctly created, persisted, loaded and/or made
|
||||
// ephemeral.
|
||||
func TestNodeKeyPersistency(t *testing.T) {
|
||||
// Create a temporary folder and make sure no key is present
|
||||
dir := t.TempDir()
|
||||
|
||||
keyfile := filepath.Join(dir, "unit-test", datadirPrivateKey)
|
||||
|
||||
// Configure a node with a preset key and ensure it's not persisted
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate one-shot node key: %v", err)
|
||||
}
|
||||
config := &Config{Name: "unit-test", DataDir: dir, P2P: p2p.Config{PrivateKey: key}}
|
||||
config.NodeKey()
|
||||
if _, err := os.Stat(keyfile); err == nil {
|
||||
t.Fatalf("one-shot node key persisted to data directory")
|
||||
}
|
||||
|
||||
// Configure a node with no preset key and ensure it is persisted this time
|
||||
config = &Config{Name: "unit-test", DataDir: dir}
|
||||
config.NodeKey()
|
||||
if _, err := os.Stat(keyfile); err != nil {
|
||||
t.Fatalf("node key not persisted to data directory: %v", err)
|
||||
}
|
||||
if _, err = crypto.LoadECDSA(keyfile); err != nil {
|
||||
t.Fatalf("failed to load freshly persisted node key: %v", err)
|
||||
}
|
||||
blob1, err := os.ReadFile(keyfile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read freshly persisted node key: %v", err)
|
||||
}
|
||||
|
||||
// Configure a new node and ensure the previously persisted key is loaded
|
||||
config = &Config{Name: "unit-test", DataDir: dir}
|
||||
config.NodeKey()
|
||||
blob2, err := os.ReadFile(keyfile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read previously persisted node key: %v", err)
|
||||
}
|
||||
if !bytes.Equal(blob1, blob2) {
|
||||
t.Fatalf("persisted node key mismatch: have %x, want %x", blob2, blob1)
|
||||
}
|
||||
|
||||
// Configure ephemeral node and ensure no key is dumped locally
|
||||
config = &Config{Name: "unit-test", DataDir: ""}
|
||||
config.NodeKey()
|
||||
if _, err := os.Stat(filepath.Join(".", "unit-test", datadirPrivateKey)); err == nil {
|
||||
t.Fatalf("ephemeral node key persisted to disk")
|
||||
}
|
||||
}
|
||||
133
node/defaults.go
133
node/defaults.go
|
|
@ -1,133 +0,0 @@
|
|||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/nat"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultHTTPHost = "localhost" // Default host interface for the HTTP RPC server
|
||||
DefaultHTTPPort = 8545 // Default TCP port for the HTTP RPC server
|
||||
DefaultWSHost = "localhost" // Default host interface for the websocket RPC server
|
||||
DefaultWSPort = 8546 // Default TCP port for the websocket RPC server
|
||||
DefaultAuthHost = "localhost" // Default host interface for the authenticated apis
|
||||
DefaultAuthPort = 8551 // Default port for the authenticated apis
|
||||
)
|
||||
|
||||
const (
|
||||
// Engine API batch limits: these are not configurable by users, and should cover the
|
||||
// needs of all CLs.
|
||||
engineAPIBatchItemLimit = 2000
|
||||
engineAPIBatchResponseSizeLimit = 250 * 1000 * 1000
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultAuthCors = []string{"localhost"} // Default cors domain for the authenticated apis
|
||||
DefaultAuthVhosts = []string{"localhost"} // Default virtual hosts for the authenticated apis
|
||||
DefaultAuthOrigins = []string{"localhost"} // Default origins for the authenticated apis
|
||||
DefaultAuthPrefix = "" // Default prefix for the authenticated apis
|
||||
DefaultAuthModules = []string{"eth", "engine"}
|
||||
)
|
||||
|
||||
// DefaultConfig contains reasonable default settings.
|
||||
var DefaultConfig = Config{
|
||||
DataDir: DefaultDataDir(),
|
||||
HTTPPort: DefaultHTTPPort,
|
||||
AuthAddr: DefaultAuthHost,
|
||||
AuthPort: DefaultAuthPort,
|
||||
AuthVirtualHosts: DefaultAuthVhosts,
|
||||
HTTPModules: []string{"net", "web3"},
|
||||
HTTPVirtualHosts: []string{"localhost"},
|
||||
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
|
||||
WSPort: DefaultWSPort,
|
||||
WSModules: []string{"net", "web3"},
|
||||
BatchRequestLimit: 1000,
|
||||
BatchResponseMaxSize: 25 * 1000 * 1000,
|
||||
GraphQLVirtualHosts: []string{"localhost"},
|
||||
P2P: p2p.Config{
|
||||
ListenAddr: ":30303",
|
||||
MaxPeers: 50,
|
||||
NAT: nat.Any(),
|
||||
},
|
||||
DBEngine: "", // Use whatever exists, will default to Pebble if non-existent and supported
|
||||
}
|
||||
|
||||
// DefaultDataDir is the default data directory to use for the databases and other
|
||||
// persistence requirements.
|
||||
func DefaultDataDir() string {
|
||||
// Try to place the data folder in the user's home dir
|
||||
home := homeDir()
|
||||
if home != "" {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return filepath.Join(home, "Library", "Ethereum")
|
||||
case "windows":
|
||||
// We used to put everything in %HOME%\AppData\Roaming, but this caused
|
||||
// problems with non-typical setups. If this fallback location exists and
|
||||
// is non-empty, use it, otherwise DTRT and check %LOCALAPPDATA%.
|
||||
fallback := filepath.Join(home, "AppData", "Roaming", "Ethereum")
|
||||
appdata := windowsAppData()
|
||||
if appdata == "" || isNonEmptyDir(fallback) {
|
||||
return fallback
|
||||
}
|
||||
return filepath.Join(appdata, "Ethereum")
|
||||
default:
|
||||
return filepath.Join(home, ".ethereum")
|
||||
}
|
||||
}
|
||||
// As we cannot guess a stable location, return empty and handle later
|
||||
return ""
|
||||
}
|
||||
|
||||
func windowsAppData() string {
|
||||
v := os.Getenv("LOCALAPPDATA")
|
||||
if v == "" {
|
||||
// Windows XP and below don't have LocalAppData. Crash here because
|
||||
// we don't support Windows XP and undefining the variable will cause
|
||||
// other issues.
|
||||
panic("environment variable LocalAppData is undefined")
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func isNonEmptyDir(dir string) bool {
|
||||
f, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
names, _ := f.Readdir(1)
|
||||
f.Close()
|
||||
return len(names) > 0
|
||||
}
|
||||
|
||||
func homeDir() string {
|
||||
if home := os.Getenv("HOME"); home != "" {
|
||||
return home
|
||||
}
|
||||
if usr, err := user.Current(); err == nil {
|
||||
return usr.HomeDir
|
||||
}
|
||||
return ""
|
||||
}
|
||||
121
node/doc.go
121
node/doc.go
|
|
@ -1,121 +0,0 @@
|
|||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/*
|
||||
Package node sets up multi-protocol Ethereum nodes.
|
||||
|
||||
In the model exposed by this package, a node is a collection of services which use shared
|
||||
resources to provide RPC APIs. Services can also offer devp2p protocols, which are wired
|
||||
up to the devp2p network when the node instance is started.
|
||||
|
||||
# Node Lifecycle
|
||||
|
||||
The Node object has a lifecycle consisting of three basic states, INITIALIZING, RUNNING
|
||||
and CLOSED.
|
||||
|
||||
●───────┐
|
||||
New()
|
||||
│
|
||||
▼
|
||||
INITIALIZING ────Start()─┐
|
||||
│ │
|
||||
│ ▼
|
||||
Close() RUNNING
|
||||
│ │
|
||||
▼ │
|
||||
CLOSED ◀──────Close()─┘
|
||||
|
||||
Creating a Node allocates basic resources such as the data directory and returns the node
|
||||
in its INITIALIZING state. Lifecycle objects, RPC APIs and peer-to-peer networking
|
||||
protocols can be registered in this state. Basic operations such as opening a key-value
|
||||
database are permitted while initializing.
|
||||
|
||||
Once everything is registered, the node can be started, which moves it into the RUNNING
|
||||
state. Starting the node starts all registered Lifecycle objects and enables RPC and
|
||||
peer-to-peer networking. Note that no additional Lifecycles, APIs or p2p protocols can be
|
||||
registered while the node is running.
|
||||
|
||||
Closing the node releases all held resources. The actions performed by Close depend on the
|
||||
state it was in. When closing a node in INITIALIZING state, resources related to the data
|
||||
directory are released. If the node was RUNNING, closing it also stops all Lifecycle
|
||||
objects and shuts down RPC and peer-to-peer networking.
|
||||
|
||||
You must always call Close on Node, even if the node was not started.
|
||||
|
||||
# Resources Managed By Node
|
||||
|
||||
All file-system resources used by a node instance are located in a directory called the
|
||||
data directory. The location of each resource can be overridden through additional node
|
||||
configuration. The data directory is optional. If it is not set and the location of a
|
||||
resource is otherwise unspecified, package node will create the resource in memory.
|
||||
|
||||
To access to the devp2p network, Node configures and starts p2p.Server. Each host on the
|
||||
devp2p network has a unique identifier, the node key. The Node instance persists this key
|
||||
across restarts. Node also loads static and trusted node lists and ensures that knowledge
|
||||
about other hosts is persisted.
|
||||
|
||||
JSON-RPC servers which run HTTP, WebSocket or IPC can be started on a Node. RPC modules
|
||||
offered by registered services will be offered on those endpoints. Users can restrict any
|
||||
endpoint to a subset of RPC modules. Node itself offers the "debug", "admin" and "web3"
|
||||
modules.
|
||||
|
||||
Service implementations can open LevelDB databases through the service context. Package
|
||||
node chooses the file system location of each database. If the node is configured to run
|
||||
without a data directory, databases are opened in memory instead.
|
||||
|
||||
Node also creates the shared store of encrypted Ethereum account keys. Services can access
|
||||
the account manager through the service context.
|
||||
|
||||
# Sharing Data Directory Among Instances
|
||||
|
||||
Multiple node instances can share a single data directory if they have distinct instance
|
||||
names (set through the Name config option). Sharing behaviour depends on the type of
|
||||
resource.
|
||||
|
||||
devp2p-related resources (node key, static/trusted node lists, known hosts database) are
|
||||
stored in a directory with the same name as the instance. Thus, multiple node instances
|
||||
using the same data directory will store this information in different subdirectories of
|
||||
the data directory.
|
||||
|
||||
LevelDB databases are also stored within the instance subdirectory. If multiple node
|
||||
instances use the same data directory, opening the databases with identical names will
|
||||
create one database for each instance.
|
||||
|
||||
The account key store is shared among all node instances using the same data directory
|
||||
unless its location is changed through the KeyStoreDir configuration option.
|
||||
|
||||
# Data Directory Sharing Example
|
||||
|
||||
In this example, two node instances named A and B are started with the same data
|
||||
directory. Node instance A opens the database "db", node instance B opens the databases
|
||||
"db" and "db-2". The following files will be created in the data directory:
|
||||
|
||||
data-directory/
|
||||
A/
|
||||
nodekey -- devp2p node key of instance A
|
||||
nodes/ -- devp2p discovery knowledge database of instance A
|
||||
db/ -- LevelDB content for "db"
|
||||
A.ipc -- JSON-RPC UNIX domain socket endpoint of instance A
|
||||
B/
|
||||
nodekey -- devp2p node key of node B
|
||||
nodes/ -- devp2p discovery knowledge database of instance B
|
||||
static-nodes.json -- devp2p static node list of instance B
|
||||
db/ -- LevelDB content for "db"
|
||||
db-2/ -- LevelDB content for "db-2"
|
||||
B.ipc -- JSON-RPC UNIX domain socket endpoint of instance B
|
||||
keystore/ -- account key store, used by both instances
|
||||
*/
|
||||
package node
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
// StartHTTPEndpoint starts the HTTP RPC endpoint.
|
||||
func StartHTTPEndpoint(endpoint string, timeouts rpc.HTTPTimeouts, handler http.Handler) (*http.Server, net.Addr, error) {
|
||||
// start the HTTP listener
|
||||
var (
|
||||
listener net.Listener
|
||||
err error
|
||||
)
|
||||
if listener, err = net.Listen("tcp", endpoint); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// make sure timeout values are meaningful
|
||||
CheckTimeouts(&timeouts)
|
||||
// Bundle and start the HTTP server
|
||||
httpSrv := &http.Server{
|
||||
Handler: handler,
|
||||
ReadTimeout: timeouts.ReadTimeout,
|
||||
ReadHeaderTimeout: timeouts.ReadHeaderTimeout,
|
||||
WriteTimeout: timeouts.WriteTimeout,
|
||||
IdleTimeout: timeouts.IdleTimeout,
|
||||
}
|
||||
go httpSrv.Serve(listener)
|
||||
return httpSrv, listener.Addr(), err
|
||||
}
|
||||
|
||||
// checkModuleAvailability checks that all names given in modules are actually
|
||||
// available API services. It assumes that the MetadataApi module ("rpc") is always available;
|
||||
// the registration of this "rpc" module happens in NewServer() and is thus common to all endpoints.
|
||||
func checkModuleAvailability(modules []string, apis []rpc.API) (bad, available []string) {
|
||||
availableSet := make(map[string]struct{})
|
||||
for _, api := range apis {
|
||||
if _, ok := availableSet[api.Namespace]; !ok {
|
||||
availableSet[api.Namespace] = struct{}{}
|
||||
available = append(available, api.Namespace)
|
||||
}
|
||||
}
|
||||
for _, name := range modules {
|
||||
if _, ok := availableSet[name]; !ok {
|
||||
if name != rpc.MetadataApi && name != rpc.EngineApi {
|
||||
bad = append(bad, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return bad, available
|
||||
}
|
||||
|
||||
// CheckTimeouts ensures that timeout values are meaningful
|
||||
func CheckTimeouts(timeouts *rpc.HTTPTimeouts) {
|
||||
if timeouts.ReadTimeout < time.Second {
|
||||
log.Warn("Sanitizing invalid HTTP read timeout", "provided", timeouts.ReadTimeout, "updated", rpc.DefaultHTTPTimeouts.ReadTimeout)
|
||||
timeouts.ReadTimeout = rpc.DefaultHTTPTimeouts.ReadTimeout
|
||||
}
|
||||
if timeouts.ReadHeaderTimeout < time.Second {
|
||||
log.Warn("Sanitizing invalid HTTP read header timeout", "provided", timeouts.ReadHeaderTimeout, "updated", rpc.DefaultHTTPTimeouts.ReadHeaderTimeout)
|
||||
timeouts.ReadHeaderTimeout = rpc.DefaultHTTPTimeouts.ReadHeaderTimeout
|
||||
}
|
||||
if timeouts.WriteTimeout < time.Second {
|
||||
log.Warn("Sanitizing invalid HTTP write timeout", "provided", timeouts.WriteTimeout, "updated", rpc.DefaultHTTPTimeouts.WriteTimeout)
|
||||
timeouts.WriteTimeout = rpc.DefaultHTTPTimeouts.WriteTimeout
|
||||
}
|
||||
if timeouts.IdleTimeout < time.Second {
|
||||
log.Warn("Sanitizing invalid HTTP idle timeout", "provided", timeouts.IdleTimeout, "updated", rpc.DefaultHTTPTimeouts.IdleTimeout)
|
||||
timeouts.IdleTimeout = rpc.DefaultHTTPTimeouts.IdleTimeout
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatadirUsed = errors.New("datadir already used by another process")
|
||||
ErrNodeStopped = errors.New("node not started")
|
||||
ErrNodeRunning = errors.New("node already running")
|
||||
ErrServiceUnknown = errors.New("unknown service")
|
||||
|
||||
datadirInUseErrnos = map[uint]bool{11: true, 32: true, 35: true}
|
||||
)
|
||||
|
||||
func convertFileLockError(err error) error {
|
||||
if errno, ok := err.(syscall.Errno); ok && datadirInUseErrnos[uint(errno)] {
|
||||
return ErrDatadirUsed
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// StopError is returned if a Node fails to stop either any of its registered
|
||||
// services or itself.
|
||||
type StopError struct {
|
||||
Server error
|
||||
Services map[reflect.Type]error
|
||||
}
|
||||
|
||||
// Error generates a textual representation of the stop error.
|
||||
func (e *StopError) Error() string {
|
||||
return fmt.Sprintf("server: %v, services: %v", e.Server, e.Services)
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
// Copyright 2022 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
// NewJWTAuth creates an rpc client authentication provider that uses JWT. The
|
||||
// secret MUST be 32 bytes (256 bits) as defined by the Engine-API authentication spec.
|
||||
//
|
||||
// See https://github.com/ethereum/execution-apis/blob/main/src/engine/authentication.md
|
||||
// for more details about this authentication scheme.
|
||||
func NewJWTAuth(jwtsecret [32]byte) rpc.HTTPAuth {
|
||||
return func(h http.Header) error {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"iat": &jwt.NumericDate{Time: time.Now()},
|
||||
})
|
||||
s, err := token.SignedString(jwtsecret[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create JWT token: %w", err)
|
||||
}
|
||||
h.Set("Authorization", "Bearer "+s)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
// Copyright 2022 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
const jwtExpiryTimeout = 60 * time.Second
|
||||
|
||||
type jwtHandler struct {
|
||||
keyFunc func(token *jwt.Token) (interface{}, error)
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
// newJWTHandler creates a http.Handler with jwt authentication support.
|
||||
func newJWTHandler(secret []byte, next http.Handler) http.Handler {
|
||||
return &jwtHandler{
|
||||
keyFunc: func(token *jwt.Token) (interface{}, error) {
|
||||
return secret, nil
|
||||
},
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler
|
||||
func (handler *jwtHandler) ServeHTTP(out http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
strToken string
|
||||
claims jwt.RegisteredClaims
|
||||
)
|
||||
if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") {
|
||||
strToken = strings.TrimPrefix(auth, "Bearer ")
|
||||
}
|
||||
if len(strToken) == 0 {
|
||||
http.Error(out, "missing token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// We explicitly set only HS256 allowed, and also disables the
|
||||
// claim-check: the RegisteredClaims internally requires 'iat' to
|
||||
// be no later than 'now', but we allow for a bit of drift.
|
||||
token, err := jwt.ParseWithClaims(strToken, &claims, handler.keyFunc,
|
||||
jwt.WithValidMethods([]string{"HS256"}),
|
||||
jwt.WithoutClaimsValidation())
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
http.Error(out, err.Error(), http.StatusUnauthorized)
|
||||
case !token.Valid:
|
||||
http.Error(out, "invalid token", http.StatusUnauthorized)
|
||||
case !claims.VerifyExpiresAt(time.Now(), false): // optional
|
||||
http.Error(out, "token is expired", http.StatusUnauthorized)
|
||||
case claims.IssuedAt == nil:
|
||||
http.Error(out, "missing issued-at", http.StatusUnauthorized)
|
||||
case time.Since(claims.IssuedAt.Time) > jwtExpiryTimeout:
|
||||
http.Error(out, "stale token", http.StatusUnauthorized)
|
||||
case time.Until(claims.IssuedAt.Time) > jwtExpiryTimeout:
|
||||
http.Error(out, "future token", http.StatusUnauthorized)
|
||||
default:
|
||||
handler.next.ServeHTTP(out, r)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <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
|
||||
}
|
||||
822
node/node.go
822
node/node.go
|
|
@ -1,822 +0,0 @@
|
|||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
crand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/gofrs/flock"
|
||||
)
|
||||
|
||||
// Node is a container on which services can be registered.
|
||||
type Node struct {
|
||||
eventmux *event.TypeMux
|
||||
config *Config
|
||||
accman *accounts.Manager
|
||||
log log.Logger
|
||||
keyDir string // key store directory
|
||||
keyDirTemp bool // If true, key directory will be removed by Stop
|
||||
dirLock *flock.Flock // prevents concurrent use of instance directory
|
||||
stop chan struct{} // Channel to wait for termination notifications
|
||||
server *p2p.Server // Currently running P2P networking layer
|
||||
startStopLock sync.Mutex // Start/Stop are protected by an additional lock
|
||||
state int // Tracks state of node lifecycle
|
||||
|
||||
lock sync.Mutex
|
||||
lifecycles []Lifecycle // All registered backends, services, and auxiliary services that have a lifecycle
|
||||
rpcAPIs []rpc.API // List of APIs currently provided by the node
|
||||
http *httpServer //
|
||||
ws *httpServer //
|
||||
httpAuth *httpServer //
|
||||
wsAuth *httpServer //
|
||||
ipc *ipcServer // Stores information about the ipc http server
|
||||
inprocHandler *rpc.Server // In-process RPC request handler to process the API requests
|
||||
|
||||
databases map[*closeTrackingDB]struct{} // All open databases
|
||||
}
|
||||
|
||||
const (
|
||||
initializingState = iota
|
||||
runningState
|
||||
closedState
|
||||
)
|
||||
|
||||
// New creates a new P2P node, ready for protocol registration.
|
||||
func New(conf *Config) (*Node, error) {
|
||||
// Copy config and resolve the datadir so future changes to the current
|
||||
// working directory don't affect the node.
|
||||
confCopy := *conf
|
||||
conf = &confCopy
|
||||
if conf.DataDir != "" {
|
||||
absdatadir, err := filepath.Abs(conf.DataDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.DataDir = absdatadir
|
||||
}
|
||||
if conf.Logger == nil {
|
||||
conf.Logger = log.New()
|
||||
}
|
||||
|
||||
// Ensure that the instance name doesn't cause weird conflicts with
|
||||
// other files in the data directory.
|
||||
if strings.ContainsAny(conf.Name, `/\`) {
|
||||
return nil, errors.New(`Config.Name must not contain '/' or '\'`)
|
||||
}
|
||||
if conf.Name == datadirDefaultKeyStore {
|
||||
return nil, errors.New(`Config.Name cannot be "` + datadirDefaultKeyStore + `"`)
|
||||
}
|
||||
if strings.HasSuffix(conf.Name, ".ipc") {
|
||||
return nil, errors.New(`Config.Name cannot end in ".ipc"`)
|
||||
}
|
||||
server := rpc.NewServer()
|
||||
server.SetBatchLimits(conf.BatchRequestLimit, conf.BatchResponseMaxSize)
|
||||
node := &Node{
|
||||
config: conf,
|
||||
inprocHandler: server,
|
||||
eventmux: new(event.TypeMux),
|
||||
log: conf.Logger,
|
||||
stop: make(chan struct{}),
|
||||
server: &p2p.Server{Config: conf.P2P},
|
||||
databases: make(map[*closeTrackingDB]struct{}),
|
||||
}
|
||||
|
||||
// Register built-in APIs.
|
||||
node.rpcAPIs = append(node.rpcAPIs, node.apis()...)
|
||||
|
||||
// Acquire the instance directory lock.
|
||||
if err := node.openDataDir(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyDir, isEphem, err := conf.GetKeyStoreDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.keyDir = keyDir
|
||||
node.keyDirTemp = isEphem
|
||||
// Creates an empty AccountManager with no backends. Callers (e.g. cmd/geth)
|
||||
// are required to add the backends later on.
|
||||
node.accman = accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: conf.InsecureUnlockAllowed})
|
||||
|
||||
// Initialize the p2p server. This creates the node key and discovery databases.
|
||||
node.server.Config.PrivateKey = node.config.NodeKey()
|
||||
node.server.Config.Name = node.config.NodeName()
|
||||
node.server.Config.Logger = node.log
|
||||
node.config.checkLegacyFiles()
|
||||
if node.server.Config.NodeDatabase == "" {
|
||||
node.server.Config.NodeDatabase = node.config.NodeDB()
|
||||
}
|
||||
|
||||
// Check HTTP/WS prefixes are valid.
|
||||
if err := validatePrefix("HTTP", conf.HTTPPathPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validatePrefix("WebSocket", conf.WSPathPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Configure RPC servers.
|
||||
node.http = newHTTPServer(node.log, conf.HTTPTimeouts)
|
||||
node.httpAuth = newHTTPServer(node.log, conf.HTTPTimeouts)
|
||||
node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
|
||||
node.wsAuth = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
|
||||
node.ipc = newIPCServer(node.log, conf.IPCEndpoint())
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// Start starts all registered lifecycles, RPC services and p2p networking.
|
||||
// Node can only be started once.
|
||||
func (n *Node) Start() error {
|
||||
n.startStopLock.Lock()
|
||||
defer n.startStopLock.Unlock()
|
||||
|
||||
n.lock.Lock()
|
||||
switch n.state {
|
||||
case runningState:
|
||||
n.lock.Unlock()
|
||||
return ErrNodeRunning
|
||||
case closedState:
|
||||
n.lock.Unlock()
|
||||
return ErrNodeStopped
|
||||
}
|
||||
n.state = runningState
|
||||
// open networking and RPC endpoints
|
||||
err := n.openEndpoints()
|
||||
lifecycles := make([]Lifecycle, len(n.lifecycles))
|
||||
copy(lifecycles, n.lifecycles)
|
||||
n.lock.Unlock()
|
||||
|
||||
// Check if endpoint startup failed.
|
||||
if err != nil {
|
||||
n.doClose(nil)
|
||||
return err
|
||||
}
|
||||
// Start all registered lifecycles.
|
||||
var started []Lifecycle
|
||||
for _, lifecycle := range lifecycles {
|
||||
if err = lifecycle.Start(); err != nil {
|
||||
break
|
||||
}
|
||||
started = append(started, lifecycle)
|
||||
}
|
||||
// Check if any lifecycle failed to start.
|
||||
if err != nil {
|
||||
n.stopServices(started)
|
||||
n.doClose(nil)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Close stops the Node and releases resources acquired in
|
||||
// Node constructor New.
|
||||
func (n *Node) Close() error {
|
||||
n.startStopLock.Lock()
|
||||
defer n.startStopLock.Unlock()
|
||||
|
||||
n.lock.Lock()
|
||||
state := n.state
|
||||
n.lock.Unlock()
|
||||
switch state {
|
||||
case initializingState:
|
||||
// The node was never started.
|
||||
return n.doClose(nil)
|
||||
case runningState:
|
||||
// The node was started, release resources acquired by Start().
|
||||
var errs []error
|
||||
if err := n.stopServices(n.lifecycles); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return n.doClose(errs)
|
||||
case closedState:
|
||||
return ErrNodeStopped
|
||||
default:
|
||||
panic(fmt.Sprintf("node is in unknown state %d", state))
|
||||
}
|
||||
}
|
||||
|
||||
// doClose releases resources acquired by New(), collecting errors.
|
||||
func (n *Node) doClose(errs []error) error {
|
||||
// Close databases. This needs the lock because it needs to
|
||||
// synchronize with OpenDatabase*.
|
||||
n.lock.Lock()
|
||||
n.state = closedState
|
||||
errs = append(errs, n.closeDatabases()...)
|
||||
n.lock.Unlock()
|
||||
|
||||
if err := n.accman.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if n.keyDirTemp {
|
||||
if err := os.RemoveAll(n.keyDir); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Release instance directory lock.
|
||||
n.closeDataDir()
|
||||
|
||||
// Unblock n.Wait.
|
||||
close(n.stop)
|
||||
|
||||
// Report any errors that might have occurred.
|
||||
switch len(errs) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return errs[0]
|
||||
default:
|
||||
return fmt.Errorf("%v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
// openEndpoints starts all network and RPC endpoints.
|
||||
func (n *Node) openEndpoints() error {
|
||||
// start networking endpoints
|
||||
n.log.Info("Starting peer-to-peer node", "instance", n.server.Name)
|
||||
if err := n.server.Start(); err != nil {
|
||||
return convertFileLockError(err)
|
||||
}
|
||||
// start RPC endpoints
|
||||
err := n.startRPC()
|
||||
if err != nil {
|
||||
n.stopRPC()
|
||||
n.server.Stop()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// containsLifecycle checks if 'lfs' contains 'l'.
|
||||
func containsLifecycle(lfs []Lifecycle, l Lifecycle) bool {
|
||||
for _, obj := range lfs {
|
||||
if obj == l {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// stopServices terminates running services, RPC and p2p networking.
|
||||
// It is the inverse of Start.
|
||||
func (n *Node) stopServices(running []Lifecycle) error {
|
||||
n.stopRPC()
|
||||
|
||||
// Stop running lifecycles in reverse order.
|
||||
failure := &StopError{Services: make(map[reflect.Type]error)}
|
||||
for i := len(running) - 1; i >= 0; i-- {
|
||||
if err := running[i].Stop(); err != nil {
|
||||
failure.Services[reflect.TypeOf(running[i])] = err
|
||||
}
|
||||
}
|
||||
|
||||
// Stop p2p networking.
|
||||
n.server.Stop()
|
||||
|
||||
if len(failure.Services) > 0 {
|
||||
return failure
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) openDataDir() error {
|
||||
if n.config.DataDir == "" {
|
||||
return nil // ephemeral
|
||||
}
|
||||
|
||||
instdir := filepath.Join(n.config.DataDir, n.config.name())
|
||||
if err := os.MkdirAll(instdir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
// Lock the instance directory to prevent concurrent use by another instance as well as
|
||||
// accidental use of the instance directory as a database.
|
||||
n.dirLock = flock.New(filepath.Join(instdir, "LOCK"))
|
||||
|
||||
if locked, err := n.dirLock.TryLock(); err != nil {
|
||||
return err
|
||||
} else if !locked {
|
||||
return ErrDatadirUsed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) closeDataDir() {
|
||||
// Release instance directory lock.
|
||||
if n.dirLock != nil && n.dirLock.Locked() {
|
||||
n.dirLock.Unlock()
|
||||
n.dirLock = nil
|
||||
}
|
||||
}
|
||||
|
||||
// obtainJWTSecret loads the jwt-secret, either from the provided config,
|
||||
// or from the default location. If neither of those are present, it generates
|
||||
// a new secret and stores to the default location.
|
||||
func (n *Node) obtainJWTSecret(cliParam string) ([]byte, error) {
|
||||
fileName := cliParam
|
||||
if len(fileName) == 0 {
|
||||
// no path provided, use default
|
||||
fileName = n.ResolvePath(datadirJWTKey)
|
||||
}
|
||||
// try reading from file
|
||||
if data, err := os.ReadFile(fileName); err == nil {
|
||||
jwtSecret := common.FromHex(strings.TrimSpace(string(data)))
|
||||
if len(jwtSecret) == 32 {
|
||||
log.Info("Loaded JWT secret file", "path", fileName, "crc32", fmt.Sprintf("%#x", crc32.ChecksumIEEE(jwtSecret)))
|
||||
return jwtSecret, nil
|
||||
}
|
||||
log.Error("Invalid JWT secret", "path", fileName, "length", len(jwtSecret))
|
||||
return nil, errors.New("invalid JWT secret")
|
||||
}
|
||||
// Need to generate one
|
||||
jwtSecret := make([]byte, 32)
|
||||
crand.Read(jwtSecret)
|
||||
// if we're in --dev mode, don't bother saving, just show it
|
||||
if fileName == "" {
|
||||
log.Info("Generated ephemeral JWT secret", "secret", hexutil.Encode(jwtSecret))
|
||||
return jwtSecret, nil
|
||||
}
|
||||
if err := os.WriteFile(fileName, []byte(hexutil.Encode(jwtSecret)), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Info("Generated JWT secret", "path", fileName)
|
||||
return jwtSecret, nil
|
||||
}
|
||||
|
||||
// startRPC is a helper method to configure all the various RPC endpoints during node
|
||||
// startup. It's not meant to be called at any time afterwards as it makes certain
|
||||
// assumptions about the state of the node.
|
||||
func (n *Node) startRPC() error {
|
||||
// Filter out personal api
|
||||
var apis []rpc.API
|
||||
for _, api := range n.rpcAPIs {
|
||||
if api.Namespace == "personal" {
|
||||
if n.config.EnablePersonal {
|
||||
log.Warn("Deprecated personal namespace activated")
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
apis = append(apis, api)
|
||||
}
|
||||
if err := n.startInProc(apis); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Configure IPC.
|
||||
if n.ipc.endpoint != "" {
|
||||
if err := n.ipc.start(apis); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var (
|
||||
servers []*httpServer
|
||||
openAPIs, allAPIs = n.getAPIs()
|
||||
)
|
||||
|
||||
rpcConfig := rpcEndpointConfig{
|
||||
batchItemLimit: n.config.BatchRequestLimit,
|
||||
batchResponseSizeLimit: n.config.BatchResponseMaxSize,
|
||||
}
|
||||
|
||||
initHttp := func(server *httpServer, port int) error {
|
||||
if err := server.setListenAddr(n.config.HTTPHost, port); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.enableRPC(openAPIs, httpConfig{
|
||||
CorsAllowedOrigins: n.config.HTTPCors,
|
||||
Vhosts: n.config.HTTPVirtualHosts,
|
||||
Modules: n.config.HTTPModules,
|
||||
prefix: n.config.HTTPPathPrefix,
|
||||
rpcEndpointConfig: rpcConfig,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
servers = append(servers, server)
|
||||
return nil
|
||||
}
|
||||
|
||||
initWS := func(port int) error {
|
||||
server := n.wsServerForPort(port, false)
|
||||
if err := server.setListenAddr(n.config.WSHost, port); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.enableWS(openAPIs, wsConfig{
|
||||
Modules: n.config.WSModules,
|
||||
Origins: n.config.WSOrigins,
|
||||
prefix: n.config.WSPathPrefix,
|
||||
rpcEndpointConfig: rpcConfig,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
servers = append(servers, server)
|
||||
return nil
|
||||
}
|
||||
|
||||
initAuth := func(port int, secret []byte) error {
|
||||
// Enable auth via HTTP
|
||||
server := n.httpAuth
|
||||
if err := server.setListenAddr(n.config.AuthAddr, port); err != nil {
|
||||
return err
|
||||
}
|
||||
sharedConfig := rpcEndpointConfig{
|
||||
jwtSecret: secret,
|
||||
batchItemLimit: engineAPIBatchItemLimit,
|
||||
batchResponseSizeLimit: engineAPIBatchResponseSizeLimit,
|
||||
}
|
||||
if err := server.enableRPC(allAPIs, httpConfig{
|
||||
CorsAllowedOrigins: DefaultAuthCors,
|
||||
Vhosts: n.config.AuthVirtualHosts,
|
||||
Modules: DefaultAuthModules,
|
||||
prefix: DefaultAuthPrefix,
|
||||
rpcEndpointConfig: sharedConfig,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
servers = append(servers, server)
|
||||
|
||||
// Enable auth via WS
|
||||
server = n.wsServerForPort(port, true)
|
||||
if err := server.setListenAddr(n.config.AuthAddr, port); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.enableWS(allAPIs, wsConfig{
|
||||
Modules: DefaultAuthModules,
|
||||
Origins: DefaultAuthOrigins,
|
||||
prefix: DefaultAuthPrefix,
|
||||
rpcEndpointConfig: sharedConfig,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
servers = append(servers, server)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set up HTTP.
|
||||
if n.config.HTTPHost != "" {
|
||||
// Configure legacy unauthenticated HTTP.
|
||||
if err := initHttp(n.http, n.config.HTTPPort); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Configure WebSocket.
|
||||
if n.config.WSHost != "" {
|
||||
// legacy unauthenticated
|
||||
if err := initWS(n.config.WSPort); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Configure authenticated API
|
||||
if len(openAPIs) != len(allAPIs) {
|
||||
jwtSecret, err := n.obtainJWTSecret(n.config.JWTSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initAuth(n.config.AuthPort, jwtSecret); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Start the servers
|
||||
for _, server := range servers {
|
||||
if err := server.start(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) wsServerForPort(port int, authenticated bool) *httpServer {
|
||||
httpServer, wsServer := n.http, n.ws
|
||||
if authenticated {
|
||||
httpServer, wsServer = n.httpAuth, n.wsAuth
|
||||
}
|
||||
if n.config.HTTPHost == "" || httpServer.port == port {
|
||||
return httpServer
|
||||
}
|
||||
return wsServer
|
||||
}
|
||||
|
||||
func (n *Node) stopRPC() {
|
||||
n.http.stop()
|
||||
n.ws.stop()
|
||||
n.httpAuth.stop()
|
||||
n.wsAuth.stop()
|
||||
n.ipc.stop()
|
||||
n.stopInProc()
|
||||
}
|
||||
|
||||
// startInProc registers all RPC APIs on the inproc server.
|
||||
func (n *Node) startInProc(apis []rpc.API) error {
|
||||
for _, api := range apis {
|
||||
if err := n.inprocHandler.RegisterName(api.Namespace, api.Service); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stopInProc terminates the in-process RPC endpoint.
|
||||
func (n *Node) stopInProc() {
|
||||
n.inprocHandler.Stop()
|
||||
}
|
||||
|
||||
// Wait blocks until the node is closed.
|
||||
func (n *Node) Wait() {
|
||||
<-n.stop
|
||||
}
|
||||
|
||||
// RegisterLifecycle registers the given Lifecycle on the node.
|
||||
func (n *Node) RegisterLifecycle(lifecycle Lifecycle) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
if n.state != initializingState {
|
||||
panic("can't register lifecycle on running/stopped node")
|
||||
}
|
||||
if containsLifecycle(n.lifecycles, lifecycle) {
|
||||
panic(fmt.Sprintf("attempt to register lifecycle %T more than once", lifecycle))
|
||||
}
|
||||
n.lifecycles = append(n.lifecycles, lifecycle)
|
||||
}
|
||||
|
||||
// RegisterProtocols adds backend's protocols to the node's p2p server.
|
||||
func (n *Node) RegisterProtocols(protocols []p2p.Protocol) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
if n.state != initializingState {
|
||||
panic("can't register protocols on running/stopped node")
|
||||
}
|
||||
n.server.Protocols = append(n.server.Protocols, protocols...)
|
||||
}
|
||||
|
||||
// RegisterAPIs registers the APIs a service provides on the node.
|
||||
func (n *Node) RegisterAPIs(apis []rpc.API) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
if n.state != initializingState {
|
||||
panic("can't register APIs on running/stopped node")
|
||||
}
|
||||
n.rpcAPIs = append(n.rpcAPIs, apis...)
|
||||
}
|
||||
|
||||
// getAPIs return two sets of APIs, both the ones that do not require
|
||||
// authentication, and the complete set
|
||||
func (n *Node) getAPIs() (unauthenticated, all []rpc.API) {
|
||||
for _, api := range n.rpcAPIs {
|
||||
if !api.Authenticated {
|
||||
unauthenticated = append(unauthenticated, api)
|
||||
}
|
||||
}
|
||||
return unauthenticated, n.rpcAPIs
|
||||
}
|
||||
|
||||
// RegisterHandler mounts a handler on the given path on the canonical HTTP server.
|
||||
//
|
||||
// The name of the handler is shown in a log message when the HTTP server starts
|
||||
// and should be a descriptive term for the service provided by the handler.
|
||||
func (n *Node) RegisterHandler(name, path string, handler http.Handler) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
if n.state != initializingState {
|
||||
panic("can't register HTTP handler on running/stopped node")
|
||||
}
|
||||
|
||||
n.http.mux.Handle(path, handler)
|
||||
n.http.handlerNames[path] = name
|
||||
}
|
||||
|
||||
// Attach creates an RPC client attached to an in-process API handler.
|
||||
func (n *Node) Attach() *rpc.Client {
|
||||
return rpc.DialInProc(n.inprocHandler)
|
||||
}
|
||||
|
||||
// RPCHandler returns the in-process RPC request handler.
|
||||
func (n *Node) RPCHandler() (*rpc.Server, error) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
if n.state == closedState {
|
||||
return nil, ErrNodeStopped
|
||||
}
|
||||
return n.inprocHandler, nil
|
||||
}
|
||||
|
||||
// Config returns the configuration of node.
|
||||
func (n *Node) Config() *Config {
|
||||
return n.config
|
||||
}
|
||||
|
||||
// Server retrieves the currently running P2P network layer. This method is meant
|
||||
// only to inspect fields of the currently running server. Callers should not
|
||||
// start or stop the returned server.
|
||||
func (n *Node) Server() *p2p.Server {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
return n.server
|
||||
}
|
||||
|
||||
// DataDir retrieves the current datadir used by the protocol stack.
|
||||
// Deprecated: No files should be stored in this directory, use InstanceDir instead.
|
||||
func (n *Node) DataDir() string {
|
||||
return n.config.DataDir
|
||||
}
|
||||
|
||||
// InstanceDir retrieves the instance directory used by the protocol stack.
|
||||
func (n *Node) InstanceDir() string {
|
||||
return n.config.instanceDir()
|
||||
}
|
||||
|
||||
// KeyStoreDir retrieves the key directory
|
||||
func (n *Node) KeyStoreDir() string {
|
||||
return n.keyDir
|
||||
}
|
||||
|
||||
// AccountManager retrieves the account manager used by the protocol stack.
|
||||
func (n *Node) AccountManager() *accounts.Manager {
|
||||
return n.accman
|
||||
}
|
||||
|
||||
// IPCEndpoint retrieves the current IPC endpoint used by the protocol stack.
|
||||
func (n *Node) IPCEndpoint() string {
|
||||
return n.ipc.endpoint
|
||||
}
|
||||
|
||||
// HTTPEndpoint returns the URL of the HTTP server. Note that this URL does not
|
||||
// contain the JSON-RPC path prefix set by HTTPPathPrefix.
|
||||
func (n *Node) HTTPEndpoint() string {
|
||||
return "http://" + n.http.listenAddr()
|
||||
}
|
||||
|
||||
// WSEndpoint returns the current JSON-RPC over WebSocket endpoint.
|
||||
func (n *Node) WSEndpoint() string {
|
||||
if n.http.wsAllowed() {
|
||||
return "ws://" + n.http.listenAddr() + n.http.wsConfig.prefix
|
||||
}
|
||||
return "ws://" + n.ws.listenAddr() + n.ws.wsConfig.prefix
|
||||
}
|
||||
|
||||
// HTTPAuthEndpoint returns the URL of the authenticated HTTP server.
|
||||
func (n *Node) HTTPAuthEndpoint() string {
|
||||
return "http://" + n.httpAuth.listenAddr()
|
||||
}
|
||||
|
||||
// WSAuthEndpoint returns the current authenticated JSON-RPC over WebSocket endpoint.
|
||||
func (n *Node) WSAuthEndpoint() string {
|
||||
if n.httpAuth.wsAllowed() {
|
||||
return "ws://" + n.httpAuth.listenAddr() + n.httpAuth.wsConfig.prefix
|
||||
}
|
||||
return "ws://" + n.wsAuth.listenAddr() + n.wsAuth.wsConfig.prefix
|
||||
}
|
||||
|
||||
// EventMux retrieves the event multiplexer used by all the network services in
|
||||
// the current protocol stack.
|
||||
func (n *Node) EventMux() *event.TypeMux {
|
||||
return n.eventmux
|
||||
}
|
||||
|
||||
// OpenDatabase opens an existing database with the given name (or creates one if no
|
||||
// previous can be found) from within the node's instance directory. If the node is
|
||||
// ephemeral, a memory database is returned.
|
||||
func (n *Node) OpenDatabase(name string, cache, handles int, namespace string, readonly bool) (ethdb.Database, error) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
if n.state == closedState {
|
||||
return nil, ErrNodeStopped
|
||||
}
|
||||
|
||||
var db ethdb.Database
|
||||
var err error
|
||||
if n.config.DataDir == "" {
|
||||
db = rawdb.NewMemoryDatabase()
|
||||
} else {
|
||||
db, err = rawdb.Open(rawdb.OpenOptions{
|
||||
Type: n.config.DBEngine,
|
||||
Directory: n.ResolvePath(name),
|
||||
Namespace: namespace,
|
||||
Cache: cache,
|
||||
Handles: handles,
|
||||
ReadOnly: readonly,
|
||||
})
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
db = n.wrapDatabase(db)
|
||||
}
|
||||
return db, err
|
||||
}
|
||||
|
||||
// OpenDatabaseWithFreezer opens an existing database with the given name (or
|
||||
// creates one if no previous can be found) from within the node's data directory,
|
||||
// also attaching a chain freezer to it that moves ancient chain data from the
|
||||
// database to immutable append-only files. If the node is an ephemeral one, a
|
||||
// memory database is returned.
|
||||
func (n *Node) OpenDatabaseWithFreezer(name string, cache, handles int, ancient string, namespace string, readonly bool) (ethdb.Database, error) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
if n.state == closedState {
|
||||
return nil, ErrNodeStopped
|
||||
}
|
||||
var db ethdb.Database
|
||||
var err error
|
||||
if n.config.DataDir == "" {
|
||||
db = rawdb.NewMemoryDatabase()
|
||||
} else {
|
||||
db, err = rawdb.Open(rawdb.OpenOptions{
|
||||
Type: n.config.DBEngine,
|
||||
Directory: n.ResolvePath(name),
|
||||
AncientsDirectory: n.ResolveAncient(name, ancient),
|
||||
Namespace: namespace,
|
||||
Cache: cache,
|
||||
Handles: handles,
|
||||
ReadOnly: readonly,
|
||||
})
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
db = n.wrapDatabase(db)
|
||||
}
|
||||
return db, err
|
||||
}
|
||||
|
||||
// ResolvePath returns the absolute path of a resource in the instance directory.
|
||||
func (n *Node) ResolvePath(x string) string {
|
||||
return n.config.ResolvePath(x)
|
||||
}
|
||||
|
||||
// ResolveAncient returns the absolute path of the root ancient directory.
|
||||
func (n *Node) ResolveAncient(name string, ancient string) string {
|
||||
switch {
|
||||
case ancient == "":
|
||||
ancient = filepath.Join(n.ResolvePath(name), "ancient")
|
||||
case !filepath.IsAbs(ancient):
|
||||
ancient = n.ResolvePath(ancient)
|
||||
}
|
||||
return ancient
|
||||
}
|
||||
|
||||
// closeTrackingDB wraps the Close method of a database. When the database is closed by the
|
||||
// service, the wrapper removes it from the node's database map. This ensures that Node
|
||||
// won't auto-close the database if it is closed by the service that opened it.
|
||||
type closeTrackingDB struct {
|
||||
ethdb.Database
|
||||
n *Node
|
||||
}
|
||||
|
||||
func (db *closeTrackingDB) Close() error {
|
||||
db.n.lock.Lock()
|
||||
delete(db.n.databases, db)
|
||||
db.n.lock.Unlock()
|
||||
return db.Database.Close()
|
||||
}
|
||||
|
||||
// wrapDatabase ensures the database will be auto-closed when Node is closed.
|
||||
func (n *Node) wrapDatabase(db ethdb.Database) ethdb.Database {
|
||||
wrapper := &closeTrackingDB{db, n}
|
||||
n.databases[wrapper] = struct{}{}
|
||||
return wrapper
|
||||
}
|
||||
|
||||
// closeDatabases closes all open databases.
|
||||
func (n *Node) closeDatabases() (errors []error) {
|
||||
for db := range n.databases {
|
||||
delete(n.databases, db)
|
||||
if err := db.Database.Close(); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
// Copyright 2022 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
type helloRPC string
|
||||
|
||||
func (ta helloRPC) HelloWorld() (string, error) {
|
||||
return string(ta), nil
|
||||
}
|
||||
|
||||
type authTest struct {
|
||||
name string
|
||||
endpoint string
|
||||
prov rpc.HTTPAuth
|
||||
expectDialFail bool
|
||||
expectCall1Fail bool
|
||||
expectCall2Fail bool
|
||||
}
|
||||
|
||||
func (at *authTest) Run(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cl, err := rpc.DialOptions(ctx, at.endpoint, rpc.WithHTTPAuth(at.prov))
|
||||
if at.expectDialFail {
|
||||
if err == nil {
|
||||
t.Fatal("expected initial dial to fail")
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial rpc endpoint: %v", err)
|
||||
}
|
||||
|
||||
var x string
|
||||
err = cl.CallContext(ctx, &x, "engine_helloWorld")
|
||||
if at.expectCall1Fail {
|
||||
if err == nil {
|
||||
t.Fatal("expected call 1 to fail")
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("failed to call rpc endpoint: %v", err)
|
||||
}
|
||||
if x != "hello engine" {
|
||||
t.Fatalf("method was silent but did not return expected value: %q", x)
|
||||
}
|
||||
|
||||
err = cl.CallContext(ctx, &x, "eth_helloWorld")
|
||||
if at.expectCall2Fail {
|
||||
if err == nil {
|
||||
t.Fatal("expected call 2 to fail")
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("failed to call rpc endpoint: %v", err)
|
||||
}
|
||||
if x != "hello eth" {
|
||||
t.Fatalf("method was silent but did not return expected value: %q", x)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthEndpoints(t *testing.T) {
|
||||
var secret [32]byte
|
||||
if _, err := crand.Read(secret[:]); err != nil {
|
||||
t.Fatalf("failed to create jwt secret: %v", err)
|
||||
}
|
||||
// Geth must read it from a file, and does not support in-memory JWT secrets, so we create a temporary file.
|
||||
jwtPath := path.Join(t.TempDir(), "jwt_secret")
|
||||
if err := os.WriteFile(jwtPath, []byte(hexutil.Encode(secret[:])), 0600); err != nil {
|
||||
t.Fatalf("failed to prepare jwt secret file: %v", err)
|
||||
}
|
||||
// We get ports assigned by the node automatically
|
||||
conf := &Config{
|
||||
HTTPHost: "127.0.0.1",
|
||||
HTTPPort: 0,
|
||||
WSHost: "127.0.0.1",
|
||||
WSPort: 0,
|
||||
AuthAddr: "127.0.0.1",
|
||||
AuthPort: 0,
|
||||
JWTSecret: jwtPath,
|
||||
|
||||
WSModules: []string{"eth", "engine"},
|
||||
HTTPModules: []string{"eth", "engine"},
|
||||
}
|
||||
node, err := New(conf)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a new node: %v", err)
|
||||
}
|
||||
// register dummy apis so we can test the modules are available and reachable with authentication
|
||||
node.RegisterAPIs([]rpc.API{
|
||||
{
|
||||
Namespace: "engine",
|
||||
Version: "1.0",
|
||||
Service: helloRPC("hello engine"),
|
||||
Public: true,
|
||||
Authenticated: true,
|
||||
},
|
||||
{
|
||||
Namespace: "eth",
|
||||
Version: "1.0",
|
||||
Service: helloRPC("hello eth"),
|
||||
Public: true,
|
||||
Authenticated: true,
|
||||
},
|
||||
})
|
||||
if err := node.Start(); err != nil {
|
||||
t.Fatalf("failed to start test node: %v", err)
|
||||
}
|
||||
defer node.Close()
|
||||
|
||||
// sanity check we are running different endpoints
|
||||
if a, b := node.WSEndpoint(), node.WSAuthEndpoint(); a == b {
|
||||
t.Fatalf("expected ws and auth-ws endpoints to be different, got: %q and %q", a, b)
|
||||
}
|
||||
if a, b := node.HTTPEndpoint(), node.HTTPAuthEndpoint(); a == b {
|
||||
t.Fatalf("expected http and auth-http endpoints to be different, got: %q and %q", a, b)
|
||||
}
|
||||
|
||||
goodAuth := NewJWTAuth(secret)
|
||||
var otherSecret [32]byte
|
||||
if _, err := crand.Read(otherSecret[:]); err != nil {
|
||||
t.Fatalf("failed to create jwt secret: %v", err)
|
||||
}
|
||||
badAuth := NewJWTAuth(otherSecret)
|
||||
|
||||
notTooLong := time.Second * 57
|
||||
tooLong := time.Second * 60
|
||||
requestDelay := time.Second
|
||||
|
||||
testCases := []authTest{
|
||||
// Auth works
|
||||
{name: "ws good", endpoint: node.WSAuthEndpoint(), prov: goodAuth, expectCall1Fail: false},
|
||||
{name: "http good", endpoint: node.HTTPAuthEndpoint(), prov: goodAuth, expectCall1Fail: false},
|
||||
|
||||
// Try a bad auth
|
||||
{name: "ws bad", endpoint: node.WSAuthEndpoint(), prov: badAuth, expectDialFail: true}, // ws auth is immediate
|
||||
{name: "http bad", endpoint: node.HTTPAuthEndpoint(), prov: badAuth, expectCall1Fail: true}, // http auth is on first call
|
||||
|
||||
// A common mistake with JWT is to allow the "none" algorithm, which is a valid JWT but not secure.
|
||||
{name: "ws none", endpoint: node.WSAuthEndpoint(), prov: noneAuth(secret), expectDialFail: true},
|
||||
{name: "http none", endpoint: node.HTTPAuthEndpoint(), prov: noneAuth(secret), expectCall1Fail: true},
|
||||
|
||||
// claims of 5 seconds or more, older or newer, are not allowed
|
||||
{name: "ws too old", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, -tooLong), expectDialFail: true},
|
||||
{name: "http too old", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, -tooLong), expectCall1Fail: true},
|
||||
// note: for it to be too long we need to add a delay, so that once we receive the request, the difference has not dipped below the "tooLong"
|
||||
{name: "ws too new", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, tooLong+requestDelay), expectDialFail: true},
|
||||
{name: "http too new", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, tooLong+requestDelay), expectCall1Fail: true},
|
||||
|
||||
// Try offset the time, but stay just within bounds
|
||||
{name: "ws old", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, -notTooLong)},
|
||||
{name: "http old", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, -notTooLong)},
|
||||
{name: "ws new", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, notTooLong)},
|
||||
{name: "http new", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, notTooLong)},
|
||||
|
||||
// ws only authenticates on initial dial, then continues communication
|
||||
{name: "ws single auth", endpoint: node.WSAuthEndpoint(), prov: changingAuth(goodAuth, badAuth)},
|
||||
{name: "http call fail auth", endpoint: node.HTTPAuthEndpoint(), prov: changingAuth(goodAuth, badAuth), expectCall2Fail: true},
|
||||
{name: "http call fail time", endpoint: node.HTTPAuthEndpoint(), prov: changingAuth(goodAuth, offsetTimeAuth(secret, tooLong+requestDelay)), expectCall2Fail: true},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, testCase.Run)
|
||||
}
|
||||
}
|
||||
|
||||
func noneAuth(secret [32]byte) rpc.HTTPAuth {
|
||||
return func(header http.Header) error {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{
|
||||
"iat": &jwt.NumericDate{Time: time.Now()},
|
||||
})
|
||||
s, err := token.SignedString(secret[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create JWT token: %w", err)
|
||||
}
|
||||
header.Set("Authorization", "Bearer "+s)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func changingAuth(provs ...rpc.HTTPAuth) rpc.HTTPAuth {
|
||||
i := 0
|
||||
return func(header http.Header) error {
|
||||
i += 1
|
||||
if i > len(provs) {
|
||||
i = len(provs)
|
||||
}
|
||||
return provs[i-1](header)
|
||||
}
|
||||
}
|
||||
|
||||
func offsetTimeAuth(secret [32]byte, offset time.Duration) rpc.HTTPAuth {
|
||||
return func(header http.Header) error {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"iat": &jwt.NumericDate{Time: time.Now().Add(offset)},
|
||||
})
|
||||
s, err := token.SignedString(secret[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create JWT token: %w", err)
|
||||
}
|
||||
header.Set("Authorization", "Bearer "+s)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
)
|
||||
|
||||
// SampleLifecycle is a trivial network service that can be attached to a node for
|
||||
// life cycle management.
|
||||
//
|
||||
// The following methods are needed to implement a node.Lifecycle:
|
||||
// - Start() error - method invoked when the node is ready to start the service
|
||||
// - Stop() error - method invoked when the node terminates the service
|
||||
type SampleLifecycle struct{}
|
||||
|
||||
func (s *SampleLifecycle) Start() error { fmt.Println("Service starting..."); return nil }
|
||||
func (s *SampleLifecycle) Stop() error { fmt.Println("Service stopping..."); return nil }
|
||||
|
||||
func ExampleLifecycle() {
|
||||
// Create a network node to run protocols with the default values.
|
||||
stack, err := node.New(&node.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create network node: %v", err)
|
||||
}
|
||||
defer stack.Close()
|
||||
|
||||
// Create and register a simple network Lifecycle.
|
||||
service := new(SampleLifecycle)
|
||||
stack.RegisterLifecycle(service)
|
||||
|
||||
// Boot up the entire protocol stack, do a restart and terminate
|
||||
if err := stack.Start(); err != nil {
|
||||
log.Fatalf("Failed to start the protocol stack: %v", err)
|
||||
}
|
||||
if err := stack.Close(); err != nil {
|
||||
log.Fatalf("Failed to stop the protocol stack: %v", err)
|
||||
}
|
||||
// Output:
|
||||
// Service starting...
|
||||
// Service stopping...
|
||||
}
|
||||
|
|
@ -1,638 +0,0 @@
|
|||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
testNodeKey, _ = crypto.GenerateKey()
|
||||
)
|
||||
|
||||
func testNodeConfig() *Config {
|
||||
return &Config{
|
||||
Name: "test node",
|
||||
P2P: p2p.Config{PrivateKey: testNodeKey},
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that an empty protocol stack can be closed more than once.
|
||||
func TestNodeCloseMultipleTimes(t *testing.T) {
|
||||
stack, err := New(testNodeConfig())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create protocol stack: %v", err)
|
||||
}
|
||||
stack.Close()
|
||||
|
||||
// Ensure that a stopped node can be stopped again
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := stack.Close(); err != ErrNodeStopped {
|
||||
t.Fatalf("iter %d: stop failure mismatch: have %v, want %v", i, err, ErrNodeStopped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeStartMultipleTimes(t *testing.T) {
|
||||
stack, err := New(testNodeConfig())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create protocol stack: %v", err)
|
||||
}
|
||||
|
||||
// Ensure that a node can be successfully started, but only once
|
||||
if err := stack.Start(); err != nil {
|
||||
t.Fatalf("failed to start node: %v", err)
|
||||
}
|
||||
if err := stack.Start(); err != ErrNodeRunning {
|
||||
t.Fatalf("start failure mismatch: have %v, want %v ", err, ErrNodeRunning)
|
||||
}
|
||||
// Ensure that a node can be stopped, but only once
|
||||
if err := stack.Close(); err != nil {
|
||||
t.Fatalf("failed to stop node: %v", err)
|
||||
}
|
||||
if err := stack.Close(); err != ErrNodeStopped {
|
||||
t.Fatalf("stop failure mismatch: have %v, want %v ", err, ErrNodeStopped)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that if the data dir is already in use, an appropriate error is returned.
|
||||
func TestNodeUsedDataDir(t *testing.T) {
|
||||
// Create a temporary folder to use as the data directory
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create a new node based on the data directory
|
||||
original, err := New(&Config{DataDir: dir})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create original protocol stack: %v", err)
|
||||
}
|
||||
defer original.Close()
|
||||
if err := original.Start(); err != nil {
|
||||
t.Fatalf("failed to start original protocol stack: %v", err)
|
||||
}
|
||||
|
||||
// Create a second node based on the same data directory and ensure failure
|
||||
_, err = New(&Config{DataDir: dir})
|
||||
if err != ErrDatadirUsed {
|
||||
t.Fatalf("duplicate datadir failure mismatch: have %v, want %v", err, ErrDatadirUsed)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests whether a Lifecycle can be registered.
|
||||
func TestLifecycleRegistry_Successful(t *testing.T) {
|
||||
stack, err := New(testNodeConfig())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create protocol stack: %v", err)
|
||||
}
|
||||
defer stack.Close()
|
||||
|
||||
noop := NewNoop()
|
||||
stack.RegisterLifecycle(noop)
|
||||
|
||||
if !containsLifecycle(stack.lifecycles, noop) {
|
||||
t.Fatalf("lifecycle was not properly registered on the node, %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests whether a service's protocols can be registered properly on the node's p2p server.
|
||||
func TestRegisterProtocols(t *testing.T) {
|
||||
stack, err := New(testNodeConfig())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create protocol stack: %v", err)
|
||||
}
|
||||
defer stack.Close()
|
||||
|
||||
fs, err := NewFullService(stack)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create full service: %v", err)
|
||||
}
|
||||
|
||||
for _, protocol := range fs.Protocols() {
|
||||
if !containsProtocol(stack.server.Protocols, protocol) {
|
||||
t.Fatalf("protocol %v was not successfully registered", protocol)
|
||||
}
|
||||
}
|
||||
|
||||
for _, api := range fs.APIs() {
|
||||
if !containsAPI(stack.rpcAPIs, api) {
|
||||
t.Fatalf("api %v was not successfully registered", api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This test checks that open databases are closed with node.
|
||||
func TestNodeCloseClosesDB(t *testing.T) {
|
||||
stack, _ := New(testNodeConfig())
|
||||
defer stack.Close()
|
||||
|
||||
db, err := stack.OpenDatabase("mydb", 0, 0, "", false)
|
||||
if err != nil {
|
||||
t.Fatal("can't open DB:", err)
|
||||
}
|
||||
if err = db.Put([]byte{}, []byte{}); err != nil {
|
||||
t.Fatal("can't Put on open DB:", err)
|
||||
}
|
||||
|
||||
stack.Close()
|
||||
if err = db.Put([]byte{}, []byte{}); err == nil {
|
||||
t.Fatal("Put succeeded after node is closed")
|
||||
}
|
||||
}
|
||||
|
||||
// This test checks that OpenDatabase can be used from within a Lifecycle Start method.
|
||||
func TestNodeOpenDatabaseFromLifecycleStart(t *testing.T) {
|
||||
stack, _ := New(testNodeConfig())
|
||||
defer stack.Close()
|
||||
|
||||
var db ethdb.Database
|
||||
var err error
|
||||
stack.RegisterLifecycle(&InstrumentedService{
|
||||
startHook: func() {
|
||||
db, err = stack.OpenDatabase("mydb", 0, 0, "", false)
|
||||
if err != nil {
|
||||
t.Fatal("can't open DB:", err)
|
||||
}
|
||||
},
|
||||
stopHook: func() {
|
||||
db.Close()
|
||||
},
|
||||
})
|
||||
|
||||
stack.Start()
|
||||
stack.Close()
|
||||
}
|
||||
|
||||
// This test checks that OpenDatabase can be used from within a Lifecycle Stop method.
|
||||
func TestNodeOpenDatabaseFromLifecycleStop(t *testing.T) {
|
||||
stack, _ := New(testNodeConfig())
|
||||
defer stack.Close()
|
||||
|
||||
stack.RegisterLifecycle(&InstrumentedService{
|
||||
stopHook: func() {
|
||||
db, err := stack.OpenDatabase("mydb", 0, 0, "", false)
|
||||
if err != nil {
|
||||
t.Fatal("can't open DB:", err)
|
||||
}
|
||||
db.Close()
|
||||
},
|
||||
})
|
||||
|
||||
stack.Start()
|
||||
stack.Close()
|
||||
}
|
||||
|
||||
// Tests that registered Lifecycles get started and stopped correctly.
|
||||
func TestLifecycleLifeCycle(t *testing.T) {
|
||||
stack, _ := New(testNodeConfig())
|
||||
defer stack.Close()
|
||||
|
||||
started := make(map[string]bool)
|
||||
stopped := make(map[string]bool)
|
||||
|
||||
// Create a batch of instrumented services
|
||||
lifecycles := map[string]Lifecycle{
|
||||
"A": &InstrumentedService{
|
||||
startHook: func() { started["A"] = true },
|
||||
stopHook: func() { stopped["A"] = true },
|
||||
},
|
||||
"B": &InstrumentedService{
|
||||
startHook: func() { started["B"] = true },
|
||||
stopHook: func() { stopped["B"] = true },
|
||||
},
|
||||
"C": &InstrumentedService{
|
||||
startHook: func() { started["C"] = true },
|
||||
stopHook: func() { stopped["C"] = true },
|
||||
},
|
||||
}
|
||||
// register lifecycles on node
|
||||
for _, lifecycle := range lifecycles {
|
||||
stack.RegisterLifecycle(lifecycle)
|
||||
}
|
||||
// Start the node and check that all services are running
|
||||
if err := stack.Start(); err != nil {
|
||||
t.Fatalf("failed to start protocol stack: %v", err)
|
||||
}
|
||||
for id := range lifecycles {
|
||||
if !started[id] {
|
||||
t.Fatalf("service %s: freshly started service not running", id)
|
||||
}
|
||||
if stopped[id] {
|
||||
t.Fatalf("service %s: freshly started service already stopped", id)
|
||||
}
|
||||
}
|
||||
// Stop the node and check that all services have been stopped
|
||||
if err := stack.Close(); err != nil {
|
||||
t.Fatalf("failed to stop protocol stack: %v", err)
|
||||
}
|
||||
for id := range lifecycles {
|
||||
if !stopped[id] {
|
||||
t.Fatalf("service %s: freshly terminated service still running", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that if a Lifecycle fails to start, all others started before it will be
|
||||
// shut down.
|
||||
func TestLifecycleStartupError(t *testing.T) {
|
||||
stack, err := New(testNodeConfig())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create protocol stack: %v", err)
|
||||
}
|
||||
defer stack.Close()
|
||||
|
||||
started := make(map[string]bool)
|
||||
stopped := make(map[string]bool)
|
||||
|
||||
// Create a batch of instrumented services
|
||||
lifecycles := map[string]Lifecycle{
|
||||
"A": &InstrumentedService{
|
||||
startHook: func() { started["A"] = true },
|
||||
stopHook: func() { stopped["A"] = true },
|
||||
},
|
||||
"B": &InstrumentedService{
|
||||
startHook: func() { started["B"] = true },
|
||||
stopHook: func() { stopped["B"] = true },
|
||||
},
|
||||
"C": &InstrumentedService{
|
||||
startHook: func() { started["C"] = true },
|
||||
stopHook: func() { stopped["C"] = true },
|
||||
},
|
||||
}
|
||||
// register lifecycles on node
|
||||
for _, lifecycle := range lifecycles {
|
||||
stack.RegisterLifecycle(lifecycle)
|
||||
}
|
||||
|
||||
// Register a service that fails to construct itself
|
||||
failure := errors.New("fail")
|
||||
failer := &InstrumentedService{start: failure}
|
||||
stack.RegisterLifecycle(failer)
|
||||
|
||||
// Start the protocol stack and ensure all started services stop
|
||||
if err := stack.Start(); err != failure {
|
||||
t.Fatalf("stack startup failure mismatch: have %v, want %v", err, failure)
|
||||
}
|
||||
for id := range lifecycles {
|
||||
if started[id] && !stopped[id] {
|
||||
t.Fatalf("service %s: started but not stopped", id)
|
||||
}
|
||||
delete(started, id)
|
||||
delete(stopped, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that even if a registered Lifecycle fails to shut down cleanly, it does
|
||||
// not influence the rest of the shutdown invocations.
|
||||
func TestLifecycleTerminationGuarantee(t *testing.T) {
|
||||
stack, err := New(testNodeConfig())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create protocol stack: %v", err)
|
||||
}
|
||||
defer stack.Close()
|
||||
|
||||
started := make(map[string]bool)
|
||||
stopped := make(map[string]bool)
|
||||
|
||||
// Create a batch of instrumented services
|
||||
lifecycles := map[string]Lifecycle{
|
||||
"A": &InstrumentedService{
|
||||
startHook: func() { started["A"] = true },
|
||||
stopHook: func() { stopped["A"] = true },
|
||||
},
|
||||
"B": &InstrumentedService{
|
||||
startHook: func() { started["B"] = true },
|
||||
stopHook: func() { stopped["B"] = true },
|
||||
},
|
||||
"C": &InstrumentedService{
|
||||
startHook: func() { started["C"] = true },
|
||||
stopHook: func() { stopped["C"] = true },
|
||||
},
|
||||
}
|
||||
// register lifecycles on node
|
||||
for _, lifecycle := range lifecycles {
|
||||
stack.RegisterLifecycle(lifecycle)
|
||||
}
|
||||
|
||||
// Register a service that fails to shot down cleanly
|
||||
failure := errors.New("fail")
|
||||
failer := &InstrumentedService{stop: failure}
|
||||
stack.RegisterLifecycle(failer)
|
||||
|
||||
// Start the protocol stack, and ensure that a failing shut down terminates all
|
||||
// Start the stack and make sure all is online
|
||||
if err := stack.Start(); err != nil {
|
||||
t.Fatalf("failed to start protocol stack: %v", err)
|
||||
}
|
||||
for id := range lifecycles {
|
||||
if !started[id] {
|
||||
t.Fatalf("service %s: service not running", id)
|
||||
}
|
||||
if stopped[id] {
|
||||
t.Fatalf("service %s: service already stopped", id)
|
||||
}
|
||||
}
|
||||
// Stop the stack, verify failure and check all terminations
|
||||
err = stack.Close()
|
||||
if err, ok := err.(*StopError); !ok {
|
||||
t.Fatalf("termination failure mismatch: have %v, want StopError", err)
|
||||
} else {
|
||||
failer := reflect.TypeOf(&InstrumentedService{})
|
||||
if err.Services[failer] != failure {
|
||||
t.Fatalf("failer termination failure mismatch: have %v, want %v", err.Services[failer], failure)
|
||||
}
|
||||
if len(err.Services) != 1 {
|
||||
t.Fatalf("failure count mismatch: have %d, want %d", len(err.Services), 1)
|
||||
}
|
||||
}
|
||||
for id := range lifecycles {
|
||||
if !stopped[id] {
|
||||
t.Fatalf("service %s: service not terminated", id)
|
||||
}
|
||||
delete(started, id)
|
||||
delete(stopped, id)
|
||||
}
|
||||
|
||||
stack.server = &p2p.Server{}
|
||||
stack.server.PrivateKey = testNodeKey
|
||||
}
|
||||
|
||||
// Tests whether a handler can be successfully mounted on the canonical HTTP server
|
||||
// on the given prefix
|
||||
func TestRegisterHandler_Successful(t *testing.T) {
|
||||
node := createNode(t, 7878, 7979)
|
||||
defer node.Close()
|
||||
// create and mount handler
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("success"))
|
||||
})
|
||||
node.RegisterHandler("test", "/test", handler)
|
||||
|
||||
// start node
|
||||
if err := node.Start(); err != nil {
|
||||
t.Fatalf("could not start node: %v", err)
|
||||
}
|
||||
|
||||
// create HTTP request
|
||||
httpReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:7878/test", nil)
|
||||
if err != nil {
|
||||
t.Error("could not issue new http request ", err)
|
||||
}
|
||||
|
||||
// check response
|
||||
resp := doHTTPRequest(t, httpReq)
|
||||
buf := make([]byte, 7)
|
||||
_, err = io.ReadFull(resp.Body, buf)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read response: %v", err)
|
||||
}
|
||||
assert.Equal(t, "success", string(buf))
|
||||
}
|
||||
|
||||
// Tests that the given handler will not be successfully mounted since no HTTP server
|
||||
// is enabled for RPC
|
||||
func TestRegisterHandler_Unsuccessful(t *testing.T) {
|
||||
node, err := New(&DefaultConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create new node: %v", err)
|
||||
}
|
||||
|
||||
// create and mount handler
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("success"))
|
||||
})
|
||||
node.RegisterHandler("test", "/test", handler)
|
||||
}
|
||||
|
||||
// Tests whether websocket requests can be handled on the same port as a regular http server.
|
||||
func TestWebsocketHTTPOnSamePort_WebsocketRequest(t *testing.T) {
|
||||
node := startHTTP(t, 0, 0)
|
||||
defer node.Close()
|
||||
|
||||
ws := strings.Replace(node.HTTPEndpoint(), "http://", "ws://", 1)
|
||||
|
||||
if node.WSEndpoint() != ws {
|
||||
t.Fatalf("endpoints should be the same")
|
||||
}
|
||||
if !checkRPC(ws) {
|
||||
t.Fatalf("ws request failed")
|
||||
}
|
||||
if !checkRPC(node.HTTPEndpoint()) {
|
||||
t.Fatalf("http request failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebsocketHTTPOnSeparatePort_WSRequest(t *testing.T) {
|
||||
// try and get a free port
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal("can't listen:", err)
|
||||
}
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
listener.Close()
|
||||
|
||||
node := startHTTP(t, 0, port)
|
||||
defer node.Close()
|
||||
|
||||
wsOnHTTP := strings.Replace(node.HTTPEndpoint(), "http://", "ws://", 1)
|
||||
ws := fmt.Sprintf("ws://127.0.0.1:%d", port)
|
||||
|
||||
if node.WSEndpoint() == wsOnHTTP {
|
||||
t.Fatalf("endpoints should not be the same")
|
||||
}
|
||||
// ensure ws endpoint matches the expected endpoint
|
||||
if node.WSEndpoint() != ws {
|
||||
t.Fatalf("ws endpoint is incorrect: expected %s, got %s", ws, node.WSEndpoint())
|
||||
}
|
||||
|
||||
if !checkRPC(ws) {
|
||||
t.Fatalf("ws request failed")
|
||||
}
|
||||
if !checkRPC(node.HTTPEndpoint()) {
|
||||
t.Fatalf("http request failed")
|
||||
}
|
||||
}
|
||||
|
||||
type rpcPrefixTest struct {
|
||||
httpPrefix, wsPrefix string
|
||||
// These lists paths on which JSON-RPC should be served / not served.
|
||||
wantHTTP []string
|
||||
wantNoHTTP []string
|
||||
wantWS []string
|
||||
wantNoWS []string
|
||||
}
|
||||
|
||||
func TestNodeRPCPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []rpcPrefixTest{
|
||||
// both off
|
||||
{
|
||||
httpPrefix: "", wsPrefix: "",
|
||||
wantHTTP: []string{"/", "/?p=1"},
|
||||
wantNoHTTP: []string{"/test", "/test?p=1"},
|
||||
wantWS: []string{"/", "/?p=1"},
|
||||
wantNoWS: []string{"/test", "/test?p=1"},
|
||||
},
|
||||
// only http prefix
|
||||
{
|
||||
httpPrefix: "/testprefix", wsPrefix: "",
|
||||
wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
|
||||
wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"},
|
||||
wantWS: []string{"/", "/?p=1"},
|
||||
wantNoWS: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"},
|
||||
},
|
||||
// only ws prefix
|
||||
{
|
||||
httpPrefix: "", wsPrefix: "/testprefix",
|
||||
wantHTTP: []string{"/", "/?p=1"},
|
||||
wantNoHTTP: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"},
|
||||
wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
|
||||
wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"},
|
||||
},
|
||||
// both set
|
||||
{
|
||||
httpPrefix: "/testprefix", wsPrefix: "/testprefix",
|
||||
wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
|
||||
wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"},
|
||||
wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
|
||||
wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
name := fmt.Sprintf("http=%s ws=%s", test.httpPrefix, test.wsPrefix)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
HTTPHost: "127.0.0.1",
|
||||
HTTPPathPrefix: test.httpPrefix,
|
||||
WSHost: "127.0.0.1",
|
||||
WSPathPrefix: test.wsPrefix,
|
||||
}
|
||||
node, err := New(cfg)
|
||||
if err != nil {
|
||||
t.Fatal("can't create node:", err)
|
||||
}
|
||||
defer node.Close()
|
||||
if err := node.Start(); err != nil {
|
||||
t.Fatal("can't start node:", err)
|
||||
}
|
||||
test.check(t, node)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (test rpcPrefixTest) check(t *testing.T, node *Node) {
|
||||
t.Helper()
|
||||
httpBase := "http://" + node.http.listenAddr()
|
||||
wsBase := "ws://" + node.http.listenAddr()
|
||||
|
||||
if node.WSEndpoint() != wsBase+test.wsPrefix {
|
||||
t.Errorf("Error: node has wrong WSEndpoint %q", node.WSEndpoint())
|
||||
}
|
||||
|
||||
for _, path := range test.wantHTTP {
|
||||
resp := rpcRequest(t, httpBase+path, testMethod)
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("Error: %s: bad status code %d, want 200", path, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
for _, path := range test.wantNoHTTP {
|
||||
resp := rpcRequest(t, httpBase+path, testMethod)
|
||||
if resp.StatusCode != 404 {
|
||||
t.Errorf("Error: %s: bad status code %d, want 404", path, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
for _, path := range test.wantWS {
|
||||
err := wsRequest(t, wsBase+path)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s: WebSocket connection failed: %v", path, err)
|
||||
}
|
||||
}
|
||||
for _, path := range test.wantNoWS {
|
||||
err := wsRequest(t, wsBase+path)
|
||||
if err == nil {
|
||||
t.Errorf("Error: %s: WebSocket connection succeeded for path in wantNoWS", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createNode(t *testing.T, httpPort, wsPort int) *Node {
|
||||
conf := &Config{
|
||||
HTTPHost: "127.0.0.1",
|
||||
HTTPPort: httpPort,
|
||||
WSHost: "127.0.0.1",
|
||||
WSPort: wsPort,
|
||||
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
|
||||
}
|
||||
node, err := New(conf)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a new node: %v", err)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func startHTTP(t *testing.T, httpPort, wsPort int) *Node {
|
||||
node := createNode(t, httpPort, wsPort)
|
||||
err := node.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("could not start http service on node: %v", err)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func doHTTPRequest(t *testing.T, req *http.Request) *http.Response {
|
||||
client := http.DefaultClient
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("could not issue a GET request to the given endpoint: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { resp.Body.Close() })
|
||||
return resp
|
||||
}
|
||||
|
||||
func containsProtocol(stackProtocols []p2p.Protocol, protocol p2p.Protocol) bool {
|
||||
for _, a := range stackProtocols {
|
||||
if reflect.DeepEqual(a, protocol) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsAPI(stackAPIs []rpc.API, api rpc.API) bool {
|
||||
for _, a := range stackAPIs {
|
||||
if reflect.DeepEqual(a, api) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
644
node/rpcstack.go
644
node/rpcstack.go
|
|
@ -1,644 +0,0 @@
|
|||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
// httpConfig is the JSON-RPC/HTTP configuration.
|
||||
type httpConfig struct {
|
||||
Modules []string
|
||||
CorsAllowedOrigins []string
|
||||
Vhosts []string
|
||||
prefix string // path prefix on which to mount http handler
|
||||
rpcEndpointConfig
|
||||
}
|
||||
|
||||
// wsConfig is the JSON-RPC/Websocket configuration
|
||||
type wsConfig struct {
|
||||
Origins []string
|
||||
Modules []string
|
||||
prefix string // path prefix on which to mount ws handler
|
||||
rpcEndpointConfig
|
||||
}
|
||||
|
||||
type rpcEndpointConfig struct {
|
||||
jwtSecret []byte // optional JWT secret
|
||||
batchItemLimit int
|
||||
batchResponseSizeLimit int
|
||||
}
|
||||
|
||||
type rpcHandler struct {
|
||||
http.Handler
|
||||
server *rpc.Server
|
||||
}
|
||||
|
||||
type httpServer struct {
|
||||
log log.Logger
|
||||
timeouts rpc.HTTPTimeouts
|
||||
mux http.ServeMux // registered handlers go here
|
||||
|
||||
mu sync.Mutex
|
||||
server *http.Server
|
||||
listener net.Listener // non-nil when server is running
|
||||
|
||||
// HTTP RPC handler things.
|
||||
|
||||
httpConfig httpConfig
|
||||
httpHandler atomic.Value // *rpcHandler
|
||||
|
||||
// WebSocket handler things.
|
||||
wsConfig wsConfig
|
||||
wsHandler atomic.Value // *rpcHandler
|
||||
|
||||
// These are set by setListenAddr.
|
||||
endpoint string
|
||||
host string
|
||||
port int
|
||||
|
||||
handlerNames map[string]string
|
||||
}
|
||||
|
||||
const (
|
||||
shutdownTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
func newHTTPServer(log log.Logger, timeouts rpc.HTTPTimeouts) *httpServer {
|
||||
h := &httpServer{log: log, timeouts: timeouts, handlerNames: make(map[string]string)}
|
||||
|
||||
h.httpHandler.Store((*rpcHandler)(nil))
|
||||
h.wsHandler.Store((*rpcHandler)(nil))
|
||||
return h
|
||||
}
|
||||
|
||||
// setListenAddr configures the listening address of the server.
|
||||
// The address can only be set while the server isn't running.
|
||||
func (h *httpServer) setListenAddr(host string, port int) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.listener != nil && (host != h.host || port != h.port) {
|
||||
return fmt.Errorf("HTTP server already running on %s", h.endpoint)
|
||||
}
|
||||
|
||||
h.host, h.port = host, port
|
||||
h.endpoint = net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
return nil
|
||||
}
|
||||
|
||||
// listenAddr returns the listening address of the server.
|
||||
func (h *httpServer) listenAddr() string {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.listener != nil {
|
||||
return h.listener.Addr().String()
|
||||
}
|
||||
return h.endpoint
|
||||
}
|
||||
|
||||
// start starts the HTTP server if it is enabled and not already running.
|
||||
func (h *httpServer) start() error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.endpoint == "" || h.listener != nil {
|
||||
return nil // already running or not configured
|
||||
}
|
||||
|
||||
// Initialize the server.
|
||||
h.server = &http.Server{Handler: h}
|
||||
if h.timeouts != (rpc.HTTPTimeouts{}) {
|
||||
CheckTimeouts(&h.timeouts)
|
||||
h.server.ReadTimeout = h.timeouts.ReadTimeout
|
||||
h.server.ReadHeaderTimeout = h.timeouts.ReadHeaderTimeout
|
||||
h.server.WriteTimeout = h.timeouts.WriteTimeout
|
||||
h.server.IdleTimeout = h.timeouts.IdleTimeout
|
||||
}
|
||||
|
||||
// Start the server.
|
||||
listener, err := net.Listen("tcp", h.endpoint)
|
||||
if err != nil {
|
||||
// If the server fails to start, we need to clear out the RPC and WS
|
||||
// configuration so they can be configured another time.
|
||||
h.disableRPC()
|
||||
h.disableWS()
|
||||
return err
|
||||
}
|
||||
h.listener = listener
|
||||
go h.server.Serve(listener)
|
||||
|
||||
if h.wsAllowed() {
|
||||
url := fmt.Sprintf("ws://%v", listener.Addr())
|
||||
if h.wsConfig.prefix != "" {
|
||||
url += h.wsConfig.prefix
|
||||
}
|
||||
h.log.Info("WebSocket enabled", "url", url)
|
||||
}
|
||||
// if server is websocket only, return after logging
|
||||
if !h.rpcAllowed() {
|
||||
return nil
|
||||
}
|
||||
// Log http endpoint.
|
||||
h.log.Info("HTTP server started",
|
||||
"endpoint", listener.Addr(), "auth", (h.httpConfig.jwtSecret != nil),
|
||||
"prefix", h.httpConfig.prefix,
|
||||
"cors", strings.Join(h.httpConfig.CorsAllowedOrigins, ","),
|
||||
"vhosts", strings.Join(h.httpConfig.Vhosts, ","),
|
||||
)
|
||||
|
||||
// Log all handlers mounted on server.
|
||||
var paths []string
|
||||
for path := range h.handlerNames {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
logged := make(map[string]bool, len(paths))
|
||||
for _, path := range paths {
|
||||
name := h.handlerNames[path]
|
||||
if !logged[name] {
|
||||
log.Info(name+" enabled", "url", "http://"+listener.Addr().String()+path)
|
||||
logged[name] = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// check if ws request and serve if ws enabled
|
||||
ws := h.wsHandler.Load().(*rpcHandler)
|
||||
if ws != nil && isWebsocket(r) {
|
||||
if checkPath(r, h.wsConfig.prefix) {
|
||||
ws.ServeHTTP(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// if http-rpc is enabled, try to serve request
|
||||
rpc := h.httpHandler.Load().(*rpcHandler)
|
||||
if rpc != nil {
|
||||
// First try to route in the mux.
|
||||
// Requests to a path below root are handled by the mux,
|
||||
// which has all the handlers registered via Node.RegisterHandler.
|
||||
// These are made available when RPC is enabled.
|
||||
muxHandler, pattern := h.mux.Handler(r)
|
||||
if pattern != "" {
|
||||
muxHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if checkPath(r, h.httpConfig.prefix) {
|
||||
rpc.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
// checkPath checks whether a given request URL matches a given path prefix.
|
||||
func checkPath(r *http.Request, path string) bool {
|
||||
// if no prefix has been specified, request URL must be on root
|
||||
if path == "" {
|
||||
return r.URL.Path == "/"
|
||||
}
|
||||
// otherwise, check to make sure prefix matches
|
||||
return len(r.URL.Path) >= len(path) && r.URL.Path[:len(path)] == path
|
||||
}
|
||||
|
||||
// validatePrefix checks if 'path' is a valid configuration value for the RPC prefix option.
|
||||
func validatePrefix(what, path string) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
if path[0] != '/' {
|
||||
return fmt.Errorf(`%s RPC path prefix %q does not contain leading "/"`, what, path)
|
||||
}
|
||||
if strings.ContainsAny(path, "?#") {
|
||||
// This is just to avoid confusion. While these would match correctly (i.e. they'd
|
||||
// match if URL-escaped into path), it's not easy to understand for users when
|
||||
// setting that on the command line.
|
||||
return fmt.Errorf("%s RPC path prefix %q contains URL meta-characters", what, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stop shuts down the HTTP server.
|
||||
func (h *httpServer) stop() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.doStop()
|
||||
}
|
||||
|
||||
func (h *httpServer) doStop() {
|
||||
if h.listener == nil {
|
||||
return // not running
|
||||
}
|
||||
|
||||
// Shut down the server.
|
||||
httpHandler := h.httpHandler.Load().(*rpcHandler)
|
||||
wsHandler := h.wsHandler.Load().(*rpcHandler)
|
||||
if httpHandler != nil {
|
||||
h.httpHandler.Store((*rpcHandler)(nil))
|
||||
httpHandler.server.Stop()
|
||||
}
|
||||
if wsHandler != nil {
|
||||
h.wsHandler.Store((*rpcHandler)(nil))
|
||||
wsHandler.server.Stop()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||
defer cancel()
|
||||
err := h.server.Shutdown(ctx)
|
||||
if err != nil && err == ctx.Err() {
|
||||
h.log.Warn("HTTP server graceful shutdown timed out")
|
||||
h.server.Close()
|
||||
}
|
||||
|
||||
h.listener.Close()
|
||||
h.log.Info("HTTP server stopped", "endpoint", h.listener.Addr())
|
||||
|
||||
// Clear out everything to allow re-configuring it later.
|
||||
h.host, h.port, h.endpoint = "", 0, ""
|
||||
h.server, h.listener = nil, nil
|
||||
}
|
||||
|
||||
// enableRPC turns on JSON-RPC over HTTP on the server.
|
||||
func (h *httpServer) enableRPC(apis []rpc.API, config httpConfig) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.rpcAllowed() {
|
||||
return fmt.Errorf("JSON-RPC over HTTP is already enabled")
|
||||
}
|
||||
|
||||
// Create RPC server and handler.
|
||||
srv := rpc.NewServer()
|
||||
srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit)
|
||||
if err := RegisterApis(apis, config.Modules, srv); err != nil {
|
||||
return err
|
||||
}
|
||||
h.httpConfig = config
|
||||
h.httpHandler.Store(&rpcHandler{
|
||||
Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts, config.jwtSecret),
|
||||
server: srv,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// disableRPC stops the HTTP RPC handler. This is internal, the caller must hold h.mu.
|
||||
func (h *httpServer) disableRPC() bool {
|
||||
handler := h.httpHandler.Load().(*rpcHandler)
|
||||
if handler != nil {
|
||||
h.httpHandler.Store((*rpcHandler)(nil))
|
||||
handler.server.Stop()
|
||||
}
|
||||
return handler != nil
|
||||
}
|
||||
|
||||
// enableWS turns on JSON-RPC over WebSocket on the server.
|
||||
func (h *httpServer) enableWS(apis []rpc.API, config wsConfig) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.wsAllowed() {
|
||||
return fmt.Errorf("JSON-RPC over WebSocket is already enabled")
|
||||
}
|
||||
// Create RPC server and handler.
|
||||
srv := rpc.NewServer()
|
||||
srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit)
|
||||
if err := RegisterApis(apis, config.Modules, srv); err != nil {
|
||||
return err
|
||||
}
|
||||
h.wsConfig = config
|
||||
h.wsHandler.Store(&rpcHandler{
|
||||
Handler: NewWSHandlerStack(srv.WebsocketHandler(config.Origins), config.jwtSecret),
|
||||
server: srv,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// stopWS disables JSON-RPC over WebSocket and also stops the server if it only serves WebSocket.
|
||||
func (h *httpServer) stopWS() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.disableWS() {
|
||||
if !h.rpcAllowed() {
|
||||
h.doStop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// disableWS disables the WebSocket handler. This is internal, the caller must hold h.mu.
|
||||
func (h *httpServer) disableWS() bool {
|
||||
ws := h.wsHandler.Load().(*rpcHandler)
|
||||
if ws != nil {
|
||||
h.wsHandler.Store((*rpcHandler)(nil))
|
||||
ws.server.Stop()
|
||||
}
|
||||
return ws != nil
|
||||
}
|
||||
|
||||
// rpcAllowed returns true when JSON-RPC over HTTP is enabled.
|
||||
func (h *httpServer) rpcAllowed() bool {
|
||||
return h.httpHandler.Load().(*rpcHandler) != nil
|
||||
}
|
||||
|
||||
// wsAllowed returns true when JSON-RPC over WebSocket is enabled.
|
||||
func (h *httpServer) wsAllowed() bool {
|
||||
return h.wsHandler.Load().(*rpcHandler) != nil
|
||||
}
|
||||
|
||||
// isWebsocket checks the header of an http request for a websocket upgrade request.
|
||||
func isWebsocket(r *http.Request) bool {
|
||||
return strings.EqualFold(r.Header.Get("Upgrade"), "websocket") &&
|
||||
strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade")
|
||||
}
|
||||
|
||||
// NewHTTPHandlerStack returns wrapped http-related handlers
|
||||
func NewHTTPHandlerStack(srv http.Handler, cors []string, vhosts []string, jwtSecret []byte) http.Handler {
|
||||
// Wrap the CORS-handler within a host-handler
|
||||
handler := newCorsHandler(srv, cors)
|
||||
handler = newVHostHandler(vhosts, handler)
|
||||
if len(jwtSecret) != 0 {
|
||||
handler = newJWTHandler(jwtSecret, handler)
|
||||
}
|
||||
return newGzipHandler(handler)
|
||||
}
|
||||
|
||||
// NewWSHandlerStack returns a wrapped ws-related handler.
|
||||
func NewWSHandlerStack(srv http.Handler, jwtSecret []byte) http.Handler {
|
||||
if len(jwtSecret) != 0 {
|
||||
return newJWTHandler(jwtSecret, srv)
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
func newCorsHandler(srv http.Handler, allowedOrigins []string) http.Handler {
|
||||
// disable CORS support if user has not specified a custom CORS configuration
|
||||
if len(allowedOrigins) == 0 {
|
||||
return srv
|
||||
}
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: allowedOrigins,
|
||||
AllowedMethods: []string{http.MethodPost, http.MethodGet},
|
||||
AllowedHeaders: []string{"*"},
|
||||
MaxAge: 600,
|
||||
})
|
||||
return c.Handler(srv)
|
||||
}
|
||||
|
||||
// virtualHostHandler is a handler which validates the Host-header of incoming requests.
|
||||
// Using virtual hosts can help prevent DNS rebinding attacks, where a 'random' domain name points to
|
||||
// the service ip address (but without CORS headers). By verifying the targeted virtual host, we can
|
||||
// ensure that it's a destination that the node operator has defined.
|
||||
type virtualHostHandler struct {
|
||||
vhosts map[string]struct{}
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
func newVHostHandler(vhosts []string, next http.Handler) http.Handler {
|
||||
vhostMap := make(map[string]struct{})
|
||||
for _, allowedHost := range vhosts {
|
||||
vhostMap[strings.ToLower(allowedHost)] = struct{}{}
|
||||
}
|
||||
return &virtualHostHandler{vhostMap, next}
|
||||
}
|
||||
|
||||
// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler
|
||||
func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// if r.Host is not set, we can continue serving since a browser would set the Host header
|
||||
if r.Host == "" {
|
||||
h.next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
// Either invalid (too many colons) or no port specified
|
||||
host = r.Host
|
||||
}
|
||||
if ipAddr := net.ParseIP(host); ipAddr != nil {
|
||||
// It's an IP address, we can serve that
|
||||
h.next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// Not an IP address, but a hostname. Need to validate
|
||||
if _, exist := h.vhosts["*"]; exist {
|
||||
h.next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if _, exist := h.vhosts[host]; exist {
|
||||
h.next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
http.Error(w, "invalid host specified", http.StatusForbidden)
|
||||
}
|
||||
|
||||
var gzPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
w := gzip.NewWriter(io.Discard)
|
||||
return w
|
||||
},
|
||||
}
|
||||
|
||||
type gzipResponseWriter struct {
|
||||
resp http.ResponseWriter
|
||||
|
||||
gz *gzip.Writer
|
||||
contentLength uint64 // total length of the uncompressed response
|
||||
written uint64 // amount of written bytes from the uncompressed response
|
||||
hasLength bool // true if uncompressed response had Content-Length
|
||||
inited bool // true after init was called for the first time
|
||||
}
|
||||
|
||||
// init runs just before response headers are written. Among other things, this function
|
||||
// also decides whether compression will be applied at all.
|
||||
func (w *gzipResponseWriter) init() {
|
||||
if w.inited {
|
||||
return
|
||||
}
|
||||
w.inited = true
|
||||
|
||||
hdr := w.resp.Header()
|
||||
length := hdr.Get("content-length")
|
||||
if len(length) > 0 {
|
||||
if n, err := strconv.ParseUint(length, 10, 64); err != nil {
|
||||
w.hasLength = true
|
||||
w.contentLength = n
|
||||
}
|
||||
}
|
||||
|
||||
// Setting Transfer-Encoding to "identity" explicitly disables compression. net/http
|
||||
// also recognizes this header value and uses it to disable "chunked" transfer
|
||||
// encoding, trimming the header from the response. This means downstream handlers can
|
||||
// set this without harm, even if they aren't wrapped by newGzipHandler.
|
||||
//
|
||||
// In go-ethereum, we use this signal to disable compression for certain error
|
||||
// responses which are flushed out close to the write deadline of the response. For
|
||||
// these cases, we want to avoid chunked transfer encoding and compression because
|
||||
// they require additional output that may not get written in time.
|
||||
passthrough := hdr.Get("transfer-encoding") == "identity"
|
||||
if !passthrough {
|
||||
w.gz = gzPool.Get().(*gzip.Writer)
|
||||
w.gz.Reset(w.resp)
|
||||
hdr.Del("content-length")
|
||||
hdr.Set("content-encoding", "gzip")
|
||||
}
|
||||
}
|
||||
|
||||
func (w *gzipResponseWriter) Header() http.Header {
|
||||
return w.resp.Header()
|
||||
}
|
||||
|
||||
func (w *gzipResponseWriter) WriteHeader(status int) {
|
||||
w.init()
|
||||
w.resp.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
w.init()
|
||||
|
||||
if w.gz == nil {
|
||||
// Compression is disabled.
|
||||
return w.resp.Write(b)
|
||||
}
|
||||
|
||||
n, err := w.gz.Write(b)
|
||||
w.written += uint64(n)
|
||||
if w.hasLength && w.written >= w.contentLength {
|
||||
// The HTTP handler has finished writing the entire uncompressed response. Close
|
||||
// the gzip stream to ensure the footer will be seen by the client in case the
|
||||
// response is flushed after this call to write.
|
||||
err = w.gz.Close()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (w *gzipResponseWriter) Flush() {
|
||||
if w.gz != nil {
|
||||
w.gz.Flush()
|
||||
}
|
||||
if f, ok := w.resp.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *gzipResponseWriter) close() {
|
||||
if w.gz == nil {
|
||||
return
|
||||
}
|
||||
w.gz.Close()
|
||||
gzPool.Put(w.gz)
|
||||
w.gz = nil
|
||||
}
|
||||
|
||||
func newGzipHandler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
wrapper := &gzipResponseWriter{resp: w}
|
||||
defer wrapper.close()
|
||||
|
||||
next.ServeHTTP(wrapper, r)
|
||||
})
|
||||
}
|
||||
|
||||
type ipcServer struct {
|
||||
log log.Logger
|
||||
endpoint string
|
||||
|
||||
mu sync.Mutex
|
||||
listener net.Listener
|
||||
srv *rpc.Server
|
||||
}
|
||||
|
||||
func newIPCServer(log log.Logger, endpoint string) *ipcServer {
|
||||
return &ipcServer{log: log, endpoint: endpoint}
|
||||
}
|
||||
|
||||
// Start starts the httpServer's http.Server
|
||||
func (is *ipcServer) start(apis []rpc.API) error {
|
||||
is.mu.Lock()
|
||||
defer is.mu.Unlock()
|
||||
|
||||
if is.listener != nil {
|
||||
return nil // already running
|
||||
}
|
||||
listener, srv, err := rpc.StartIPCEndpoint(is.endpoint, apis)
|
||||
if err != nil {
|
||||
is.log.Warn("IPC opening failed", "url", is.endpoint, "error", err)
|
||||
return err
|
||||
}
|
||||
is.log.Info("IPC endpoint opened", "url", is.endpoint)
|
||||
is.listener, is.srv = listener, srv
|
||||
return nil
|
||||
}
|
||||
|
||||
func (is *ipcServer) stop() error {
|
||||
is.mu.Lock()
|
||||
defer is.mu.Unlock()
|
||||
|
||||
if is.listener == nil {
|
||||
return nil // not running
|
||||
}
|
||||
err := is.listener.Close()
|
||||
is.srv.Stop()
|
||||
is.listener, is.srv = nil, nil
|
||||
is.log.Info("IPC endpoint closed", "url", is.endpoint)
|
||||
return err
|
||||
}
|
||||
|
||||
// RegisterApis checks the given modules' availability, generates an allowlist based on the allowed modules,
|
||||
// and then registers all of the APIs exposed by the services.
|
||||
func RegisterApis(apis []rpc.API, modules []string, srv *rpc.Server) error {
|
||||
if bad, available := checkModuleAvailability(modules, apis); len(bad) > 0 {
|
||||
log.Error("Unavailable modules in HTTP API list", "unavailable", bad, "available", available)
|
||||
}
|
||||
// Generate the allow list based on the allowed modules
|
||||
allowList := make(map[string]bool)
|
||||
for _, module := range modules {
|
||||
allowList[module] = true
|
||||
}
|
||||
// Register all the APIs exposed by the services
|
||||
for _, api := range apis {
|
||||
if allowList[api.Namespace] || len(allowList) == 0 {
|
||||
if err := srv.RegisterName(api.Namespace, api.Service); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,616 +0,0 @@
|
|||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/internal/testlog"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const testMethod = "rpc_modules"
|
||||
|
||||
// TestCorsHandler makes sure CORS are properly handled on the http server.
|
||||
func TestCorsHandler(t *testing.T) {
|
||||
srv := createAndStartServer(t, &httpConfig{CorsAllowedOrigins: []string{"test", "test.com"}}, false, &wsConfig{}, nil)
|
||||
defer srv.stop()
|
||||
url := "http://" + srv.listenAddr()
|
||||
|
||||
resp := rpcRequest(t, url, testMethod, "origin", "test.com")
|
||||
assert.Equal(t, "test.com", resp.Header.Get("Access-Control-Allow-Origin"))
|
||||
|
||||
resp2 := rpcRequest(t, url, testMethod, "origin", "bad")
|
||||
assert.Equal(t, "", resp2.Header.Get("Access-Control-Allow-Origin"))
|
||||
}
|
||||
|
||||
// TestVhosts makes sure vhosts are properly handled on the http server.
|
||||
func TestVhosts(t *testing.T) {
|
||||
srv := createAndStartServer(t, &httpConfig{Vhosts: []string{"test"}}, false, &wsConfig{}, nil)
|
||||
defer srv.stop()
|
||||
url := "http://" + srv.listenAddr()
|
||||
|
||||
resp := rpcRequest(t, url, testMethod, "host", "test")
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
|
||||
resp2 := rpcRequest(t, url, testMethod, "host", "bad")
|
||||
assert.Equal(t, resp2.StatusCode, http.StatusForbidden)
|
||||
}
|
||||
|
||||
type originTest struct {
|
||||
spec string
|
||||
expOk []string
|
||||
expFail []string
|
||||
}
|
||||
|
||||
// splitAndTrim splits input separated by a comma
|
||||
// and trims excessive white space from the substrings.
|
||||
// Copied over from flags.go
|
||||
func splitAndTrim(input string) (ret []string) {
|
||||
l := strings.Split(input, ",")
|
||||
for _, r := range l {
|
||||
r = strings.TrimSpace(r)
|
||||
if len(r) > 0 {
|
||||
ret = append(ret, r)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// TestWebsocketOrigins makes sure the websocket origins are properly handled on the websocket server.
|
||||
func TestWebsocketOrigins(t *testing.T) {
|
||||
tests := []originTest{
|
||||
{
|
||||
spec: "*", // allow all
|
||||
expOk: []string{"", "http://test", "https://test", "http://test:8540", "https://test:8540",
|
||||
"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"},
|
||||
},
|
||||
{
|
||||
spec: "test",
|
||||
expOk: []string{"http://test", "https://test", "http://test:8540", "https://test:8540"},
|
||||
expFail: []string{"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"},
|
||||
},
|
||||
// scheme tests
|
||||
{
|
||||
spec: "https://test",
|
||||
expOk: []string{"https://test", "https://test:9999"},
|
||||
expFail: []string{
|
||||
"test", // no scheme, required by spec
|
||||
"http://test", // wrong scheme
|
||||
"http://test.foo", "https://a.test.x", // subdomain variations
|
||||
"http://testx:8540", "https://xtest:8540"},
|
||||
},
|
||||
// ip tests
|
||||
{
|
||||
spec: "https://12.34.56.78",
|
||||
expOk: []string{"https://12.34.56.78", "https://12.34.56.78:8540"},
|
||||
expFail: []string{
|
||||
"http://12.34.56.78", // wrong scheme
|
||||
"http://12.34.56.78:443", // wrong scheme
|
||||
"http://1.12.34.56.78", // wrong 'domain name'
|
||||
"http://12.34.56.78.a", // wrong 'domain name'
|
||||
"https://87.65.43.21", "http://87.65.43.21:8540", "https://87.65.43.21:8540"},
|
||||
},
|
||||
// port tests
|
||||
{
|
||||
spec: "test:8540",
|
||||
expOk: []string{"http://test:8540", "https://test:8540"},
|
||||
expFail: []string{
|
||||
"http://test", "https://test", // spec says port required
|
||||
"http://test:8541", "https://test:8541", // wrong port
|
||||
"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
|
||||
},
|
||||
// scheme and port
|
||||
{
|
||||
spec: "https://test:8540",
|
||||
expOk: []string{"https://test:8540"},
|
||||
expFail: []string{
|
||||
"https://test", // missing port
|
||||
"http://test", // missing port, + wrong scheme
|
||||
"http://test:8540", // wrong scheme
|
||||
"http://test:8541", "https://test:8541", // wrong port
|
||||
"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
|
||||
},
|
||||
// several allowed origins
|
||||
{
|
||||
spec: "localhost,http://127.0.0.1",
|
||||
expOk: []string{"localhost", "http://localhost", "https://localhost:8443",
|
||||
"http://127.0.0.1", "http://127.0.0.1:8080"},
|
||||
expFail: []string{
|
||||
"https://127.0.0.1", // wrong scheme
|
||||
"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
srv := createAndStartServer(t, &httpConfig{}, true, &wsConfig{Origins: splitAndTrim(tc.spec)}, nil)
|
||||
url := fmt.Sprintf("ws://%v", srv.listenAddr())
|
||||
for _, origin := range tc.expOk {
|
||||
if err := wsRequest(t, url, "Origin", origin); err != nil {
|
||||
t.Errorf("spec '%v', origin '%v': expected ok, got %v", tc.spec, origin, err)
|
||||
}
|
||||
}
|
||||
for _, origin := range tc.expFail {
|
||||
if err := wsRequest(t, url, "Origin", origin); err == nil {
|
||||
t.Errorf("spec '%v', origin '%v': expected not to allow, got ok", tc.spec, origin)
|
||||
}
|
||||
}
|
||||
srv.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsWebsocket tests if an incoming websocket upgrade request is handled properly.
|
||||
func TestIsWebsocket(t *testing.T) {
|
||||
r, _ := http.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
assert.False(t, isWebsocket(r))
|
||||
r.Header.Set("upgrade", "websocket")
|
||||
assert.False(t, isWebsocket(r))
|
||||
r.Header.Set("connection", "upgrade")
|
||||
assert.True(t, isWebsocket(r))
|
||||
r.Header.Set("connection", "upgrade,keep-alive")
|
||||
assert.True(t, isWebsocket(r))
|
||||
r.Header.Set("connection", " UPGRADE,keep-alive")
|
||||
assert.True(t, isWebsocket(r))
|
||||
}
|
||||
|
||||
func Test_checkPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
req *http.Request
|
||||
prefix string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
req: &http.Request{URL: &url.URL{Path: "/test"}},
|
||||
prefix: "/test",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
req: &http.Request{URL: &url.URL{Path: "/testing"}},
|
||||
prefix: "/test",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
req: &http.Request{URL: &url.URL{Path: "/"}},
|
||||
prefix: "/test",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
req: &http.Request{URL: &url.URL{Path: "/fail"}},
|
||||
prefix: "/test",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
req: &http.Request{URL: &url.URL{Path: "/"}},
|
||||
prefix: "",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
req: &http.Request{URL: &url.URL{Path: "/fail"}},
|
||||
prefix: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
req: &http.Request{URL: &url.URL{Path: "/"}},
|
||||
prefix: "/",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
req: &http.Request{URL: &url.URL{Path: "/testing"}},
|
||||
prefix: "/",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, checkPath(tt.req, tt.prefix))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createAndStartServer(t *testing.T, conf *httpConfig, ws bool, wsConf *wsConfig, timeouts *rpc.HTTPTimeouts) *httpServer {
|
||||
t.Helper()
|
||||
|
||||
if timeouts == nil {
|
||||
timeouts = &rpc.DefaultHTTPTimeouts
|
||||
}
|
||||
srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), *timeouts)
|
||||
assert.NoError(t, srv.enableRPC(apis(), *conf))
|
||||
if ws {
|
||||
assert.NoError(t, srv.enableWS(nil, *wsConf))
|
||||
}
|
||||
assert.NoError(t, srv.setListenAddr("localhost", 0))
|
||||
assert.NoError(t, srv.start())
|
||||
return srv
|
||||
}
|
||||
|
||||
// wsRequest attempts to open a WebSocket connection to the given URL.
|
||||
func wsRequest(t *testing.T, url string, extraHeaders ...string) error {
|
||||
t.Helper()
|
||||
//t.Logf("checking WebSocket on %s (origin %q)", url, browserOrigin)
|
||||
|
||||
headers := make(http.Header)
|
||||
// Apply extra headers.
|
||||
if len(extraHeaders)%2 != 0 {
|
||||
panic("odd extraHeaders length")
|
||||
}
|
||||
for i := 0; i < len(extraHeaders); i += 2 {
|
||||
key, value := extraHeaders[i], extraHeaders[i+1]
|
||||
headers.Set(key, value)
|
||||
}
|
||||
conn, _, err := websocket.DefaultDialer.Dial(url, headers)
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// rpcRequest performs a JSON-RPC request to the given URL.
|
||||
func rpcRequest(t *testing.T, url, method string, extraHeaders ...string) *http.Response {
|
||||
t.Helper()
|
||||
|
||||
body := fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"%s","params":[]}`, method)
|
||||
return baseRpcRequest(t, url, body, extraHeaders...)
|
||||
}
|
||||
|
||||
func batchRpcRequest(t *testing.T, url string, methods []string, extraHeaders ...string) *http.Response {
|
||||
reqs := make([]string, len(methods))
|
||||
for i, m := range methods {
|
||||
reqs[i] = fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"%s","params":[]}`, m)
|
||||
}
|
||||
body := fmt.Sprintf(`[%s]`, strings.Join(reqs, ","))
|
||||
return baseRpcRequest(t, url, body, extraHeaders...)
|
||||
}
|
||||
|
||||
func baseRpcRequest(t *testing.T, url, bodyStr string, extraHeaders ...string) *http.Response {
|
||||
t.Helper()
|
||||
|
||||
// Create the request.
|
||||
body := bytes.NewReader([]byte(bodyStr))
|
||||
req, err := http.NewRequest(http.MethodPost, url, body)
|
||||
if err != nil {
|
||||
t.Fatal("could not create http request:", err)
|
||||
}
|
||||
req.Header.Set("content-type", "application/json")
|
||||
req.Header.Set("accept-encoding", "identity")
|
||||
|
||||
// Apply extra headers.
|
||||
if len(extraHeaders)%2 != 0 {
|
||||
panic("odd extraHeaders length")
|
||||
}
|
||||
for i := 0; i < len(extraHeaders); i += 2 {
|
||||
key, value := extraHeaders[i], extraHeaders[i+1]
|
||||
if strings.EqualFold(key, "host") {
|
||||
req.Host = value
|
||||
} else {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the request.
|
||||
t.Logf("checking RPC/HTTP on %s %v", url, extraHeaders)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { resp.Body.Close() })
|
||||
return resp
|
||||
}
|
||||
|
||||
type testClaim map[string]interface{}
|
||||
|
||||
func (testClaim) Valid() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestJWT(t *testing.T) {
|
||||
var secret = []byte("secret")
|
||||
issueToken := func(secret []byte, method jwt.SigningMethod, input map[string]interface{}) string {
|
||||
if method == nil {
|
||||
method = jwt.SigningMethodHS256
|
||||
}
|
||||
ss, _ := jwt.NewWithClaims(method, testClaim(input)).SignedString(secret)
|
||||
return ss
|
||||
}
|
||||
cfg := rpcEndpointConfig{jwtSecret: []byte("secret")}
|
||||
httpcfg := &httpConfig{rpcEndpointConfig: cfg}
|
||||
wscfg := &wsConfig{Origins: []string{"*"}, rpcEndpointConfig: cfg}
|
||||
srv := createAndStartServer(t, httpcfg, true, wscfg, nil)
|
||||
wsUrl := fmt.Sprintf("ws://%v", srv.listenAddr())
|
||||
htUrl := fmt.Sprintf("http://%v", srv.listenAddr())
|
||||
|
||||
expOk := []func() string{
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + 4}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - 4}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
|
||||
"iat": time.Now().Unix(),
|
||||
"exp": time.Now().Unix() + 2,
|
||||
}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
|
||||
"iat": time.Now().Unix(),
|
||||
"bar": "baz",
|
||||
}))
|
||||
},
|
||||
}
|
||||
for i, tokenFn := range expOk {
|
||||
token := tokenFn()
|
||||
if err := wsRequest(t, wsUrl, "Authorization", token); err != nil {
|
||||
t.Errorf("test %d-ws, token '%v': expected ok, got %v", i, token, err)
|
||||
}
|
||||
token = tokenFn()
|
||||
if resp := rpcRequest(t, htUrl, testMethod, "Authorization", token); resp.StatusCode != 200 {
|
||||
t.Errorf("test %d-http, token '%v': expected ok, got %v", i, token, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
expFail := []func() string{
|
||||
// future
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + int64(jwtExpiryTimeout.Seconds()) + 1}))
|
||||
},
|
||||
// stale
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - int64(jwtExpiryTimeout.Seconds()) - 1}))
|
||||
},
|
||||
// wrong algo
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, jwt.SigningMethodHS512, testClaim{"iat": time.Now().Unix() + 4}))
|
||||
},
|
||||
// expired
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix(), "exp": time.Now().Unix()}))
|
||||
},
|
||||
// missing mandatory iat
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{}))
|
||||
},
|
||||
// wrong secret
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken([]byte("wrong"), nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken([]byte{}, nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(nil, nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
// Various malformed syntax
|
||||
func() string {
|
||||
return fmt.Sprintf("%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer: %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer:%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer\t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
func() string {
|
||||
return fmt.Sprintf("Bearer \t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
|
||||
},
|
||||
}
|
||||
for i, tokenFn := range expFail {
|
||||
token := tokenFn()
|
||||
if err := wsRequest(t, wsUrl, "Authorization", token); err == nil {
|
||||
t.Errorf("tc %d-ws, token '%v': expected not to allow, got ok", i, token)
|
||||
}
|
||||
|
||||
token = tokenFn()
|
||||
resp := rpcRequest(t, htUrl, testMethod, "Authorization", token)
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("tc %d-http, token '%v': expected not to allow, got %v", i, token, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
srv.stop()
|
||||
}
|
||||
|
||||
func TestGzipHandler(t *testing.T) {
|
||||
type gzipTest struct {
|
||||
name string
|
||||
handler http.HandlerFunc
|
||||
status int
|
||||
isGzip bool
|
||||
header map[string]string
|
||||
}
|
||||
tests := []gzipTest{
|
||||
{
|
||||
name: "Write",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("response"))
|
||||
},
|
||||
isGzip: true,
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
name: "WriteHeader",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("x-foo", "bar")
|
||||
w.WriteHeader(205)
|
||||
w.Write([]byte("response"))
|
||||
},
|
||||
isGzip: true,
|
||||
status: 205,
|
||||
header: map[string]string{"x-foo": "bar"},
|
||||
},
|
||||
{
|
||||
name: "WriteContentLength",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("content-length", "8")
|
||||
w.Write([]byte("response"))
|
||||
},
|
||||
isGzip: true,
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
name: "Flush",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("res"))
|
||||
w.(http.Flusher).Flush()
|
||||
w.Write([]byte("ponse"))
|
||||
},
|
||||
isGzip: true,
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
name: "disable",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("transfer-encoding", "identity")
|
||||
w.Header().Set("x-foo", "bar")
|
||||
w.Write([]byte("response"))
|
||||
},
|
||||
isGzip: false,
|
||||
status: 200,
|
||||
header: map[string]string{"x-foo": "bar"},
|
||||
},
|
||||
{
|
||||
name: "disable-WriteHeader",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("transfer-encoding", "identity")
|
||||
w.Header().Set("x-foo", "bar")
|
||||
w.WriteHeader(205)
|
||||
w.Write([]byte("response"))
|
||||
},
|
||||
isGzip: false,
|
||||
status: 205,
|
||||
header: map[string]string{"x-foo": "bar"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
srv := httptest.NewServer(newGzipHandler(test.handler))
|
||||
defer srv.Close()
|
||||
|
||||
resp, err := http.Get(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
content, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wasGzip := resp.Uncompressed
|
||||
|
||||
if string(content) != "response" {
|
||||
t.Fatalf("wrong response content %q", content)
|
||||
}
|
||||
if wasGzip != test.isGzip {
|
||||
t.Fatalf("response gzipped == %t, want %t", wasGzip, test.isGzip)
|
||||
}
|
||||
if resp.StatusCode != test.status {
|
||||
t.Fatalf("response status == %d, want %d", resp.StatusCode, test.status)
|
||||
}
|
||||
for name, expectedValue := range test.header {
|
||||
if v := resp.Header.Get(name); v != expectedValue {
|
||||
t.Fatalf("response header %s == %s, want %s", name, v, expectedValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPWriteTimeout(t *testing.T) {
|
||||
const (
|
||||
timeoutRes = `{"jsonrpc":"2.0","id":1,"error":{"code":-32002,"message":"request timed out"}}`
|
||||
greetRes = `{"jsonrpc":"2.0","id":1,"result":"Hello"}`
|
||||
)
|
||||
// Set-up server
|
||||
timeouts := rpc.DefaultHTTPTimeouts
|
||||
timeouts.WriteTimeout = time.Second
|
||||
srv := createAndStartServer(t, &httpConfig{Modules: []string{"test"}}, false, &wsConfig{}, &timeouts)
|
||||
url := fmt.Sprintf("http://%v", srv.listenAddr())
|
||||
|
||||
// Send normal request
|
||||
t.Run("message", func(t *testing.T) {
|
||||
resp := rpcRequest(t, url, "test_sleep")
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(body) != timeoutRes {
|
||||
t.Errorf("wrong response. have %s, want %s", string(body), timeoutRes)
|
||||
}
|
||||
})
|
||||
|
||||
// Batch request
|
||||
t.Run("batch", func(t *testing.T) {
|
||||
want := fmt.Sprintf("[%s,%s,%s]", greetRes, timeoutRes, timeoutRes)
|
||||
resp := batchRpcRequest(t, url, []string{"test_greet", "test_sleep", "test_greet"})
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(body) != want {
|
||||
t.Errorf("wrong response. have %s, want %s", string(body), want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func apis() []rpc.API {
|
||||
return []rpc.API{
|
||||
{
|
||||
Namespace: "test",
|
||||
Service: &testService{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type testService struct{}
|
||||
|
||||
func (s *testService) Greet() string {
|
||||
return "Hello"
|
||||
}
|
||||
|
||||
func (s *testService) Sleep() {
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Contains a batch of utility type declarations used by the tests. As the node
|
||||
// operates on unique types, a lot of them are needed to check various features.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
// NoopLifecycle is a trivial implementation of the Service interface.
|
||||
type NoopLifecycle struct{}
|
||||
|
||||
func (s *NoopLifecycle) Start() error { return nil }
|
||||
func (s *NoopLifecycle) Stop() error { return nil }
|
||||
|
||||
func NewNoop() *Noop {
|
||||
noop := new(Noop)
|
||||
return noop
|
||||
}
|
||||
|
||||
// Set of services all wrapping the base NoopLifecycle resulting in the same method
|
||||
// signatures but different outer types.
|
||||
type Noop struct{ NoopLifecycle }
|
||||
|
||||
// InstrumentedService is an implementation of Lifecycle for which all interface
|
||||
// methods can be instrumented both return value as well as event hook wise.
|
||||
type InstrumentedService struct {
|
||||
start error
|
||||
stop error
|
||||
|
||||
startHook func()
|
||||
stopHook func()
|
||||
}
|
||||
|
||||
func (s *InstrumentedService) Start() error {
|
||||
if s.startHook != nil {
|
||||
s.startHook()
|
||||
}
|
||||
return s.start
|
||||
}
|
||||
|
||||
func (s *InstrumentedService) Stop() error {
|
||||
if s.stopHook != nil {
|
||||
s.stopHook()
|
||||
}
|
||||
return s.stop
|
||||
}
|
||||
|
||||
type FullService struct{}
|
||||
|
||||
func NewFullService(stack *Node) (*FullService, error) {
|
||||
fs := new(FullService)
|
||||
|
||||
stack.RegisterProtocols(fs.Protocols())
|
||||
stack.RegisterAPIs(fs.APIs())
|
||||
stack.RegisterLifecycle(fs)
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
func (f *FullService) Start() error { return nil }
|
||||
|
||||
func (f *FullService) Stop() error { return nil }
|
||||
|
||||
func (f *FullService) Protocols() []p2p.Protocol {
|
||||
return []p2p.Protocol{
|
||||
{
|
||||
Name: "test1",
|
||||
Version: uint(1),
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Version: uint(2),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FullService) APIs() []rpc.API {
|
||||
return []rpc.API{
|
||||
{
|
||||
Namespace: "admin",
|
||||
},
|
||||
{
|
||||
Namespace: "debug",
|
||||
},
|
||||
{
|
||||
Namespace: "net",
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue