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 }