diff --git a/Release.md b/Release.md index 94cba964..07c58d4a 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,3 @@ -### Bug Fixes +## Features -* **VirtualNet:** Resolved various issues related to connection handling, TUN device management, and stability in the virtual network feature. \ No newline at end of file +* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter. \ No newline at end of file diff --git a/pkg/config/load.go b/pkg/config/load.go index fa394dda..bb050b40 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -111,6 +111,33 @@ func LoadConfigureFromFile(path string, c any, strict bool) error { return LoadConfigure(content, c, strict) } +// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling +// This function handles both cases efficiently: with or without dot fields +func parseYAMLWithDotFieldsHandling(content []byte, target any) error { + var temp any + if err := yaml.Unmarshal(content, &temp); err != nil { + return err + } + + // Remove dot fields if it's a map + if tempMap, ok := temp.(map[string]any); ok { + for key := range tempMap { + if strings.HasPrefix(key, ".") { + delete(tempMap, key) + } + } + } + + // Convert to JSON and decode with strict validation + jsonBytes, err := json.Marshal(temp) + if err != nil { + return err + } + decoder := json.NewDecoder(bytes.NewReader(jsonBytes)) + decoder.DisallowUnknownFields() + return decoder.Decode(target) +} + // LoadConfigure loads configuration from bytes and unmarshal into c. // Now it supports json, yaml and toml format. func LoadConfigure(b []byte, c any, strict bool) error { @@ -134,10 +161,13 @@ func LoadConfigure(b []byte, c any, strict bool) error { } return decoder.Decode(c) } - // It wasn't JSON. Unmarshal as YAML. + + // Handle YAML content if strict { - return yaml.UnmarshalStrict(b, c) + // In strict mode, always use our custom handler to support YAML merge + return parseYAMLWithDotFieldsHandling(b, c) } + // Non-strict mode, parse normally return yaml.Unmarshal(b, c) } diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go index 980a332a..95d6101e 100644 --- a/pkg/config/load_test.go +++ b/pkg/config/load_test.go @@ -187,3 +187,122 @@ unixPath = "/tmp/uds.sock" err = LoadConfigure([]byte(pluginStr), &clientCfg, true) require.Error(err) } + +// TestYAMLMergeInStrictMode tests that YAML merge functionality works +// even in strict mode by properly handling dot-prefixed fields +func TestYAMLMergeInStrictMode(t *testing.T) { + require := require.New(t) + + yamlContent := ` +serverAddr: "127.0.0.1" +serverPort: 7000 + +.common: &common + type: stcp + secretKey: "test-secret" + localIP: 127.0.0.1 + transport: + useEncryption: true + useCompression: true + +proxies: +- name: ssh + localPort: 22 + <<: *common +- name: web + localPort: 80 + <<: *common +` + + clientCfg := v1.ClientConfig{} + // This should work in strict mode + err := LoadConfigure([]byte(yamlContent), &clientCfg, true) + require.NoError(err) + + // Verify the merge worked correctly + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Equal(7000, clientCfg.ServerPort) + require.Len(clientCfg.Proxies, 2) + + // Check first proxy + sshProxy := clientCfg.Proxies[0].ProxyConfigurer + require.Equal("ssh", sshProxy.GetBaseConfig().Name) + require.Equal("stcp", sshProxy.GetBaseConfig().Type) + + // Check second proxy + webProxy := clientCfg.Proxies[1].ProxyConfigurer + require.Equal("web", webProxy.GetBaseConfig().Name) + require.Equal("stcp", webProxy.GetBaseConfig().Type) +} + +// TestOptimizedYAMLProcessing tests the optimization logic for YAML processing +func TestOptimizedYAMLProcessing(t *testing.T) { + require := require.New(t) + + yamlWithDotFields := []byte(` +serverAddr: "127.0.0.1" +.common: &common + type: stcp +proxies: +- name: test + <<: *common +`) + + yamlWithoutDotFields := []byte(` +serverAddr: "127.0.0.1" +proxies: +- name: test + type: tcp + localPort: 22 +`) + + // Test that YAML without dot fields works in strict mode + clientCfg := v1.ClientConfig{} + err := LoadConfigure(yamlWithoutDotFields, &clientCfg, true) + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Len(clientCfg.Proxies, 1) + require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name) + + // Test that YAML with dot fields still works in strict mode + err = LoadConfigure(yamlWithDotFields, &clientCfg, true) + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Len(clientCfg.Proxies, 1) + require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name) + require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type) +} + +// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types +func TestYAMLEdgeCases(t *testing.T) { + require := require.New(t) + + // Test array at root (should fail for frp config) + arrayYAML := []byte(` +- item1 +- item2 +`) + clientCfg := v1.ClientConfig{} + err := LoadConfigure(arrayYAML, &clientCfg, true) + require.Error(err) // Should fail because ClientConfig expects an object + + // Test scalar at root (should fail for frp config) + scalarYAML := []byte(`"just a string"`) + err = LoadConfigure(scalarYAML, &clientCfg, true) + require.Error(err) // Should fail because ClientConfig expects an object + + // Test empty object (should work) + emptyYAML := []byte(`{}`) + err = LoadConfigure(emptyYAML, &clientCfg, true) + require.NoError(err) + + // Test nested structure without dots (should work) + nestedYAML := []byte(` +serverAddr: "127.0.0.1" +serverPort: 7000 +`) + err = LoadConfigure(nestedYAML, &clientCfg, true) + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Equal(7000, clientCfg.ServerPort) +} diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 8fa6bc19..966a942f 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.62.1" +var version = "0.63.0" func Full() string { return version