diff --git a/config.go b/config.go index 12cb82e7e..e89f37bae 100644 --- a/config.go +++ b/config.go @@ -2111,3 +2111,96 @@ func checkEstimateMode(estimateMode string) error { return fmt.Errorf("estimatemode must be one of the following: %v", bitcoindEstimateModes[:]) } + +// configToFlatMap converts the given config struct into a flat map of key/value +// pairs using the dot notation we are used to from the config file or command +// line flags. +func configToFlatMap(cfg Config) (map[string]string, error) { + result := make(map[string]string) + + // redact is the helper function that redacts sensitive values like + // passwords. + redact := func(key, value string) string { + sensitiveKeySuffixes := []string{ + "pass", + "password", + "dsn", + } + for _, suffix := range sensitiveKeySuffixes { + if strings.HasSuffix(key, suffix) { + return "[redacted]" + } + } + + return value + } + + // printConfig is the helper function that goes into nested structs + // recursively. Because we call it recursively, we need to declare it + // before we define it. + var printConfig func(reflect.Value, string) + printConfig = func(obj reflect.Value, prefix string) { + // Turn struct pointers into the actual struct, so we can + // iterate over the fields as we would with a struct value. + if obj.Kind() == reflect.Ptr { + obj = obj.Elem() + } + + // Abort on nil values. + if !obj.IsValid() { + return + } + + // Loop over all fields of the struct and inspect the type. + for i := 0; i < obj.NumField(); i++ { + field := obj.Field(i) + fieldType := obj.Type().Field(i) + + longName := fieldType.Tag.Get("long") + namespace := fieldType.Tag.Get("namespace") + group := fieldType.Tag.Get("group") + switch { + // We have a long name defined, this is a config value. + case longName != "": + key := longName + if prefix != "" { + key = prefix + "." + key + } + + // Add the value directly to the flattened map. + result[key] = redact(key, fmt.Sprintf( + "%v", field.Interface(), + )) + + // We have no long name but a namespace, this is a + // nested struct. + case longName == "" && namespace != "": + key := namespace + if prefix != "" { + key = prefix + "." + key + } + + printConfig(field, key) + + // Just a group means this is a dummy struct to house + // multiple config values, the group name doesn't go + // into the final field name. + case longName == "" && group != "": + printConfig(field, prefix) + + // Anonymous means embedded struct. We need to recurse + // into it but without adding anything to the prefix. + case fieldType.Anonymous: + printConfig(field, prefix) + + default: + continue + } + } + } + + // Turn the whole config struct into a flat map. + printConfig(reflect.ValueOf(cfg), "") + + return result, nil +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 000000000..1f1740a0d --- /dev/null +++ b/config_test.go @@ -0,0 +1,48 @@ +package lnd + +import ( + "fmt" + "testing" + + "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/routing" + "github.com/stretchr/testify/require" +) + +var ( + testPassword = "testpassword" + redactedPassword = "[redacted]" +) + +// TestConfigToFlatMap tests that the configToFlatMap function works as +// expected on the default configuration. +func TestConfigToFlatMap(t *testing.T) { + cfg := DefaultConfig() + cfg.BitcoindMode.RPCPass = testPassword + cfg.BtcdMode.RPCPass = testPassword + cfg.Tor.Password = testPassword + cfg.DB.Etcd.Pass = testPassword + cfg.DB.Postgres.Dsn = testPassword + + result, err := configToFlatMap(cfg) + require.NoError(t, err) + + // Pick a couple of random values to check. + require.Equal(t, DefaultLndDir, result["lnddir"]) + require.Equal( + t, fmt.Sprintf("%v", chainreg.DefaultBitcoinTimeLockDelta), + result["bitcoin.timelockdelta"], + ) + require.Equal( + t, fmt.Sprintf("%v", routing.DefaultAprioriWeight), + result["routerrpc.apriori.weight"], + ) + require.Contains(t, result, "routerrpc.routermacaroonpath") + + // Check that sensitive values are not included. + require.Equal(t, redactedPassword, result["bitcoind.rpcpass"]) + require.Equal(t, redactedPassword, result["btcd.rpcpass"]) + require.Equal(t, redactedPassword, result["tor.password"]) + require.Equal(t, redactedPassword, result["db.etcd.pass"]) + require.Equal(t, redactedPassword, result["db.postgres.dsn"]) +}