1
0
mirror of https://github.com/nitezs/sub2sing-box.git synced 2024-12-24 11:14:41 -05:00

feat: 节点删除\重命名\去重

This commit is contained in:
Nite07 2024-03-11 23:39:58 +08:00
parent daa3ab6867
commit 043167cccf
25 changed files with 288 additions and 205 deletions

View File

@ -1,7 +1,5 @@
# sub2sing-box # sub2sing-box
## 使用指南
``` ```
Convert common proxy to sing-box proxy Convert common proxy to sing-box proxy
@ -15,3 +13,16 @@ Flags:
-s, --subscription strings subscription urls -s, --subscription strings subscription urls
-t, --template string template file path -t, --template string template file path
``` ```
## Template
Template 中使用 `<all-proxy-tags>` 指明节点插入位置,例如
```
{
"type": "selector",
"tag": "节点选择",
"outbounds": ["<all-proxy-tags>", "direct"],
"interrupt_exist_connections": true
},
```

View File

@ -2,30 +2,36 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"net/http"
"os" "os"
"strings" "sub2sing-box/internal/model"
"sub2sing-box/constant" . "sub2sing-box/pkg/util"
"sub2sing-box/model"
. "sub2sing-box/util"
"github.com/spf13/cobra" "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{ var convertCmd = &cobra.Command{
Use: "convert", Use: "convert",
Long: "Convert common proxy to sing-box proxy", Long: "Convert common proxy to sing-box proxy",
Short: "Convert common proxy to sing-box proxy", Short: "Convert common proxy to sing-box proxy",
Run: func(cmd *cobra.Command, args []string) { 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 := "" result := ""
var err error var err error
@ -43,6 +49,57 @@ var convertCmd = &cobra.Command{
proxyList = append(proxyList, p) 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 != "" { if template != "" {
result, err = MergeTemplate(proxyList, template) result, err = MergeTemplate(proxyList, template)
if err != nil { if err != nil {
@ -67,168 +124,5 @@ var convertCmd = &cobra.Command{
} else { } else {
fmt.Println(string(result)) 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 == "<all-proxy-tags>" {
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
}

View File

@ -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,
}

View File

@ -1,4 +1,4 @@
package util package internal
import ( import (
"encoding/base64" "encoding/base64"

View File

@ -5,7 +5,7 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "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 //hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks

View File

@ -5,7 +5,7 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "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 // hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com

15
pkg/parser/parsers_map.go Normal file
View File

@ -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,
}

View File

@ -5,8 +5,8 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sub2sing-box/model" "sub2sing-box/internal"
. "sub2sing-box/util" "sub2sing-box/internal/model"
) )
func ParseShadowsocks(proxy string) (model.Proxy, error) { 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") return model.Proxy{}, errors.New("invalid ss Url")
} }
if !strings.Contains(parts[0], ":") { if !strings.Contains(parts[0], ":") {
decoded, err := DecodeBase64(parts[0]) decoded, err := internal.DecodeBase64(parts[0])
if err != nil { if err != nil {
return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) return model.Proxy{}, errors.New("invalid ss Url" + err.Error())
} }

View File

@ -5,7 +5,7 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sub2sing-box/model" "sub2sing-box/internal/model"
) )
func ParseTrojan(proxy string) (model.Proxy, error) { func ParseTrojan(proxy string) (model.Proxy, error) {

View File

@ -5,7 +5,7 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sub2sing-box/model" "sub2sing-box/internal/model"
) )
func ParseVless(proxy string) (model.Proxy, error) { func ParseVless(proxy string) (model.Proxy, error) {

View File

@ -6,15 +6,15 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sub2sing-box/model" "sub2sing-box/internal"
. "sub2sing-box/util" "sub2sing-box/internal/model"
) )
func ParseVmess(proxy string) (model.Proxy, error) { func ParseVmess(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, "vmess://") { if !strings.HasPrefix(proxy, "vmess://") {
return model.Proxy{}, errors.New("invalid vmess url") 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 { if err != nil {
return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
} }

179
pkg/util/convert.go Normal file
View File

@ -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 == "<all-proxy-tags>" {
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
}