diff --git a/config.yml b/config.yml index 0476eef1..b6f684d7 100644 --- a/config.yml +++ b/config.yml @@ -1,5 +1,3 @@ - - ################################################ # global configuration ################################################ diff --git a/pkgconfig/config.go b/pkgconfig/config.go index 5ead8ba6..d1e6f847 100644 --- a/pkgconfig/config.go +++ b/pkgconfig/config.go @@ -1,6 +1,7 @@ package pkgconfig import ( + "fmt" "io" "os" "reflect" @@ -139,6 +140,109 @@ func CheckConfig(userConfigPath string) error { } } + // detect bad keyword position + err = checkKeywordsPosition(userConfigMap, defaultConfigMap, defaultConfigMap, "") + if err != nil { + return err + } + return nil +} + +func checkKeywordsPosition(nextUserCfg, nextDefCfg map[string]interface{}, defaultConf map[string]interface{}, sectionName string) error { + for k, v := range nextUserCfg { + // Check if the key is present in the default config + if _, ok := nextDefCfg[k]; !ok { + if sectionName == "" { + return errors.Errorf("invalid key `%s` at root", k) + } + return errors.Errorf("invalid key `%s` in section `%s`", k, sectionName) + } + + // If the value is a map, recursively check for invalid keywords + // Recursive call ? + val := reflect.ValueOf(v) + if val.Kind() == reflect.Map { + nextSectionName := fmt.Sprintf("%s.%s", sectionName, k) + if err := checkKeywordsPosition(v.(map[string]interface{}), nextDefCfg[k].(map[string]interface{}), defaultConf, nextSectionName); err != nil { + return err + } + } + + // If the value is a slice and we are in the multiplexer part + // Multiplixer part is dynamic, we need specific function to check it + if val.Kind() == reflect.Slice && sectionName == ".multiplexer" { + if err := checkMultiplexerConfig(val, nextDefCfg[k].([]interface{}), defaultConf, k); err != nil { + return err + } + } + + } + return nil +} + +func checkMultiplexerConfig(currentVal reflect.Value, currentRef []interface{}, defaultConf map[string]interface{}, k string) error { + refLoggers := defaultConf[KeyLoggers].(map[string]interface{}) + refCollectors := defaultConf[KeyCollectors].(map[string]interface{}) + refTransforms := defaultConf["collectors-transformers"].(map[string]interface{}) + + // iter over the slice + for pos, item := range currentVal.Interface().([]interface{}) { + valReflect := reflect.ValueOf(item) + refItem := currentRef[0].(map[string]interface{}) + if valReflect.Kind() == reflect.Map { + for _, key := range valReflect.MapKeys() { + strKey := key.Interface().(string) + mapVal := valReflect.MapIndex(key) + + // First, check in the initial configuration reference. + // If not found, then look in the logger and collector references. + if _, ok := refItem[strKey]; !ok { + // we are in routes section ? + if !(k == KeyCollectors || k == KeyLoggers) { + return errors.Errorf("invalid `%s` in `%s` list at position %d", strKey, k, pos) + } + + // Check if the key exists in neither loggers nor collectors + loggerExists := refLoggers[strKey] != nil + collectorExists := refCollectors[strKey] != nil + if !loggerExists && !collectorExists { + return errors.Errorf("invalid `%s` in `%s` list at position %d", strKey, k, pos) + } + + // check logger + if k == KeyLoggers || k == KeyCollectors { + nextSectionName := fmt.Sprintf("%s[%d].%s", k, pos, strKey) + refMap := refLoggers + if k == KeyCollectors { + refMap = refCollectors + } + + // Type assertion to check if the value is a map + if value, ok := mapVal.Interface().(map[string]interface{}); ok { + if err := checkKeywordsPosition(value, refMap[strKey].(map[string]interface{}), defaultConf, nextSectionName); err != nil { + return err + } + } else { + return errors.Errorf("invalid `%s` value in `%s` list at position %d", strKey, k, pos) + } + } + } + + // Check transforms section + // Type assertion to check if the value is a map + if strKey == "transforms" { + nextSectionName := fmt.Sprintf("%s.%s", k, strKey) + if value, ok := mapVal.Interface().(map[string]interface{}); ok { + if err := checkKeywordsPosition(value, refTransforms, defaultConf, nextSectionName); err != nil { + return err + } + } else { + return errors.Errorf("invalid `%s` value in `%s` list at position %d", strKey, k, pos) + } + } + } + } + } return nil } diff --git a/pkgconfig/config_test.go b/pkgconfig/config_test.go index 1a399497..0cb0c8c6 100644 --- a/pkgconfig/config_test.go +++ b/pkgconfig/config_test.go @@ -93,3 +93,92 @@ multiplexer: t.Errorf("Expected error %v, but got %v", expectedError, err) } } + +// Keywork exist but not at the good position +func TestConfig_CheckConfig_BadKeywordPosition(t *testing.T) { + userConfigFile, err := os.CreateTemp("", "user-config.yaml") + if err != nil { + t.Fatal("Error creating temporary file:", err) + } + defer os.Remove(userConfigFile.Name()) + defer userConfigFile.Close() + + userConfigContent := ` +global: + trace: false + logger: bad-position +` + err = os.WriteFile(userConfigFile.Name(), []byte(userConfigContent), 0644) + if err != nil { + t.Fatal("Error writing to user configuration file:", err) + } + if err := CheckConfig(userConfigFile.Name()); err == nil { + t.Errorf("Expected error, but got %v", err) + } +} + +// Valid multiplexer configuration +func TestConfig_CheckMultiplexerConfig_Valid(t *testing.T) { + userConfigFile, err := os.CreateTemp("", "user-config.yaml") + if err != nil { + t.Fatal("Error creating temporary file:", err) + } + defer os.Remove(userConfigFile.Name()) + defer userConfigFile.Close() + + userConfigContent := ` +multiplexer: + collectors: + - name: tap + dnstap: + listen-ip: 0.0.0.0 + listen-port: 6000 + transforms: + normalize: + qname-lowercase: false + loggers: + - name: console + stdout: + mode: text + routes: + - from: [ tap ] + to: [ console ] +` + err = os.WriteFile(userConfigFile.Name(), []byte(userConfigContent), 0644) + if err != nil { + t.Fatal("Error writing to user configuration file:", err) + } + if err := CheckConfig(userConfigFile.Name()); err != nil { + t.Errorf("failed: Unexpected error: %v", err) + } +} + +// Invalid multiplexer configuration +func TestConfig_CheckMultiplexerConfig_Invalid(t *testing.T) { + userConfigFile, err := os.CreateTemp("", "user-config.yaml") + if err != nil { + t.Fatal("Error creating temporary file:", err) + } + defer os.Remove(userConfigFile.Name()) + defer userConfigFile.Close() + + userConfigContent := ` +global: + trace: false +multiplexer: +- name: block + dnstap: + listen-ip: 0.0.0.0 + transforms: + normalize: + qname-lowercase: true +` + + err = os.WriteFile(userConfigFile.Name(), []byte(userConfigContent), 0644) + if err != nil { + t.Fatal("Error writing to user configuration file:", err) + } + if err := CheckConfig(userConfigFile.Name()); err == nil { + t.Errorf("Expected error, but got %v", err) + } +} diff --git a/pkgconfig/constants.go b/pkgconfig/constants.go index f33841d8..7e8a90f7 100644 --- a/pkgconfig/constants.go +++ b/pkgconfig/constants.go @@ -12,6 +12,9 @@ const ( AnyIP = "0.0.0.0" HTTPOK = "HTTP/1.1 200 OK\r\n\r\n" + KeyCollectors = "collectors" + KeyLoggers = "loggers" + ValidDomain = "dnscollector.dev." BadDomainLabel = "ultramegaverytoolonglabel-ultramegaverytoolonglabel-ultramegaverytoolonglabel.dnscollector.dev." badLongLabel = "ultramegaverytoolonglabel-ultramegaverytoolonglabel-"