package config import ( "bytes" "context" _ "embed" "encoding/json" "fmt" "os" "path/filepath" "github.com/hiddify/ray2sing/ray2sing" "github.com/sagernet/sing-box/experimental/libbox" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/batch" SJ "github.com/sagernet/sing/common/json" "github.com/xmdhs/clash2singbox/convert" "github.com/xmdhs/clash2singbox/model/clash" "gopkg.in/yaml.v3" ) //go:embed config.json.template var configByte []byte func ParseConfig(path string, debug bool) ([]byte, error) { content, err := os.ReadFile(path) os.Chdir(filepath.Dir(path)) if err != nil { return nil, err } return ParseConfigContent(string(content), debug, nil, false) } func ParseConfigContentToOptions(contentstr string, debug bool, configOpt *HiddifyOptions, fullConfig bool) (*option.Options, error) { content, err := ParseConfigContent(contentstr, debug, configOpt, fullConfig) if err != nil { return nil, err } var options option.Options err = json.Unmarshal(content, &options) if err != nil { return nil, err } return &options, nil } func ParseConfigContent(contentstr string, debug bool, configOpt *HiddifyOptions, fullConfig bool) ([]byte, error) { if configOpt == nil { configOpt = DefaultHiddifyOptions() } content := []byte(contentstr) var jsonObj map[string]interface{} = make(map[string]interface{}) fmt.Printf("Convert using json\n") var tmpJsonResult any jsonDecoder := json.NewDecoder(SJ.NewCommentFilter(bytes.NewReader(content))) if err := jsonDecoder.Decode(&tmpJsonResult); err == nil { if tmpJsonObj, ok := tmpJsonResult.(map[string]interface{}); ok { if tmpJsonObj["outbounds"] == nil { jsonObj["outbounds"] = []interface{}{jsonObj} } else { if fullConfig || (configOpt != nil && configOpt.EnableFullConfig) { jsonObj = tmpJsonObj } else { jsonObj["outbounds"] = tmpJsonObj["outbounds"] } } } else if jsonArray, ok := tmpJsonResult.([]map[string]interface{}); ok { jsonObj["outbounds"] = jsonArray } else { return nil, fmt.Errorf("[SingboxParser] Incorrect Json Format") } newContent, _ := json.MarshalIndent(jsonObj, "", " ") return patchConfig(newContent, "SingboxParser", configOpt) } v2rayStr, err := ray2sing.Ray2Singbox(string(content), configOpt.UseXrayCoreWhenPossible) if err == nil { return patchConfig([]byte(v2rayStr), "V2rayParser", configOpt) } fmt.Printf("Convert using clash\n") clashObj := clash.Clash{} if err := yaml.Unmarshal(content, &clashObj); err == nil && clashObj.Proxies != nil { if len(clashObj.Proxies) == 0 { return nil, fmt.Errorf("[ClashParser] no outbounds found") } converted, err := convert.Clash2sing(clashObj) if err != nil { return nil, fmt.Errorf("[ClashParser] converting clash to sing-box error: %w", err) } output := configByte output, err = convert.Patch(output, converted, "", "", nil) if err != nil { return nil, fmt.Errorf("[ClashParser] patching clash config error: %w", err) } return patchConfig(output, "ClashParser", configOpt) } return nil, fmt.Errorf("unable to determine config format") } func patchConfig(content []byte, name string, configOpt *HiddifyOptions) ([]byte, error) { options := option.Options{} err := json.Unmarshal(content, &options) if err != nil { return nil, fmt.Errorf("[SingboxParser] unmarshal error: %w", err) } b, _ := batch.New(context.Background(), batch.WithConcurrencyNum[*option.Outbound](2)) for _, base := range options.Outbounds { out := base b.Go(base.Tag, func() (*option.Outbound, error) { err := patchWarp(&out, configOpt, false, nil) if err != nil { return nil, fmt.Errorf("[Warp] patch warp error: %w", err) } // options.Outbounds[i] = base return &out, nil }) } if res, err := b.WaitAndGetResult(); err != nil { return nil, err } else { for i, base := range options.Outbounds { options.Outbounds[i] = *res[base.Tag].Value } } content, _ = json.MarshalIndent(options, "", " ") fmt.Printf("%s\n", content) return validateResult(content, name) } func validateResult(content []byte, name string) ([]byte, error) { err := libbox.CheckConfig(string(content)) if err != nil { return nil, fmt.Errorf("[%s] invalid sing-box config: %w", name, err) } return content, nil }