diff --git a/Readme.md b/Readme.md index 357d139..4eab448 100644 --- a/Readme.md +++ b/Readme.md @@ -1,7 +1,5 @@ # sub2sing-box -## 使用指南 - ``` Convert common proxy to sing-box proxy @@ -14,4 +12,17 @@ Flags: -p, --proxy strings common proxies -s, --subscription strings subscription urls -t, --template string template file path -``` \ No newline at end of file +``` + +## Template + +Template 中使用 `` 指明节点插入位置,例如 + +``` +{ + "type": "selector", + "tag": "节点选择", + "outbounds": ["", "direct"], + "interrupt_exist_connections": true +}, +``` diff --git a/cmd/convert.go b/cmd/convert.go index bd531ed..387c98c 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -2,30 +2,36 @@ package cmd import ( "encoding/json" - "errors" "fmt" - "io" - "net/http" "os" - "strings" - "sub2sing-box/constant" - "sub2sing-box/model" - . "sub2sing-box/util" + "sub2sing-box/internal/model" + . "sub2sing-box/pkg/util" "github.com/spf13/cobra" ) -//TODO: 过滤、去重、分组、排序 +var subscriptions []string +var proxies []string +var template string +var output string +var delete string +var rename map[string]string + +func init() { + convertCmd.Flags().StringSliceVarP(&subscriptions, "subscription", "s", []string{}, "subscription urls") + convertCmd.Flags().StringSliceVarP(&proxies, "proxy", "p", []string{}, "common proxies") + convertCmd.Flags().StringVarP(&template, "template", "t", "", "template file path") + convertCmd.Flags().StringVarP(&output, "output", "o", "", "output file path") + convertCmd.Flags().StringVarP(&delete, "delete", "d", "", "delete proxy with regex") + convertCmd.Flags().StringToStringVarP(&rename, "rename", "r", map[string]string{}, "rename proxy with regex") + RootCmd.AddCommand(convertCmd) +} var convertCmd = &cobra.Command{ Use: "convert", Long: "Convert common proxy to sing-box proxy", Short: "Convert common proxy to sing-box proxy", Run: func(cmd *cobra.Command, args []string) { - subscriptions, _ := cmd.Flags().GetStringSlice("subscription") - proxies, _ := cmd.Flags().GetStringSlice("proxy") - template, _ := cmd.Flags().GetString("template") - output, _ := cmd.Flags().GetString("output") result := "" var err error @@ -43,6 +49,57 @@ var convertCmd = &cobra.Command{ proxyList = append(proxyList, p) } + if delete != "" { + proxyList, err = DeleteProxy(proxyList, delete) + if err != nil { + fmt.Println(err) + return + } + } + + for k, v := range rename { + proxyList, err = RenameProxy(proxyList, k, v) + if err != nil { + fmt.Println(err) + return + } + } + + keep := make(map[int]bool) + set := make(map[string]struct { + Proxy model.Proxy + Count int + }) + for i, p := range proxyList { + if _, exists := set[p.Tag]; !exists { + keep[i] = true + set[p.Tag] = struct { + Proxy model.Proxy + Count int + }{p, 0} + } else { + p1, _ := json.Marshal(p) + p2, _ := json.Marshal(set[p.Tag]) + if string(p1) != string(p2) { + set[p.Tag] = struct { + Proxy model.Proxy + Count int + }{p, set[p.Tag].Count + 1} + keep[i] = true + proxyList[i].Tag = fmt.Sprintf("%s %d", p.Tag, set[p.Tag].Count) + } else { + keep[i] = false + } + } + } + var newProxyList []model.Proxy + for i, p := range proxyList { + if keep[i] { + newProxyList = append(newProxyList, p) + } + } + proxyList = newProxyList + if template != "" { result, err = MergeTemplate(proxyList, template) if err != nil { @@ -67,168 +124,5 @@ var convertCmd = &cobra.Command{ } else { fmt.Println(string(result)) } - }, } - -func init() { - convertCmd.Flags().StringSliceP("subscription", "s", []string{}, "subscription urls") - convertCmd.Flags().StringSliceP("proxy", "p", []string{}, "common proxies") - convertCmd.Flags().StringP("template", "t", "", "template file path") - convertCmd.Flags().StringP("output", "o", "", "output file path") - convertCmd.Flags().StringP("filter", "f", "", "outbound tag filter (support regex)") - RootCmd.AddCommand(convertCmd) -} - -func Convert(urls []string, proxies []string) ([]model.Proxy, error) { - proxyList := make([]model.Proxy, 0) - newProxies, err := ConvertSubscriptionsToSProxy(urls) - if err != nil { - return nil, err - } - proxyList = append(proxyList, newProxies...) - for _, p := range proxies { - proxy, err := ConvertCProxyToSProxy(p) - if err != nil { - return nil, err - } - proxyList = append(proxyList, proxy) - } - return proxyList, nil -} - -func MergeTemplate(proxies []model.Proxy, template string) (string, error) { - config, err := ReadTemplate(template) - proxyTags := make([]string, 0) - if err != nil { - return "", err - } - for _, p := range proxies { - proxyTags = append(proxyTags, p.Tag) - } - ps, err := json.Marshal(&proxies) - fmt.Print(string(ps)) - if err != nil { - return "", err - } - var newOutbounds []model.Outbound - err = json.Unmarshal(ps, &newOutbounds) - if err != nil { - return "", err - } - for i, outbound := range config.Outbounds { - if outbound.Type == "urltest" || outbound.Type == "selector" { - var parsedOutbound []string = make([]string, 0) - for _, o := range outbound.Outbounds { - if o == "" { - parsedOutbound = append(parsedOutbound, proxyTags...) - } else { - parsedOutbound = append(parsedOutbound, o) - } - } - config.Outbounds[i].Outbounds = parsedOutbound - } - } - config.Outbounds = append(config.Outbounds, newOutbounds...) - data, err := json.Marshal(config) - if err != nil { - return "", err - } - return string(data), nil -} - -func ConvertCProxyToSProxy(proxy string) (model.Proxy, error) { - for prefix, parseFunc := range constant.ParserMap { - if strings.HasPrefix(proxy, prefix) { - proxy, err := parseFunc(proxy) - if err != nil { - return model.Proxy{}, err - } - return proxy, nil - } - } - return model.Proxy{}, errors.New("Unknown proxy format") -} - -func ConvertCProxyToJson(proxy string) (string, error) { - sProxy, err := ConvertCProxyToSProxy(proxy) - if err != nil { - return "", err - } - data, err := json.Marshal(&sProxy) - if err != nil { - return "", err - } - return string(data), nil -} - -func FetchSubscription(url string, maxRetryTime int) (string, error) { - retryTime := 0 - var err error - for retryTime < maxRetryTime { - resp, err := http.Get(url) - if err != nil { - retryTime++ - continue - } - data, err := io.ReadAll(resp.Body) - if err != nil { - retryTime++ - continue - } - return string(data), err - } - return "", err -} - -func ConvertSubscriptionsToSProxy(urls []string) ([]model.Proxy, error) { - proxyList := make([]model.Proxy, 0) - for _, url := range urls { - data, err := FetchSubscription(url, 3) - if err != nil { - return nil, err - } - proxy, err := DecodeBase64(data) - if err != nil { - return nil, err - } - proxies := strings.Split(proxy, "\n") - for _, p := range proxies { - for prefix, parseFunc := range constant.ParserMap { - if strings.HasPrefix(p, prefix) { - proxy, err := parseFunc(p) - if err != nil { - return nil, err - } - proxyList = append(proxyList, proxy) - } - } - } - } - return proxyList, nil -} - -func ConvertSubscriptionsToJson(urls []string) (string, error) { - proxyList, err := ConvertSubscriptionsToSProxy(urls) - if err != nil { - return "", err - } - result, err := json.Marshal(proxyList) - if err != nil { - return "", err - } - return string(result), nil -} - -func ReadTemplate(path string) (model.Config, error) { - data, err := os.ReadFile(path) - if err != nil { - return model.Config{}, err - } - var res model.Config - err = json.Unmarshal(data, &res) - if err != nil { - return model.Config{}, err - } - return res, nil -} diff --git a/constant/parsers_map.go b/constant/parsers_map.go deleted file mode 100644 index 437c92c..0000000 --- a/constant/parsers_map.go +++ /dev/null @@ -1,16 +0,0 @@ -package constant - -import ( - "sub2sing-box/model" - "sub2sing-box/parser" -) - -var ParserMap map[string]func(string) (model.Proxy, error) = map[string]func(string) (model.Proxy, error){ - "ss://": parser.ParseShadowsocks, - "vmess://": parser.ParseVmess, - "trojan://": parser.ParseTrojan, - "vless://": parser.ParseVless, - "hysteria://": parser.ParseHysteria, - "hy2://": parser.ParseHysteria2, - "hysteria2://": parser.ParseHysteria2, -} diff --git a/util/base64.go b/internal/base64.go similarity index 94% rename from util/base64.go rename to internal/base64.go index d22bfb2..1cdd891 100644 --- a/util/base64.go +++ b/internal/base64.go @@ -1,4 +1,4 @@ -package util +package internal import ( "encoding/base64" diff --git a/model/config.go b/internal/model/config.go similarity index 100% rename from model/config.go rename to internal/model/config.go diff --git a/model/hysteria.go b/internal/model/hysteria.go similarity index 100% rename from model/hysteria.go rename to internal/model/hysteria.go diff --git a/model/hysteria2.go b/internal/model/hysteria2.go similarity index 100% rename from model/hysteria2.go rename to internal/model/hysteria2.go diff --git a/model/multiplex.go b/internal/model/multiplex.go similarity index 100% rename from model/multiplex.go rename to internal/model/multiplex.go diff --git a/model/proxy.go b/internal/model/proxy.go similarity index 100% rename from model/proxy.go rename to internal/model/proxy.go diff --git a/model/shadowsocks.go b/internal/model/shadowsocks.go similarity index 100% rename from model/shadowsocks.go rename to internal/model/shadowsocks.go diff --git a/model/tls.go b/internal/model/tls.go similarity index 100% rename from model/tls.go rename to internal/model/tls.go diff --git a/model/trojan.go b/internal/model/trojan.go similarity index 100% rename from model/trojan.go rename to internal/model/trojan.go diff --git a/model/tuic.go b/internal/model/tuic.go similarity index 100% rename from model/tuic.go rename to internal/model/tuic.go diff --git a/model/udp_over_tcp.go b/internal/model/udp_over_tcp.go similarity index 100% rename from model/udp_over_tcp.go rename to internal/model/udp_over_tcp.go diff --git a/model/v2ray_transport.go b/internal/model/v2ray_transport.go similarity index 100% rename from model/v2ray_transport.go rename to internal/model/v2ray_transport.go diff --git a/model/vless.go b/internal/model/vless.go similarity index 100% rename from model/vless.go rename to internal/model/vless.go diff --git a/model/vmess.go b/internal/model/vmess.go similarity index 100% rename from model/vmess.go rename to internal/model/vmess.go diff --git a/parser/hysteria.go b/pkg/parser/hysteria.go similarity index 98% rename from parser/hysteria.go rename to pkg/parser/hysteria.go index 5b3be9a..7f02fa0 100644 --- a/parser/hysteria.go +++ b/pkg/parser/hysteria.go @@ -5,7 +5,7 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/model" + "sub2sing-box/internal/model" ) //hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks diff --git a/parser/hysteria2.go b/pkg/parser/hysteria2.go similarity index 98% rename from parser/hysteria2.go rename to pkg/parser/hysteria2.go index ab09dee..c1ce410 100644 --- a/parser/hysteria2.go +++ b/pkg/parser/hysteria2.go @@ -5,7 +5,7 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/model" + "sub2sing-box/internal/model" ) // hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com diff --git a/pkg/parser/parsers_map.go b/pkg/parser/parsers_map.go new file mode 100644 index 0000000..06082c1 --- /dev/null +++ b/pkg/parser/parsers_map.go @@ -0,0 +1,15 @@ +package parser + +import ( + "sub2sing-box/internal/model" +) + +var ParserMap map[string]func(string) (model.Proxy, error) = map[string]func(string) (model.Proxy, error){ + "ss://": ParseShadowsocks, + "vmess://": ParseVmess, + "trojan://": ParseTrojan, + "vless://": ParseVless, + "hysteria://": ParseHysteria, + "hy2://": ParseHysteria2, + "hysteria2://": ParseHysteria2, +} diff --git a/parser/shadowsocks.go b/pkg/parser/shadowsocks.go similarity index 93% rename from parser/shadowsocks.go rename to pkg/parser/shadowsocks.go index 7417277..398fc8f 100644 --- a/parser/shadowsocks.go +++ b/pkg/parser/shadowsocks.go @@ -5,8 +5,8 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/model" - . "sub2sing-box/util" + "sub2sing-box/internal" + "sub2sing-box/internal/model" ) func ParseShadowsocks(proxy string) (model.Proxy, error) { @@ -18,7 +18,7 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) { return model.Proxy{}, errors.New("invalid ss Url") } if !strings.Contains(parts[0], ":") { - decoded, err := DecodeBase64(parts[0]) + decoded, err := internal.DecodeBase64(parts[0]) if err != nil { return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) } diff --git a/parser/trojan.go b/pkg/parser/trojan.go similarity index 98% rename from parser/trojan.go rename to pkg/parser/trojan.go index f2253e3..95051c9 100644 --- a/parser/trojan.go +++ b/pkg/parser/trojan.go @@ -5,7 +5,7 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/model" + "sub2sing-box/internal/model" ) func ParseTrojan(proxy string) (model.Proxy, error) { diff --git a/parser/vless.go b/pkg/parser/vless.go similarity index 99% rename from parser/vless.go rename to pkg/parser/vless.go index f9beccc..30005c3 100644 --- a/parser/vless.go +++ b/pkg/parser/vless.go @@ -5,7 +5,7 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/model" + "sub2sing-box/internal/model" ) func ParseVless(proxy string) (model.Proxy, error) { diff --git a/parser/vmess.go b/pkg/parser/vmess.go similarity index 95% rename from parser/vmess.go rename to pkg/parser/vmess.go index f757453..1605677 100644 --- a/parser/vmess.go +++ b/pkg/parser/vmess.go @@ -6,15 +6,15 @@ import ( "net/url" "strconv" "strings" - "sub2sing-box/model" - . "sub2sing-box/util" + "sub2sing-box/internal" + "sub2sing-box/internal/model" ) func ParseVmess(proxy string) (model.Proxy, error) { if !strings.HasPrefix(proxy, "vmess://") { return model.Proxy{}, errors.New("invalid vmess url") } - base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) + base64, err := internal.DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) if err != nil { return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) } diff --git a/pkg/util/convert.go b/pkg/util/convert.go new file mode 100644 index 0000000..02d1ef8 --- /dev/null +++ b/pkg/util/convert.go @@ -0,0 +1,179 @@ +package util + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "regexp" + "strings" + + . "sub2sing-box/internal" + "sub2sing-box/internal/model" + "sub2sing-box/pkg/parser" +) + +func MergeTemplate(proxies []model.Proxy, template string) (string, error) { + config, err := ReadTemplate(template) + proxyTags := make([]string, 0) + if err != nil { + return "", err + } + for _, p := range proxies { + proxyTags = append(proxyTags, p.Tag) + } + ps, err := json.Marshal(&proxies) + fmt.Print(string(ps)) + if err != nil { + return "", err + } + var newOutbounds []model.Outbound + err = json.Unmarshal(ps, &newOutbounds) + if err != nil { + return "", err + } + for i, outbound := range config.Outbounds { + if outbound.Type == "urltest" || outbound.Type == "selector" { + var parsedOutbound []string = make([]string, 0) + for _, o := range outbound.Outbounds { + if o == "" { + parsedOutbound = append(parsedOutbound, proxyTags...) + } else { + parsedOutbound = append(parsedOutbound, o) + } + } + config.Outbounds[i].Outbounds = parsedOutbound + } + } + config.Outbounds = append(config.Outbounds, newOutbounds...) + data, err := json.Marshal(config) + if err != nil { + return "", err + } + return string(data), nil +} + +func ConvertCProxyToSProxy(proxy string) (model.Proxy, error) { + for prefix, parseFunc := range parser.ParserMap { + if strings.HasPrefix(proxy, prefix) { + proxy, err := parseFunc(proxy) + if err != nil { + return model.Proxy{}, err + } + return proxy, nil + } + } + return model.Proxy{}, errors.New("Unknown proxy format") +} + +func ConvertCProxyToJson(proxy string) (string, error) { + sProxy, err := ConvertCProxyToSProxy(proxy) + if err != nil { + return "", err + } + data, err := json.Marshal(&sProxy) + if err != nil { + return "", err + } + return string(data), nil +} + +func FetchSubscription(url string, maxRetryTimes int) (string, error) { + retryTime := 0 + var err error + for retryTime < maxRetryTimes { + resp, err := http.Get(url) + if err != nil { + retryTime++ + continue + } + data, err := io.ReadAll(resp.Body) + if err != nil { + retryTime++ + continue + } + return string(data), err + } + return "", err +} + +func ConvertSubscriptionsToSProxy(urls []string) ([]model.Proxy, error) { + proxyList := make([]model.Proxy, 0) + for _, url := range urls { + data, err := FetchSubscription(url, 3) + if err != nil { + return nil, err + } + proxy, err := DecodeBase64(data) + if err != nil { + return nil, err + } + proxies := strings.Split(proxy, "\n") + for _, p := range proxies { + for prefix, parseFunc := range parser.ParserMap { + if strings.HasPrefix(p, prefix) { + proxy, err := parseFunc(p) + if err != nil { + return nil, err + } + proxyList = append(proxyList, proxy) + } + } + } + } + return proxyList, nil +} + +func ConvertSubscriptionsToJson(urls []string) (string, error) { + proxyList, err := ConvertSubscriptionsToSProxy(urls) + if err != nil { + return "", err + } + result, err := json.Marshal(proxyList) + if err != nil { + return "", err + } + return string(result), nil +} + +func ReadTemplate(path string) (model.Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return model.Config{}, err + } + var res model.Config + err = json.Unmarshal(data, &res) + if err != nil { + return model.Config{}, err + } + return res, nil +} + +func DeleteProxy(proxies []model.Proxy, regex string) ([]model.Proxy, error) { + reg, err := regexp.Compile(regex) + if err != nil { + return nil, err + } + var newProxies []model.Proxy + for _, p := range proxies { + if !reg.MatchString(p.Tag) { + newProxies = append(newProxies, p) + } + } + return newProxies, nil +} + +func RenameProxy(proxies []model.Proxy, regex string, replaceText string) ([]model.Proxy, error) { + reg, err := regexp.Compile(regex) + if err != nil { + return nil, err + } + for i, p := range proxies { + if reg.MatchString(p.Tag) { + proxies[i].Tag = reg.ReplaceAllString(p.Tag, replaceText) + } + } + return proxies, nil +}