diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index 47b8a00a..5ef16042 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -1,11 +1,11 @@ package main import ( - "bytes" "os" "path/filepath" - "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/conf" + "github.com/sagernet/sing-box/common/conf/mergers" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" @@ -13,7 +13,8 @@ import ( "github.com/spf13/cobra" ) -var commandFormatFlagWrite bool +var commandFormatWrite string +var commandEncodeFormat string var commandFormat = &cobra.Command{ Use: "format", @@ -28,44 +29,55 @@ var commandFormat = &cobra.Command{ } func init() { - commandFormat.Flags().BoolVarP(&commandFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandFormat.Flags().StringVarP(&commandFormatWrite, "write", "w", "", "write result to (source) file instead of stdout") + commandFormat.Flags().StringVarP(&commandEncodeFormat, "encode", "e", string(mergers.FormatJSON), "encode format") mainCommand.AddCommand(commandFormat) } func format() error { - configContent, err := os.ReadFile(configPath) + var ( + configContent []byte + err error + ) + format := mergers.ParseFormat(configFormat) + encode := mergers.ParseFormat(commandEncodeFormat) + if encode == mergers.FormatAuto { + encode = mergers.FormatJSON + } + if len(configPaths) == 1 && configPaths[0] == "stdin" { + configContent, err = conf.ReaderToJSON(os.Stdin, format) + } else { + configContent, err = conf.FilesToJSON(configPaths, format, configRecursive) + } if err != nil { return E.Cause(err, "read config") } + var options option.Options err = options.UnmarshalJSON(configContent) if err != nil { return E.Cause(err, "decode config") } - buffer := new(bytes.Buffer) - encoder := json.NewEncoder(buffer) - encoder.SetIndent("", " ") - err = encoder.Encode(options) + content, err := conf.Sprint(options, encode) if err != nil { return E.Cause(err, "encode config") } - if !commandFormatFlagWrite { - os.Stdout.WriteString(buffer.String() + "\n") + + if commandFormatWrite == "" { + os.Stdout.WriteString(content + "\n") return nil } - if bytes.Equal(configContent, buffer.Bytes()) { - return nil - } - output, err := os.Create(configPath) + + output, err := os.Create(commandFormatWrite) if err != nil { return E.Cause(err, "open output") } - _, err = output.Write(buffer.Bytes()) + _, err = output.WriteString(content) output.Close() if err != nil { return E.Cause(err, "write output") } - outputPath, _ := filepath.Abs(configPath) + outputPath, _ := filepath.Abs(commandFormatWrite) os.Stderr.WriteString(outputPath + "\n") return nil } diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go index 77572ff6..7415acd4 100644 --- a/cmd/sing-box/cmd_run.go +++ b/cmd/sing-box/cmd_run.go @@ -2,13 +2,14 @@ package main import ( "context" - "io" "os" "os/signal" runtimeDebug "runtime/debug" "syscall" - "github.com/sagernet/sing-box" + box "github.com/sagernet/sing-box" + "github.com/sagernet/sing-box/common/conf" + "github.com/sagernet/sing-box/common/conf/mergers" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" @@ -36,10 +37,11 @@ func readConfig() (option.Options, error) { configContent []byte err error ) - if configPath == "stdin" { - configContent, err = io.ReadAll(os.Stdin) + format := mergers.ParseFormat(configFormat) + if len(configPaths) == 1 && configPaths[0] == "stdin" { + configContent, err = conf.ReaderToJSON(os.Stdin, format) } else { - configContent, err = os.ReadFile(configPath) + configContent, err = conf.FilesToJSON(configPaths, format, configRecursive) } if err != nil { return option.Options{}, E.Cause(err, "read config") diff --git a/cmd/sing-box/main.go b/cmd/sing-box/main.go index 7bebefed..7e08c226 100644 --- a/cmd/sing-box/main.go +++ b/cmd/sing-box/main.go @@ -3,15 +3,18 @@ package main import ( "os" + "github.com/sagernet/sing-box/common/conf/mergers" "github.com/sagernet/sing-box/log" "github.com/spf13/cobra" ) var ( - configPath string - workingDir string - disableColor bool + configPaths []string + configFormat string + configRecursive bool + workingDir string + disableColor bool ) var mainCommand = &cobra.Command{ @@ -20,7 +23,9 @@ var mainCommand = &cobra.Command{ } func init() { - mainCommand.PersistentFlags().StringVarP(&configPath, "config", "c", "config.json", "set configuration file path") + mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", []string{"config.json"}, "set configuration file path") + mainCommand.PersistentFlags().StringVarP(&configFormat, "format", "", string(mergers.FormatAuto), "load configuration directories recursively") + mainCommand.PersistentFlags().BoolVarP(&configRecursive, "recursive", "r", false, "load configuration directories recursively") mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory") mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output") } diff --git a/common/conf/conf.go b/common/conf/conf.go new file mode 100644 index 00000000..b1bba157 --- /dev/null +++ b/common/conf/conf.go @@ -0,0 +1,72 @@ +package conf + +import ( + "encoding/json" + "io" + + "github.com/sagernet/sing-box/common/conf/merge" + "github.com/sagernet/sing-box/common/conf/mergers" +) + +// Reader loads json data to v. +func Reader(v interface{}, r io.Reader, format mergers.Format) error { + data, err := ReaderToJSON(r, format) + if err != nil { + return err + } + return json.Unmarshal(data, v) +} + +// Files load config files to v. +// it will resolve folder to files +func Files(v interface{}, files []string, format mergers.Format, recursively bool) error { + data, err := FilesToJSON(files, format, recursively) + if err != nil { + return err + } + return json.Unmarshal(data, v) +} + +// ReaderToJSON load reader content as JSON bytes. +func ReaderToJSON(r io.Reader, format mergers.Format) ([]byte, error) { + m := make(map[string]interface{}) + err := mergers.MergeAs(format, r, m) + if err != nil { + return nil, err + } + err = merge.ApplyRules(m) + if err != nil { + return nil, err + } + merge.RemoveHelperFields(m) + return json.Marshal(m) +} + +// FilesToJSON merges config files JSON bytes +func FilesToJSON(files []string, format mergers.Format, recursively bool) ([]byte, error) { + var err error + if len(files) > 0 { + var extensions []string + extensions, err := mergers.GetExtensions(format) + if err != nil { + return nil, err + } + files, err = resolveFolderToFiles(files, extensions, recursively) + if err != nil { + return nil, err + } + } + m := make(map[string]interface{}) + if len(files) > 0 { + err = mergers.MergeAs(format, files, m) + if err != nil { + return nil, err + } + } + err = merge.ApplyRules(m) + if err != nil { + return nil, err + } + merge.RemoveHelperFields(m) + return json.Marshal(m) +} diff --git a/common/conf/fs.go b/common/conf/fs.go new file mode 100644 index 00000000..02934e85 --- /dev/null +++ b/common/conf/fs.go @@ -0,0 +1,83 @@ +package conf + +import ( + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "strings" +) + +// resolveFolderToFiles expands folder path (if any and it exists) to file paths. +// Any other paths, like file, even URL, it returns them as is. +func resolveFolderToFiles(paths []string, extensions []string, recursively bool) ([]string, error) { + dirReader := readDir + if recursively { + dirReader = readDirRecursively + } + files := make([]string, 0) + for _, p := range paths { + i, err := os.Stat(p) + // don't raise error for net url + if err != nil && isPathOnly(p) { + return nil, err + } + if err == nil && i.IsDir() { + fs, err := dirReader(p, extensions) + if err != nil { + return nil, fmt.Errorf("failed to read dir %s: %s", p, err) + } + files = append(files, fs...) + continue + } + files = append(files, p) + } + return files, nil +} + +// readDir finds files according to extensions in the dir +func readDir(dir string, extensions []string) ([]string, error) { + confs, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + files := make([]string, 0) + for _, f := range confs { + ext := filepath.Ext(f.Name()) + for _, e := range extensions { + if strings.EqualFold(ext, e) { + files = append(files, filepath.Join(dir, f.Name())) + break + } + } + } + return files, nil +} + +// readDirRecursively finds files according to extensions in the dir recursively +func readDirRecursively(dir string, extensions []string) ([]string, error) { + files := make([]string, 0) + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + ext := filepath.Ext(path) + for _, e := range extensions { + if strings.EqualFold(ext, e) { + files = append(files, path) + break + } + } + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} + +func isPathOnly(p string) bool { + u, err := url.Parse(p) + if err != nil { + return false + } + return u.Path == p +} diff --git a/common/conf/jsonc/decode.go b/common/conf/jsonc/decode.go new file mode 100644 index 00000000..4bf02e7f --- /dev/null +++ b/common/conf/jsonc/decode.go @@ -0,0 +1,57 @@ +package jsonc + +import ( + "bytes" + "encoding/json" + "fmt" + "io" +) + +type offset struct { + line int + char int +} + +func findOffset(b []byte, o int) *offset { + if o >= len(b) || o < 0 { + return nil + } + line := 1 + char := 0 + for i, x := range b { + if i == o { + break + } + if x == '\n' { + line++ + char = 0 + } else { + char++ + } + } + return &offset{line: line, char: char} +} + +// Decode reads from reader and decode into target +// syntax error could be detected. +func Decode(reader io.Reader, target interface{}) error { + jsonContent := bytes.NewBuffer(make([]byte, 0, 10240)) + jsonReader := io.TeeReader(&Reader{ + Reader: reader, + }, jsonContent) + decoder := json.NewDecoder(jsonReader) + + if err := decoder.Decode(target); err != nil { + var pos *offset + if tErr, ok := err.(*json.SyntaxError); ok { + pos = findOffset(jsonContent.Bytes(), int(tErr.Offset)) + } else if tErr, ok := err.(*json.UnmarshalTypeError); ok { + pos = findOffset(jsonContent.Bytes(), int(tErr.Offset)) + } + if pos != nil { + return fmt.Errorf("failed to decode jsonc at line %d char %d: %s", pos.line, pos.char, err) + } + return fmt.Errorf("failed to decode jsonc: %s", err) + } + return nil +} diff --git a/common/conf/jsonc/decode_test.go b/common/conf/jsonc/decode_test.go new file mode 100644 index 00000000..57a784c3 --- /dev/null +++ b/common/conf/jsonc/decode_test.go @@ -0,0 +1,45 @@ +package jsonc_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/sagernet/sing-box/common/conf/jsonc" +) + +func TestLoaderError(t *testing.T) { + testCases := []struct { + Input string + Output string + }{ + { + Input: `{ + "log": { + // abcd + 0, + "loglevel": "info" + } + }`, + Output: "line 4 char 6", + }, + { + Input: `{ + "log": { + // abcd + "loglevel": "info", + } + }`, + Output: "line 5 char 5", + }, + } + for _, testCase := range testCases { + reader := bytes.NewReader([]byte(testCase.Input)) + m := make(map[string]interface{}) + err := jsonc.Decode(reader, m) + errString := err.Error() + if !strings.Contains(errString, testCase.Output) { + t.Error("unexpected output from json: ", testCase.Input, ". expected ", testCase.Output, ", but actually ", errString) + } + } +} diff --git a/common/conf/jsonc/reader.go b/common/conf/jsonc/reader.go new file mode 100644 index 00000000..dcd77aaa --- /dev/null +++ b/common/conf/jsonc/reader.go @@ -0,0 +1,129 @@ +package jsonc + +import ( + "io" +) + +// State is the internal state of parser. +type State byte + +// states +const ( + StateContent State = iota + StateEscape + StateDoubleQuote + StateDoubleQuoteEscape + StateSingleQuote + StateSingleQuoteEscape + StateComment + StateSlash + StateMultilineComment + StateMultilineCommentStar +) + +// Reader is a reader for filtering comments. +// It supports Java style single and multi line comment syntax, and Python style single line comment syntax. +type Reader struct { + io.Reader + + state State +} + +// Read implements io.Reader.Read() +func (v *Reader) Read(b []byte) (int, error) { + var sb [1]byte + p := b[:0] + for len(p) < len(b)-2 { + _, err := v.Reader.Read(sb[:]) + x := sb[0] + if err != nil { + if len(p) == 0 { + return 0, err + } + return len(p), nil + } + switch v.state { + case StateContent: + switch x { + case '"': + v.state = StateDoubleQuote + p = append(p, x) + case '\'': + v.state = StateSingleQuote + p = append(p, x) + case '\\': + v.state = StateEscape + case '#': + v.state = StateComment + case '/': + v.state = StateSlash + default: + p = append(p, x) + } + case StateEscape: + p = append(p, '\\', x) + v.state = StateContent + case StateDoubleQuote: + switch x { + case '"': + v.state = StateContent + p = append(p, x) + case '\\': + v.state = StateDoubleQuoteEscape + default: + p = append(p, x) + } + case StateDoubleQuoteEscape: + p = append(p, '\\', x) + v.state = StateDoubleQuote + case StateSingleQuote: + switch x { + case '\'': + v.state = StateContent + p = append(p, x) + case '\\': + v.state = StateSingleQuoteEscape + default: + p = append(p, x) + } + case StateSingleQuoteEscape: + p = append(p, '\\', x) + v.state = StateSingleQuote + case StateComment: + if x == '\n' { + v.state = StateContent + p = append(p, '\n') + } + case StateSlash: + switch x { + case '/': + v.state = StateComment + case '*': + v.state = StateMultilineComment + default: + p = append(p, '/', x) + } + case StateMultilineComment: + switch x { + case '*': + v.state = StateMultilineCommentStar + case '\n': + p = append(p, '\n') + } + case StateMultilineCommentStar: + switch x { + case '/': + v.state = StateContent + case '*': + // Stay + case '\n': + p = append(p, '\n') + default: + v.state = StateMultilineComment + } + default: + panic("Unknown state.") + } + } + return len(p), nil +} diff --git a/common/conf/jsonc/reader_test.go b/common/conf/jsonc/reader_test.go new file mode 100644 index 00000000..c01244a2 --- /dev/null +++ b/common/conf/jsonc/reader_test.go @@ -0,0 +1,100 @@ +package jsonc_test + +import ( + "bytes" + "io" + "testing" + + "github.com/sagernet/sing-box/common/conf/jsonc" +) + +func TestReader(t *testing.T) { + data := []struct { + input string + output string + }{ + { + ` +content #comment 1 +#comment 2 +content 2`, + ` +content + +content 2`}, + {`content`, `content`}, + {" ", " "}, + {`con/*abcd*/tent`, "content"}, + {` +text // adlkhdf /* +//comment adfkj +text 2*/`, ` +text + +text 2*`}, + {`"//"content`, `"//"content`}, + {`abcd'//'abcd`, `abcd'//'abcd`}, + {`"\""`, `"\""`}, + {`\"/*abcd*/\"`, `\"\"`}, + } + + for _, testCase := range data { + reader := &jsonc.Reader{ + Reader: bytes.NewReader([]byte(testCase.input)), + } + + buf := make([]byte, 1024) + n, err := reader.Read(buf) + if err != nil { + t.Error(err) + return + } + got := buf[:n] + if string(got) != testCase.output { + t.Errorf("want: %s, got: %s", testCase.output, got) + return + } + } +} + +func TestReader1(t *testing.T) { + type dataStruct struct { + input string + output string + } + + bufLen := 8 + + data := []dataStruct{ + {"loooooooooooooooooooooooooooooooooooooooog", "loooooooooooooooooooooooooooooooooooooooog"}, + {`{"t": "\/testlooooooooooooooooooooooooooooong"}`, `{"t": "\/testlooooooooooooooooooooooooooooong"}`}, + {`{"t": "\/test"}`, `{"t": "\/test"}`}, + {`"\// fake comment"`, `"\// fake comment"`}, + {`"\/\/\/\/\/"`, `"\/\/\/\/\/"`}, + } + + for _, testCase := range data { + reader := &jsonc.Reader{ + Reader: bytes.NewReader([]byte(testCase.input)), + } + target := make([]byte, 0) + buf := make([]byte, bufLen) + var n int + var err error + for n, err = reader.Read(buf); err == nil; n, err = reader.Read(buf) { + if n > len(buf) { + t.Error("n: ", n) + } + target = append(target, buf[:n]...) + buf = make([]byte, bufLen) + } + if err != nil && err != io.EOF { + t.Error("error: ", err) + } + got := string(target) + if string(got) != testCase.output { + t.Errorf("want: %s, got: %s", testCase.output, got) + return + } + } +} diff --git a/common/conf/merge/convert.go b/common/conf/merge/convert.go new file mode 100644 index 00000000..03a4e308 --- /dev/null +++ b/common/conf/merge/convert.go @@ -0,0 +1,37 @@ +package merge + +import ( + "fmt" +) + +// Convert converts map[interface{}]interface{} to map[string]interface{} which +// is mergable by merge.Maps +func Convert(m map[interface{}]interface{}) map[string]interface{} { + return convert(m) +} + +func convert(m map[interface{}]interface{}) map[string]interface{} { + res := map[string]interface{}{} + for k, v := range m { + var value interface{} + switch v2 := v.(type) { + case map[interface{}]interface{}: + value = convert(v2) + case []interface{}: + for i, el := range v2 { + if m, ok := el.(map[interface{}]interface{}); ok { + v2[i] = convert(m) + } + } + value = v2 + default: + value = v + } + key := "null" + if k != nil { + key = fmt.Sprint(k) + } + res[key] = value + } + return res +} diff --git a/common/conf/merge/map.go b/common/conf/merge/map.go new file mode 100644 index 00000000..433c304a --- /dev/null +++ b/common/conf/merge/map.go @@ -0,0 +1,55 @@ +// Copyright 2020 Jebbs. All rights reserved. +// Use of this source code is governed by MIT +// license that can be found in the LICENSE file. + +package merge + +import ( + "fmt" +) + +// Maps merges source maps into target +func Maps(target map[string]interface{}, sources ...map[string]interface{}) (err error) { + for _, source := range sources { + err = mergeMaps(target, source) + if err != nil { + return err + } + } + return nil +} + +// mergeMaps merges source map into target +// it supports only map[string]interface{} type for any children of the map tree +func mergeMaps(target map[string]interface{}, source map[string]interface{}) (err error) { + for key, value := range source { + target[key], err = mergeField(target[key], value) + if err != nil { + return + } + } + return +} + +func mergeField(target interface{}, source interface{}) (interface{}, error) { + if source == nil { + return target, nil + } + if target == nil { + return source, nil + } + if slice, ok := source.([]interface{}); ok { + if tslice, ok := target.([]interface{}); ok { + tslice = append(tslice, slice...) + return tslice, nil + } + return nil, fmt.Errorf("value type mismatch, source is 'slice' but target not: %s", source) + } else if smap, ok := source.(map[string]interface{}); ok { + if tmap, ok := target.(map[string]interface{}); ok { + err := mergeMaps(tmap, smap) + return tmap, err + } + return nil, fmt.Errorf("value type mismatch, source is 'map[string]interface{}' but target not: %s", source) + } + return source, nil +} diff --git a/common/conf/merge/merge_test.go b/common/conf/merge/merge_test.go new file mode 100644 index 00000000..af6dc617 --- /dev/null +++ b/common/conf/merge/merge_test.go @@ -0,0 +1,189 @@ +// Copyright 2020 Jebbs. All rights reserved. +// Use of this source code is governed by MIT +// license that can be found in the LICENSE file. + +package merge_test + +import ( + "encoding/json" + "reflect" + "strings" + "testing" + + "github.com/sagernet/sing-box/common/conf/jsonc" + "github.com/sagernet/sing-box/common/conf/merge" +) + +func TestMergeV2Style(t *testing.T) { + json1 := ` + { + "log": {"level": "debug"}, + "nodeA": [{"_tag": "a1","value": "a1"}], + "nodeB": [{"_priority": 100, "_tag": "b1","value": "b1"}], + "nodeC": [ + {"_tag":"c1","aTag":["a1"],"bTag":"b1"} + ] + } +` + json2 := ` + { + "log": {"level": "error"}, + "nodeA": [{"_tag": "a2","value": "a2"}], + "nodeB": [{"_priority": -100, "_tag": "b2","value": "b2"}], + "nodeC": [ + {"aTag":["a2"],"bTag":"b2"}, + {"_tag":"c1","aTag":["a1.1"],"bTag":"b1.1"} + ] + } +` + expected := ` + { + // level is overwritten + "log": {"level": "error"}, + "nodeA": [{"value": "a1"}, {"value": "a2"}], + "nodeB": [ + {"value": "b2"}, // the order is affected by priority + {"value": "b1"} + ], + "nodeC": [ + // 3 items are merged into 2, and bTag is overwritten, + // because 2 of them has same tag + {"aTag":["a1","a1.1"],"bTag":"b1.1"}, + {"aTag":["a2"],"bTag":"b2"} + ] + } + ` + m, err := jsonsToMap(json1, json2) + if err != nil { + t.Error(err) + } + assertResult(t, m, expected) +} + +func TestMergeTagValueTypes(t *testing.T) { + json1 := ` + { + "array_1": [{ + "_tag":"1", + "array_2": [{ + "_tag":"2", + "array_3.1": ["string",true,false], + "array_3.2": [1,2,3], + "number_1": 1, + "number_2": 1, + "bool_1": true, + "bool_2": true + }] + }] + } +` + json2 := ` + { + "array_1": [{ + "_tag":"1", + "array_2": [{ + "_tag":"2", + "array_3.1": [0,1,null], + "array_3.2": null, + "number_1": 0, + "number_2": 1, + "bool_1": true, + "bool_2": false, + "null_1": null + }] + }] + } +` + expected := ` + { + "array_1": [{ + "array_2": [{ + "array_3.1": ["string",true,false,0,1,null], + "array_3.2": [1,2,3], + "number_1": 0, + "number_2": 1, + "bool_1": true, + "bool_2": false, + "null_1": null + }] + }] + } + ` + m, err := jsonsToMap(json1, json2) + if err != nil { + t.Error(err) + } + assertResult(t, m, expected) +} + +func TestMergeTagDeep(t *testing.T) { + json1 := ` + { + "array_1": [{ + "_tag":"1", + "array_2": [{ + "_tag":"2", + "array_3": [true,false,"string"] + }] + }] + } +` + json2 := ` + { + "array_1": [{ + "_tag":"1", + "array_2": [{ + "_tag":"2", + "_priority":-100, + "array_3": [0,1,null] + }] + }] + } +` + expected := ` + { + "array_1": [{ + "array_2": [{ + "array_3": [0,1,null,true,false,"string"] + }] + }] + } + ` + m, err := jsonsToMap(json1, json2) + if err != nil { + t.Error(err) + } + assertResult(t, m, expected) +} + +func jsonsToMap(jsonStrs ...string) (map[string]interface{}, error) { + merged := make(map[string]interface{}) + maps := make([]map[string]interface{}, len(jsonStrs)) + for _, j := range jsonStrs { + m := map[string]interface{}{} + json.Unmarshal([]byte(j), &m) + maps = append(maps, m) + } + err := merge.Maps(merged, maps...) + if err != nil { + return nil, err + } + err = merge.ApplyRules(merged) + if err != nil { + return nil, err + } + merge.RemoveHelperFields(merged) + return merged, nil +} + +func assertResult(t *testing.T, got map[string]interface{}, want string) { + e := make(map[string]interface{}) + err := jsonc.Decode(strings.NewReader(want), &e) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(got, e) { + b, _ := json.Marshal(got) + t.Fatalf("want:\n%s\n\ngot:\n%s", want, string(b)) + } +} diff --git a/common/conf/merge/priority.go b/common/conf/merge/priority.go new file mode 100644 index 00000000..7865b5fa --- /dev/null +++ b/common/conf/merge/priority.go @@ -0,0 +1,31 @@ +// Copyright 2020 Jebbs. All rights reserved. +// Use of this source code is governed by MIT +// license that can be found in the LICENSE file. + +package merge + +import "sort" + +func getPriority(v interface{}) float64 { + var m map[string]interface{} + var ok bool + if m, ok = v.(map[string]interface{}); !ok { + return 0 + } + if i, ok := m[priorityKey]; ok { + if p, ok := i.(float64); ok { + return p + } + } + return 0 +} + +// sortByPriority sort slice by priority fields of their elements +func sortByPriority(slice []interface{}) { + sort.Slice( + slice, + func(i, j int) bool { + return getPriority(slice[i]) < getPriority(slice[j]) + }, + ) +} diff --git a/common/conf/merge/rules.go b/common/conf/merge/rules.go new file mode 100644 index 00000000..d5a8c3d7 --- /dev/null +++ b/common/conf/merge/rules.go @@ -0,0 +1,56 @@ +// Copyright 2020 Jebbs. All rights reserved. +// Use of this source code is governed by MIT +// license that can be found in the LICENSE file. + +package merge + +const priorityKey string = "_priority" +const tagKey string = "_tag" + +// ApplyRules applies merge rules according to _tag, _priority fields +func ApplyRules(m map[string]interface{}) error { + return sortMergeSlices(m) +} + +// RemoveHelperFields removes helper fields from a map, including "_priority" and "_tag" +func RemoveHelperFields(target map[string]interface{}) { + removeHelperFields(target) +} + +// sortMergeSlices enumerates all slices in a map, to sort by priority and merge by tag +func sortMergeSlices(target map[string]interface{}) error { + for key, value := range target { + if slice, ok := value.([]interface{}); ok { + sortByPriority(slice) + s, err := mergeSameTag(slice) + if err != nil { + return err + } + target[key] = s + for _, item := range s { + if m, ok := item.(map[string]interface{}); ok { + sortMergeSlices(m) + } + } + } else if field, ok := value.(map[string]interface{}); ok { + sortMergeSlices(field) + } + } + return nil +} + +func removeHelperFields(target map[string]interface{}) { + for key, value := range target { + if key == priorityKey || key == tagKey { + delete(target, key) + } else if slice, ok := value.([]interface{}); ok { + for _, e := range slice { + if el, ok := e.(map[string]interface{}); ok { + removeHelperFields(el) + } + } + } else if field, ok := value.(map[string]interface{}); ok { + removeHelperFields(field) + } + } +} diff --git a/common/conf/merge/tag.go b/common/conf/merge/tag.go new file mode 100644 index 00000000..28695380 --- /dev/null +++ b/common/conf/merge/tag.go @@ -0,0 +1,53 @@ +// Copyright 2020 Jebbs. All rights reserved. +// Use of this source code is governed by MIT +// license that can be found in the LICENSE file. + +package merge + +func getTag(v map[string]interface{}) string { + if field, ok := v[tagKey]; ok { + if t, ok := field.(string); ok { + return t + } + } + return "" +} + +func mergeSameTag(s []interface{}) ([]interface{}, error) { + // from: [a,"",b,"",a,"",b,""] + // to: [a,"",b,"",merged,"",merged,""] + merged := &struct{}{} + for i, item1 := range s { + map1, ok := item1.(map[string]interface{}) + if !ok { + continue + } + tag1 := getTag(map1) + if tag1 == "" { + continue + } + for j := i + 1; j < len(s); j++ { + map2, ok := s[j].(map[string]interface{}) + if !ok { + continue + } + tag2 := getTag(map2) + if tag1 == tag2 { + s[j] = merged + err := mergeMaps(map1, map2) + if err != nil { + return nil, err + } + } + } + } + // remove merged + ns := make([]interface{}, 0) + for _, item := range s { + if item == merged { + continue + } + ns = append(ns, item) + } + return ns, nil +} diff --git a/common/conf/mergers/extensions.go b/common/conf/mergers/extensions.go new file mode 100644 index 00000000..fbd1ec22 --- /dev/null +++ b/common/conf/mergers/extensions.go @@ -0,0 +1,26 @@ +package mergers + +import ( + "fmt" +) + +// GetExtensions get extensions of given format +func GetExtensions(formatName Format) ([]string, error) { + if formatName == FormatAuto { + return GetAllExtensions(), nil + } + f, found := mergersByName[formatName] + if !found { + return nil, fmt.Errorf("%s not found", formatName) + } + return f.Extensions, nil +} + +// GetAllExtensions get all extensions supported +func GetAllExtensions() []string { + extensions := make([]string, 0) + for _, f := range mergersByName { + extensions = append(extensions, f.Extensions...) + } + return extensions +} diff --git a/common/conf/mergers/formats.go b/common/conf/mergers/formats.go new file mode 100644 index 00000000..f5588122 --- /dev/null +++ b/common/conf/mergers/formats.go @@ -0,0 +1,29 @@ +package mergers + +import ( + "strings" +) + +// Format is the supported format of mergers +type Format string + +// Supported formats +const ( + FormatAuto Format = "auto" + FormatJSON Format = "json" + FormatJSONC Format = "jsonc" + FormatTOML Format = "toml" + FormatYAML Format = "yaml" +) + +// ParseFormat parses format from name, it returns FormatAuto if fails. +func ParseFormat(name string) Format { + name = strings.ToLower(strings.TrimSpace(name)) + format := Format(name) + switch format { + case FormatAuto, FormatJSON, FormatJSONC, FormatTOML, FormatYAML: + return format + default: + return FormatAuto + } +} diff --git a/common/conf/mergers/load.go b/common/conf/mergers/load.go new file mode 100644 index 00000000..2f4a57f0 --- /dev/null +++ b/common/conf/mergers/load.go @@ -0,0 +1,62 @@ +package mergers + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" +) + +// loadToBytes loads one arg to []byte, maybe an remote url, or local file path +func loadToBytes(arg string) (out []byte, err error) { + switch { + case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"): + out, err = fetchHTTPContent(arg) + default: + out, err = ioutil.ReadFile(arg) + } + if err != nil { + return + } + return +} + +// fetchHTTPContent dials https for remote content +func fetchHTTPContent(target string) ([]byte, error) { + parsedTarget, err := url.Parse(target) + if err != nil { + return nil, fmt.Errorf("invalid URL: %s", target) + } + + if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" { + return nil, fmt.Errorf("invalid scheme: %s", parsedTarget.Scheme) + } + + client := &http.Client{ + Timeout: 30 * time.Second, + } + resp, err := client.Do(&http.Request{ + Method: "GET", + URL: parsedTarget, + Close: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to dial to %s", target) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode) + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.New("failed to read HTTP response") + } + + return content, nil +} diff --git a/common/conf/mergers/merge.go b/common/conf/mergers/merge.go new file mode 100644 index 00000000..bd85b83c --- /dev/null +++ b/common/conf/mergers/merge.go @@ -0,0 +1,89 @@ +package mergers + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "path/filepath" + "strings" +) + +// MergeAs load input and merge as specified format into m +func MergeAs(formatName Format, input interface{}, m map[string]interface{}) error { + f, found := mergersByName[formatName] + if !found { + return fmt.Errorf("format merger not found for: %s", formatName) + } + return f.Merge(input, m) +} + +// Merge loads inputs and merges them into target +// it detects extension for merger selecting, or try all mergers +// if no extension found +func Merge(input interface{}, target map[string]interface{}) error { + switch v := input.(type) { + case string: + err := mergeSingleFile(v, target) + if err != nil { + return err + } + case []string: + for _, file := range v { + err := mergeSingleFile(file, target) + if err != nil { + return err + } + } + case []byte: + err := mergeSingleFile(v, target) + if err != nil { + return err + } + case io.Reader: + // read to []byte incase it tries different mergers + bs, err := ioutil.ReadAll(v) + if err != nil { + return err + } + err = mergeSingleFile(bs, target) + if err != nil { + return err + } + default: + return errors.New("unknow merge input type") + } + return nil +} + +func mergeSingleFile(input interface{}, m map[string]interface{}) error { + if file, ok := input.(string); ok { + ext := getExtension(file) + if ext != "" { + lext := strings.ToLower(ext) + f, found := mergersByExt[lext] + if !found { + return fmt.Errorf("unmergeable format extension: %s", ext) + } + return f.Merge(file, m) + } + } + var errs []string + // no extension, try all mergers + for _, f := range mergersByName { + if f.Name == FormatAuto { + continue + } + err := f.Merge(input, m) + if err == nil { + return nil + } + errs = append(errs, fmt.Sprintf("[%s] %s", f.Name, err)) + } + return fmt.Errorf("tried all mergers but failed for: \n\n%s\n\nreason:\n\n %s", input, strings.Join(errs, "\n ")) +} + +func getExtension(filename string) string { + ext := filepath.Ext(filename) + return strings.ToLower(ext) +} diff --git a/common/conf/mergers/merger_base.go b/common/conf/mergers/merger_base.go new file mode 100644 index 00000000..10d608d6 --- /dev/null +++ b/common/conf/mergers/merger_base.go @@ -0,0 +1,100 @@ +package mergers + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + + "github.com/sagernet/sing-box/common/conf/merge" +) + +// Merger is a configurable format merger for V2Ray config files. +type Merger struct { + Name Format + Extensions []string + Merge mergeFunc +} + +// mergeFunc is a utility to merge the input into map[string]interface{} +type mergeFunc func(interface{}, map[string]interface{}) error + +// mapFunc converts the input bytes of a config content to map[string]interface{} +type mapFunc func([]byte) (map[string]interface{}, error) + +// makeMerger makes a merger who merge the format by converting it to JSON +func makeMerger(name Format, extensions []string, converter mapFunc) *Merger { + return &Merger{ + Name: name, + Extensions: extensions, + Merge: makeMergeFunc(converter), + } +} + +// makeMergeFunc makes a merge func who merge the input to +func makeMergeFunc(converter mapFunc) mergeFunc { + return func(input interface{}, target map[string]interface{}) error { + if target == nil { + panic("merge target is nil") + } + switch v := input.(type) { + case string: + err := loadFile(v, target, converter) + if err != nil { + return err + } + case []string: + err := loadFiles(v, target, converter) + if err != nil { + return err + } + case []byte: + err := loadBytes(v, target, converter) + if err != nil { + return err + } + case io.Reader: + err := loadReader(v, target, converter) + if err != nil { + return err + } + default: + return errors.New("unknow merge input type") + } + return nil + } +} + +func loadFiles(files []string, target map[string]interface{}, converter mapFunc) error { + for _, file := range files { + err := loadFile(file, target, converter) + if err != nil { + return err + } + } + return nil +} + +func loadFile(file string, target map[string]interface{}, converter mapFunc) error { + bs, err := loadToBytes(file) + if err != nil { + return fmt.Errorf("fail to load %s: %s", file, err) + } + return loadBytes(bs, target, converter) +} + +func loadReader(reader io.Reader, target map[string]interface{}, converter mapFunc) error { + bs, err := ioutil.ReadAll(reader) + if err != nil { + return err + } + return loadBytes(bs, target, converter) +} + +func loadBytes(bs []byte, target map[string]interface{}, converter mapFunc) error { + m, err := converter(bs) + if err != nil { + return err + } + return merge.Maps(target, m) +} diff --git a/common/conf/mergers/mergers.go b/common/conf/mergers/mergers.go new file mode 100644 index 00000000..a795270f --- /dev/null +++ b/common/conf/mergers/mergers.go @@ -0,0 +1,91 @@ +package mergers + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "github.com/pelletier/go-toml" + "github.com/sagernet/sing-box/common/conf/jsonc" + "github.com/sagernet/sing-box/common/conf/merge" + "github.com/sagernet/sing/common" + "gopkg.in/yaml.v2" +) + +func init() { + common.Must(registerMerger(makeMerger( + FormatJSON, + []string{".json"}, + func(v []byte) (map[string]interface{}, error) { + m := make(map[string]interface{}) + if err := json.Unmarshal(v, &m); err != nil { + return nil, err + } + return m, nil + }, + ))) + common.Must(registerMerger(makeMerger( + FormatJSONC, + []string{".jsonc"}, + func(v []byte) (map[string]interface{}, error) { + m := make(map[string]interface{}) + if err := jsonc.Decode(bytes.NewReader(v), &m); err != nil { + return nil, err + } + return m, nil + }, + ))) + common.Must(registerMerger(makeMerger( + FormatTOML, + []string{".toml"}, + func(v []byte) (map[string]interface{}, error) { + m := make(map[string]interface{}) + if err := toml.Unmarshal(v, &m); err != nil { + return nil, err + } + return m, nil + }, + ))) + common.Must(registerMerger(makeMerger( + FormatYAML, + []string{".yml", ".yaml"}, + func(v []byte) (map[string]interface{}, error) { + m1 := make(map[interface{}]interface{}) + err := yaml.Unmarshal(v, &m1) + if err != nil { + return nil, err + } + m2 := merge.Convert(m1) + return m2, nil + }, + ))) + common.Must(registerMerger( + &Merger{ + Name: FormatAuto, + Extensions: nil, + Merge: Merge, + }), + ) +} + +var ( + mergersByName = make(map[Format]*Merger) + mergersByExt = make(map[string]*Merger) +) + +// registerMerger add a new Merger. +func registerMerger(format *Merger) error { + if _, found := mergersByName[format.Name]; found { + return fmt.Errorf("%s already registered", format.Name) + } + mergersByName[format.Name] = format + for _, ext := range format.Extensions { + lext := strings.ToLower(ext) + if f, found := mergersByExt[lext]; found { + return fmt.Errorf("%s already registered to %s", ext, f.Name) + } + mergersByExt[lext] = format + } + return nil +} diff --git a/common/conf/output.go b/common/conf/output.go new file mode 100644 index 00000000..a93c513b --- /dev/null +++ b/common/conf/output.go @@ -0,0 +1,50 @@ +package conf + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/pelletier/go-toml" + "github.com/sagernet/sing-box/common/conf/mergers" + "gopkg.in/yaml.v2" +) + +// Sprint returns the text of give format for v +func Sprint(v interface{}, format mergers.Format) (string, error) { + var ( + out []byte + err error + ) + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(v) + if err != nil { + return "", fmt.Errorf("failed to convert to json: %s", err) + } + if format == mergers.FormatJSON || format == mergers.FormatJSONC { + return string(buffer.Bytes()), nil + } + + m := make(map[string]interface{}) + err = json.Unmarshal(buffer.Bytes(), &m) + if err != nil { + return "", err + } + switch format { + case mergers.FormatTOML: + out, err = toml.Marshal(m) + if err != nil { + return "", fmt.Errorf("failed to convert to toml: %s", err) + } + case mergers.FormatYAML: + out, err = yaml.Marshal(m) + if err != nil { + return "", fmt.Errorf("failed to convert to yaml: %s", err) + } + default: + return "", fmt.Errorf("invalid output format: %s", format) + } + return string(out), nil +} diff --git a/go.mod b/go.mod index 957211f4..0888cad7 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/logrusorgru/aurora v2.0.3+incompatible github.com/mholt/acmez v1.0.4 github.com/oschwald/maxminddb-golang v1.10.0 + github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.6.2 github.com/sagernet/certmagic v0.0.0-20220819042630-4a57f8b6853a github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb @@ -31,11 +32,12 @@ require ( go.uber.org/atomic v1.10.0 go4.org/netipx v0.0.0-20220812043211-3cc044ffd68d golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 - golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b + golang.org/x/net v0.0.0-20220909164309-bea034e7d591 golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 golang.zx2c4.com/wireguard v0.0.0-20220829161405-d1d08426b27b google.golang.org/grpc v1.49.0 google.golang.org/protobuf v1.28.1 + gopkg.in/yaml.v2 v2.4.0 gvisor.dev/gvisor v0.0.0-20220819163037-ba6e795b139a ) @@ -50,6 +52,7 @@ require ( github.com/google/btree v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.1.0 // indirect + github.com/kr/pretty v0.3.0 // indirect github.com/libdns/libdns v0.2.1 // indirect github.com/marten-seemann/qpack v0.2.1 // indirect github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect @@ -57,6 +60,7 @@ require ( github.com/nxadm/tail v1.4.8 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e // indirect github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect github.com/sagernet/netlink v0.0.0-20220826133217-3fb4ff92ea17 // indirect @@ -70,7 +74,7 @@ require ( golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect - google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f // indirect + google.golang.org/genproto v0.0.0-20220323144105-ec3c684e5b14 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.1.7 // indirect diff --git a/go.sum b/go.sum index 3e1d2678..29b87d39 100644 --- a/go.sum +++ b/go.sum @@ -9,11 +9,16 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= @@ -28,7 +33,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -82,11 +87,13 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= @@ -112,14 +119,20 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e h1:5CFRo8FJbCuf5s/eTBdZpmMbn8Fe2eSMLNAYfKanA34= github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e/go.mod h1:qbt0dWObotCfcjAJJ9AxtFPNSDUfZF+6dCpgKEOBn/g= @@ -190,8 +203,6 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= @@ -212,8 +223,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -264,7 +275,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -284,15 +294,15 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f h1:YORWxaStkWBnWgELOHTmDrqNlFXuVGEbhwbB5iK94bQ= -google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20220323144105-ec3c684e5b14 h1:17TOyVD+9MLIDtDJW9PdtMuVT7gNLEkN+G/xFYjZmr8= +google.golang.org/genproto v0.0.0-20220323144105-ec3c684e5b14/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -312,6 +322,7 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=