diff --git a/.env.example b/.env.example deleted file mode 100644 index e6f415d..0000000 --- a/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -PORT=8011 -META_TEMPLATE=meta_template.json -CLASH_TEMPLATE=clash_template.json -REQUEST_RETRY_TIMES=3 -REQUEST_MAX_FILE_SIZE=1048576 -CACHE_EXPIRE=300 -LOG_LEVEL=info diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0420b4e..fc9180f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -18,7 +18,7 @@ jobs: with: images: | nite07/sub2clash - ghcr.io/nitezs/sub2clash + ghcr.io/bestnite/sub2clash tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} diff --git a/README.md b/README.md index 54b9568..ace2e96 100644 --- a/README.md +++ b/README.md @@ -25,22 +25,83 @@ ### 部署 - [docker compose](./docker-compose.yml) -- 运行[二进制文件](https://github.com/nitezs/sub2clash/releases/latest) +- 运行[二进制文件](https://github.com/bestnite/sub2clash/releases/latest) ### 配置 -可以通过编辑 .env 文件来修改默认配置,docker 直接添加环境变量 +支持多种配置方式,按优先级排序: -| 变量名 | 说明 | 默认值 | -| --------------------- | ---------------------------------------------- | --------------------- | -| PORT | 端口 | `8011` | -| META_TEMPLATE | 默认 meta 模板文件名 | `template_meta.yaml` | -| CLASH_TEMPLATE | 默认 clash 模板文件名 | `template_clash.yaml` | -| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` | -| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小(byte) | `1048576` | -| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` | -| LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` | -| SHORT_LINK_LENGTH | 短链长度 | `6` | +1. **配置文件**:支持多种格式(YAML、JSON、TOML、INI),按以下优先级搜索: + - `config.yaml` / `config.yml` + - `config.json` + - `config.toml` + - `config.ini` + - `sub2clash.yaml` / `sub2clash.yml` + - `sub2clash.json` + - `sub2clash.toml` + - `sub2clash.ini` +2. **环境变量**:使用 `SUB2CLASH_` 前缀,例如 `SUB2CLASH_PORT=8080` +3. **默认值**:内置默认配置 + +| 配置项 | 环境变量 | 说明 | 默认值 | +| --------------------- | ------------------------------- | --------------------------------------- | -------------- | +| port | SUB2CLASH_PORT | 服务端口 | `8011` | +| meta_template | SUB2CLASH_META_TEMPLATE | 默认 meta 模板 URL | [默认模板链接] | +| clash_template | SUB2CLASH_CLASH_TEMPLATE | 默认 clash 模板 URL | [默认模板链接] | +| request_retry_times | SUB2CLASH_REQUEST_RETRY_TIMES | 请求重试次数 | `3` | +| request_max_file_size | SUB2CLASH_REQUEST_MAX_FILE_SIZE | 请求文件最大大小(byte) | `1048576` | +| cache_expire | SUB2CLASH_CACHE_EXPIRE | 订阅缓存时间(秒) | `300` | +| log_level | SUB2CLASH_LOG_LEVEL | 日志等级:`debug`,`info`,`warn`,`error` | `info` | +| short_link_length | SUB2CLASH_SHORT_LINK_LENGTH | 短链长度 | `6` | + +#### 配置文件示例 + +参考示例文件: + +- [config.example.yaml](./config.example.yaml) - YAML 格式 +- [config.example.json](./config.example.json) - JSON 格式 +- [config.example.toml](./config.example.toml) - TOML 格式 + +**YAML 格式** (`config.yaml`): + +```yaml +port: 8011 +meta_template: "https://example.com/meta.yaml" +clash_template: "https://example.com/clash.yaml" +request_retry_times: 5 +request_max_file_size: 2097152 # 2MB +cache_expire: 600 # 10 minutes +log_level: "debug" +short_link_length: 8 +``` + +**JSON 格式** (`config.json`): + +```json +{ + "port": 8011, + "meta_template": "https://example.com/meta.yaml", + "clash_template": "https://example.com/clash.yaml", + "request_retry_times": 5, + "request_max_file_size": 2097152, + "cache_expire": 600, + "log_level": "debug", + "short_link_length": 8 +} +``` + +**TOML 格式** (`config.toml`): + +```toml +port = 8011 +meta_template = "https://example.com/meta.yaml" +clash_template = "https://example.com/clash.yaml" +request_retry_times = 5 +request_max_file_size = 2097152 +cache_expire = 600 +log_level = "debug" +short_link_length = 8 +``` ### API diff --git a/api/handler/clash.go b/api/handler/clash.go deleted file mode 100644 index 82a0af2..0000000 --- a/api/handler/clash.go +++ /dev/null @@ -1,44 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/nitezs/sub2clash/config" - "github.com/nitezs/sub2clash/model" - "github.com/nitezs/sub2clash/validator" - - "github.com/gin-gonic/gin" - "gopkg.in/yaml.v3" -) - -func SubmodHandler(c *gin.Context) { - - query, err := validator.ParseQuery(c) - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return - } - sub, err := BuildSub(model.Clash, query, config.Default.ClashTemplate) - if err != nil { - c.String(http.StatusInternalServerError, err.Error()) - return - } - - if query.NodeListMode { - nodelist := model.NodeList{} - nodelist.Proxies = sub.Proxies - marshal, err := yaml.Marshal(nodelist) - if err != nil { - c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error()) - return - } - c.String(http.StatusOK, string(marshal)) - return - } - marshal, err := yaml.Marshal(sub) - if err != nil { - c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error()) - return - } - c.String(http.StatusOK, string(marshal)) -} diff --git a/api/handler/default.go b/api/handler/default.go deleted file mode 100644 index eeff99a..0000000 --- a/api/handler/default.go +++ /dev/null @@ -1,308 +0,0 @@ -package handler - -import ( - "crypto/sha256" - "encoding/hex" - "errors" - "net/url" - "regexp" - "sort" - "strconv" - "strings" - - "github.com/nitezs/sub2clash/common" - "github.com/nitezs/sub2clash/logger" - "github.com/nitezs/sub2clash/model" - "github.com/nitezs/sub2clash/parser" - "github.com/nitezs/sub2clash/validator" - - "go.uber.org/zap" - "gopkg.in/yaml.v3" -) - -func BuildSub(clashType model.ClashType, query validator.SubValidator, template string) ( - *model.Subscription, error, -) { - - var temp = &model.Subscription{} - var sub = &model.Subscription{} - var err error - var templateBytes []byte - - if query.Template != "" { - template = query.Template - } - if strings.HasPrefix(template, "http") { - templateBytes, err = common.LoadSubscription(template, query.Refresh, query.UserAgent) - if err != nil { - logger.Logger.Debug( - "load template failed", zap.String("template", template), zap.Error(err), - ) - return nil, errors.New("加载模板失败: " + err.Error()) - } - } else { - unescape, err := url.QueryUnescape(template) - if err != nil { - return nil, errors.New("加载模板失败: " + err.Error()) - } - templateBytes, err = common.LoadTemplate(unescape) - if err != nil { - logger.Logger.Debug( - "load template failed", zap.String("template", template), zap.Error(err), - ) - return nil, errors.New("加载模板失败: " + err.Error()) - } - } - - err = yaml.Unmarshal(templateBytes, &temp) - if err != nil { - logger.Logger.Debug("parse template failed", zap.Error(err)) - return nil, errors.New("解析模板失败: " + err.Error()) - } - var proxyList []model.Proxy - - for i := range query.Subs { - data, err := common.LoadSubscription(query.Subs[i], query.Refresh, query.UserAgent) - subName := "" - if strings.Contains(query.Subs[i], "#") { - subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:] - } - if err != nil { - logger.Logger.Debug( - "load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err), - ) - return nil, errors.New("加载订阅失败: " + err.Error()) - } - - err = yaml.Unmarshal(data, &sub) - var newProxies []model.Proxy - if err != nil { - reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria|hy2|hysteria2)://") - if reg.Match(data) { - p := common.ParseProxy(strings.Split(string(data), "\n")...) - newProxies = p - } else { - - base64, err := parser.DecodeBase64(string(data)) - if err != nil { - logger.Logger.Debug( - "parse subscription failed", zap.String("url", query.Subs[i]), - zap.String("data", string(data)), - zap.Error(err), - ) - return nil, errors.New("加载订阅失败: " + err.Error()) - } - p := common.ParseProxy(strings.Split(base64, "\n")...) - newProxies = p - } - } else { - newProxies = sub.Proxies - } - if subName != "" { - for i := range newProxies { - newProxies[i].SubName = subName - } - } - proxyList = append(proxyList, newProxies...) - } - - if len(query.Proxies) != 0 { - proxyList = append(proxyList, common.ParseProxy(query.Proxies...)...) - } - - for i := range proxyList { - if proxyList[i].SubName != "" { - proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name) - } - } - - proxies := make(map[string]*model.Proxy) - newProxies := make([]model.Proxy, 0, len(proxyList)) - for i := range proxyList { - key := proxyList[i].Server + strconv.Itoa(proxyList[i].Port) + proxyList[i].Type + proxyList[i].UUID + proxyList[i].Password - if proxyList[i].Network == "ws" { - key += proxyList[i].WSOpts.Path + proxyList[i].WSOpts.Headers["Host"] - } - if _, exist := proxies[key]; !exist { - proxies[key] = &proxyList[i] - newProxies = append(newProxies, proxyList[i]) - } - } - proxyList = newProxies - - if strings.TrimSpace(query.Remove) != "" { - newProxyList := make([]model.Proxy, 0, len(proxyList)) - for i := range proxyList { - removeReg, err := regexp.Compile(query.Remove) - if err != nil { - logger.Logger.Debug("remove regexp compile failed", zap.Error(err)) - return nil, errors.New("remove 参数非法: " + err.Error()) - } - - if removeReg.MatchString(proxyList[i].Name) { - continue - } - newProxyList = append(newProxyList, proxyList[i]) - } - proxyList = newProxyList - } - - if len(query.ReplaceKeys) != 0 { - - replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys)) - for _, v := range query.ReplaceKeys { - replaceReg, err := regexp.Compile(v) - if err != nil { - logger.Logger.Debug("replace regexp compile failed", zap.Error(err)) - return nil, errors.New("replace 参数非法: " + err.Error()) - } - replaceRegs = append(replaceRegs, replaceReg) - } - for i := range proxyList { - - for j, v := range replaceRegs { - if v.MatchString(proxyList[i].Name) { - proxyList[i].Name = v.ReplaceAllString( - proxyList[i].Name, query.ReplaceTo[j], - ) - } - } - } - } - - names := make(map[string]int) - for i := range proxyList { - if _, exist := names[proxyList[i].Name]; exist { - names[proxyList[i].Name] = names[proxyList[i].Name] + 1 - proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name]) - } else { - names[proxyList[i].Name] = 0 - } - } - - for i := range proxyList { - proxyList[i].Name = strings.TrimSpace(proxyList[i].Name) - } - - var t = &model.Subscription{} - common.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...) - - switch query.Sort { - case "sizeasc": - sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups)) - case "sizedesc": - sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroups))) - case "nameasc": - sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups)) - case "namedesc": - sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroups))) - default: - sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups)) - } - - MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup) - - for _, v := range query.Rules { - if v.Prepend { - common.PrependRules(temp, v.Rule) - } else { - common.AppendRules(temp, v.Rule) - } - } - - for _, v := range query.RuleProviders { - hash := sha256.Sum224([]byte(v.Url)) - name := hex.EncodeToString(hash[:]) - provider := model.RuleProvider{ - Type: "http", - Behavior: v.Behavior, - Url: v.Url, - Path: "./" + name + ".yaml", - Interval: 3600, - } - if v.Prepend { - common.PrependRuleProvider( - temp, v.Name, v.Group, provider, - ) - } else { - common.AppenddRuleProvider( - temp, v.Name, v.Group, provider, - ) - } - } - return temp, nil -} - -func fetchSubscriptionUserInfo(url string, userAgent string) (string, error) { - resp, err := common.Head(url, common.WithUserAgent(userAgent)) - if err != nil { - logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err)) - return "", err - } - defer resp.Body.Close() - if userInfo := resp.Header.Get("subscription-userinfo"); userInfo != "" { - return userInfo, nil - } - - logger.Logger.Debug("目标 URL 未返回 subscription-userinfo 头", zap.Error(err)) - return "", err -} - -func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) { - - var countryGroupNames []string - for _, proxyGroup := range sub.ProxyGroups { - if proxyGroup.IsCountryGrop { - countryGroupNames = append( - countryGroupNames, proxyGroup.Name, - ) - } - } - var proxyNames []string - for _, proxy := range sub.Proxies { - proxyNames = append(proxyNames, proxy.Name) - } - - temp.Proxies = append(temp.Proxies, sub.Proxies...) - - for i := range temp.ProxyGroups { - if temp.ProxyGroups[i].IsCountryGrop { - continue - } - newProxies := make([]string, 0) - countryGroupMap := make(map[string]model.ProxyGroup) - for _, v := range sub.ProxyGroups { - if v.IsCountryGrop { - countryGroupMap[v.Name] = v - } - } - for j := range temp.ProxyGroups[i].Proxies { - reg := regexp.MustCompile("<(.*?)>") - if reg.Match([]byte(temp.ProxyGroups[i].Proxies[j])) { - key := reg.FindStringSubmatch(temp.ProxyGroups[i].Proxies[j])[1] - switch key { - case "all": - newProxies = append(newProxies, proxyNames...) - case "countries": - if !igcg { - newProxies = append(newProxies, countryGroupNames...) - } - default: - if !igcg { - if len(key) == 2 { - newProxies = append( - newProxies, countryGroupMap[common.GetContryName(key)].Proxies..., - ) - } - } - } - } else { - newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j]) - } - } - temp.ProxyGroups[i].Proxies = newProxies - } - if !igcg { - temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...) - } -} diff --git a/api/handler/meta.go b/api/handler/meta.go deleted file mode 100644 index 6be9fae..0000000 --- a/api/handler/meta.go +++ /dev/null @@ -1,53 +0,0 @@ -package handler - -import ( - _ "embed" - "net/http" - - "github.com/nitezs/sub2clash/config" - "github.com/nitezs/sub2clash/model" - "github.com/nitezs/sub2clash/validator" - - "github.com/gin-gonic/gin" - "gopkg.in/yaml.v3" -) - -func SubHandler(c *gin.Context) { - - query, err := validator.ParseQuery(c) - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return - } - sub, err := BuildSub(model.ClashMeta, query, config.Default.MetaTemplate) - if err != nil { - c.String(http.StatusInternalServerError, err.Error()) - return - } - - if len(query.Subs) == 1 { - userInfoHeader, err := fetchSubscriptionUserInfo(query.Subs[0], "clash") - if err != nil { - c.String(http.StatusInternalServerError, err.Error()) - } - c.Header("subscription-userinfo", userInfoHeader) - } - - if query.NodeListMode { - nodelist := model.NodeList{} - nodelist.Proxies = sub.Proxies - marshal, err := yaml.Marshal(nodelist) - if err != nil { - c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error()) - return - } - c.String(http.StatusOK, string(marshal)) - return - } - marshal, err := yaml.Marshal(sub) - if err != nil { - c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error()) - return - } - c.String(http.StatusOK, string(marshal)) -} diff --git a/common/database/database.go b/common/database/database.go index 638ea31..26d639c 100644 --- a/common/database/database.go +++ b/common/database/database.go @@ -5,7 +5,7 @@ import ( "errors" "path/filepath" - "github.com/nitezs/sub2clash/model" + "github.com/bestnite/sub2clash/model" "go.etcd.io/bbolt" ) diff --git a/common/get.go b/common/get.go deleted file mode 100644 index d5b1525..0000000 --- a/common/get.go +++ /dev/null @@ -1,102 +0,0 @@ -package common - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/nitezs/sub2clash/config" -) - -type GetConfig struct { - userAgent string -} - -type GetOption func(*GetConfig) - -func WithUserAgent(userAgent string) GetOption { - return func(config *GetConfig) { - config.userAgent = userAgent - } -} - -func Get(url string, options ...GetOption) (resp *http.Response, err error) { - retryTimes := config.Default.RequestRetryTimes - haveTried := 0 - retryDelay := time.Second - getConfig := GetConfig{} - for _, option := range options { - option(&getConfig) - } - var req *http.Request - var get *http.Response - for haveTried < retryTimes { - client := &http.Client{} - //client.Timeout = time.Second * 10 - req, err = http.NewRequest("GET", url, nil) - if err != nil { - haveTried++ - time.Sleep(retryDelay) - continue - } - if getConfig.userAgent != "" { - req.Header.Set("User-Agent", getConfig.userAgent) - } - get, err = client.Do(req) - if err != nil { - haveTried++ - time.Sleep(retryDelay) - continue - } else { - if get != nil && get.ContentLength > config.Default.RequestMaxFileSize { - return nil, errors.New("文件过大") - } - return get, nil - } - - } - return nil, fmt.Errorf("请求失败:%v", err) -} - -func Head(url string, options ...GetOption) (resp *http.Response, err error) { - retryTimes := config.Default.RequestRetryTimes - haveTried := 0 - retryDelay := time.Second - - // 解析可选参数(如 User-Agent) - getConfig := GetConfig{} - for _, option := range options { - option(&getConfig) - } - - var req *http.Request - var headResp *http.Response - - for haveTried < retryTimes { - client := &http.Client{} - req, err = http.NewRequest("HEAD", url, nil) - if err != nil { - haveTried++ - time.Sleep(retryDelay) - continue - } - - // 设置 User-Agent(如果提供) - if getConfig.userAgent != "" { - req.Header.Set("User-Agent", getConfig.userAgent) - } - - headResp, err = client.Do(req) - if err != nil { - haveTried++ - time.Sleep(retryDelay) - continue - } - - // HEAD 请求不检查 ContentLength,因为没有响应体 - return headResp, nil - } - - return nil, fmt.Errorf("HEAD 请求失败:%v", err) -} diff --git a/common/mkdir.go b/common/mkdir.go index f353fb2..9ac0bcd 100644 --- a/common/mkdir.go +++ b/common/mkdir.go @@ -20,9 +20,6 @@ func MkEssentialDir() error { if err := MKDir("subs"); err != nil { return errors.New("create subs dir failed" + err.Error()) } - if err := MKDir("templates"); err != nil { - return errors.New("create templates dir failed" + err.Error()) - } if err := MKDir("logs"); err != nil { return errors.New("create logs dir failed" + err.Error()) } diff --git a/common/proxy.go b/common/proxy.go index bf65344..03322ec 100644 --- a/common/proxy.go +++ b/common/proxy.go @@ -3,12 +3,8 @@ package common import ( "strings" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/logger" - "github.com/nitezs/sub2clash/model" - "github.com/nitezs/sub2clash/parser" - - "go.uber.org/zap" + "github.com/bestnite/sub2clash/model" + "github.com/bestnite/sub2clash/model/proxy" ) func GetContryName(countryKey string) string { @@ -52,7 +48,7 @@ func GetContryName(countryKey string) string { func AddProxy( sub *model.Subscription, autotest bool, - lazy bool, clashType model.ClashType, proxies ...model.Proxy, + lazy bool, clashType model.ClashType, proxies ...proxy.Proxy, ) { proxyTypes := model.GetSupportProxyTypes(clashType) @@ -98,49 +94,3 @@ func AddProxy( } } } - -func ParseProxy(proxies ...string) []model.Proxy { - var result []model.Proxy - for _, proxy := range proxies { - if proxy != "" { - var proxyItem model.Proxy - var err error - - if strings.HasPrefix(proxy, constant.ShadowsocksPrefix) { - proxyItem, err = parser.ParseShadowsocks(proxy) - } - if strings.HasPrefix(proxy, constant.TrojanPrefix) { - proxyItem, err = parser.ParseTrojan(proxy) - } - if strings.HasPrefix(proxy, constant.VMessPrefix) { - proxyItem, err = parser.ParseVmess(proxy) - } - if strings.HasPrefix(proxy, constant.VLESSPrefix) { - proxyItem, err = parser.ParseVless(proxy) - } - if strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) { - proxyItem, err = parser.ParseShadowsocksR(proxy) - } - if strings.HasPrefix(proxy, constant.Hysteria2Prefix1) || strings.HasPrefix(proxy, constant.Hysteria2Prefix2) { - proxyItem, err = parser.ParseHysteria2(proxy) - } - if strings.HasPrefix(proxy, constant.HysteriaPrefix) { - proxyItem, err = parser.ParseHysteria(proxy) - } - if strings.HasPrefix(proxy, constant.SocksPrefix) { - proxyItem, err = parser.ParseSocks(proxy) - } - if strings.HasPrefix(proxy, constant.AnytlsPrefix) { - proxyItem, err = parser.ParseAnytls(proxy) - } - if err == nil { - result = append(result, proxyItem) - } else { - logger.Logger.Debug( - "parse proxy failed", zap.String("proxy", proxy), zap.Error(err), - ) - } - } - } - return result -} diff --git a/common/request.go b/common/request.go new file mode 100644 index 0000000..4454735 --- /dev/null +++ b/common/request.go @@ -0,0 +1,12 @@ +package common + +import ( + "resty.dev/v3" +) + +func Request(retryTimes int) *resty.Client { + client := resty.New() + client. + SetRetryCount(retryTimes) + return client +} diff --git a/common/rule.go b/common/rule.go index a354335..593e877 100644 --- a/common/rule.go +++ b/common/rule.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/nitezs/sub2clash/model" + "github.com/bestnite/sub2clash/model" ) func PrependRuleProvider( diff --git a/common/sub.go b/common/sub.go index 4ef3652..b8d2e35 100644 --- a/common/sub.go +++ b/common/sub.go @@ -3,23 +3,33 @@ package common import ( "crypto/sha256" "encoding/hex" + "errors" "fmt" "io" - "net/http" + "net/url" "os" "path/filepath" + "regexp" + "sort" + "strconv" + "strings" "sync" "time" - "github.com/nitezs/sub2clash/config" + "github.com/bestnite/sub2clash/logger" + "github.com/bestnite/sub2clash/model" + P "github.com/bestnite/sub2clash/model/proxy" + "github.com/bestnite/sub2clash/parser" + "go.uber.org/zap" + "gopkg.in/yaml.v3" ) var subsDir = "subs" var fileLock sync.RWMutex -func LoadSubscription(url string, refresh bool, userAgent string) ([]byte, error) { +func LoadSubscription(url string, refresh bool, userAgent string, cacheExpire int64, retryTimes int) ([]byte, error) { if refresh { - return FetchSubscriptionFromAPI(url, userAgent) + return FetchSubscriptionFromAPI(url, userAgent, retryTimes) } hash := sha256.Sum224([]byte(url)) fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:])) @@ -28,10 +38,10 @@ func LoadSubscription(url string, refresh bool, userAgent string) ([]byte, error if !os.IsNotExist(err) { return nil, err } - return FetchSubscriptionFromAPI(url, userAgent) + return FetchSubscriptionFromAPI(url, userAgent, retryTimes) } lastGetTime := stat.ModTime().Unix() - if lastGetTime+config.Default.CacheExpire > time.Now().Unix() { + if lastGetTime+cacheExpire > time.Now().Unix() { file, err := os.Open(fileName) if err != nil { return nil, err @@ -49,21 +59,18 @@ func LoadSubscription(url string, refresh bool, userAgent string) ([]byte, error } return subContent, nil } - return FetchSubscriptionFromAPI(url, userAgent) + return FetchSubscriptionFromAPI(url, userAgent, retryTimes) } -func FetchSubscriptionFromAPI(url string, userAgent string) ([]byte, error) { +func FetchSubscriptionFromAPI(url string, userAgent string, retryTimes int) ([]byte, error) { hash := sha256.Sum224([]byte(url)) fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:])) - resp, err := Get(url, WithUserAgent(userAgent)) + client := Request(retryTimes) + defer client.Close() + resp, err := client.R().SetHeader("User-Agent", userAgent).Get(url) if err != nil { return nil, err } - defer func(resp *http.Response) { - if resp != nil && resp.Body != nil { - _ = resp.Body.Close() - } - }(resp) data, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) @@ -85,3 +92,289 @@ func FetchSubscriptionFromAPI(url string, userAgent string) ([]byte, error) { } return data, nil } + +func BuildSub(clashType model.ClashType, query model.SubConfig, template string, cacheExpire int64, retryTimes int) ( + *model.Subscription, error, +) { + var temp = &model.Subscription{} + var sub = &model.Subscription{} + var err error + var templateBytes []byte + + if query.Template != "" { + template = query.Template + } + if strings.HasPrefix(template, "http") { + templateBytes, err = LoadSubscription(template, query.Refresh, query.UserAgent, cacheExpire, retryTimes) + if err != nil { + logger.Logger.Debug( + "load template failed", zap.String("template", template), zap.Error(err), + ) + return nil, errors.New("加载模板失败: " + err.Error()) + } + } else { + unescape, err := url.QueryUnescape(template) + if err != nil { + return nil, errors.New("加载模板失败: " + err.Error()) + } + templateBytes, err = LoadTemplate(unescape) + if err != nil { + logger.Logger.Debug( + "load template failed", zap.String("template", template), zap.Error(err), + ) + return nil, errors.New("加载模板失败: " + err.Error()) + } + } + + err = yaml.Unmarshal(templateBytes, &temp) + if err != nil { + logger.Logger.Debug("parse template failed", zap.Error(err)) + return nil, errors.New("解析模板失败: " + err.Error()) + } + var proxyList []P.Proxy + + for i := range query.Subs { + data, err := LoadSubscription(query.Subs[i], query.Refresh, query.UserAgent, cacheExpire, retryTimes) + subName := "" + if strings.Contains(query.Subs[i], "#") { + subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:] + } + if err != nil { + logger.Logger.Debug( + "load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err), + ) + return nil, errors.New("加载订阅失败: " + err.Error()) + } + + err = yaml.Unmarshal(data, &sub) + var newProxies []P.Proxy + if err != nil { + reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria|hy2|hysteria2)://") + if reg.Match(data) { + p := parser.ParseProxies(strings.Split(string(data), "\n")...) + newProxies = p + } else { + base64, err := parser.DecodeBase64(string(data)) + if err != nil { + logger.Logger.Debug( + "parse subscription failed", zap.String("url", query.Subs[i]), + zap.String("data", string(data)), + zap.Error(err), + ) + return nil, errors.New("加载订阅失败: " + err.Error()) + } + p := parser.ParseProxies(strings.Split(base64, "\n")...) + newProxies = p + } + } else { + newProxies = sub.Proxies + } + if subName != "" { + for i := range newProxies { + newProxies[i].SubName = subName + } + } + proxyList = append(proxyList, newProxies...) + } + + if len(query.Proxies) != 0 { + proxyList = append(proxyList, parser.ParseProxies(query.Proxies...)...) + } + + for i := range proxyList { + if proxyList[i].SubName != "" { + proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name) + } + } + + proxies := make(map[string]*P.Proxy) + newProxies := make([]P.Proxy, 0, len(proxyList)) + for i := range proxyList { + key := proxyList[i].Server + strconv.Itoa(proxyList[i].Port) + proxyList[i].Type + proxyList[i].UUID + proxyList[i].Password + if proxyList[i].Network == "ws" { + key += proxyList[i].WSOpts.Path + proxyList[i].WSOpts.Headers["Host"] + } + if _, exist := proxies[key]; !exist { + proxies[key] = &proxyList[i] + newProxies = append(newProxies, proxyList[i]) + } + } + proxyList = newProxies + + if strings.TrimSpace(query.Remove) != "" { + newProxyList := make([]P.Proxy, 0, len(proxyList)) + for i := range proxyList { + removeReg, err := regexp.Compile(query.Remove) + if err != nil { + logger.Logger.Debug("remove regexp compile failed", zap.Error(err)) + return nil, errors.New("remove 参数非法: " + err.Error()) + } + + if removeReg.MatchString(proxyList[i].Name) { + continue + } + newProxyList = append(newProxyList, proxyList[i]) + } + proxyList = newProxyList + } + + if len(query.ReplaceKeys) != 0 { + + replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys)) + for _, v := range query.ReplaceKeys { + replaceReg, err := regexp.Compile(v) + if err != nil { + logger.Logger.Debug("replace regexp compile failed", zap.Error(err)) + return nil, errors.New("replace 参数非法: " + err.Error()) + } + replaceRegs = append(replaceRegs, replaceReg) + } + for i := range proxyList { + + for j, v := range replaceRegs { + if v.MatchString(proxyList[i].Name) { + proxyList[i].Name = v.ReplaceAllString( + proxyList[i].Name, query.ReplaceTo[j], + ) + } + } + } + } + + names := make(map[string]int) + for i := range proxyList { + if _, exist := names[proxyList[i].Name]; exist { + names[proxyList[i].Name] = names[proxyList[i].Name] + 1 + proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name]) + } else { + names[proxyList[i].Name] = 0 + } + } + + for i := range proxyList { + proxyList[i].Name = strings.TrimSpace(proxyList[i].Name) + } + + var t = &model.Subscription{} + AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...) + + switch query.Sort { + case "sizeasc": + sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups)) + case "sizedesc": + sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroups))) + case "nameasc": + sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups)) + case "namedesc": + sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroups))) + default: + sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups)) + } + + MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup) + + for _, v := range query.Rules { + if v.Prepend { + PrependRules(temp, v.Rule) + } else { + AppendRules(temp, v.Rule) + } + } + + for _, v := range query.RuleProviders { + hash := sha256.Sum224([]byte(v.Url)) + name := hex.EncodeToString(hash[:]) + provider := model.RuleProvider{ + Type: "http", + Behavior: v.Behavior, + Url: v.Url, + Path: "./" + name + ".yaml", + Interval: 3600, + } + if v.Prepend { + PrependRuleProvider( + temp, v.Name, v.Group, provider, + ) + } else { + AppenddRuleProvider( + temp, v.Name, v.Group, provider, + ) + } + } + return temp, nil +} + +func FetchSubscriptionUserInfo(url string, userAgent string, retryTimes int) (string, error) { + client := Request(retryTimes) + defer client.Close() + resp, err := client.R().SetHeader("User-Agent", userAgent).Head(url) + if err != nil { + logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err)) + return "", err + } + defer resp.Body.Close() + if userInfo := resp.Header().Get("subscription-userinfo"); userInfo != "" { + return userInfo, nil + } + + logger.Logger.Debug("目标 URL 未返回 subscription-userinfo 头", zap.Error(err)) + return "", err +} + +func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) { + var countryGroupNames []string + for _, proxyGroup := range sub.ProxyGroups { + if proxyGroup.IsCountryGrop { + countryGroupNames = append( + countryGroupNames, proxyGroup.Name, + ) + } + } + var proxyNames []string + for _, proxy := range sub.Proxies { + proxyNames = append(proxyNames, proxy.Name) + } + + temp.Proxies = append(temp.Proxies, sub.Proxies...) + + for i := range temp.ProxyGroups { + if temp.ProxyGroups[i].IsCountryGrop { + continue + } + newProxies := make([]string, 0) + countryGroupMap := make(map[string]model.ProxyGroup) + for _, v := range sub.ProxyGroups { + if v.IsCountryGrop { + countryGroupMap[v.Name] = v + } + } + for j := range temp.ProxyGroups[i].Proxies { + reg := regexp.MustCompile("<(.*?)>") + if reg.Match([]byte(temp.ProxyGroups[i].Proxies[j])) { + key := reg.FindStringSubmatch(temp.ProxyGroups[i].Proxies[j])[1] + switch key { + case "all": + newProxies = append(newProxies, proxyNames...) + case "countries": + if !igcg { + newProxies = append(newProxies, countryGroupNames...) + } + default: + if !igcg { + if len(key) == 2 { + newProxies = append( + newProxies, countryGroupMap[GetContryName(key)].Proxies..., + ) + } + } + } + } else { + newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j]) + } + } + temp.ProxyGroups[i].Proxies = newProxies + } + if !igcg { + temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...) + } +} diff --git a/common/template.go b/common/template.go index 733290e..925e7dd 100644 --- a/common/template.go +++ b/common/template.go @@ -4,13 +4,11 @@ import ( "errors" "io" "os" - "path/filepath" ) -func LoadTemplate(template string) ([]byte, error) { - tPath := filepath.Join("templates", template) - if _, err := os.Stat(tPath); err == nil { - file, err := os.Open(tPath) +func LoadTemplate(templatePath string) ([]byte, error) { + if _, err := os.Stat(templatePath); err == nil { + file, err := os.Open(templatePath) if err != nil { return nil, err } diff --git a/common/write_default_template.go b/common/write_default_template.go deleted file mode 100644 index d3511f7..0000000 --- a/common/write_default_template.go +++ /dev/null @@ -1,40 +0,0 @@ -package common - -import ( - "os" - "path/filepath" - - "github.com/nitezs/sub2clash/config" -) - -func writeTemplate(path string, template string) error { - tPath := filepath.Join( - "templates", path, - ) - if _, err := os.Stat(tPath); os.IsNotExist(err) { - file, err := os.Create(tPath) - if err != nil { - return err - } - defer func(file *os.File) { - if file != nil { - _ = file.Close() - } - }(file) - _, err = file.WriteString(template) - if err != nil { - return err - } - } - return nil -} - -func WriteDefalutTemplate(templateMeta string, templateClash string) error { - if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil { - return err - } - if err := writeTemplate(config.Default.ClashTemplate, templateClash); err != nil { - return err - } - return nil -} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..963f5b7 --- /dev/null +++ b/compose.yml @@ -0,0 +1,12 @@ +name: sub2clash +services: + sub2clash: + container_name: sub2clash + restart: unless-stopped + image: nite07/sub2clash:latest + ports: + - "8011:8011" + volumes: + - ./logs:/app/logs + - ./templates:/app/templates + - ./data:/app/data diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..285222a --- /dev/null +++ b/config.example.json @@ -0,0 +1,10 @@ +{ + "port": 8011, + "meta_template": "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml", + "clash_template": "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml", + "request_retry_times": 3, + "request_max_file_size": 1048576, + "cache_expire": 300, + "log_level": "info", + "short_link_length": 6 +} \ No newline at end of file diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..06a263f --- /dev/null +++ b/config.example.toml @@ -0,0 +1,22 @@ +# Sub2Clash 配置文件示例 (TOML 格式) +# 复制此文件为 config.toml 并根据需要修改配置 + +# 服务端口 +port = 8011 + +# 模板配置 +meta_template = "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml" +clash_template = "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml" + +# 请求配置 +request_retry_times = 3 +request_max_file_size = 1048576 # 1MB in bytes + +# 缓存配置 (秒) +cache_expire = 300 # 5 minutes + +# 日志级别 (debug, info, warn, error) +log_level = "info" + +# 短链接长度 +short_link_length = 6 diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..6c671cd --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,22 @@ +# Sub2Clash 配置文件示例 +# 复制此文件为 config.yaml 并根据需要修改配置 + +# 服务端口 +port: 8011 + +# 模板配置 +meta_template: "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml" +clash_template: "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml" + +# 请求配置 +request_retry_times: 3 +request_max_file_size: 1048576 # 1MB in bytes + +# 缓存配置 (秒) +cache_expire: 300 # 5 minutes + +# 日志级别 (debug, info, warn, error) +log_level: "info" + +# 短链接长度 +short_link_length: 6 diff --git a/config/config.go b/config/config.go index 8a1c94e..30d3650 100644 --- a/config/config.go +++ b/config/config.go @@ -1,90 +1,76 @@ package config import ( - "errors" - "os" - "strconv" + "strings" - "github.com/joho/godotenv" + "github.com/spf13/viper" ) type Config struct { - Port int - MetaTemplate string - ClashTemplate string - RequestRetryTimes int - RequestMaxFileSize int64 - CacheExpire int64 - LogLevel string - //BasePath string - ShortLinkLength int + Address string `mapstructure:"address"` + MetaTemplate string `mapstructure:"meta_template"` + ClashTemplate string `mapstructure:"clash_template"` + RequestRetryTimes int `mapstructure:"request_retry_times"` + RequestMaxFileSize int64 `mapstructure:"request_max_file_size"` + CacheExpire int64 `mapstructure:"cache_expire"` + LogLevel string `mapstructure:"log_level"` + ShortLinkLength int `mapstructure:"short_link_length"` } -var Default *Config +var GlobalConfig *Config var Dev string func LoadConfig() error { - Default = &Config{ - MetaTemplate: "template_meta.yaml", - ClashTemplate: "template_clash.yaml", - RequestRetryTimes: 3, - RequestMaxFileSize: 1024 * 1024 * 1, - Port: 8011, - CacheExpire: 60 * 5, - LogLevel: "info", - //BasePath: "/", - ShortLinkLength: 6, - } - _ = godotenv.Load() - if os.Getenv("PORT") != "" { - atoi, err := strconv.Atoi(os.Getenv("PORT")) - if err != nil { - return errors.New("PORT invalid") + v := viper.New() + + // 添加配置文件搜索路径 + v.AddConfigPath(".") + v.AddConfigPath("./config") + v.AddConfigPath("/etc/sub2clash/") + + // 设置默认值 + setDefaults(v) + + // 设置环境变量前缀和自动绑定 + v.SetEnvPrefix("SUB2CLASH") + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // 尝试按优先级加载不同格式的配置文件 + configLoaded := false + configNames := []string{"config", "sub2clash"} + configExts := []string{"yaml", "yml", "json", "toml", "ini"} + + for _, name := range configNames { + for _, ext := range configExts { + v.SetConfigName(name) + v.SetConfigType(ext) + if err := v.ReadInConfig(); err == nil { + configLoaded = true + break + } } - Default.Port = atoi - } - if os.Getenv("META_TEMPLATE") != "" { - Default.MetaTemplate = os.Getenv("META_TEMPLATE") - } - if os.Getenv("CLASH_TEMPLATE") != "" { - Default.ClashTemplate = os.Getenv("CLASH_TEMPLATE") - } - if os.Getenv("REQUEST_RETRY_TIMES") != "" { - atoi, err := strconv.Atoi(os.Getenv("REQUEST_RETRY_TIMES")) - if err != nil { - return errors.New("REQUEST_RETRY_TIMES invalid") + if configLoaded { + break } - Default.RequestRetryTimes = atoi } - if os.Getenv("REQUEST_MAX_FILE_SIZE") != "" { - atoi, err := strconv.Atoi(os.Getenv("REQUEST_MAX_FILE_SIZE")) - if err != nil { - return errors.New("REQUEST_MAX_FILE_SIZE invalid") - } - Default.RequestMaxFileSize = int64(atoi) - } - if os.Getenv("CACHE_EXPIRE") != "" { - atoi, err := strconv.Atoi(os.Getenv("CACHE_EXPIRE")) - if err != nil { - return errors.New("CACHE_EXPIRE invalid") - } - Default.CacheExpire = int64(atoi) - } - if os.Getenv("LOG_LEVEL") != "" { - Default.LogLevel = os.Getenv("LOG_LEVEL") - } - //if os.Getenv("BASE_PATH") != "" { - // Default.BasePath = os.Getenv("BASE_PATH") - // if Default.BasePath[len(Default.BasePath)-1] != '/' { - // Default.BasePath += "/" - // } - //} - if os.Getenv("SHORT_LINK_LENGTH") != "" { - atoi, err := strconv.Atoi(os.Getenv("SHORT_LINK_LENGTH")) - if err != nil { - return errors.New("SHORT_LINK_LENGTH invalid") - } - Default.ShortLinkLength = atoi + + // 将配置解析到结构体 + GlobalConfig = &Config{} + if err := v.Unmarshal(GlobalConfig); err != nil { + return err } + return nil } + +func setDefaults(v *viper.Viper) { + v.SetDefault("address", "0.0.0.0:8011") + v.SetDefault("meta_template", "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml") + v.SetDefault("clash_template", "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml") + v.SetDefault("request_retry_times", 3) + v.SetDefault("request_max_file_size", 1024*1024*1) + v.SetDefault("cache_expire", 60*5) + v.SetDefault("log_level", "info") + v.SetDefault("short_link_length", 6) +} diff --git a/constant/prefix.go b/constant/prefix.go deleted file mode 100644 index 395ca14..0000000 --- a/constant/prefix.go +++ /dev/null @@ -1,14 +0,0 @@ -package constant - -const ( - HysteriaPrefix string = "hysteria://" - Hysteria2Prefix1 string = "hysteria2://" - Hysteria2Prefix2 string = "hy2://" - ShadowsocksPrefix string = "ss://" - ShadowsocksRPrefix string = "ssr://" - TrojanPrefix string = "trojan://" - VLESSPrefix string = "vless://" - VMessPrefix string = "vmess://" - SocksPrefix string = "socks" - AnytlsPrefix string = "anytls://" -) diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index eac6c34..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: "3" - -services: - sub2clash: - container_name: sub2clash - restart: unless-stopped - image: nite07/sub2clash:latest - ports: - - "8011:8011" - volumes: - - ./logs:/app/logs - - ./templates:/app/templates - - ./data:/app/data - # environment: - # - PORT=8011 - # - META_TEMPLATE=template_meta.yaml - # - PROXY_TEMPLATE=template_clash.yaml - # - REQUEST_RETRY_TIMES=3 - # - REQUEST_MAX_FILE_SIZE=1048576 - # - CACHE_EXPIRE=300 - # - LOG_LEVEL=info diff --git a/parser/error.go b/error/parse_error.go similarity index 97% rename from parser/error.go rename to error/parse_error.go index af95f0b..2662d64 100644 --- a/parser/error.go +++ b/error/parse_error.go @@ -1,4 +1,4 @@ -package parser +package error type ParseError struct { Type ParseErrorType diff --git a/go.mod b/go.mod index e0ec322..827420d 100644 --- a/go.mod +++ b/go.mod @@ -1,42 +1,52 @@ -module github.com/nitezs/sub2clash +module github.com/bestnite/sub2clash -go 1.21 +go 1.21.0 + +toolchain go1.24.3 require ( - github.com/gin-gonic/gin v1.9.1 - github.com/joho/godotenv v1.5.1 + github.com/gin-gonic/gin v1.10.1 + github.com/spf13/viper v1.20.1 go.etcd.io/bbolt v1.3.9 go.uber.org/zap v1.27.0 - golang.org/x/text v0.14.0 + golang.org/x/text v0.21.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 + resty.dev/v3 v3.0.0-beta.3 ) require ( - github.com/bytedance/sonic v1.11.5 // indirect + github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cloudwego/base64x v0.1.3 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.19.0 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/arch v0.7.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.29.0 // indirect + google.golang.org/protobuf v1.36.1 // indirect ) diff --git a/go.sum b/go.sum index 564ab43..137c753 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,49 @@ -github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k= -github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw= -github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg= -github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= -github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -46,22 +53,36 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= -github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= @@ -75,28 +96,31 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= -golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E= +resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go index 4aa8320..8ddeb8c 100644 --- a/main.go +++ b/main.go @@ -3,24 +3,17 @@ package main import ( _ "embed" "io" - "strconv" - "github.com/nitezs/sub2clash/api" - "github.com/nitezs/sub2clash/common" - "github.com/nitezs/sub2clash/common/database" - "github.com/nitezs/sub2clash/config" - "github.com/nitezs/sub2clash/logger" + "github.com/bestnite/sub2clash/common" + "github.com/bestnite/sub2clash/common/database" + "github.com/bestnite/sub2clash/config" + "github.com/bestnite/sub2clash/logger" + "github.com/bestnite/sub2clash/server" "github.com/gin-gonic/gin" "go.uber.org/zap" ) -//go:embed templates/template_meta.yaml -var templateMeta string - -//go:embed templates/template_clash.yaml -var templateClash string - func init() { var err error @@ -31,16 +24,11 @@ func init() { err = config.LoadConfig() - logger.InitLogger(config.Default.LogLevel) + logger.InitLogger(config.GlobalConfig.LogLevel) if err != nil { logger.Logger.Panic("load config failed", zap.Error(err)) } - err = common.WriteDefalutTemplate(templateMeta, templateClash) - if err != nil { - logger.Logger.Panic("write default template failed", zap.Error(err)) - } - err = database.ConnectDB() if err != nil { logger.Logger.Panic("database connect failed", zap.Error(err)) @@ -49,16 +37,15 @@ func init() { } func main() { - gin.SetMode(gin.ReleaseMode) gin.DefaultWriter = io.Discard r := gin.Default() - api.SetRoute(r) - logger.Logger.Info("server is running at http://localhost:" + strconv.Itoa(config.Default.Port)) - err := r.Run(":" + strconv.Itoa(config.Default.Port)) + server.SetRoute(r) + logger.Logger.Info("server is running at " + config.GlobalConfig.Address) + err := r.Run(config.GlobalConfig.Address) if err != nil { logger.Logger.Error("server running failed", zap.Error(err)) return diff --git a/model/clash.go b/model/clash.go index 25d6804..7025cb4 100644 --- a/model/clash.go +++ b/model/clash.go @@ -17,6 +17,7 @@ func GetSupportProxyTypes(clashType ClashType) map[string]bool { "socks5": true, } } + if clashType == ClashMeta { return map[string]bool{ "ss": true, @@ -30,5 +31,6 @@ func GetSupportProxyTypes(clashType ClashType) map[string]bool { "anytls": true, } } + return nil } diff --git a/model/github.go b/model/github.go deleted file mode 100644 index eef70db..0000000 --- a/model/github.go +++ /dev/null @@ -1,12 +0,0 @@ -package model - -type Tags []struct { - Name string `json:"name"` - ZipballUrl string `json:"zipball_url"` - TarballUrl string `json:"tarball_url"` - Commit struct { - Sha string `json:"sha"` - Url string `json:"url"` - } - NodeId string `json:"node_id"` -} diff --git a/model/proxy_group.go b/model/group.go similarity index 100% rename from model/proxy_group.go rename to model/group.go diff --git a/model/proxy_anytls.go b/model/proxy/anytls.go similarity index 85% rename from model/proxy_anytls.go rename to model/proxy/anytls.go index 00fffc6..8b382d6 100644 --- a/model/proxy_anytls.go +++ b/model/proxy/anytls.go @@ -1,4 +1,4 @@ -package model +package proxy type Anytls struct { Type string `yaml:"type"` @@ -35,3 +35,17 @@ func ProxyToAnytls(p Proxy) Anytls { MinIdleSession: p.MinIdleSession, } } + +type AnytlsMarshaler struct{} + +func (m *AnytlsMarshaler) GetType() string { + return "anytls" +} + +func (m *AnytlsMarshaler) MarshalProxy(p Proxy) (interface{}, error) { + return ProxyToAnytls(p), nil +} + +func init() { + RegisterMarshaler(&AnytlsMarshaler{}) +} diff --git a/model/proxy_hysteria.go b/model/proxy/hysteria.go similarity index 91% rename from model/proxy_hysteria.go rename to model/proxy/hysteria.go index d87a3a2..14b8d1e 100644 --- a/model/proxy_hysteria.go +++ b/model/proxy/hysteria.go @@ -1,4 +1,4 @@ -package model +package proxy type Hysteria struct { Type string `yaml:"type"` @@ -56,3 +56,13 @@ func ProxyToHysteria(p Proxy) Hysteria { HopInterval: p.HopInterval, } } + +type HysteriaMarshaler struct{} + +func (m *HysteriaMarshaler) GetType() string { + return "hysteria" +} + +func (m *HysteriaMarshaler) MarshalProxy(p Proxy) (interface{}, error) { + return ProxyToHysteria(p), nil +} diff --git a/model/proxy_hysteria2.go b/model/proxy/hysteria2.go similarity index 85% rename from model/proxy_hysteria2.go rename to model/proxy/hysteria2.go index b97b656..c0bc7e8 100644 --- a/model/proxy_hysteria2.go +++ b/model/proxy/hysteria2.go @@ -1,4 +1,4 @@ -package model +package proxy type Hysteria2 struct { Type string `yaml:"type"` @@ -39,3 +39,13 @@ func ProxyToHysteria2(p Proxy) Hysteria2 { CWND: p.CWND, } } + +type Hysteria2Marshaler struct{} + +func (m *Hysteria2Marshaler) GetType() string { + return "hysteria2" +} + +func (m *Hysteria2Marshaler) MarshalProxy(p Proxy) (interface{}, error) { + return ProxyToHysteria2(p), nil +} diff --git a/model/proxy.go b/model/proxy/proxy.go similarity index 96% rename from model/proxy.go rename to model/proxy/proxy.go index 8759e99..cedc0f6 100644 --- a/model/proxy.go +++ b/model/proxy/proxy.go @@ -1,4 +1,4 @@ -package model +package proxy type SmuxStruct struct { Enabled bool `yaml:"enable"` @@ -86,6 +86,12 @@ type WireGuardPeerOption struct { type _Proxy Proxy func (p Proxy) MarshalYAML() (interface{}, error) { + // 尝试使用注册的序列化器 + if marshaler, exists := GetMarshaler(p.Type); exists { + return marshaler.MarshalProxy(p) + } + + // 保持向后兼容,对于未注册的类型使用原有逻辑 switch p.Type { case "vmess": return ProxyToVmess(p), nil diff --git a/model/proxy/registry.go b/model/proxy/registry.go new file mode 100644 index 0000000..b334cf3 --- /dev/null +++ b/model/proxy/registry.go @@ -0,0 +1,64 @@ +package proxy + +import ( + "sync" +) + +// ProxyMarshaler 代理YAML序列化接口 +type ProxyMarshaler interface { + // MarshalProxy 将通用Proxy对象序列化为特定协议的YAML结构 + MarshalProxy(p Proxy) (interface{}, error) + // GetType 返回支持的协议类型 + GetType() string +} + +// marshalerRegistry YAML序列化器注册中心 +type marshalerRegistry struct { + mu sync.RWMutex + marshalers map[string]ProxyMarshaler // type -> marshaler +} + +var yamlRegistry = &marshalerRegistry{ + marshalers: make(map[string]ProxyMarshaler), +} + +// RegisterMarshaler 注册YAML序列化器 +func RegisterMarshaler(marshaler ProxyMarshaler) { + yamlRegistry.mu.Lock() + defer yamlRegistry.mu.Unlock() + + yamlRegistry.marshalers[marshaler.GetType()] = marshaler +} + +// GetMarshaler 根据协议类型获取序列化器 +func GetMarshaler(proxyType string) (ProxyMarshaler, bool) { + yamlRegistry.mu.RLock() + defer yamlRegistry.mu.RUnlock() + + marshaler, exists := yamlRegistry.marshalers[proxyType] + return marshaler, exists +} + +// GetAllMarshalers 获取所有注册的序列化器 +func GetAllMarshalers() map[string]ProxyMarshaler { + yamlRegistry.mu.RLock() + defer yamlRegistry.mu.RUnlock() + + result := make(map[string]ProxyMarshaler) + for k, v := range yamlRegistry.marshalers { + result[k] = v + } + return result +} + +// GetSupportedTypes 获取所有支持的协议类型 +func GetSupportedTypes() []string { + yamlRegistry.mu.RLock() + defer yamlRegistry.mu.RUnlock() + + types := make([]string, 0, len(yamlRegistry.marshalers)) + for proxyType := range yamlRegistry.marshalers { + types = append(types, proxyType) + } + return types +} diff --git a/model/proxy_shadowsocks.go b/model/proxy/shadowsocks.go similarity index 72% rename from model/proxy_shadowsocks.go rename to model/proxy/shadowsocks.go index 18602ab..dd89d38 100644 --- a/model/proxy_shadowsocks.go +++ b/model/proxy/shadowsocks.go @@ -1,4 +1,4 @@ -package model +package proxy type ShadowSocks struct { Type string `yaml:"type"` @@ -31,3 +31,21 @@ func ProxyToShadowSocks(p Proxy) ShadowSocks { ClientFingerprint: p.ClientFingerprint, } } + +// ShadowsocksMarshaler Shadowsocks协议的YAML序列化器 +type ShadowsocksMarshaler struct{} + +// GetType 返回协议类型 +func (m *ShadowsocksMarshaler) GetType() string { + return "ss" +} + +// MarshalProxy 序列化Shadowsocks代理 +func (m *ShadowsocksMarshaler) MarshalProxy(p Proxy) (interface{}, error) { + return ProxyToShadowSocks(p), nil +} + +// 注册序列化器 +func init() { + RegisterMarshaler(&ShadowsocksMarshaler{}) +} diff --git a/model/proxy_shadowsocksr.go b/model/proxy/shadowsocksr.go similarity index 78% rename from model/proxy_shadowsocksr.go rename to model/proxy/shadowsocksr.go index 6b33ce8..22f7c3b 100644 --- a/model/proxy_shadowsocksr.go +++ b/model/proxy/shadowsocksr.go @@ -1,4 +1,4 @@ -package model +package proxy type ShadowSocksR struct { Type string `yaml:"type"` @@ -29,3 +29,13 @@ func ProxyToShadowSocksR(p Proxy) ShadowSocksR { UDP: p.UDP, } } + +type ShadowsocksRMarshaler struct{} + +func (m *ShadowsocksRMarshaler) GetType() string { + return "ssr" +} + +func (m *ShadowsocksRMarshaler) MarshalProxy(p Proxy) (interface{}, error) { + return ProxyToShadowSocksR(p), nil +} diff --git a/model/proxy_trojan.go b/model/proxy/trojan.go similarity index 87% rename from model/proxy_trojan.go rename to model/proxy/trojan.go index 7d54831..1a8f4ac 100644 --- a/model/proxy_trojan.go +++ b/model/proxy/trojan.go @@ -1,4 +1,4 @@ -package model +package proxy type Trojan struct { Type string `yaml:"type"` @@ -37,3 +37,13 @@ func ProxyToTrojan(p Proxy) Trojan { ClientFingerprint: p.ClientFingerprint, } } + +type TrojanMarshaler struct{} + +func (m *TrojanMarshaler) GetType() string { + return "trojan" +} + +func (m *TrojanMarshaler) MarshalProxy(p Proxy) (interface{}, error) { + return ProxyToTrojan(p), nil +} diff --git a/model/proxy_vless.go b/model/proxy/vless.go similarity index 91% rename from model/proxy_vless.go rename to model/proxy/vless.go index e143296..eea1450 100644 --- a/model/proxy_vless.go +++ b/model/proxy/vless.go @@ -1,4 +1,4 @@ -package model +package proxy type Vless struct { Type string `yaml:"type"` @@ -55,3 +55,13 @@ func ProxyToVless(p Proxy) Vless { ClientFingerprint: p.ClientFingerprint, } } + +type VlessMarshaler struct{} + +func (m *VlessMarshaler) GetType() string { + return "vless" +} + +func (m *VlessMarshaler) MarshalProxy(p Proxy) (interface{}, error) { + return ProxyToVless(p), nil +} diff --git a/model/proxy_vmess.go b/model/proxy/vmess.go similarity index 94% rename from model/proxy_vmess.go rename to model/proxy/vmess.go index f9c5116..70e90aa 100644 --- a/model/proxy_vmess.go +++ b/model/proxy/vmess.go @@ -1,4 +1,4 @@ -package model +package proxy type HTTPOptions struct { Method string `yaml:"method,omitempty"` @@ -102,3 +102,13 @@ func ProxyToVmess(p Proxy) Vmess { ClientFingerprint: p.ClientFingerprint, } } + +type VmessMarshaler struct{} + +func (m *VmessMarshaler) GetType() string { + return "vmess" +} + +func (m *VmessMarshaler) MarshalProxy(p Proxy) (interface{}, error) { + return ProxyToVmess(p), nil +} diff --git a/validator/sub.go b/model/sub_config.go similarity index 84% rename from validator/sub.go rename to model/sub_config.go index ca0c95f..f221729 100644 --- a/validator/sub.go +++ b/model/sub_config.go @@ -1,4 +1,4 @@ -package validator +package model import ( "crypto/sha256" @@ -11,7 +11,7 @@ import ( "github.com/gin-gonic/gin" ) -type SubValidator struct { +type SubConfig struct { Sub string `form:"sub" binding:""` Subs []string `form:"-" binding:""` Proxy string `form:"proxy" binding:""` @@ -47,22 +47,22 @@ type RuleStruct struct { Prepend bool } -func ParseQuery(c *gin.Context) (SubValidator, error) { - var query SubValidator +func ParseSubQuery(c *gin.Context) (SubConfig, error) { + var query SubConfig if err := c.ShouldBind(&query); err != nil { - return SubValidator{}, errors.New("参数错误: " + err.Error()) + return SubConfig{}, errors.New("参数错误: " + err.Error()) } if query.Sub == "" && query.Proxy == "" { - return SubValidator{}, errors.New("参数错误: sub 和 proxy 不能同时为空") + return SubConfig{}, errors.New("参数错误: sub 和 proxy 不能同时为空") } if query.Sub != "" { query.Subs = strings.Split(query.Sub, ",") for i := range query.Subs { if !strings.HasPrefix(query.Subs[i], "http") { - return SubValidator{}, errors.New("参数错误: sub 格式错误") + return SubConfig{}, errors.New("参数错误: sub 格式错误") } if _, err := url.ParseRequestURI(query.Subs[i]); err != nil { - return SubValidator{}, errors.New("参数错误: " + err.Error()) + return SubConfig{}, errors.New("参数错误: " + err.Error()) } } } else { @@ -77,7 +77,7 @@ func ParseQuery(c *gin.Context) (SubValidator, error) { if strings.HasPrefix(query.Template, "http") { uri, err := url.ParseRequestURI(query.Template) if err != nil { - return SubValidator{}, err + return SubConfig{}, err } query.Template = uri.String() } @@ -89,12 +89,12 @@ func ParseQuery(c *gin.Context) (SubValidator, error) { length := len(ruleProviders) parts := strings.Split(ruleProviders[length-i-1][1], ",") if len(parts) < 4 { - return SubValidator{}, errors.New("参数错误: ruleProvider 格式错误") + return SubConfig{}, errors.New("参数错误: ruleProvider 格式错误") } u := parts[1] uri, err := url.ParseRequestURI(u) if err != nil { - return SubValidator{}, errors.New("参数错误: " + err.Error()) + return SubConfig{}, errors.New("参数错误: " + err.Error()) } u = uri.String() if len(parts) == 4 { @@ -115,7 +115,7 @@ func ParseQuery(c *gin.Context) (SubValidator, error) { names := make(map[string]bool) for _, ruleProvider := range query.RuleProviders { if _, ok := names[ruleProvider.Name]; ok { - return SubValidator{}, errors.New("参数错误: Rule-Provider 名称重复") + return SubConfig{}, errors.New("参数错误: Rule-Provider 名称重复") } names[ruleProvider.Name] = true } @@ -148,7 +148,7 @@ func ParseQuery(c *gin.Context) (SubValidator, error) { for i := range replaces { length := len(replaces[i]) if length != 3 { - return SubValidator{}, errors.New("参数错误: replace 格式错误") + return SubConfig{}, errors.New("参数错误: replace 格式错误") } query.ReplaceKeys = append(query.ReplaceKeys, replaces[i][1]) query.ReplaceTo = append(query.ReplaceTo, replaces[i][2]) diff --git a/model/sub.go b/model/subscription.go similarity index 98% rename from model/sub.go rename to model/subscription.go index 50adb5d..1f41ff4 100644 --- a/model/sub.go +++ b/model/subscription.go @@ -1,7 +1,9 @@ package model +import "github.com/bestnite/sub2clash/model/proxy" + type NodeList struct { - Proxies []Proxy `yaml:"proxies,omitempty"` + Proxies []proxy.Proxy `yaml:"proxies,omitempty"` } type Subscription struct { @@ -57,7 +59,7 @@ type Subscription struct { Experimental Experimental `yaml:"experimental,omitempty"` Profile Profile `yaml:"profile,omitempty"` GeoXUrl GeoXUrl `yaml:"geox-url,omitempty"` - Proxies []Proxy `yaml:"proxies,omitempty"` + Proxies []proxy.Proxy `yaml:"proxies,omitempty"` ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"` Rules []string `yaml:"rules,omitempty"` SubRules map[string][]string `yaml:"sub-rules,omitempty"` diff --git a/parser/anytls.go b/parser/anytls.go index 05a6630..64dbadc 100644 --- a/parser/anytls.go +++ b/parser/anytls.go @@ -5,19 +5,29 @@ import ( "net/url" "strings" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/model" + E "github.com/bestnite/sub2clash/error" + P "github.com/bestnite/sub2clash/model/proxy" ) -func ParseAnytls(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, constant.AnytlsPrefix) { - return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} +type AnytlsParser struct{} + +func (p *AnytlsParser) GetPrefixes() []string { + return []string{"anytls://"} +} + +func (p *AnytlsParser) GetType() string { + return "anytls" +} + +func (p *AnytlsParser) Parse(proxy string) (P.Proxy, error) { + if !hasPrefix(proxy, p.GetPrefixes()) { + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy} } link, err := url.Parse(proxy) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "url parse error", Raw: proxy, } @@ -32,24 +42,24 @@ func ParseAnytls(proxy string) (model.Proxy, error) { query := link.Query() server := link.Hostname() if server == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server host", Raw: proxy, } } portStr := link.Port() if portStr == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server port", Raw: proxy, } } port, err := ParsePort(portStr) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidPort, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidPort, Raw: portStr, } } @@ -61,8 +71,8 @@ func ParseAnytls(proxy string) (model.Proxy, error) { } remarks = strings.TrimSpace(remarks) - result := model.Proxy{ - Type: "anytls", + result := P.Proxy{ + Type: p.GetType(), Name: remarks, Server: server, Port: port, @@ -72,3 +82,7 @@ func ParseAnytls(proxy string) (model.Proxy, error) { } return result, nil } + +func init() { + RegisterParser(&AnytlsParser{}) +} diff --git a/parser/base64.go b/parser/base64.go deleted file mode 100644 index e3677bb..0000000 --- a/parser/base64.go +++ /dev/null @@ -1,23 +0,0 @@ -package parser - -import ( - "encoding/base64" - "strings" -) - -func DecodeBase64(s string) (string, error) { - s = strings.TrimSpace(s) - - if strings.Contains(s, "-") || strings.Contains(s, "_") { - s = strings.Replace(s, "-", "+", -1) - s = strings.Replace(s, "_", "/", -1) - } - if len(s)%4 != 0 { - s += strings.Repeat("=", 4-len(s)%4) - } - decodeStr, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return "", err - } - return string(decodeStr), nil -} diff --git a/parser/common.go b/parser/common.go new file mode 100644 index 0000000..96b46d5 --- /dev/null +++ b/parser/common.go @@ -0,0 +1,87 @@ +package parser + +import ( + "encoding/base64" + "errors" + "strconv" + "strings" + + "github.com/bestnite/sub2clash/logger" + P "github.com/bestnite/sub2clash/model/proxy" + "go.uber.org/zap" +) + +func hasPrefix(proxy string, prefixes []string) bool { + hasPrefix := false + for _, prefix := range prefixes { + if strings.HasPrefix(proxy, prefix) { + hasPrefix = true + break + } + } + return hasPrefix +} + +func ParsePort(portStr string) (int, error) { + port, err := strconv.Atoi(portStr) + + if err != nil { + return 0, err + } + if port < 1 || port > 65535 { + return 0, errors.New("invaild port range") + } + return port, nil +} + +func isLikelyBase64(s string) bool { + if len(s)%4 == 0 && strings.HasSuffix(s, "=") && !strings.Contains(strings.TrimSuffix(s, "="), "=") { + s = strings.TrimSuffix(s, "=") + chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + for _, c := range s { + if !strings.ContainsRune(chars, c) { + return false + } + } + return true + } + return false +} + +func DecodeBase64(s string) (string, error) { + s = strings.TrimSpace(s) + + if strings.Contains(s, "-") || strings.Contains(s, "_") { + s = strings.Replace(s, "-", "+", -1) + s = strings.Replace(s, "_", "/", -1) + } + if len(s)%4 != 0 { + s += strings.Repeat("=", 4-len(s)%4) + } + decodeStr, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "", err + } + return string(decodeStr), nil +} + +func ParseProxies(proxies ...string) []P.Proxy { + var result []P.Proxy + for _, proxy := range proxies { + if proxy != "" { + var proxyItem P.Proxy + var err error + + proxyItem, err = ParseProxyWithRegistry(proxy) + + if err == nil { + result = append(result, proxyItem) + } else { + logger.Logger.Debug( + "parse proxy failed", zap.String("proxy", proxy), zap.Error(err), + ) + } + } + } + return result +} diff --git a/parser/hysteria.go b/parser/hysteria.go index 8a73045..1624220 100644 --- a/parser/hysteria.go +++ b/parser/hysteria.go @@ -6,27 +6,37 @@ import ( "strconv" "strings" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/model" + E "github.com/bestnite/sub2clash/error" + P "github.com/bestnite/sub2clash/model/proxy" ) -func ParseHysteria(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, constant.HysteriaPrefix) { - return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} +type HysteriaParser struct{} + +func (p *HysteriaParser) GetPrefixes() []string { + return []string{"hysteria://"} +} + +func (p *HysteriaParser) GetType() string { + return "hysteria" +} + +func (p *HysteriaParser) Parse(proxy string) (P.Proxy, error) { + if !hasPrefix(proxy, p.GetPrefixes()) { + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy} } link, err := url.Parse(proxy) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "url parse error", Raw: proxy, } } server := link.Hostname() if server == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server host", Raw: proxy, } @@ -34,8 +44,8 @@ func ParseHysteria(proxy string) (model.Proxy, error) { portStr := link.Port() if portStr == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server port", Raw: proxy, } @@ -43,8 +53,8 @@ func ParseHysteria(proxy string) (model.Proxy, error) { port, err := ParsePort(portStr) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidPort, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidPort, Message: err.Error(), Raw: proxy, } @@ -70,8 +80,8 @@ func ParseHysteria(proxy string) (model.Proxy, error) { } remarks = strings.TrimSpace(remarks) - result := model.Proxy{ - Type: "hysteria", + result := P.Proxy{ + Type: p.GetType(), Name: remarks, Server: server, Port: port, @@ -86,3 +96,7 @@ func ParseHysteria(proxy string) (model.Proxy, error) { } return result, nil } + +func init() { + RegisterParser(&HysteriaParser{}) +} diff --git a/parser/hysteria2.go b/parser/hysteria2.go index ee005d4..0734c5d 100644 --- a/parser/hysteria2.go +++ b/parser/hysteria2.go @@ -5,20 +5,29 @@ import ( "net/url" "strings" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/model" + E "github.com/bestnite/sub2clash/error" + P "github.com/bestnite/sub2clash/model/proxy" ) -func ParseHysteria2(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, constant.Hysteria2Prefix1) && - !strings.HasPrefix(proxy, constant.Hysteria2Prefix2) { - return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} +type Hysteria2Parser struct{} + +func (p *Hysteria2Parser) GetPrefixes() []string { + return []string{"hysteria2://", "hy2://"} +} + +func (p *Hysteria2Parser) GetType() string { + return "hysteria2" +} + +func (p *Hysteria2Parser) Parse(proxy string) (P.Proxy, error) { + if !hasPrefix(proxy, p.GetPrefixes()) { + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy} } link, err := url.Parse(proxy) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "url parse error", Raw: proxy, } @@ -33,24 +42,24 @@ func ParseHysteria2(proxy string) (model.Proxy, error) { query := link.Query() server := link.Hostname() if server == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server host", Raw: proxy, } } portStr := link.Port() if portStr == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server port", Raw: proxy, } } port, err := ParsePort(portStr) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidPort, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidPort, Raw: portStr, } } @@ -63,8 +72,8 @@ func ParseHysteria2(proxy string) (model.Proxy, error) { } remarks = strings.TrimSpace(remarks) - result := model.Proxy{ - Type: "hysteria2", + result := P.Proxy{ + Type: p.GetType(), Name: remarks, Server: server, Port: port, @@ -78,3 +87,7 @@ func ParseHysteria2(proxy string) (model.Proxy, error) { } return result, nil } + +func init() { + RegisterParser(&Hysteria2Parser{}) +} diff --git a/parser/port.go b/parser/port.go deleted file mode 100644 index eda7351..0000000 --- a/parser/port.go +++ /dev/null @@ -1,18 +0,0 @@ -package parser - -import ( - "errors" - "strconv" -) - -func ParsePort(portStr string) (int, error) { - port, err := strconv.Atoi(portStr) - - if err != nil { - return 0, err - } - if port < 1 || port > 65535 { - return 0, errors.New("invaild port range") - } - return port, nil -} diff --git a/parser/registry.go b/parser/registry.go new file mode 100644 index 0000000..829fbf9 --- /dev/null +++ b/parser/registry.go @@ -0,0 +1,77 @@ +package parser + +import ( + "strings" + "sync" + + E "github.com/bestnite/sub2clash/error" + P "github.com/bestnite/sub2clash/model/proxy" +) + +// ProxyParser 定义代理解析器接口 +type ProxyParser interface { + // Parse 解析代理字符串并返回Proxy对象 + Parse(proxy string) (P.Proxy, error) + // GetPrefix 返回支持的协议前缀(可以返回多个) + GetPrefixes() []string + // GetType 返回协议类型名称 + GetType() string +} + +// parserRegistry 解析器注册中心 +type parserRegistry struct { + mu sync.RWMutex + parsers map[string]ProxyParser // prefix -> parser +} + +var registry = &parserRegistry{ + parsers: make(map[string]ProxyParser), +} + +// RegisterParser 注册解析器 +func RegisterParser(parser ProxyParser) { + registry.mu.Lock() + defer registry.mu.Unlock() + + for _, prefix := range parser.GetPrefixes() { + registry.parsers[prefix] = parser + } +} + +// GetParser 根据前缀获取解析器 +func GetParser(prefix string) (ProxyParser, bool) { + registry.mu.RLock() + defer registry.mu.RUnlock() + + parser, exists := registry.parsers[prefix] + return parser, exists +} + +// GetAllParsers 获取所有注册的解析器 +func GetAllParsers() map[string]ProxyParser { + registry.mu.RLock() + defer registry.mu.RUnlock() + + result := make(map[string]ProxyParser) + for k, v := range registry.parsers { + result[k] = v + } + return result +} + +// ParseProxyWithRegistry 使用注册机制解析代理 +func ParseProxyWithRegistry(proxy string) (P.Proxy, error) { + proxy = strings.TrimSpace(proxy) + if proxy == "" { + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: "empty proxy string"} + } + + // 查找匹配的解析器 + for prefix, parser := range registry.parsers { + if strings.HasPrefix(proxy, prefix) { + return parser.Parse(proxy) + } + } + + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy, Message: "unsupported protocol"} +} diff --git a/parser/shadowsocks.go b/parser/shadowsocks.go index a4f18b8..240133b 100644 --- a/parser/shadowsocks.go +++ b/parser/shadowsocks.go @@ -5,20 +5,41 @@ import ( "net/url" "strings" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/model" + E "github.com/bestnite/sub2clash/error" + P "github.com/bestnite/sub2clash/model/proxy" ) -func ParseShadowsocks(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) { - return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} +// ShadowsocksParser Shadowsocks协议解析器 +type ShadowsocksParser struct{} + +// GetPrefixes 返回支持的协议前缀 +func (p *ShadowsocksParser) GetPrefixes() []string { + return []string{"ss://"} +} + +// GetType 返回协议类型 +func (p *ShadowsocksParser) GetType() string { + return "ss" +} + +// Parse 解析Shadowsocks代理 +func (p *ShadowsocksParser) Parse(proxy string) (P.Proxy, error) { + if !hasPrefix(proxy, p.GetPrefixes()) { + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy} } + if !strings.Contains(proxy, "@") { s := strings.SplitN(proxy, "#", 2) - d, err := DecodeBase64(strings.TrimPrefix(s[0], "ss://")) + for _, prefix := range p.GetPrefixes() { + if strings.HasPrefix(s[0], prefix) { + s[0] = strings.TrimPrefix(s[0], prefix) + break + } + } + d, err := DecodeBase64(s[0]) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "url parse error", Raw: proxy, } @@ -31,8 +52,8 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) { } link, err := url.Parse(proxy) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "url parse error", Raw: proxy, } @@ -40,8 +61,8 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) { server := link.Hostname() if server == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server host", Raw: proxy, } @@ -49,16 +70,16 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) { portStr := link.Port() if portStr == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server port", Raw: proxy, } } port, err := ParsePort(portStr) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Raw: proxy, } } @@ -79,8 +100,8 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) { if isLikelyBase64(password) { password, err = DecodeBase64(password) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "password decode error", Raw: proxy, } @@ -93,8 +114,8 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) { } remarks = strings.TrimSpace(remarks) - result := model.Proxy{ - Type: "ss", + result := P.Proxy{ + Type: p.GetType(), Cipher: method, Password: password, Server: server, @@ -105,16 +126,7 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) { return result, nil } -func isLikelyBase64(s string) bool { - if len(s)%4 == 0 && strings.HasSuffix(s, "=") && !strings.Contains(strings.TrimSuffix(s, "="), "=") { - s = strings.TrimSuffix(s, "=") - chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" - for _, c := range s { - if !strings.ContainsRune(chars, c) { - return false - } - } - return true - } - return false +// 注册解析器 +func init() { + RegisterParser(&ShadowsocksParser{}) } diff --git a/parser/shadowsocksr.go b/parser/shadowsocksr.go index 9139068..da68a53 100644 --- a/parser/shadowsocksr.go +++ b/parser/shadowsocksr.go @@ -5,20 +5,36 @@ import ( "strconv" "strings" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/model" + E "github.com/bestnite/sub2clash/error" + P "github.com/bestnite/sub2clash/model/proxy" ) -func ParseShadowsocksR(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) { - return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} +type ShadowsocksRParser struct{} + +func (p *ShadowsocksRParser) GetPrefixes() []string { + return []string{"ssr://"} +} + +func (p *ShadowsocksRParser) GetType() string { + return "ssr" +} + +func (p *ShadowsocksRParser) Parse(proxy string) (P.Proxy, error) { + if !hasPrefix(proxy, p.GetPrefixes()) { + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy} + } + + for _, prefix := range p.GetPrefixes() { + if strings.HasPrefix(proxy, prefix) { + proxy = strings.TrimPrefix(proxy, prefix) + break + } } - proxy = strings.TrimPrefix(proxy, constant.ShadowsocksRPrefix) proxy, err := DecodeBase64(proxy) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidBase64, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidBase64, Raw: proxy, } } @@ -30,16 +46,16 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) { obfs := parts[4] password, err := DecodeBase64(parts[5]) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Raw: proxy, Message: err.Error(), } } port, err := ParsePort(parts[1]) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidPort, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidPort, Message: err.Error(), Raw: proxy, } @@ -51,8 +67,8 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) { if len(serverInfoAndParams) == 2 { params, err := url.ParseQuery(serverInfoAndParams[1]) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrCannotParseParams, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrCannotParseParams, Raw: proxy, Message: err.Error(), } @@ -69,17 +85,17 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) { remarks = server + ":" + strconv.Itoa(port) } if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Raw: proxy, Message: err.Error(), } } } - result := model.Proxy{ + result := P.Proxy{ Name: remarks, - Type: "ssr", + Type: p.GetType(), Server: server, Port: port, Protocol: protocol, @@ -92,3 +108,7 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) { return result, nil } + +func init() { + RegisterParser(&ShadowsocksRParser{}) +} diff --git a/parser/socks.go b/parser/socks.go index 677ad66..ec3968d 100644 --- a/parser/socks.go +++ b/parser/socks.go @@ -2,44 +2,56 @@ package parser import ( "fmt" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/model" "net/url" "strings" + + E "github.com/bestnite/sub2clash/error" + P "github.com/bestnite/sub2clash/model/proxy" ) -func ParseSocks(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, constant.SocksPrefix) { - return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} +type SocksParser struct{} + +func (p *SocksParser) GetPrefixes() []string { + return []string{"socks://"} +} + +func (p *SocksParser) GetType() string { + return "socks5" +} + +func (p *SocksParser) Parse(proxy string) (P.Proxy, error) { + if !hasPrefix(proxy, p.GetPrefixes()) { + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy} } + link, err := url.Parse(proxy) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "url parse error", Raw: proxy, } } server := link.Hostname() if server == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server host", Raw: proxy, } } portStr := link.Port() if portStr == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server port", Raw: proxy, } } port, err := ParsePort(portStr) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidPort, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidPort, Raw: portStr, } } @@ -56,8 +68,8 @@ func ParseSocks(proxy string) (model.Proxy, error) { decodeStr, err := DecodeBase64(encodeStr) splitStr := strings.Split(decodeStr, ":") if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "url parse error", Raw: proxy, } @@ -67,8 +79,8 @@ func ParseSocks(proxy string) (model.Proxy, error) { password = splitStr[1] } } - return model.Proxy{ - Type: "socks5", + return P.Proxy{ + Type: p.GetType(), Name: remarks, Server: server, Port: port, @@ -77,3 +89,7 @@ func ParseSocks(proxy string) (model.Proxy, error) { }, nil } + +func init() { + RegisterParser(&SocksParser{}) +} diff --git a/parser/trojan.go b/parser/trojan.go index 2ccdcec..f639c13 100644 --- a/parser/trojan.go +++ b/parser/trojan.go @@ -5,19 +5,29 @@ import ( "net/url" "strings" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/model" + E "github.com/bestnite/sub2clash/error" + P "github.com/bestnite/sub2clash/model/proxy" ) -func ParseTrojan(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, constant.TrojanPrefix) { - return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} +type TrojanParser struct{} + +func (p *TrojanParser) GetPrefixes() []string { + return []string{"trojan://"} +} + +func (p *TrojanParser) GetType() string { + return "trojan" +} + +func (p *TrojanParser) Parse(proxy string) (P.Proxy, error) { + if !hasPrefix(proxy, p.GetPrefixes()) { + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy} } link, err := url.Parse(proxy) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "url parse error", Raw: proxy, } @@ -26,16 +36,16 @@ func ParseTrojan(proxy string) (model.Proxy, error) { password := link.User.Username() server := link.Hostname() if server == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server host", Raw: proxy, } } portStr := link.Port() if portStr == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server port", Raw: proxy, } @@ -43,8 +53,8 @@ func ParseTrojan(proxy string) (model.Proxy, error) { port, err := ParsePort(portStr) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidPort, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidPort, Message: err.Error(), Raw: proxy, } @@ -66,8 +76,8 @@ func ParseTrojan(proxy string) (model.Proxy, error) { alpn = nil } - result := model.Proxy{ - Type: "trojan", + result := P.Proxy{ + Type: p.GetType(), Server: server, Port: port, Password: password, @@ -84,7 +94,7 @@ func ParseTrojan(proxy string) (model.Proxy, error) { if security == "reality" { result.TLS = true result.Sni = sni - result.RealityOpts = model.RealityOptions{ + result.RealityOpts = P.RealityOptions{ PublicKey: pbk, ShortID: sid, } @@ -93,7 +103,7 @@ func ParseTrojan(proxy string) (model.Proxy, error) { if network == "ws" { result.Network = "ws" - result.WSOpts = model.WSOptions{ + result.WSOpts = P.WSOptions{ Path: path, Headers: map[string]string{ "Host": host, @@ -102,10 +112,14 @@ func ParseTrojan(proxy string) (model.Proxy, error) { } if network == "grpc" { - result.GrpcOpts = model.GrpcOptions{ + result.GrpcOpts = P.GrpcOptions{ GrpcServiceName: serviceName, } } return result, nil } + +func init() { + RegisterParser(&TrojanParser{}) +} diff --git a/parser/vless.go b/parser/vless.go index c515f2c..ad3a17e 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -5,19 +5,29 @@ import ( "net/url" "strings" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/model" + E "github.com/bestnite/sub2clash/error" + P "github.com/bestnite/sub2clash/model/proxy" ) -func ParseVless(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, constant.VLESSPrefix) { - return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} +type VlessParser struct{} + +func (p *VlessParser) GetPrefixes() []string { + return []string{"vless://"} +} + +func (p *VlessParser) GetType() string { + return "vless" +} + +func (p *VlessParser) Parse(proxy string) (P.Proxy, error) { + if !hasPrefix(proxy, p.GetPrefixes()) { + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy} } link, err := url.Parse(proxy) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "url parse error", Raw: proxy, } @@ -25,8 +35,8 @@ func ParseVless(proxy string) (model.Proxy, error) { server := link.Hostname() if server == "" { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidStruct, Message: "missing server host", Raw: proxy, } @@ -34,8 +44,8 @@ func ParseVless(proxy string) (model.Proxy, error) { portStr := link.Port() port, err := ParsePort(portStr) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidPort, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidPort, Message: err.Error(), Raw: proxy, } @@ -58,8 +68,8 @@ func ParseVless(proxy string) (model.Proxy, error) { } remarks = strings.TrimSpace(remarks) - result := model.Proxy{ - Type: "vless", + result := P.Proxy{ + Type: p.GetType(), Server: server, Name: remarks, Port: port, @@ -78,7 +88,7 @@ func ParseVless(proxy string) (model.Proxy, error) { if security == "reality" { result.TLS = true result.Servername = sni - result.RealityOpts = model.RealityOptions{ + result.RealityOpts = P.RealityOptions{ PublicKey: pbk, ShortID: sid, } @@ -87,7 +97,7 @@ func ParseVless(proxy string) (model.Proxy, error) { if _type == "ws" { result.Network = "ws" - result.WSOpts = model.WSOptions{ + result.WSOpts = P.WSOptions{ Path: path, } if host != "" { @@ -98,21 +108,21 @@ func ParseVless(proxy string) (model.Proxy, error) { if _type == "grpc" { result.Network = "grpc" - result.GrpcOpts = model.GrpcOptions{ + result.GrpcOpts = P.GrpcOptions{ GrpcServiceName: serviceName, } } if _type == "http" { - result.HTTPOpts = model.HTTPOptions{} + result.HTTPOpts = P.HTTPOptions{} result.HTTPOpts.Headers = map[string][]string{} result.HTTPOpts.Path = strings.Split(path, ",") hosts, err := url.QueryUnescape(host) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrCannotParseParams, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrCannotParseParams, Raw: proxy, Message: err.Error(), } @@ -125,3 +135,7 @@ func ParseVless(proxy string) (model.Proxy, error) { return result, nil } + +func init() { + RegisterParser(&VlessParser{}) +} diff --git a/parser/vmess.go b/parser/vmess.go index d6fe022..0206f94 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -6,25 +6,40 @@ import ( "strconv" "strings" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/model" + E "github.com/bestnite/sub2clash/error" + P "github.com/bestnite/sub2clash/model/proxy" ) -func ParseVmess(proxy string) (model.Proxy, error) { - if !strings.HasPrefix(proxy, constant.VMessPrefix) { - return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} +type VmessParser struct{} + +func (p *VmessParser) GetPrefixes() []string { + return []string{"vmess://"} +} + +func (p *VmessParser) GetType() string { + return "vmess" +} + +func (p *VmessParser) Parse(proxy string) (P.Proxy, error) { + if !hasPrefix(proxy, p.GetPrefixes()) { + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy} } - proxy = strings.TrimPrefix(proxy, constant.VMessPrefix) + for _, prefix := range p.GetPrefixes() { + if strings.HasPrefix(proxy, prefix) { + proxy = strings.TrimPrefix(proxy, prefix) + break + } + } base64, err := DecodeBase64(proxy) if err != nil { - return model.Proxy{}, &ParseError{Type: ErrInvalidBase64, Raw: proxy, Message: err.Error()} + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidBase64, Raw: proxy, Message: err.Error()} } - var vmess model.VmessJson + var vmess P.VmessJson err = json.Unmarshal([]byte(base64), &vmess) if err != nil { - return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()} + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: err.Error()} } var port int @@ -32,8 +47,8 @@ func ParseVmess(proxy string) (model.Proxy, error) { case string: port, err = ParsePort(vmess.Port.(string)) if err != nil { - return model.Proxy{}, &ParseError{ - Type: ErrInvalidPort, + return P.Proxy{}, &E.ParseError{ + Type: E.ErrInvalidPort, Message: err.Error(), Raw: proxy, } @@ -47,7 +62,7 @@ func ParseVmess(proxy string) (model.Proxy, error) { case string: aid, err = strconv.Atoi(vmess.Aid.(string)) if err != nil { - return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()} + return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: err.Error()} } case float64: aid = int(vmess.Aid.(float64)) @@ -62,9 +77,9 @@ func ParseVmess(proxy string) (model.Proxy, error) { name = vmess.Ps } - result := model.Proxy{ + result := P.Proxy{ Name: name, - Type: "vmess", + Type: p.GetType(), Server: vmess.Add, Port: port, UUID: vmess.Id, @@ -93,7 +108,7 @@ func ParseVmess(proxy string) (model.Proxy, error) { vmess.Host = vmess.Add } result.Network = "ws" - result.WSOpts = model.WSOptions{ + result.WSOpts = P.WSOptions{ Path: vmess.Path, Headers: map[string]string{ "Host": vmess.Host, @@ -102,14 +117,14 @@ func ParseVmess(proxy string) (model.Proxy, error) { } if vmess.Net == "grpc" { - result.GrpcOpts = model.GrpcOptions{ + result.GrpcOpts = P.GrpcOptions{ GrpcServiceName: vmess.Path, } result.Network = "grpc" } if vmess.Net == "h2" { - result.HTTP2Opts = model.HTTP2Options{ + result.HTTP2Opts = P.HTTP2Options{ Host: strings.Split(vmess.Host, ","), Path: vmess.Path, } @@ -118,3 +133,7 @@ func ParseVmess(proxy string) (model.Proxy, error) { return result, nil } + +func init() { + RegisterParser(&VmessParser{}) +} diff --git a/api/handler/short_link.go b/server/handler/short_link.go similarity index 82% rename from api/handler/short_link.go rename to server/handler/short_link.go index 650b052..e6061f1 100644 --- a/api/handler/short_link.go +++ b/server/handler/short_link.go @@ -3,26 +3,36 @@ package handler import ( "io" "net/http" - "strconv" "strings" "time" - "github.com/nitezs/sub2clash/common" - "github.com/nitezs/sub2clash/common/database" - "github.com/nitezs/sub2clash/config" - "github.com/nitezs/sub2clash/model" - "github.com/nitezs/sub2clash/validator" + "github.com/bestnite/sub2clash/common" + "github.com/bestnite/sub2clash/common/database" + "github.com/bestnite/sub2clash/config" + "github.com/bestnite/sub2clash/model" "github.com/gin-gonic/gin" ) +type shortLinkGenRequset struct { + Url string `form:"url" binding:"required"` + Password string `form:"password"` + CustomID string `form:"customId"` +} + +type shortLinkUpdateRequest struct { + Hash string `form:"hash" binding:"required"` + Url string `form:"url" binding:"required"` + Password string `form:"password" binding:"required"` +} + func respondWithError(c *gin.Context, code int, message string) { c.String(code, message) c.Abort() } func GenerateLinkHandler(c *gin.Context) { - var params validator.ShortLinkGenValidator + var params shortLinkGenRequset if err := c.ShouldBind(¶ms); err != nil { respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error()) return @@ -35,7 +45,7 @@ func GenerateLinkHandler(c *gin.Context) { var hash string var password string var err error - + if params.CustomID != "" { // 检查自定义ID是否已存在 exists, err := database.CheckShortLinkHashExists(params.CustomID) @@ -51,7 +61,7 @@ func GenerateLinkHandler(c *gin.Context) { password = params.Password } else { // 自动生成短链ID和密码 - hash, err = generateUniqueHash() + hash, err = generateUniqueHash(config.GlobalConfig.ShortLinkLength) if err != nil { respondWithError(c, http.StatusInternalServerError, "生成短链接失败") return @@ -76,15 +86,15 @@ func GenerateLinkHandler(c *gin.Context) { // 返回生成的短链ID和密码 response := map[string]string{ - "hash": hash, + "hash": hash, "password": password, } c.JSON(http.StatusOK, response) } -func generateUniqueHash() (string, error) { +func generateUniqueHash(length int) (string, error) { for { - hash := common.RandomString(config.Default.ShortLinkLength) + hash := common.RandomString(length) exists, err := database.CheckShortLinkHashExists(hash) if err != nil { return "", err @@ -96,7 +106,7 @@ func generateUniqueHash() (string, error) { } func UpdateLinkHandler(c *gin.Context) { - var params validator.ShortLinkUpdateValidator + var params shortLinkUpdateRequest if err := c.ShouldBindJSON(¶ms); err != nil { respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error()) return @@ -158,7 +168,7 @@ func GetRawConfHandler(c *gin.Context) { return } - response, err := http.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url) + response, err := http.Get(strings.TrimSuffix(config.GlobalConfig.Address, "/") + "/" + shortLink.Url) if err != nil { respondWithError(c, http.StatusInternalServerError, "请求错误: "+err.Error()) return diff --git a/server/handler/sub.go b/server/handler/sub.go new file mode 100644 index 0000000..76b66fe --- /dev/null +++ b/server/handler/sub.go @@ -0,0 +1,54 @@ +package handler + +import ( + _ "embed" + "net/http" + + "github.com/bestnite/sub2clash/common" + "github.com/bestnite/sub2clash/config" + M "github.com/bestnite/sub2clash/model" + + "github.com/gin-gonic/gin" + "gopkg.in/yaml.v3" +) + +func SubHandler(model M.ClashType, template string) func(c *gin.Context) { + return func(c *gin.Context) { + query, err := M.ParseSubQuery(c) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) + return + } + sub, err := common.BuildSub(model, query, template, config.GlobalConfig.CacheExpire, config.GlobalConfig.RequestRetryTimes) + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + + if len(query.Subs) == 1 { + userInfoHeader, err := common.FetchSubscriptionUserInfo(query.Subs[0], "clash", config.GlobalConfig.RequestRetryTimes) + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + } + c.Header("subscription-userinfo", userInfoHeader) + } + + if query.NodeListMode { + nodelist := M.NodeList{} + nodelist.Proxies = sub.Proxies + marshal, err := yaml.Marshal(nodelist) + if err != nil { + c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error()) + return + } + c.String(http.StatusOK, string(marshal)) + return + } + marshal, err := yaml.Marshal(sub) + if err != nil { + c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error()) + return + } + c.String(http.StatusOK, string(marshal)) + } +} diff --git a/middleware/logger.go b/server/middleware/logger.go similarity index 94% rename from middleware/logger.go rename to server/middleware/logger.go index 10011e2..6226472 100644 --- a/middleware/logger.go +++ b/server/middleware/logger.go @@ -4,7 +4,7 @@ import ( "strconv" "time" - "github.com/nitezs/sub2clash/logger" + "github.com/bestnite/sub2clash/logger" "github.com/gin-gonic/gin" "go.uber.org/zap" diff --git a/api/route.go b/server/route.go similarity index 66% rename from api/route.go rename to server/route.go index 118738c..7078c80 100644 --- a/api/route.go +++ b/server/route.go @@ -1,4 +1,4 @@ -package api +package server import ( "embed" @@ -6,9 +6,11 @@ import ( "log" "net/http" - "github.com/nitezs/sub2clash/api/handler" - "github.com/nitezs/sub2clash/constant" - "github.com/nitezs/sub2clash/middleware" + "github.com/bestnite/sub2clash/config" + "github.com/bestnite/sub2clash/constant" + "github.com/bestnite/sub2clash/model" + "github.com/bestnite/sub2clash/server/handler" + "github.com/bestnite/sub2clash/server/middleware" "github.com/gin-gonic/gin" ) @@ -40,8 +42,8 @@ func SetRoute(r *gin.Engine) { ) }, ) - r.GET("/clash", handler.SubmodHandler) - r.GET("/meta", handler.SubHandler) + r.GET("/clash", handler.SubHandler(model.Clash, config.GlobalConfig.ClashTemplate)) + r.GET("/meta", handler.SubHandler(model.ClashMeta, config.GlobalConfig.MetaTemplate)) r.GET("/s/:hash", handler.GetRawConfHandler) r.POST("/short", handler.GenerateLinkHandler) r.PUT("/short", handler.UpdateLinkHandler) diff --git a/api/static/index.html b/server/static/index.html similarity index 98% rename from api/static/index.html rename to server/static/index.html index 3bb78f4..721b6ed 100644 --- a/api/static/index.html +++ b/server/static/index.html @@ -38,7 +38,7 @@

sub2clash

通用订阅链接转 Clash(Meta) 配置工具 - 使用文档
使用文档
注意:本程序非纯前端程序,输入的订阅将被后端缓存,请确保您信任当前站点
@@ -174,7 +174,7 @@ diff --git a/api/static/index.js b/server/static/index.js similarity index 100% rename from api/static/index.js rename to server/static/index.js diff --git a/validator/short_link.go b/validator/short_link.go deleted file mode 100644 index f09c7b1..0000000 --- a/validator/short_link.go +++ /dev/null @@ -1,18 +0,0 @@ -package validator - -type ShortLinkGenValidator struct { - Url string `form:"url" binding:"required"` - Password string `form:"password"` - CustomID string `form:"customId"` -} - -type GetUrlValidator struct { - Hash string `form:"hash" binding:"required"` - Password string `form:"password"` -} - -type ShortLinkUpdateValidator struct { - Hash string `form:"hash" binding:"required"` - Url string `form:"url" binding:"required"` - Password string `form:"password" binding:"required"` -}