2013-08-22 18:30:38 +02:00
/ *
2014-01-09 20:12:20 +01:00
* Copyright ( c ) 2013 , 2014 Conformal Systems LLC < info @ conformal . com >
2013-08-22 18:30:38 +02:00
*
* Permission to use , copy , modify , and distribute this software for any
* purpose with or without fee is hereby granted , provided that the above
* copyright notice and this permission notice appear in all copies .
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS . IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL , DIRECT , INDIRECT , OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE , DATA OR PROFITS , WHETHER IN AN
* ACTION OF CONTRACT , NEGLIGENCE OR OTHER TORTIOUS ACTION , ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE .
* /
2013-08-21 16:37:30 +02:00
package main
import (
"fmt"
2013-11-12 21:25:56 +01:00
"github.com/conformal/btcutil"
2013-10-16 23:29:48 +02:00
"github.com/conformal/btcwire"
2013-08-21 16:37:30 +02:00
"github.com/conformal/go-flags"
2013-11-19 18:21:54 +01:00
"net"
2013-08-21 16:37:30 +02:00
"os"
"path/filepath"
2014-06-20 01:11:47 +02:00
"sort"
2013-08-21 16:37:30 +02:00
"strings"
)
const (
2013-12-03 16:58:57 +01:00
defaultCAFilename = "btcd.cert"
2013-08-21 16:37:30 +02:00
defaultConfigFilename = "btcwallet.conf"
2013-10-16 23:29:48 +02:00
defaultBtcNet = btcwire . TestNet3
2013-08-21 16:37:30 +02:00
defaultLogLevel = "info"
2014-06-20 01:11:47 +02:00
defaultLogDirname = "logs"
defaultLogFilename = "btcwallet.log"
2014-01-15 23:29:01 +01:00
defaultKeypoolSize = 100
2014-01-28 18:55:42 +01:00
defaultDisallowFree = false
2013-08-21 16:37:30 +02:00
)
var (
2014-01-10 17:34:06 +01:00
btcdHomeDir = btcutil . AppDataDir ( "btcd" , false )
2013-12-03 16:52:09 +01:00
btcwalletHomeDir = btcutil . AppDataDir ( "btcwallet" , false )
2014-01-10 17:34:06 +01:00
btcdHomedirCAFile = filepath . Join ( btcdHomeDir , "rpc.cert" )
2013-12-03 16:52:09 +01:00
defaultConfigFile = filepath . Join ( btcwalletHomeDir , defaultConfigFilename )
defaultDataDir = btcwalletHomeDir
defaultRPCKeyFile = filepath . Join ( btcwalletHomeDir , "rpc.key" )
defaultRPCCertFile = filepath . Join ( btcwalletHomeDir , "rpc.cert" )
2014-06-20 01:11:47 +02:00
defaultLogDir = filepath . Join ( btcwalletHomeDir , defaultLogDirname )
2013-08-21 16:37:30 +02:00
)
type config struct {
2013-12-05 23:20:52 +01:00
ShowVersion bool ` short:"V" long:"version" description:"Display version information and exit" `
CAFile string ` long:"cafile" description:"File containing root certificates to authenticate a TLS connections with btcd" `
2014-05-29 23:15:32 +02:00
RPCConnect string ` short:"c" long:"rpcconnect" description:"Hostname/IP and port of btcd RPC server to connect to (default localhost:18334, mainnet: localhost:8334, simnet: localhost:18556)" `
2013-12-05 23:20:52 +01:00
DebugLevel string ` short:"d" long:"debuglevel" description:"Logging level { trace, debug, info, warn, error, critical}" `
ConfigFile string ` short:"C" long:"configfile" description:"Path to configuration file" `
2014-05-29 23:32:30 +02:00
SvrListeners [ ] string ` long:"rpclisten" description:"Listen for RPC/websocket connections on this interface/port (default port: 18332, mainnet: 8332, simnet: 18554)" `
2013-12-05 23:20:52 +01:00
DataDir string ` short:"D" long:"datadir" description:"Directory to store wallets and transactions" `
2014-06-20 15:29:54 +02:00
LogDir string ` long:"logdir" description:"Directory to log output." `
2014-05-16 19:58:33 +02:00
Username string ` short:"u" long:"username" description:"Username for client and btcd authorization" `
Password string ` short:"P" long:"password" default-mask:"-" description:"Password for client and btcd authorization" `
BtcdUsername string ` long:"btcdusername" description:"Alternative username for btcd authorization" `
BtcdPassword string ` long:"btcdpassword" default-mask:"-" description:"Alternative password for btcd authorization" `
2013-12-05 23:20:52 +01:00
RPCCert string ` long:"rpccert" description:"File containing the certificate file" `
RPCKey string ` long:"rpckey" description:"File containing the certificate key" `
2013-12-09 23:44:19 +01:00
MainNet bool ` long:"mainnet" description:"Use the main Bitcoin network (default testnet3)" `
2014-05-29 23:15:32 +02:00
SimNet bool ` long:"simnet" description:"Use the simulation test network (default testnet3)" `
2014-01-15 23:29:01 +01:00
KeypoolSize uint ` short:"k" long:"keypoolsize" description:"Maximum number of addresses in keypool" `
2014-01-28 18:55:42 +01:00
DisallowFree bool ` long:"disallowfree" description:"Force transactions to always include a fee" `
2013-12-05 23:20:52 +01:00
Proxy string ` long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)" `
ProxyUser string ` long:"proxyuser" description:"Username for proxy server" `
ProxyPass string ` long:"proxypass" default-mask:"-" description:"Password for proxy server" `
Profile string ` long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536" `
2013-08-21 16:37:30 +02:00
}
2013-11-21 16:02:27 +01:00
// cleanAndExpandPath expands environement variables and leading ~ in the
// passed path, cleans the result, and returns it.
func cleanAndExpandPath ( path string ) string {
// Expand initial ~ to OS specific home directory.
if strings . HasPrefix ( path , "~" ) {
homeDir := filepath . Dir ( btcwalletHomeDir )
path = strings . Replace ( path , "~" , homeDir , 1 )
}
// NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%,
// but they variables can still be expanded via POSIX-style $VARIABLE.
return filepath . Clean ( os . ExpandEnv ( path ) )
}
2014-06-20 01:11:47 +02:00
// validLogLevel returns whether or not logLevel is a valid debug log level.
func validLogLevel ( logLevel string ) bool {
switch logLevel {
case "trace" :
fallthrough
case "debug" :
fallthrough
case "info" :
fallthrough
case "warn" :
fallthrough
case "error" :
fallthrough
case "critical" :
return true
}
return false
}
// supportedSubsystems returns a sorted slice of the supported subsystems for
// logging purposes.
func supportedSubsystems ( ) [ ] string {
// Convert the subsystemLoggers map keys to a slice.
subsystems := make ( [ ] string , 0 , len ( subsystemLoggers ) )
for subsysID := range subsystemLoggers {
subsystems = append ( subsystems , subsysID )
}
// Sort the subsytems for stable display.
sort . Strings ( subsystems )
return subsystems
}
// parseAndSetDebugLevels attempts to parse the specified debug level and set
// the levels accordingly. An appropriate error is returned if anything is
// invalid.
func parseAndSetDebugLevels ( debugLevel string ) error {
// When the specified string doesn't have any delimters, treat it as
// the log level for all subsystems.
if ! strings . Contains ( debugLevel , "," ) && ! strings . Contains ( debugLevel , "=" ) {
// Validate debug log level.
if ! validLogLevel ( debugLevel ) {
str := "The specified debug level [%v] is invalid"
return fmt . Errorf ( str , debugLevel )
}
// Change the logging level for all subsystems.
setLogLevels ( debugLevel )
return nil
}
// Split the specified string into subsystem/level pairs while detecting
// issues and update the log levels accordingly.
for _ , logLevelPair := range strings . Split ( debugLevel , "," ) {
if ! strings . Contains ( logLevelPair , "=" ) {
str := "The specified debug level contains an invalid " +
"subsystem/level pair [%v]"
return fmt . Errorf ( str , logLevelPair )
}
// Extract the specified subsystem and log level.
fields := strings . Split ( logLevelPair , "=" )
subsysID , logLevel := fields [ 0 ] , fields [ 1 ]
// Validate subsystem.
if _ , exists := subsystemLoggers [ subsysID ] ; ! exists {
str := "The specified subsystem [%v] is invalid -- " +
"supported subsytems %v"
return fmt . Errorf ( str , subsysID , supportedSubsystems ( ) )
}
// Validate log level.
if ! validLogLevel ( logLevel ) {
str := "The specified debug level [%v] is invalid"
return fmt . Errorf ( str , logLevel )
}
setLogLevel ( subsysID , logLevel )
}
return nil
}
2013-12-05 23:20:52 +01:00
// removeDuplicateAddresses returns a new slice with all duplicate entries in
// addrs removed.
func removeDuplicateAddresses ( addrs [ ] string ) [ ] string {
2014-04-11 20:58:04 +02:00
result := [ ] string { }
2013-12-05 23:20:52 +01:00
seen := map [ string ] bool { }
for _ , val := range addrs {
if _ , ok := seen [ val ] ; ! ok {
result = append ( result , val )
seen [ val ] = true
}
2013-10-16 23:29:48 +02:00
}
2013-12-05 23:20:52 +01:00
return result
}
2013-10-16 23:29:48 +02:00
2013-12-05 23:20:52 +01:00
// normalizeAddresses returns a new slice with all the passed peer addresses
// normalized with the given default port, and all duplicates removed.
func normalizeAddresses ( addrs [ ] string , defaultPort string ) [ ] string {
for i , addr := range addrs {
addrs [ i ] = normalizeAddress ( addr , defaultPort )
2013-10-16 23:29:48 +02:00
}
2013-12-05 23:20:52 +01:00
return removeDuplicateAddresses ( addrs )
2013-10-16 23:29:48 +02:00
}
2013-08-21 16:37:30 +02:00
// filesExists reports whether the named file or directory exists.
func fileExists ( name string ) bool {
if _ , err := os . Stat ( name ) ; err != nil {
if os . IsNotExist ( err ) {
return false
}
}
return true
}
2013-11-19 18:21:54 +01:00
// normalizeAddress returns addr with the passed default port appended if
// there is not already a port specified.
func normalizeAddress ( addr , defaultPort string ) string {
_ , _ , err := net . SplitHostPort ( addr )
if err != nil {
return net . JoinHostPort ( addr , defaultPort )
}
return addr
}
2013-08-21 16:37:30 +02:00
// loadConfig initializes and parses the config using a config file and command
// line options.
//
// The configuration proceeds as follows:
// 1) Start with a default config with sane settings
// 2) Pre-parse the command line to check for an alternative config file
// 3) Load configuration file overwriting defaults with any specified options
// 4) Parse CLI options and overwrite/add any specified options
//
// The above results in btcwallet functioning properly without any config
// settings while still allowing the user to override settings with config files
// and command line options. Command line options always take precedence.
func loadConfig ( ) ( * config , [ ] string , error ) {
// Default config.
cfg := config {
2014-01-28 18:55:42 +01:00
DebugLevel : defaultLogLevel ,
ConfigFile : defaultConfigFile ,
DataDir : defaultDataDir ,
2014-06-20 01:11:47 +02:00
LogDir : defaultLogDir ,
2014-01-28 18:55:42 +01:00
RPCKey : defaultRPCKeyFile ,
RPCCert : defaultRPCCertFile ,
KeypoolSize : defaultKeypoolSize ,
DisallowFree : defaultDisallowFree ,
2013-08-21 16:37:30 +02:00
}
// A config file in the current directory takes precedence.
if fileExists ( defaultConfigFilename ) {
cfg . ConfigFile = defaultConfigFile
}
// Pre-parse the command line options to see if an alternative config
// file or the version flag was specified.
preCfg := cfg
preParser := flags . NewParser ( & preCfg , flags . Default )
_ , err := preParser . Parse ( )
if err != nil {
if e , ok := err . ( * flags . Error ) ; ! ok || e . Type != flags . ErrHelp {
preParser . WriteHelp ( os . Stderr )
}
return nil , nil , err
}
// Show the version and exit if the version flag was specified.
if preCfg . ShowVersion {
appName := filepath . Base ( os . Args [ 0 ] )
appName = strings . TrimSuffix ( appName , filepath . Ext ( appName ) )
fmt . Println ( appName , "version" , version ( ) )
os . Exit ( 0 )
}
// Load additional config from file.
2013-11-25 18:20:37 +01:00
var configFileError error
2013-08-21 16:37:30 +02:00
parser := flags . NewParser ( & cfg , flags . Default )
2013-11-25 18:20:37 +01:00
err = flags . NewIniParser ( parser ) . ParseFile ( preCfg . ConfigFile )
2013-08-21 16:37:30 +02:00
if err != nil {
if _ , ok := err . ( * os . PathError ) ; ! ok {
fmt . Fprintln ( os . Stderr , err )
parser . WriteHelp ( os . Stderr )
return nil , nil , err
}
2013-11-25 18:20:37 +01:00
configFileError = err
2013-08-21 16:37:30 +02:00
}
// Parse command line options again to ensure they take precedence.
remainingArgs , err := parser . Parse ( )
if err != nil {
if e , ok := err . ( * flags . Error ) ; ! ok || e . Type != flags . ErrHelp {
parser . WriteHelp ( os . Stderr )
}
return nil , nil , err
}
2013-11-25 18:20:37 +01:00
// Warn about missing config file after the final command line parse
// succeeds. This prevents the warning on help messages and invalid
// options.
if configFileError != nil {
log . Warnf ( "%v" , configFileError )
}
2014-05-28 19:55:37 +02:00
// If an alternate data directory was specified, and paths with defaults
// relative to the data dir are unchanged, modify each path to be
// relative to the new data dir.
if cfg . DataDir != defaultDataDir {
if cfg . RPCKey == defaultRPCKeyFile {
cfg . RPCKey = filepath . Join ( cfg . DataDir , "rpc.key" )
}
if cfg . RPCCert == defaultRPCCertFile {
cfg . RPCCert = filepath . Join ( cfg . DataDir , "rpc.cert" )
}
}
2014-05-29 23:15:32 +02:00
// Multiple networks can't be selected simultaneously.
numNets := 0
2013-10-16 23:29:48 +02:00
if cfg . MainNet {
2014-05-29 23:15:32 +02:00
numNets ++
}
if cfg . SimNet {
numNets ++
}
if numNets > 1 {
str := "%s: The mainnet and simnet params can't be used " +
2014-06-02 18:15:27 +02:00
"together -- choose one"
2014-05-29 23:15:32 +02:00
err := fmt . Errorf ( str , "loadConfig" )
fmt . Fprintln ( os . Stderr , err )
parser . WriteHelp ( os . Stderr )
return nil , nil , err
}
// Choose the active network params based on the selected network.
switch {
case cfg . MainNet :
2014-05-23 04:16:50 +02:00
activeNet = & mainNetParams
2014-05-29 23:15:32 +02:00
case cfg . SimNet :
activeNet = & simNetParams
2013-10-16 23:29:48 +02:00
}
2014-06-20 01:11:47 +02:00
// Special show command to list supported subsystems and exit.
if cfg . DebugLevel == "show" {
fmt . Println ( "Supported subsystems" , supportedSubsystems ( ) )
os . Exit ( 0 )
}
// Initialize logging at the default logging level.
initSeelogLogger ( filepath . Join ( cfg . LogDir , defaultLogFilename ) )
setLogLevels ( defaultLogLevel )
// Parse, validate, and set debug log level(s).
if err := parseAndSetDebugLevels ( cfg . DebugLevel ) ; err != nil {
err := fmt . Errorf ( "%s: %v" , "loadConfig" , err . Error ( ) )
2013-10-29 15:38:51 +01:00
fmt . Fprintln ( os . Stderr , err )
parser . WriteHelp ( os . Stderr )
return nil , nil , err
}
2014-05-06 20:25:56 +02:00
if cfg . RPCConnect == "" {
2014-05-23 04:16:50 +02:00
cfg . RPCConnect = activeNet . connect
2013-12-09 22:46:38 +01:00
}
2013-11-19 18:21:54 +01:00
// Add default port to connect flag if missing.
2014-05-23 04:16:50 +02:00
cfg . RPCConnect = normalizeAddress ( cfg . RPCConnect , activeNet . btcdPort )
2013-11-19 18:21:54 +01:00
2014-01-10 17:34:06 +01:00
// If CAFile is unset, choose either the copy or local btcd cert.
if cfg . CAFile == "" {
2014-05-28 19:55:37 +02:00
cfg . CAFile = filepath . Join ( cfg . DataDir , defaultCAFilename )
2014-01-10 17:34:06 +01:00
// If the CA copy does not exist, check if we're connecting to
// a local btcd and switch to its RPC cert if it exists.
if ! fileExists ( cfg . CAFile ) {
2014-05-06 20:25:56 +02:00
host , _ , err := net . SplitHostPort ( cfg . RPCConnect )
2014-01-10 17:34:06 +01:00
if err != nil {
return nil , nil , err
}
switch host {
case "localhost" :
fallthrough
case "127.0.0.1" :
fallthrough
case "::1" :
if fileExists ( btcdHomedirCAFile ) {
cfg . CAFile = btcdHomedirCAFile
}
}
}
}
2013-12-05 23:20:52 +01:00
if len ( cfg . SvrListeners ) == 0 {
addrs , err := net . LookupHost ( "localhost" )
if err != nil {
return nil , nil , err
}
cfg . SvrListeners = make ( [ ] string , 0 , len ( addrs ) )
for _ , addr := range addrs {
2014-05-23 04:16:50 +02:00
addr = net . JoinHostPort ( addr , activeNet . svrPort )
2013-12-05 23:20:52 +01:00
cfg . SvrListeners = append ( cfg . SvrListeners , addr )
}
}
// Add default port to all rpc listener addresses if needed and remove
// duplicate addresses.
cfg . SvrListeners = normalizeAddresses ( cfg . SvrListeners ,
2014-05-23 04:16:50 +02:00
activeNet . svrPort )
2013-12-05 23:20:52 +01:00
2013-11-21 16:02:27 +01:00
// Expand environment variable and leading ~ for filepaths.
cfg . CAFile = cleanAndExpandPath ( cfg . CAFile )
2014-05-16 19:58:33 +02:00
// If the btcd username or password are unset, use the same auth as for
// the client. The two settings were previously shared for btcd and
// client auth, so this avoids breaking backwards compatibility while
// allowing users to use different auth settings for btcd and wallet.
if cfg . BtcdUsername == "" {
cfg . BtcdUsername = cfg . Username
}
if cfg . BtcdPassword == "" {
cfg . BtcdPassword = cfg . Password
}
2013-08-21 16:37:30 +02:00
return & cfg , remainingArgs , nil
}