decred.org/dcrdex@v1.0.5/client/app/config.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package app
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  	"os"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strings"
    13  
    14  	"decred.org/dcrdex/client/core"
    15  	"decred.org/dcrdex/client/mm"
    16  	"decred.org/dcrdex/client/rpcserver"
    17  	"decred.org/dcrdex/client/webserver"
    18  	"decred.org/dcrdex/dex"
    19  	"decred.org/dcrdex/dex/version"
    20  	"github.com/decred/dcrd/dcrutil/v4"
    21  	"github.com/jessevdk/go-flags"
    22  )
    23  
    24  const (
    25  	defaultRPCCertFile      = "rpc.cert"
    26  	defaultRPCKeyFile       = "rpc.key"
    27  	defaultMainnetHost      = "127.0.0.1"
    28  	defaultTestnetHost      = "127.0.0.2"
    29  	defaultSimnetHost       = "127.0.0.3"
    30  	walletPairOneHost       = "127.0.0.6"
    31  	walletPairTwoHost       = "127.0.0.7"
    32  	defaultRPCPort          = "5757"
    33  	defaultWebPort          = "5758"
    34  	defaultLogLevel         = "debug"
    35  	configFilename          = "dexc.conf"
    36  	defaultArchiveSizeLimit = 1000
    37  )
    38  
    39  var (
    40  	defaultApplicationDirectory = dcrutil.AppDataDir("dexc", false)
    41  	defaultConfigPath           = filepath.Join(defaultApplicationDirectory, configFilename)
    42  )
    43  
    44  // RPCConfig encapsulates the configuration needed for the RPC server.
    45  type RPCConfig struct {
    46  	RPCAddr string `long:"rpcaddr" description:"RPC server listen address"`
    47  	RPCUser string `long:"rpcuser" description:"RPC server user name"`
    48  	RPCPass string `long:"rpcpass" description:"RPC server password"`
    49  	RPCCert string `long:"rpccert" description:"RPC server certificate file location"`
    50  	RPCKey  string `long:"rpckey" description:"RPC server key file location"`
    51  	// CertHosts is a list of hosts given to certgen.NewTLSCertPair for the
    52  	// "Subject Alternate Name" values of the generated TLS certificate. It is
    53  	// set automatically, not via the config file or cli args.
    54  	CertHosts []string
    55  }
    56  
    57  // RPC creates a rpc server configuration.
    58  func (cfg *RPCConfig) RPC(c *core.Core, marketMaker *mm.MarketMaker, log dex.Logger) *rpcserver.Config {
    59  	bwMajor, bwMinor, bwPatch, bwPreRel, bwBuildMeta, err := version.ParseSemVer(Version)
    60  	if err != nil {
    61  		panic(fmt.Errorf("failed to parse version: %w", err))
    62  	}
    63  
    64  	runtimeVer := strings.Replace(runtime.Version(), ".", "-", -1)
    65  	runBuildMeta := version.NormalizeString(runtimeVer)
    66  	build := version.NormalizeString(bwBuildMeta)
    67  	if build != "" {
    68  		bwBuildMeta = fmt.Sprintf("%s.%s", build, runBuildMeta)
    69  	}
    70  	bwVersion := &rpcserver.SemVersion{
    71  		VersionString: Version,
    72  		Major:         bwMajor,
    73  		Minor:         bwMinor,
    74  		Patch:         bwPatch,
    75  		Prerelease:    bwPreRel,
    76  		BuildMetadata: bwBuildMeta,
    77  	}
    78  
    79  	rpcserver.SetLogger(log)
    80  	return &rpcserver.Config{
    81  		Core:        c,
    82  		MarketMaker: marketMaker,
    83  		Addr:        cfg.RPCAddr,
    84  		User:        cfg.RPCUser,
    85  		Pass:        cfg.RPCPass,
    86  		Cert:        cfg.RPCCert,
    87  		Key:         cfg.RPCKey,
    88  		BWVersion:   bwVersion,
    89  		CertHosts: []string{
    90  			defaultTestnetHost, defaultSimnetHost, defaultMainnetHost,
    91  			walletPairOneHost, walletPairTwoHost,
    92  		},
    93  	}
    94  }
    95  
    96  // CoreConfig encapsulates the settings specific to core.Core.
    97  type CoreConfig struct {
    98  	DBPath       string `long:"db" description:"Database filepath. Database will be created if it does not exist."`
    99  	Onion        string `long:"onion" description:"Proxy for .onion addresses, if torproxy not set (eg. 127.0.0.1:9050)."`
   100  	TorProxy     string `long:"torproxy" description:"Connect via TOR (eg. 127.0.0.1:9050)."`
   101  	TorIsolation bool   `long:"torisolation" description:"Enable TOR circuit isolation."`
   102  	// Net is a derivative field set by ResolveConfig.
   103  	Net dex.Network
   104  
   105  	TheOneHost string `long:"onehost" description:"Only connect with this server."`
   106  
   107  	NoAutoWalletLock   bool `long:"no-wallet-lock" description:"Disable locking of wallets on shutdown or logout. Use this if you want your external wallets to stay unlocked after closing the DEX app."`
   108  	NoAutoDBBackup     bool `long:"no-db-backup" description:"Disable creation of a database backup on shutdown."`
   109  	UnlockCoinsOnLogin bool `long:"release-wallet-coins" description:"On login or wallet creation, instruct the wallet to release any coins that it may have locked."`
   110  
   111  	ExtensionModeFile string `long:"extension-mode-file" description:"path to a file that specifies options for running core as an extension."`
   112  
   113  	PruneArchive uint64 `long:"prunearchive" description:"prune that order archive to the specified number of most recent orders. zero means no pruning."`
   114  }
   115  
   116  // WebConfig encapsulates the configuration needed for the web server.
   117  type WebConfig struct {
   118  	WebAddr     string `long:"webaddr" description:"HTTP server address"`
   119  	WebTLS      bool   `long:"webtls" description:"Use a self-signed certificate for HTTPS with the web server. This is implied for a publicly routable (not loopback or private subnet) webaddr. When changing webaddr, you mean need to delete web.cert and web.key."`
   120  	SiteDir     string `long:"sitedir" description:"Path to the 'site' directory with packaged web files. Unspecified = default is good in most cases."`
   121  	NoEmbedSite bool   `long:"no-embed-site" description:"Use on-disk UI files instead of embedded resources. This also reloads the html template with every request. For development purposes."`
   122  	HTTPProfile bool   `long:"httpprof" description:"Start HTTP profiler on /pprof."`
   123  	// Deprecated
   124  	Experimental bool `long:"experimental" description:"DEPRECATED: Enable experimental features"`
   125  }
   126  
   127  // LogConfig encapsulates the logging-related settings.
   128  type LogConfig struct {
   129  	LogPath    string `long:"logpath" description:"A file to save app logs"`
   130  	DebugLevel string `long:"log" description:"Logging level {trace, debug, info, warn, error, critical}"`
   131  	LocalLogs  bool   `long:"loglocal" description:"Use local time zone time stamps in log entries."`
   132  }
   133  
   134  // MMConfig encapsulates the settings specific to market making.
   135  type MMConfig struct {
   136  	BotConfigPath  string `long:"botConfigPath"`
   137  	EventLogDBPath string `long:"eventLogDBPath"`
   138  }
   139  
   140  // Config is the common application configuration definition. This composite
   141  // struct captures the configuration needed for core and both web and rpc
   142  // servers, as well as some application-level directives.
   143  type Config struct {
   144  	CoreConfig
   145  	RPCConfig
   146  	WebConfig
   147  	LogConfig
   148  	MMConfig
   149  	// AppData and ConfigPath should be parsed from the command-line,
   150  	// as it makes no sense to set these in the config file itself. If no values
   151  	// are assigned, defaults will be used.
   152  	AppData    string `long:"appdata" description:"Path to application directory."`
   153  	ConfigPath string `long:"config" description:"Path to an INI configuration file."`
   154  	// Testnet and Simnet are used to set the derivative CoreConfig.Net
   155  	// dex.Network field.
   156  	Testnet    bool   `long:"testnet" description:"use testnet"`
   157  	Simnet     bool   `long:"simnet" description:"use simnet"`
   158  	RPCOn      bool   `long:"rpc" description:"turn on the rpc server"`
   159  	NoWeb      bool   `long:"noweb" description:"disable the web server."`
   160  	CPUProfile string `long:"cpuprofile" description:"File for CPU profiling."`
   161  	ShowVer    bool   `short:"V" long:"version" description:"Display version information and exit"`
   162  	Language   string `long:"lang" description:"BCP 47 tag for preferred language, e.g. en-GB, fr, zh-CN"`
   163  }
   164  
   165  // Web creates a configuration for the webserver. This is a Config method
   166  // instead of a WebConfig method because Language is an app-level setting used
   167  // by both core and rpcserver.
   168  func (cfg *Config) Web(c *core.Core, mm *mm.MarketMaker, log dex.Logger, utc bool) *webserver.Config {
   169  	addr := cfg.WebAddr
   170  	host, _, err := net.SplitHostPort(addr)
   171  	if err == nil && host != "" {
   172  		addr = host
   173  	} else {
   174  		// If SplitHostPort failed, IPv6 addresses may still have brackets.
   175  		addr = strings.Trim(addr, "[]")
   176  	}
   177  	ip := net.ParseIP(addr)
   178  
   179  	var mmCore webserver.MMCore
   180  	if mm != nil {
   181  		mmCore = mm
   182  	}
   183  
   184  	var certFile, keyFile string
   185  	if cfg.WebTLS || (ip != nil && !ip.IsLoopback() && !ip.IsPrivate()) || (ip == nil && addr != "localhost") {
   186  		certFile = filepath.Join(cfg.AppData, "web.cert")
   187  		keyFile = filepath.Join(cfg.AppData, "web.key")
   188  	}
   189  
   190  	return &webserver.Config{
   191  		Core:          c,
   192  		MarketMaker:   mmCore,
   193  		Addr:          cfg.WebAddr,
   194  		CustomSiteDir: cfg.SiteDir,
   195  		Logger:        log,
   196  		UTC:           utc,
   197  		CertFile:      certFile,
   198  		KeyFile:       keyFile,
   199  		NoEmbed:       cfg.NoEmbedSite,
   200  		HttpProf:      cfg.HTTPProfile,
   201  		AppVersion:    userAppVersion(Version),
   202  		Language:      cfg.Language,
   203  	}
   204  }
   205  
   206  // Core creates a core.Core configuration. This is a Config method
   207  // instead of a CoreConfig method because Language is an app-level setting used
   208  // by both core and rpcserver.
   209  func (cfg *Config) Core(log dex.Logger) *core.Config {
   210  	return &core.Config{
   211  		DBPath:             cfg.DBPath,
   212  		Net:                cfg.Net,
   213  		Logger:             log,
   214  		Onion:              cfg.Onion,
   215  		TorProxy:           cfg.TorProxy,
   216  		TorIsolation:       cfg.TorIsolation,
   217  		Language:           cfg.Language,
   218  		UnlockCoinsOnLogin: cfg.UnlockCoinsOnLogin,
   219  		NoAutoWalletLock:   cfg.NoAutoWalletLock,
   220  		NoAutoDBBackup:     cfg.NoAutoDBBackup,
   221  		ExtensionModeFile:  cfg.ExtensionModeFile,
   222  		TheOneHost:         cfg.TheOneHost,
   223  		PruneArchive:       cfg.PruneArchive,
   224  	}
   225  }
   226  
   227  var DefaultConfig = Config{
   228  	AppData:    defaultApplicationDirectory,
   229  	ConfigPath: defaultConfigPath,
   230  	LogConfig:  LogConfig{DebugLevel: defaultLogLevel},
   231  	RPCConfig: RPCConfig{
   232  		CertHosts: []string{defaultTestnetHost, defaultSimnetHost, defaultMainnetHost},
   233  	},
   234  }
   235  
   236  // ParseCLIConfig parses the command-line arguments into the provided struct
   237  // with go-flags tags. If the --help flag has been passed, the struct is
   238  // described back to the terminal and the program exits using os.Exit.
   239  func ParseCLIConfig(cfg any) error {
   240  	preParser := flags.NewParser(cfg, flags.HelpFlag|flags.PassDoubleDash)
   241  	_, flagerr := preParser.Parse()
   242  
   243  	if flagerr != nil {
   244  		e, ok := flagerr.(*flags.Error)
   245  		if !ok || e.Type != flags.ErrHelp {
   246  			preParser.WriteHelp(os.Stderr)
   247  		}
   248  		if ok && e.Type == flags.ErrHelp {
   249  			preParser.WriteHelp(os.Stdout)
   250  			os.Exit(0)
   251  		}
   252  		return flagerr
   253  	}
   254  	return nil
   255  }
   256  
   257  // ResolveCLIConfigPaths resolves the app data directory path and the
   258  // configuration file path from the CLI config, (presumably parsed with
   259  // ParseCLIConfig).
   260  func ResolveCLIConfigPaths(cfg *Config) (appData, configPath string) {
   261  	// If the app directory has been changed, replace shortcut chars such
   262  	// as "~" with the full path.
   263  	if cfg.AppData != defaultApplicationDirectory {
   264  		cfg.AppData = dex.CleanAndExpandPath(cfg.AppData)
   265  		// If the app directory has been changed, but the config file path hasn't,
   266  		// reform the config file path with the new directory.
   267  		if cfg.ConfigPath == defaultConfigPath {
   268  			cfg.ConfigPath = filepath.Join(cfg.AppData, configFilename)
   269  		}
   270  	}
   271  	cfg.ConfigPath = dex.CleanAndExpandPath(cfg.ConfigPath)
   272  	return cfg.AppData, cfg.ConfigPath
   273  }
   274  
   275  // ParseFileConfig parses the INI file into the provided struct with go-flags
   276  // tags. The CLI args are then parsed, and take precedence over the file values.
   277  func ParseFileConfig(path string, cfg any) error {
   278  	parser := flags.NewParser(cfg, flags.Default)
   279  	err := flags.NewIniParser(parser).ParseFile(path)
   280  	if err != nil {
   281  		if _, ok := err.(*os.PathError); !ok {
   282  			fmt.Fprintln(os.Stderr, err)
   283  			parser.WriteHelp(os.Stderr)
   284  			return err
   285  		}
   286  		// Missing file is not an error.
   287  	}
   288  
   289  	// Parse command line options again to ensure they take precedence.
   290  	_, err = parser.Parse()
   291  	if err != nil {
   292  		if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
   293  			parser.WriteHelp(os.Stderr)
   294  		}
   295  		return err
   296  	}
   297  	return nil
   298  }
   299  
   300  // ResolveConfig sets derivative fields of the Config struct using the specified
   301  // app data directory (presumably returned from ResolveCLIConfigPaths). Some
   302  // unset values are given defaults.
   303  func ResolveConfig(appData string, cfg *Config) error {
   304  	if cfg.Simnet && cfg.Testnet {
   305  		return fmt.Errorf("simnet and testnet cannot both be specified")
   306  	}
   307  
   308  	cfg.AppData = appData
   309  
   310  	var defaultDBPath, defaultLogPath, defaultMMEventLogDBPath, defaultMMConfigPath string
   311  	switch {
   312  	case cfg.Testnet:
   313  		cfg.Net = dex.Testnet
   314  		defaultDBPath, defaultLogPath, defaultMMEventLogDBPath, defaultMMConfigPath = setNet(appData, "testnet")
   315  	case cfg.Simnet:
   316  		cfg.Net = dex.Simnet
   317  		defaultDBPath, defaultLogPath, defaultMMEventLogDBPath, defaultMMConfigPath = setNet(appData, "simnet")
   318  	default:
   319  		cfg.Net = dex.Mainnet
   320  		defaultDBPath, defaultLogPath, defaultMMEventLogDBPath, defaultMMConfigPath = setNet(appData, "mainnet")
   321  	}
   322  	defaultHost := DefaultHostByNetwork(cfg.Net)
   323  
   324  	// If web or RPC server addresses not set, use network specific
   325  	// defaults
   326  	if cfg.WebAddr == "" {
   327  		cfg.WebAddr = net.JoinHostPort(defaultHost, defaultWebPort)
   328  	}
   329  	if cfg.RPCAddr == "" {
   330  		cfg.RPCAddr = net.JoinHostPort(defaultHost, defaultRPCPort)
   331  	}
   332  
   333  	if cfg.RPCCert == "" {
   334  		cfg.RPCCert = filepath.Join(appData, defaultRPCCertFile)
   335  	}
   336  
   337  	if cfg.RPCKey == "" {
   338  		cfg.RPCKey = filepath.Join(appData, defaultRPCKeyFile)
   339  	}
   340  
   341  	if cfg.DBPath == "" {
   342  		cfg.DBPath = defaultDBPath
   343  	}
   344  
   345  	if cfg.LogPath == "" {
   346  		cfg.LogPath = defaultLogPath
   347  	}
   348  
   349  	if cfg.MMConfig.BotConfigPath == "" {
   350  		cfg.MMConfig.BotConfigPath = defaultMMConfigPath
   351  	}
   352  
   353  	if cfg.MMConfig.EventLogDBPath == "" {
   354  		cfg.MMConfig.EventLogDBPath = defaultMMEventLogDBPath
   355  	}
   356  
   357  	return nil
   358  }
   359  
   360  // userAppVersion returns a simple user-facing version: maj.min.patch.
   361  func userAppVersion(fullVersion string) string {
   362  	parts := strings.Split(fullVersion, "-")
   363  	return parts[0]
   364  }
   365  
   366  // setNet sets the filepath for the network directory and some network specific
   367  // files. It returns a suggested path for the database file and a log file. If
   368  // using a file rotator, the directory of the log filepath as parsed  by
   369  // filepath.Dir is suitable for use.
   370  func setNet(applicationDirectory, net string) (dbPath, logPath, mmEventDBPath, mmCfgPath string) {
   371  	netDirectory := filepath.Join(applicationDirectory, net)
   372  	logDirectory := filepath.Join(netDirectory, "logs")
   373  	logFilename := filepath.Join(logDirectory, "dexc.log")
   374  	mmEventLogDBFilename := filepath.Join(netDirectory, "eventlog.db")
   375  	mmCfgFilename := filepath.Join(netDirectory, "mm_cfg.json")
   376  	err := os.MkdirAll(netDirectory, 0700)
   377  	if err != nil {
   378  		fmt.Fprintf(os.Stderr, "failed to create net directory: %v\n", err)
   379  		os.Exit(1)
   380  	}
   381  	err = os.MkdirAll(logDirectory, 0700)
   382  	if err != nil {
   383  		fmt.Fprintf(os.Stderr, "failed to create log directory: %v\n", err)
   384  		os.Exit(1)
   385  	}
   386  	return filepath.Join(netDirectory, "dexc.db"), logFilename, mmEventLogDBFilename, mmCfgFilename
   387  }
   388  
   389  // DefaultHostByNetwork accepts configured network and returns the network
   390  // specific default host
   391  func DefaultHostByNetwork(network dex.Network) string {
   392  	switch network {
   393  	case dex.Testnet:
   394  		return defaultTestnetHost
   395  	case dex.Simnet:
   396  		return defaultSimnetHost
   397  	default:
   398  		return defaultMainnetHost
   399  	}
   400  }