// 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" "io/ioutil" "os" "path/filepath" "runtime" "strings" "sync" "github.com/ava-labs/coreth/accounts" "github.com/ava-labs/coreth/accounts/external" "github.com/ava-labs/coreth/accounts/keystore" "github.com/ava-labs/coreth/accounts/scwallet" "github.com/ava-labs/coreth/rpc" "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/p2p/enode" ) const ( datadirPrivateKey = "nodekey" // Path within the datadir to the node's private key 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. NoUSB 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 // 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"` // 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"` staticNodesWarning bool trustedNodesWarning bool oldGethResourceWarning bool } // 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 fmt.Sprintf("%s:%d", c.HTTPHost, c.HTTPPort) } // DefaultHTTPEndpoint returns the HTTP endpoint used by default. func DefaultHTTPEndpoint() string { config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort} 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 fmt.Sprintf("%s:%d", c.WSHost, 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.warnOnce(&c.oldGethResourceWarning, "Using deprecated resource file %s, please move this file to the 'geth' subdirectory of datadir.", 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 } // StaticNodes returns a list of node enode URLs configured as static nodes. func (c *Config) StaticNodes() []*enode.Node { return c.parsePersistentNodes(&c.staticNodesWarning, c.ResolvePath(datadirStaticNodes)) } // TrustedNodes returns a list of node enode URLs configured as trusted nodes. func (c *Config) TrustedNodes() []*enode.Node { return c.parsePersistentNodes(&c.trustedNodesWarning, c.ResolvePath(datadirTrustedNodes)) } // parsePersistentNodes parses a list of discovery node URLs loaded from a .json // file from within the data directory. func (c *Config) parsePersistentNodes(w *bool, path string) []*enode.Node { // Short circuit if no node config is present if c.DataDir == "" { return nil } if _, err := os.Stat(path); err != nil { return nil } c.warnOnce(w, "Found deprecated node list file %s, please use the TOML config file instead.", path) // Load the nodes from the config file. var nodelist []string if err := common.LoadJSON(path, &nodelist); err != nil { log.Error(fmt.Sprintf("Can't load node list file: %v", err)) return nil } // Interpret the list as a discovery node array var nodes []*enode.Node for _, url := range nodelist { if url == "" { continue } node, err := enode.Parse(enode.ValidSchemes, url) if err != nil { log.Error(fmt.Sprintf("Node URL %s: %v\n", url, err)) continue } nodes = append(nodes, node) } return nodes } // AccountConfig determines the settings for scrypt and keydirectory func (c *Config) AccountConfig() (int, int, string, error) { scryptN := keystore.StandardScryptN scryptP := keystore.StandardScryptP if c.UseLightweightKDF { scryptN = keystore.LightScryptN scryptP = keystore.LightScryptP } 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 scryptN, scryptP, keydir, err } func makeAccountManager(conf *Config) (*accounts.Manager, string, error) { scryptN, scryptP, keydir, err := conf.AccountConfig() var ephemeral string if keydir == "" { // There is no datadir. keydir, err = ioutil.TempDir("", "go-ethereum-keystore") ephemeral = keydir } if err != nil { return nil, "", err } if err := os.MkdirAll(keydir, 0700); err != nil { return nil, "", err } // Assemble the account manager and supported backends var backends []accounts.Backend if len(conf.ExternalSigner) > 0 { log.Info("Using external signer", "url", conf.ExternalSigner) if extapi, err := external.NewExternalBackend(conf.ExternalSigner); err == nil { backends = append(backends, extapi) } else { return nil, "", fmt.Errorf("error connecting to external signer: %v", err) } } if len(backends) == 0 { // For now, we're using EITHER external signer OR local signers. // If/when we implement some form of lockfile for USB and keystore wallets, // we can have both, but it's very confusing for the user to see the same // accounts in both externally and locally, plus very racey. backends = append(backends, keystore.NewKeyStore(keydir, scryptN, scryptP)) //if !conf.NoUSB { // // Start a USB hub for Ledger hardware wallets // if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { // log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err)) // } else { // backends = append(backends, ledgerhub) // } // // Start a USB hub for Trezor hardware wallets (HID version) // if trezorhub, err := usbwallet.NewTrezorHubWithHID(); err != nil { // log.Warn(fmt.Sprintf("Failed to start HID Trezor hub, disabling: %v", err)) // } else { // backends = append(backends, trezorhub) // } // // Start a USB hub for Trezor hardware wallets (WebUSB version) // if trezorhub, err := usbwallet.NewTrezorHubWithWebUSB(); err != nil { // log.Warn(fmt.Sprintf("Failed to start WebUSB Trezor hub, disabling: %v", err)) // } else { // backends = append(backends, trezorhub) // } //} if len(conf.SmartCardDaemonPath) > 0 { // Start a smart card hub if schub, err := scwallet.NewHub(conf.SmartCardDaemonPath, scwallet.Scheme, keydir); err != nil { log.Warn(fmt.Sprintf("Failed to start smart card hub, disabling: %v", err)) } else { backends = append(backends, schub) } } } return accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: conf.InsecureUnlockAllowed}, backends...), ephemeral, nil } var warnLock sync.Mutex func (c *Config) warnOnce(w *bool, format string, args ...interface{}) { warnLock.Lock() defer warnLock.Unlock() if *w { return } l := c.Logger if l == nil { l = log.Root() } l.Warn(fmt.Sprintf(format, args...)) *w = true }