diff --git a/aperture.go b/aperture.go index 8434d9a..b3d4ef1 100644 --- a/aperture.go +++ b/aperture.go @@ -2,6 +2,7 @@ package aperture import ( "crypto/tls" + "errors" "fmt" "io" "io/ioutil" @@ -11,9 +12,11 @@ import ( "sync" "time" + flags "github.com/jessevdk/go-flags" "github.com/lightninglabs/aperture/auth" "github.com/lightninglabs/aperture/mint" "github.com/lightninglabs/aperture/proxy" + "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/cert" "github.com/lightningnetwork/lnd/lnrpc" @@ -71,10 +74,21 @@ var ( func Main() { // TODO: Prevent from running twice. err := run() - if err != nil { - _, _ = fmt.Fprintln(os.Stderr, err) - os.Exit(1) + + // Unwrap our error and check whether help was requested from our flag + // library. If the error is not wrapped, Unwrap returns nil. It is + // still safe to check the type of this nil error. + flagErr, isFlagErr := errors.Unwrap(err).(*flags.Error) + isHelpErr := isFlagErr && flagErr.Type == flags.ErrHelp + + // If we got a nil error, or help was requested, just exit. + if err == nil || isHelpErr { + os.Exit(0) } + + // Print any other non-help related errors. + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) } // run sets up the proxy server and runs it. This function blocks until a @@ -88,10 +102,9 @@ func run() error { } // Next, parse configuration file and set up logging. - configFile := filepath.Join(apertureDataDir, defaultConfigFilename) - cfg, err := getConfig(configFile) + cfg, err := getConfig() if err != nil { - return fmt.Errorf("unable to parse config file: %v", err) + return fmt.Errorf("unable to parse config file: %w", err) } err = setupLogging(cfg, interceptor) if err != nil { @@ -305,22 +318,66 @@ func fileExists(name string) bool { // getConfig loads and parses the configuration file then checks it for valid // content. -func getConfig(configFile string) (*Config, error) { +func getConfig() (*Config, error) { + // Pre-parse command line flags to determine whether we've been pointed + // to a custom config file. cfg := &Config{} - b, err := ioutil.ReadFile(configFile) - if err != nil { - return nil, err - } - err = yaml.Unmarshal(b, cfg) - if err != nil { + if _, err := flags.Parse(cfg); err != nil { return nil, err } + // If a custom config file is provided, we require that it exists. + var mustExist bool + + configFile := filepath.Join(apertureDataDir, defaultConfigFilename) + if cfg.ConfigFile != "" { + configFile = lnd.CleanAndExpandPath(cfg.ConfigFile) + mustExist = true + } + + // Read our config file, either from the custom path provided or our + // default location. + b, err := ioutil.ReadFile(configFile) + switch { + // If the file was found, unmarshal it. + case err == nil: + err = yaml.Unmarshal(b, cfg) + if err != nil { + return nil, err + } + + // If the error is unrelated to the existence of the file, we must + // always return it. + case !os.IsNotExist(err): + return nil, err + + // If we require that the config file exists and we got an error + // related to file existence, we must fail. + case mustExist && os.IsNotExist(err): + return nil, fmt.Errorf("config file: %v must exist: %w", + configFile, err) + } + + // Finally, parse the remaining command line options again to ensure + // they take precedence. + if _, err := flags.Parse(cfg); err != nil { + return nil, err + } + + // Clean and expand our cert and macaroon paths. + cfg.Authenticator.TLSPath = lnd.CleanAndExpandPath( + cfg.Authenticator.TLSPath, + ) + cfg.Authenticator.MacDir = lnd.CleanAndExpandPath( + cfg.Authenticator.MacDir, + ) + // Then check the configuration that we got from the config file, all // required values need to be set at this point. - if cfg.ListenAddr == "" { - return nil, fmt.Errorf("missing listen address for server") + if err := cfg.validate(); err != nil { + return nil, err } + return cfg, nil } diff --git a/config.go b/config.go index 42a51f6..cfd9b69 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,9 @@ package aperture import ( + "errors" + "fmt" + "github.com/btcsuite/btcutil" "github.com/lightninglabs/aperture/proxy" ) @@ -26,13 +29,34 @@ type AuthConfig struct { // LndHost is the hostname of the LND instance to connect to. LndHost string `long:"lndhost" description:"Hostname of the LND instance to connect to"` - TLSPath string `long:"tlspath"` + TLSPath string `long:"tlspath" description:"Path to LND instance's tls certificate"` - MacDir string `long:"macdir"` + MacDir string `long:"macdir" description:"Directory containing LND instance's macaroons"` - Network string `long:"network"` + Network string `long:"network" description:"The network LND is connected to." choice:"regtest" choice:"simnet" choice:"testnet" choice:"mainnet"` - Disable bool `long:"disable"` + Disable bool `long:"disable" description:"Whether to disable LND auth."` +} + +func (a *AuthConfig) validate() error { + // If we're disabled, we don't mind what these values are. + if a.Disable { + return nil + } + + if a.LndHost == "" { + return errors.New("lnd host required") + } + + if a.TLSPath == "" { + return errors.New("lnd tls required") + } + + if a.MacDir == "" { + return errors.New("lnd mac dir required") + } + + return nil } type TorConfig struct { @@ -67,11 +91,11 @@ type Config struct { // directory defined by StaticRoot. ServeStatic bool `long:"servestatic" description:"Flag to enable or disable static content serving."` - Etcd *EtcdConfig `long:"etcd" description:"Configuration for the etcd instance backing the proxy."` + Etcd *EtcdConfig `group:"etcd" namespace:"etcd"` - Authenticator *AuthConfig `long:"authenticator" description:"Configuration for the authenticator."` + Authenticator *AuthConfig `group:"authenticator" namespace:"authenticator"` - Tor *TorConfig `long:"tor" description:"Configuration for the Tor instance backing the proxy."` + Tor *TorConfig `group:"tor" namespace:"tor"` // Services is a list of JSON objects in string format, which specify // each backend service to Aperture. @@ -80,4 +104,21 @@ type Config struct { // DebugLevel is a string defining the log level for the service either // for all subsystems the same or individual level by subsystem. DebugLevel string `long:"debuglevel" description:"Debug level for the Aperture application and its subsystems."` + + // ConfigFile points aperture to an alternative config file. + ConfigFile string `long:"configfile" description:"Custom path to a config file."` +} + +func (c *Config) validate() error { + if c.Authenticator != nil { + if err := c.Authenticator.validate(); err != nil { + return err + } + } + + if c.ListenAddr == "" { + return fmt.Errorf("missing listen address for server") + } + + return nil } diff --git a/go.mod b/go.mod index 09586e1..a5972fc 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210706234807-aaf03fee735a github.com/fortytw2/leaktest v1.3.0 github.com/golang/protobuf v1.5.2 + github.com/jessevdk/go-flags v1.4.0 github.com/lightninglabs/lndclient v0.12.0-9 github.com/lightningnetwork/lnd v0.13.0-beta.rc5.0.20210728112744-ebabda671786 github.com/lightningnetwork/lnd/cert v1.0.3