// Copyright 2023 The frp Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "bytes" "encoding/json" "fmt" "os" "path/filepath" "strings" "text/template" toml "github.com/pelletier/go-toml/v2" "github.com/samber/lo" "gopkg.in/ini.v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/yaml" "github.com/fatedier/frp/pkg/config/legacy" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/util/util" ) var glbEnvs map[string]string func init() { glbEnvs = make(map[string]string) envs := os.Environ() for _, env := range envs { pair := strings.SplitN(env, "=", 2) if len(pair) != 2 { continue } glbEnvs[pair[0]] = pair[1] } } type Values struct { Envs map[string]string // environment vars } func GetValues() *Values { return &Values{ Envs: glbEnvs, } } func DetectLegacyINIFormat(content []byte) bool { f, err := ini.Load(content) if err != nil { return false } if _, err := f.GetSection("common"); err == nil { return true } return false } func DetectLegacyINIFormatFromFile(path string) bool { b, err := os.ReadFile(path) if err != nil { return false } return DetectLegacyINIFormat(b) } func RenderWithTemplate(in []byte, values *Values) ([]byte, error) { tmpl, err := template.New("frp").Funcs(template.FuncMap{ "parseNumberRange": parseNumberRange, "parseNumberRangePair": parseNumberRangePair, }).Parse(string(in)) if err != nil { return nil, err } buffer := bytes.NewBufferString("") if err := tmpl.Execute(buffer, values); err != nil { return nil, err } return buffer.Bytes(), nil } func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) { b, err := os.ReadFile(path) if err != nil { return nil, err } return RenderWithTemplate(b, values) } func LoadConfigureFromFile(path string, c any, strict bool) error { content, err := LoadFileContentWithTemplate(path, GetValues()) if err != nil { return err } 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 { return v1.WithDisallowUnknownFields(strict, func() error { var tomlObj any // Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML). if err := toml.Unmarshal(b, &tomlObj); err == nil { var err error b, err = json.Marshal(&tomlObj) if err != nil { return err } } // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly. if yaml.IsJSONBuffer(b) { decoder := json.NewDecoder(bytes.NewBuffer(b)) if strict { decoder.DisallowUnknownFields() } return decoder.Decode(c) } // Handle YAML content if strict { // 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) }) } func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) { m.ProxyType = util.EmptyOr(m.ProxyType, string(v1.ProxyTypeTCP)) configurer := v1.NewProxyConfigurerByType(v1.ProxyType(m.ProxyType)) if configurer == nil { return nil, fmt.Errorf("unknown proxy type: %s", m.ProxyType) } configurer.UnmarshalFromMsg(m) configurer.Complete() if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil { return nil, err } return configurer, nil } func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) { var ( svrCfg *v1.ServerConfig isLegacyFormat bool ) // detect legacy ini format if DetectLegacyINIFormatFromFile(path) { content, err := legacy.GetRenderedConfFromFile(path) if err != nil { return nil, true, err } legacyCfg, err := legacy.UnmarshalServerConfFromIni(content) if err != nil { return nil, true, err } svrCfg = legacy.Convert_ServerCommonConf_To_v1(&legacyCfg) isLegacyFormat = true } else { svrCfg = &v1.ServerConfig{} if err := LoadConfigureFromFile(path, svrCfg, strict); err != nil { return nil, false, err } } if svrCfg != nil { if err := svrCfg.Complete(); err != nil { return nil, isLegacyFormat, err } } return svrCfg, isLegacyFormat, nil } // ClientConfigLoadResult contains the result of loading a client configuration file. type ClientConfigLoadResult struct { // Common contains the common client configuration. Common *v1.ClientCommonConfig // Proxies contains proxy configurations from inline [[proxies]] and includeConfigFiles. // These are NOT completed (user prefix not added). Proxies []v1.ProxyConfigurer // Visitors contains visitor configurations from inline [[visitors]] and includeConfigFiles. // These are NOT completed. Visitors []v1.VisitorConfigurer // IsLegacyFormat indicates whether the config file is in legacy INI format. IsLegacyFormat bool } // LoadClientConfigResult loads and parses a client configuration file. // It returns the raw configuration without completing proxies/visitors. // The caller should call Complete on the configs manually for legacy behavior. func LoadClientConfigResult(path string, strict bool) (*ClientConfigLoadResult, error) { result := &ClientConfigLoadResult{ Proxies: make([]v1.ProxyConfigurer, 0), Visitors: make([]v1.VisitorConfigurer, 0), } if DetectLegacyINIFormatFromFile(path) { legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path) if err != nil { return nil, err } result.Common = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon) for _, c := range legacyProxyCfgs { result.Proxies = append(result.Proxies, legacy.Convert_ProxyConf_To_v1(c)) } for _, c := range legacyVisitorCfgs { result.Visitors = append(result.Visitors, legacy.Convert_VisitorConf_To_v1(c)) } result.IsLegacyFormat = true } else { allCfg := v1.ClientConfig{} if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil { return nil, err } result.Common = &allCfg.ClientCommonConfig for _, c := range allCfg.Proxies { result.Proxies = append(result.Proxies, c.ProxyConfigurer) } for _, c := range allCfg.Visitors { result.Visitors = append(result.Visitors, c.VisitorConfigurer) } } // Load additional config from includes. // legacy ini format already handle this in ParseClientConfig. if len(result.Common.IncludeConfigFiles) > 0 && !result.IsLegacyFormat { extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(result.Common.IncludeConfigFiles, result.IsLegacyFormat, strict) if err != nil { return nil, err } result.Proxies = append(result.Proxies, extProxyCfgs...) result.Visitors = append(result.Visitors, extVisitorCfgs...) } // Complete the common config if result.Common != nil { if err := result.Common.Complete(); err != nil { return nil, err } } return result, nil } func LoadClientConfig(path string, strict bool) ( *v1.ClientCommonConfig, []v1.ProxyConfigurer, []v1.VisitorConfigurer, bool, error, ) { result, err := LoadClientConfigResult(path, strict) if err != nil { return nil, nil, nil, result != nil && result.IsLegacyFormat, err } proxyCfgs := result.Proxies visitorCfgs := result.Visitors proxyCfgs, visitorCfgs = FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs) proxyCfgs = CompleteProxyConfigurers(proxyCfgs) visitorCfgs = CompleteVisitorConfigurers(visitorCfgs) return result.Common, proxyCfgs, visitorCfgs, result.IsLegacyFormat, nil } func CompleteProxyConfigurers(proxies []v1.ProxyConfigurer) []v1.ProxyConfigurer { proxyCfgs := proxies for _, c := range proxyCfgs { c.Complete() } return proxyCfgs } func CompleteVisitorConfigurers(visitors []v1.VisitorConfigurer) []v1.VisitorConfigurer { visitorCfgs := visitors for _, c := range visitorCfgs { c.Complete() } return visitorCfgs } func FilterClientConfigurers( common *v1.ClientCommonConfig, proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer, ) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) { if common == nil { common = &v1.ClientCommonConfig{} } proxyCfgs := proxies visitorCfgs := visitors // Filter by start across merged configurers from all sources. // For example, store entries are also filtered by this set. if len(common.Start) > 0 { startSet := sets.New(common.Start...) proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool { return startSet.Has(c.GetBaseConfig().Name) }) visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool { return startSet.Has(c.GetBaseConfig().Name) }) } // Filter by enabled field in each proxy // nil or true means enabled, false means disabled proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool { enabled := c.GetBaseConfig().Enabled return enabled == nil || *enabled }) visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool { enabled := c.GetBaseConfig().Enabled return enabled == nil || *enabled }) return proxyCfgs, visitorCfgs } func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { proxyCfgs := make([]v1.ProxyConfigurer, 0) visitorCfgs := make([]v1.VisitorConfigurer, 0) for _, path := range paths { absDir, err := filepath.Abs(filepath.Dir(path)) if err != nil { return nil, nil, err } if _, err := os.Stat(absDir); os.IsNotExist(err) { return nil, nil, err } files, err := os.ReadDir(absDir) if err != nil { return nil, nil, err } for _, fi := range files { if fi.IsDir() { continue } absFile := filepath.Join(absDir, fi.Name()) if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched { // support yaml/json/toml cfg := v1.ClientConfig{} if err := LoadConfigureFromFile(absFile, &cfg, strict); err != nil { return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err) } for _, c := range cfg.Proxies { proxyCfgs = append(proxyCfgs, c.ProxyConfigurer) } for _, c := range cfg.Visitors { visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) } } } } return proxyCfgs, visitorCfgs, nil }