decred.org/dcrdex@v1.0.3/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 Language: cfg.Language, 202 } 203 } 204 205 // Core creates a core.Core configuration. This is a Config method 206 // instead of a CoreConfig method because Language is an app-level setting used 207 // by both core and rpcserver. 208 func (cfg *Config) Core(log dex.Logger) *core.Config { 209 return &core.Config{ 210 DBPath: cfg.DBPath, 211 Net: cfg.Net, 212 Logger: log, 213 Onion: cfg.Onion, 214 TorProxy: cfg.TorProxy, 215 TorIsolation: cfg.TorIsolation, 216 Language: cfg.Language, 217 UnlockCoinsOnLogin: cfg.UnlockCoinsOnLogin, 218 NoAutoWalletLock: cfg.NoAutoWalletLock, 219 NoAutoDBBackup: cfg.NoAutoDBBackup, 220 ExtensionModeFile: cfg.ExtensionModeFile, 221 TheOneHost: cfg.TheOneHost, 222 PruneArchive: cfg.PruneArchive, 223 } 224 } 225 226 var DefaultConfig = Config{ 227 AppData: defaultApplicationDirectory, 228 ConfigPath: defaultConfigPath, 229 LogConfig: LogConfig{DebugLevel: defaultLogLevel}, 230 RPCConfig: RPCConfig{ 231 CertHosts: []string{defaultTestnetHost, defaultSimnetHost, defaultMainnetHost}, 232 }, 233 } 234 235 // ParseCLIConfig parses the command-line arguments into the provided struct 236 // with go-flags tags. If the --help flag has been passed, the struct is 237 // described back to the terminal and the program exits using os.Exit. 238 func ParseCLIConfig(cfg any) error { 239 preParser := flags.NewParser(cfg, flags.HelpFlag|flags.PassDoubleDash) 240 _, flagerr := preParser.Parse() 241 242 if flagerr != nil { 243 e, ok := flagerr.(*flags.Error) 244 if !ok || e.Type != flags.ErrHelp { 245 preParser.WriteHelp(os.Stderr) 246 } 247 if ok && e.Type == flags.ErrHelp { 248 preParser.WriteHelp(os.Stdout) 249 os.Exit(0) 250 } 251 return flagerr 252 } 253 return nil 254 } 255 256 // ResolveCLIConfigPaths resolves the app data directory path and the 257 // configuration file path from the CLI config, (presumably parsed with 258 // ParseCLIConfig). 259 func ResolveCLIConfigPaths(cfg *Config) (appData, configPath string) { 260 // If the app directory has been changed, replace shortcut chars such 261 // as "~" with the full path. 262 if cfg.AppData != defaultApplicationDirectory { 263 cfg.AppData = dex.CleanAndExpandPath(cfg.AppData) 264 // If the app directory has been changed, but the config file path hasn't, 265 // reform the config file path with the new directory. 266 if cfg.ConfigPath == defaultConfigPath { 267 cfg.ConfigPath = filepath.Join(cfg.AppData, configFilename) 268 } 269 } 270 cfg.ConfigPath = dex.CleanAndExpandPath(cfg.ConfigPath) 271 return cfg.AppData, cfg.ConfigPath 272 } 273 274 // ParseFileConfig parses the INI file into the provided struct with go-flags 275 // tags. The CLI args are then parsed, and take precedence over the file values. 276 func ParseFileConfig(path string, cfg any) error { 277 parser := flags.NewParser(cfg, flags.Default) 278 err := flags.NewIniParser(parser).ParseFile(path) 279 if err != nil { 280 if _, ok := err.(*os.PathError); !ok { 281 fmt.Fprintln(os.Stderr, err) 282 parser.WriteHelp(os.Stderr) 283 return err 284 } 285 // Missing file is not an error. 286 } 287 288 // Parse command line options again to ensure they take precedence. 289 _, err = parser.Parse() 290 if err != nil { 291 if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp { 292 parser.WriteHelp(os.Stderr) 293 } 294 return err 295 } 296 return nil 297 } 298 299 // ResolveConfig sets derivative fields of the Config struct using the specified 300 // app data directory (presumably returned from ResolveCLIConfigPaths). Some 301 // unset values are given defaults. 302 func ResolveConfig(appData string, cfg *Config) error { 303 if cfg.Simnet && cfg.Testnet { 304 return fmt.Errorf("simnet and testnet cannot both be specified") 305 } 306 307 cfg.AppData = appData 308 309 var defaultDBPath, defaultLogPath, defaultMMEventLogDBPath, defaultMMConfigPath string 310 switch { 311 case cfg.Testnet: 312 cfg.Net = dex.Testnet 313 defaultDBPath, defaultLogPath, defaultMMEventLogDBPath, defaultMMConfigPath = setNet(appData, "testnet") 314 case cfg.Simnet: 315 cfg.Net = dex.Simnet 316 defaultDBPath, defaultLogPath, defaultMMEventLogDBPath, defaultMMConfigPath = setNet(appData, "simnet") 317 default: 318 cfg.Net = dex.Mainnet 319 defaultDBPath, defaultLogPath, defaultMMEventLogDBPath, defaultMMConfigPath = setNet(appData, "mainnet") 320 } 321 defaultHost := DefaultHostByNetwork(cfg.Net) 322 323 // If web or RPC server addresses not set, use network specific 324 // defaults 325 if cfg.WebAddr == "" { 326 cfg.WebAddr = net.JoinHostPort(defaultHost, defaultWebPort) 327 } 328 if cfg.RPCAddr == "" { 329 cfg.RPCAddr = net.JoinHostPort(defaultHost, defaultRPCPort) 330 } 331 332 if cfg.RPCCert == "" { 333 cfg.RPCCert = filepath.Join(appData, defaultRPCCertFile) 334 } 335 336 if cfg.RPCKey == "" { 337 cfg.RPCKey = filepath.Join(appData, defaultRPCKeyFile) 338 } 339 340 if cfg.DBPath == "" { 341 cfg.DBPath = defaultDBPath 342 } 343 344 if cfg.LogPath == "" { 345 cfg.LogPath = defaultLogPath 346 } 347 348 if cfg.MMConfig.BotConfigPath == "" { 349 cfg.MMConfig.BotConfigPath = defaultMMConfigPath 350 } 351 352 if cfg.MMConfig.EventLogDBPath == "" { 353 cfg.MMConfig.EventLogDBPath = defaultMMEventLogDBPath 354 } 355 return nil 356 } 357 358 // setNet sets the filepath for the network directory and some network specific 359 // files. It returns a suggested path for the database file and a log file. If 360 // using a file rotator, the directory of the log filepath as parsed by 361 // filepath.Dir is suitable for use. 362 func setNet(applicationDirectory, net string) (dbPath, logPath, mmEventDBPath, mmCfgPath string) { 363 netDirectory := filepath.Join(applicationDirectory, net) 364 logDirectory := filepath.Join(netDirectory, "logs") 365 logFilename := filepath.Join(logDirectory, "dexc.log") 366 mmEventLogDBFilename := filepath.Join(netDirectory, "eventlog.db") 367 mmCfgFilename := filepath.Join(netDirectory, "mm_cfg.json") 368 err := os.MkdirAll(netDirectory, 0700) 369 if err != nil { 370 fmt.Fprintf(os.Stderr, "failed to create net directory: %v\n", err) 371 os.Exit(1) 372 } 373 err = os.MkdirAll(logDirectory, 0700) 374 if err != nil { 375 fmt.Fprintf(os.Stderr, "failed to create log directory: %v\n", err) 376 os.Exit(1) 377 } 378 return filepath.Join(netDirectory, "dexc.db"), logFilename, mmEventLogDBFilename, mmCfgFilename 379 } 380 381 // DefaultHostByNetwork accepts configured network and returns the network 382 // specific default host 383 func DefaultHostByNetwork(network dex.Network) string { 384 switch network { 385 case dex.Testnet: 386 return defaultTestnetHost 387 case dex.Simnet: 388 return defaultSimnetHost 389 default: 390 return defaultMainnetHost 391 } 392 }