diff --git a/.env.example b/.env.example index 163dd1b..ac2538b 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,6 @@ PORT=8011 META_TEMPLATE=meta_template.json CLASH_TEMPLATE=clash_template.json REQUEST_RETRY_TIMES=3 -REQUEST_MAX_FILE_SIZE=1048576 \ No newline at end of file +REQUEST_MAX_FILE_SIZE=1048576 +CACHE_EXPIRE=300 +LOG_LEVEL=info \ No newline at end of file diff --git a/.gitignore b/.gitignore index 434f1a1..e7bff70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea dist subs -test \ No newline at end of file +test +logs \ No newline at end of file diff --git a/README.md b/README.md index 2e4ec33..43379e1 100644 --- a/README.md +++ b/README.md @@ -19,26 +19,29 @@ ## API -### /clash +### `/clash`,`/meta` -获取 Clash 配置链接 +获取 Clash/Clash.Meta 配置链接 -| Query 参数 | 类型 | 说明 | -|----------|--------|-------------------------| -| sub | string | 订阅链接(可以输入多个订阅,用 `,` 分隔) | -| refresh | bool | 强制刷新配置(默认缓存 5 分钟) | +| Query 参数 | 类型 | 是否必须 | 说明 | +|--------------|--------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| sub | string | sub/proxy 至少有一项存在 | 订阅链接(可以输入多个,用 `,` 分隔) | +| proxy | string | sub/proxy 至少有一项存在 | 节点分享链接(可以输入多个,用 `,` 分隔) | +| refresh | bool | 否(默认 `false`) | 强制刷新配置(默认缓存 5 分钟) | +| template | string | 否 | 外部模板 | +| ruleProvider | string | 否 | 格式 `[Behavior,Url,Group,Prepend],[Behavior,Url,Group,Prepend],...`,其中 `Group` 是该规则集所走的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) | +| rule | string | 否 | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) | +| autoTest | bool | 否(默认 `false`) | 指定国家策略组是否自动测速 | +| lazy | bool | 否(默认 `false`) | 自动测速是否启用 lazy | -### /meta +## 默认模板 -获取 Meta 配置链接 +- [Clash](./templates/template_clash.yaml) +- [Clash.Meta](./templates/template_meta.yaml) -| Query 参数 | 类型 | 说明 | -|----------|--------|-------------------------| -| sub | string | 订阅链接(可以输入多个订阅,用 `,` 分隔) | -| refresh | bool | 强制刷新配置(默认缓存 5 分钟) | +## 已知问题 + +[代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue ## TODO -- [ ] 完善日志功能 -- [ ] 支持自动测速分组 -- [ ] 完善配置模板 diff --git a/api/controller/clash.go b/api/controller/clash.go index 272110b..1a70fcc 100644 --- a/api/controller/clash.go +++ b/api/controller/clash.go @@ -1,36 +1,30 @@ package controller import ( - "net/http" - "strings" - "sub2clash/config" - "sub2clash/validator" - "github.com/gin-gonic/gin" "gopkg.in/yaml.v3" + "net/http" + "sub2clash/config" + "sub2clash/validator" ) func SubmodHandler(c *gin.Context) { // 从请求中获取参数 - var query validator.SubQuery - if err := c.ShouldBind(&query); err != nil { - c.String(http.StatusBadRequest, "参数错误: "+err.Error()) + query, err := validator.ParseQuery(c) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) return } - // 混合订阅和模板节点 - sub, err := MixinSubsAndTemplate( - strings.Split(query.Sub, ","), query.Refresh, config.Default.ClashTemplate, - ) + sub, err := BuildSub(query, config.Default.ClashTemplate) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } - // 添加自定义节点、规则 // 输出 - bytes, err := yaml.Marshal(sub) + marshal, err := yaml.Marshal(sub) if err != nil { - c.String(http.StatusInternalServerError, err.Error()) + c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error()) return } - c.String(http.StatusOK, string(bytes)) + c.String(http.StatusOK, string(marshal)) } diff --git a/api/controller/default.go b/api/controller/default.go index d4fb0e7..55760a6 100644 --- a/api/controller/default.go +++ b/api/controller/default.go @@ -1,42 +1,47 @@ package controller import ( + "crypto/md5" + "encoding/hex" "errors" "gopkg.in/yaml.v3" - "net/url" + "regexp" "strings" "sub2clash/model" "sub2clash/parser" "sub2clash/utils" + "sub2clash/validator" ) -func MixinSubsAndTemplate(subs []string, refresh bool, template string) ( +func BuildSub(query validator.SubQuery, template string) ( *model.Subscription, error, ) { // 定义变量 + var externalTemplate = query.Template != "" var temp *model.Subscription var sub *model.Subscription + var err error + var templateBytes []byte // 加载模板 - template, err := utils.LoadTemplate(template) - if err != nil { - return nil, errors.New("加载模板失败: " + err.Error()) + if !externalTemplate { + templateBytes, err = utils.LoadTemplate(template) + if err != nil { + return nil, errors.New("加载模板失败: " + err.Error()) + } + } else { + templateBytes, err = utils.LoadSubscription(template, query.Refresh) + if err != nil { + return nil, errors.New("加载模板失败: " + err.Error()) + } } // 解析模板 - err = yaml.Unmarshal([]byte(template), &temp) + err = yaml.Unmarshal(templateBytes, &temp) if err != nil { return nil, errors.New("解析模板失败: " + err.Error()) } - var proxies []model.Proxy // 加载订阅 - for i := range subs { - subs[i], _ = url.QueryUnescape(subs[i]) - if _, err := url.ParseRequestURI(subs[i]); err != nil { - return nil, errors.New("订阅地址错误: " + err.Error()) - } - data, err := utils.LoadSubscription( - subs[i], - refresh, - ) + for i := range query.Subs { + data, err := utils.LoadSubscription(query.Subs[i], query.Refresh) if err != nil { return nil, errors.New("加载订阅失败: " + err.Error()) } @@ -44,18 +49,52 @@ func MixinSubsAndTemplate(subs []string, refresh bool, template string) ( var proxyList []model.Proxy err = yaml.Unmarshal(data, &sub) if err != nil { - // 如果无法直接解析,尝试Base64解码 - base64, err := parser.DecodeBase64(string(data)) - if err != nil { - return nil, errors.New("加载订阅失败: " + err.Error()) + reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|http|https)://") + if reg.Match(data) { + proxyList = utils.ParseProxy(strings.Split(string(data), "\n")...) + } else { + // 如果无法直接解析,尝试Base64解码 + base64, err := parser.DecodeBase64(string(data)) + if err != nil { + return nil, errors.New("加载订阅失败: " + err.Error()) + } + proxyList = utils.ParseProxy(strings.Split(base64, "\n")...) } - proxyList = utils.ParseProxy(strings.Split(base64, "\n")...) } else { proxyList = sub.Proxies } - proxies = append(proxies, proxyList...) + utils.AddProxy(temp, query.AutoTest, query.Lazy, proxyList...) + } + // 处理自定义代理 + utils.AddProxy(temp, query.AutoTest, query.Lazy, utils.ParseProxy(query.Proxies...)...) + // 处理自定义规则 + for _, v := range query.Rules { + if v.Prepend { + utils.PrependRules(temp, v.Rule) + } else { + utils.AppendRules(temp, v.Rule) + } + } + // 处理自定义 ruleProvider + for _, v := range query.RuleProviders { + hash := md5.Sum([]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 { + utils.PrependRuleProvider( + temp, name, v.Group, provider, + ) + } else { + utils.AppenddRuleProvider( + temp, name, v.Group, provider, + ) + } } - // 添加节点 - utils.AddProxy(temp, proxies...) return temp, nil } diff --git a/api/controller/meta.go b/api/controller/meta.go index 95df826..8b28dec 100644 --- a/api/controller/meta.go +++ b/api/controller/meta.go @@ -2,31 +2,25 @@ package controller import ( _ "embed" - "net/http" - "strings" - "sub2clash/config" - "sub2clash/validator" - "github.com/gin-gonic/gin" "gopkg.in/yaml.v3" + "net/http" + "sub2clash/config" + "sub2clash/validator" ) func SubHandler(c *gin.Context) { // 从请求中获取参数 - var query validator.SubQuery - if err := c.ShouldBind(&query); err != nil { - c.String(http.StatusBadRequest, "参数错误: "+err.Error()) + query, err := validator.ParseQuery(c) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) return } - // 混合订阅和模板节点 - sub, err := MixinSubsAndTemplate( - strings.Split(query.Sub, ","), query.Refresh, config.Default.MetaTemplate, - ) + sub, err := BuildSub(query, config.Default.MetaTemplate) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } - // 添加自定义节点、规则 // 输出 marshal, err := yaml.Marshal(sub) if err != nil { diff --git a/api/route.go b/api/route.go index 657d27e..4feb5e9 100644 --- a/api/route.go +++ b/api/route.go @@ -3,9 +3,11 @@ package api import ( "github.com/gin-gonic/gin" "sub2clash/api/controller" + "sub2clash/middleware" ) func SetRoute(r *gin.Engine) { + r.Use(middleware.ZapLogger()) r.GET( "/clash", func(c *gin.Context) { controller.SubmodHandler(c) diff --git a/config/config.go b/config/config.go index b36b9c5..dd9ee6e 100644 --- a/config/config.go +++ b/config/config.go @@ -1,7 +1,6 @@ package config import ( - "fmt" "github.com/joho/godotenv" "os" "strconv" @@ -13,6 +12,8 @@ type Config struct { ClashTemplate string RequestRetryTimes int RequestMaxFileSize int64 + CacheExpire int64 + LogLevel string } var Default *Config @@ -24,6 +25,8 @@ func init() { RequestRetryTimes: 3, RequestMaxFileSize: 1024 * 1024 * 1, Port: 8011, + CacheExpire: 60 * 5, + LogLevel: "info", } err := godotenv.Load() if err != nil { @@ -32,7 +35,7 @@ func init() { if os.Getenv("PORT") != "" { atoi, err := strconv.Atoi(os.Getenv("PORT")) if err != nil { - fmt.Println("PORT 不合法") + panic("PORT invalid") } Default.Port = atoi } @@ -45,15 +48,25 @@ func init() { if os.Getenv("REQUEST_RETRY_TIMES") != "" { atoi, err := strconv.Atoi(os.Getenv("REQUEST_RETRY_TIMES")) if err != nil { - fmt.Println("REQUEST_RETRY_TIMES 不合法") + panic("REQUEST_RETRY_TIMES invalid") } Default.RequestRetryTimes = atoi } if os.Getenv("REQUEST_MAX_FILE_SIZE") != "" { atoi, err := strconv.Atoi(os.Getenv("REQUEST_MAX_FILE_SIZE")) if err != nil { - fmt.Println("REQUEST_MAX_FILE_SIZE 不合法") + panic("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 { + panic("CACHE_EXPIRE invalid") + } + Default.CacheExpire = int64(atoi) + } + if os.Getenv("LOG_LEVEL") != "" { + Default.LogLevel = os.Getenv("LOG_LEVEL") + } } diff --git a/go.mod b/go.mod index 123d61f..73da870 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module sub2clash go 1.21 require ( - github.com/bytedance/sonic v1.10.0 // indirect + github.com/bytedance/sonic v1.10.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect @@ -11,7 +11,7 @@ require ( github.com/gin-gonic/gin v1.9.1 // 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.15.3 // indirect + github.com/go-playground/validator/v10 v10.15.4 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -23,6 +23,8 @@ require ( github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.25.0 // indirect golang.org/x/arch v0.5.0 // indirect golang.org/x/crypto v0.13.0 // indirect golang.org/x/net v0.15.0 // indirect diff --git a/go.sum b/go.sum index dda18bb..f00a5b9 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1 github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk= github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= +github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= @@ -22,6 +24,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo= github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs= +github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -44,6 +48,8 @@ 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/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -61,6 +67,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..e141cf9 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,72 @@ +package logger + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "path/filepath" + "sub2clash/config" + "sub2clash/utils" + "sync" + "time" +) + +var ( + Logger *zap.Logger + lock sync.Mutex +) + +func init() { + buildLogger() + go rotateLogs() +} + +func buildLogger() { + lock.Lock() + defer lock.Unlock() + var level zapcore.Level + switch config.Default.LogLevel { + case "error": + level = zap.ErrorLevel + case "debug": + level = zap.DebugLevel + case "warn": + level = zap.WarnLevel + case "info": + level = zap.InfoLevel + default: + level = zap.InfoLevel + } + err := utils.MKDir("logs") + if err != nil { + panic("创建日志失败" + err.Error()) + } + zapConfig := zap.NewProductionConfig() + zapConfig.Encoding = "console" + zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + zapConfig.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder + zapConfig.OutputPaths = []string{"stdout", getLogFileName("info")} + zapConfig.ErrorOutputPaths = []string{"stderr", getLogFileName("error")} + zapConfig.Level = zap.NewAtomicLevelAt(level) + Logger, err = zapConfig.Build() + if err != nil { + panic("创建日志失败" + err.Error()) + } +} + +// 根据日期获得日志文件 +func getLogFileName(name string) string { + return filepath.Join("logs", time.Now().Format("2006-01-02")+"-"+name+".log") +} + +func rotateLogs() { + for { + now := time.Now() + nextMidnight := time.Date( + now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location(), + ).Add(24 * time.Hour) + durationUntilMidnight := nextMidnight.Sub(now) + + time.Sleep(durationUntilMidnight) + buildLogger() + } +} diff --git a/main.go b/main.go index 9f2b4b9..19158a2 100644 --- a/main.go +++ b/main.go @@ -2,14 +2,16 @@ package main import ( _ "embed" - "fmt" "github.com/gin-gonic/gin" + "go.uber.org/zap" + "io" "os" "path/filepath" "strconv" "sub2clash/api" "sub2clash/config" - _ "sub2clash/config" + "sub2clash/logger" + "sub2clash/utils" ) //go:embed templates/template_meta.yaml @@ -25,29 +27,13 @@ func writeTemplate(path string, template string) error { if _, err := os.Stat(tPath); os.IsNotExist(err) { file, err := os.Create(tPath) if err != nil { - fmt.Println(err) return err } defer func(file *os.File) { - err := file.Close() - if err != nil { - fmt.Println(err) - } + _ = file.Close() }(file) _, err = file.WriteString(template) if err != nil { - fmt.Println(err) - return err - } - } - return nil -} - -func mkDir(dir string) error { - if _, err := os.Stat(dir); os.IsNotExist(err) { - err := os.MkdirAll(dir, os.ModePerm) - if err != nil { - fmt.Println(err) return err } } @@ -55,10 +41,10 @@ func mkDir(dir string) error { } func init() { - if err := mkDir("subs"); err != nil { + if err := utils.MKDir("subs"); err != nil { os.Exit(1) } - if err := mkDir("templates"); err != nil { + if err := utils.MKDir("templates"); err != nil { os.Exit(1) } if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil { @@ -72,14 +58,16 @@ func init() { func main() { // 设置运行模式 gin.SetMode(gin.ReleaseMode) + // 关闭 Gin 的日志输出 + gin.DefaultWriter = io.Discard // 创建路由 r := gin.Default() // 设置路由 api.SetRoute(r) - fmt.Println("Server is running at 8011") + logger.Logger.Info("Server is running at http://localhost:" + strconv.Itoa(config.Default.Port)) err := r.Run(":" + strconv.Itoa(config.Default.Port)) if err != nil { - fmt.Println(err) + logger.Logger.Error("Server run error", zap.Error(err)) return } } diff --git a/middleware/logger.go b/middleware/logger.go new file mode 100644 index 0000000..470eebd --- /dev/null +++ b/middleware/logger.go @@ -0,0 +1,39 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "strconv" + "sub2clash/logger" + "time" +) + +func ZapLogger() gin.HandlerFunc { + return func(c *gin.Context) { + startTime := time.Now() + + c.Next() + + endTime := time.Now() + latencyTime := endTime.Sub(startTime).Milliseconds() + reqMethod := c.Request.Method + reqURI := c.Request.RequestURI + statusCode := c.Writer.Status() + clientIP := c.ClientIP() + + logger.Logger.Info( + "Request", + zap.Int("status", statusCode), + zap.String("method", reqMethod), + zap.String("uri", reqURI), + zap.String("ip", clientIP), + zap.String("latency", strconv.Itoa(int(latencyTime))+"ms"), + ) + + if len(c.Errors) > 0 { + for _, e := range c.Errors.Errors() { + logger.Logger.Error(e) + } + } + } +} diff --git a/model/sub.go b/model/sub.go index 7ace64a..4d8d3d4 100644 --- a/model/sub.go +++ b/model/sub.go @@ -3,9 +3,9 @@ package model type Subscription struct { Port int `yaml:"port,omitempty"` SocksPort int `yaml:"socks-port,omitempty"` - AllowLan bool `yaml:"allow-lan,omitempty"` + AllowLan bool `yaml:"allow-lan"` Mode string `yaml:"mode,omitempty"` - LogLevel string `yaml:"log-level,omitempty"` + LogLevel string `yaml:"logger-level,omitempty"` ExternalController string `yaml:"external-controller,omitempty"` Proxies []Proxy `yaml:"proxies,omitempty"` ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"` @@ -18,12 +18,16 @@ type ProxyGroup struct { Type string `yaml:"type,omitempty"` Proxies []string `yaml:"proxies,omitempty"` IsCountryGrop bool `yaml:"-"` + Url string `yaml:"url,omitempty"` + Interval int `yaml:"interval,omitempty"` + Tolerance int `yaml:"tolerance,omitempty"` + Lazy bool `yaml:"lazy"` } type RuleProvider struct { Type string `yaml:"type,omitempty"` Behavior string `yaml:"behavior,omitempty"` - URL string `yaml:"url,omitempty"` + Url string `yaml:"url,omitempty"` Path string `yaml:"path,omitempty"` Interval int `yaml:"interval,omitempty"` } diff --git a/parser/ss.go b/parser/ss.go index 2d39340..eebeae5 100644 --- a/parser/ss.go +++ b/parser/ss.go @@ -8,16 +8,16 @@ import ( "sub2clash/model" ) -// ParseSS 解析 SS(Shadowsocks)URL +// ParseSS 解析 SS(Shadowsocks)Url func ParseSS(proxy string) (model.Proxy, error) { // 判断是否以 ss:// 开头 if !strings.HasPrefix(proxy, "ss://") { - return model.Proxy{}, fmt.Errorf("无效的 ss URL") + return model.Proxy{}, fmt.Errorf("无效的 ss Url") } // 分割 parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2) if len(parts) != 2 { - return model.Proxy{}, fmt.Errorf("无效的 ss URL") + return model.Proxy{}, fmt.Errorf("无效的 ss Url") } if !strings.Contains(parts[0], ":") { // 解码 diff --git a/parser/ssr.go b/parser/ssr.go index eaafccf..e485dfa 100644 --- a/parser/ssr.go +++ b/parser/ssr.go @@ -11,7 +11,7 @@ import ( func ParseShadowsocksR(proxy string) (model.Proxy, error) { // 判断是否以 ssr:// 开头 if !strings.HasPrefix(proxy, "ssr://") { - return model.Proxy{}, fmt.Errorf("无效的 ssr URL") + return model.Proxy{}, fmt.Errorf("无效的 ssr Url") } var err error if !strings.Contains(proxy, ":") { diff --git a/parser/trojan.go b/parser/trojan.go index 2b5b7f9..c8c2d2c 100644 --- a/parser/trojan.go +++ b/parser/trojan.go @@ -11,12 +11,12 @@ import ( func ParseTrojan(proxy string) (model.Proxy, error) { // 判断是否以 trojan:// 开头 if !strings.HasPrefix(proxy, "trojan://") { - return model.Proxy{}, fmt.Errorf("无效的 trojan URL") + return model.Proxy{}, fmt.Errorf("无效的 trojan Url") } // 分割 parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) if len(parts) != 2 { - return model.Proxy{}, fmt.Errorf("无效的 trojan URL") + return model.Proxy{}, fmt.Errorf("无效的 trojan Url") } // 分割 serverInfo := strings.SplitN(parts[1], "#", 2) diff --git a/parser/vless.go b/parser/vless.go index e457df9..6740855 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -11,12 +11,12 @@ import ( func ParseVless(proxy string) (model.Proxy, error) { // 判断是否以 vless:// 开头 if !strings.HasPrefix(proxy, "vless://") { - return model.Proxy{}, fmt.Errorf("无效的 vless URL") + return model.Proxy{}, fmt.Errorf("无效的 vless Url") } // 分割 parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) if len(parts) != 2 { - return model.Proxy{}, fmt.Errorf("无效的 vless URL") + return model.Proxy{}, fmt.Errorf("无效的 vless Url") } // 分割 serverInfo := strings.SplitN(parts[1], "#", 2) @@ -46,12 +46,14 @@ func ParseVless(proxy string) (model.Proxy, error) { TLS: params.Get("security") == "tls", Flow: params.Get("flow"), Fingerprint: params.Get("fp"), - Alpn: strings.Split(params.Get("alpn"), ","), Servername: params.Get("sni"), RealityOpts: model.RealityOptsStruct{ PublicKey: params.Get("pbk"), }, } + if params.Get("alpn") != "" { + result.Alpn = strings.Split(params.Get("alpn"), ",") + } if params.Get("type") == "ws" { result.WSOpts = model.WSOptsStruct{ Path: params.Get("path"), diff --git a/parser/vmess.go b/parser/vmess.go index 7e06672..0dcd268 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -12,23 +12,23 @@ import ( func ParseVmess(proxy string) (model.Proxy, error) { // 判断是否以 vmess:// 开头 if !strings.HasPrefix(proxy, "vmess://") { - return model.Proxy{}, fmt.Errorf("无效的 vmess URL") + return model.Proxy{}, fmt.Errorf("无效的 vmess Url") } // 解码 base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) if err != nil { - return model.Proxy{}, errors.New("无效的 vmess URL") + return model.Proxy{}, errors.New("无效的 vmess Url") } // 解析 var vmess model.Vmess err = json.Unmarshal([]byte(base64), &vmess) if err != nil { - return model.Proxy{}, errors.New("无效的 vmess URL") + return model.Proxy{}, errors.New("无效的 vmess Url") } // 处理端口 port, err := strconv.Atoi(strings.TrimSpace(vmess.Port)) if err != nil { - return model.Proxy{}, errors.New("无效的 vmess URL") + return model.Proxy{}, errors.New("无效的 vmess Url") } if vmess.Scy == "" { vmess.Scy = "auto" diff --git a/templates/template_clash.yaml b/templates/template_clash.yaml index a7ea768..fbbefff 100644 --- a/templates/template_clash.yaml +++ b/templates/template_clash.yaml @@ -999,8 +999,8 @@ rules: - DOMAIN-SUFFIX,itsdata.map.baidu.com,应用净化 - DOMAIN-SUFFIX,j.br.baidu.com,应用净化 - DOMAIN-SUFFIX,kstj.baidu.com,应用净化 - - DOMAIN-SUFFIX,log.music.baidu.com,应用净化 - - DOMAIN-SUFFIX,log.nuomi.com,应用净化 + - DOMAIN-SUFFIX,logger.music.baidu.com,应用净化 + - DOMAIN-SUFFIX,logger.nuomi.com,应用净化 - DOMAIN-SUFFIX,m1.baidu.com,应用净化 - DOMAIN-SUFFIX,ma.baidu.cn,应用净化 - DOMAIN-SUFFIX,ma.baidu.com,应用净化 @@ -1130,7 +1130,7 @@ rules: - DOMAIN-SUFFIX,ad.toutiao.com,应用净化 - DOMAIN-SUFFIX,dsp.toutiao.com,应用净化 - DOMAIN-SUFFIX,ic.snssdk.com,应用净化 - - DOMAIN-SUFFIX,log.snssdk.com,应用净化 + - DOMAIN-SUFFIX,logger.snssdk.com,应用净化 - DOMAIN-SUFFIX,nativeapp.toutiao.com,应用净化 - DOMAIN-SUFFIX,pangolin-sdk-toutiao-b.com,应用净化 - DOMAIN-SUFFIX,pangolin-sdk-toutiao.com,应用净化 @@ -1192,8 +1192,8 @@ rules: - DOMAIN-SUFFIX,install2.kugou.com,应用净化 - DOMAIN-SUFFIX,kgmobilestat.kugou.com,应用净化 - DOMAIN-SUFFIX,kuaikaiapp.com,应用净化 - - DOMAIN-SUFFIX,log.stat.kugou.com,应用净化 - - DOMAIN-SUFFIX,log.web.kugou.com,应用净化 + - DOMAIN-SUFFIX,logger.stat.kugou.com,应用净化 + - DOMAIN-SUFFIX,logger.web.kugou.com,应用净化 - DOMAIN-SUFFIX,minidcsc.kugou.com,应用净化 - DOMAIN-SUFFIX,mo.kugou.com,应用净化 - DOMAIN-SUFFIX,mobilelog.kugou.com,应用净化 @@ -1210,7 +1210,7 @@ rules: - DOMAIN-SUFFIX,g.koowo.com,应用净化 - DOMAIN-SUFFIX,g.kuwo.cn,应用净化 - DOMAIN-SUFFIX,kwmsg.kuwo.cn,应用净化 - - DOMAIN-SUFFIX,log.kuwo.cn,应用净化 + - DOMAIN-SUFFIX,logger.kuwo.cn,应用净化 - DOMAIN-SUFFIX,mobilead.kuwo.cn,应用净化 - DOMAIN-SUFFIX,msclick2.kuwo.cn,应用净化 - DOMAIN-SUFFIX,msphoneclick.kuwo.cn,应用净化 @@ -1279,7 +1279,7 @@ rules: - DOMAIN-SUFFIX,cdn.moji002.com,应用净化 - DOMAIN-SUFFIX,cdn2.moji002.com,应用净化 - DOMAIN-SUFFIX,fds.api.moji.com,应用净化 - - DOMAIN-SUFFIX,log.moji.com,应用净化 + - DOMAIN-SUFFIX,logger.moji.com,应用净化 - DOMAIN-SUFFIX,stat.moji.com,应用净化 - DOMAIN-SUFFIX,ugc.moji001.com,应用净化 - DOMAIN-SUFFIX,ad.qingting.fm,应用净化 @@ -1331,7 +1331,7 @@ rules: - DOMAIN-SUFFIX,game.weibo.com.cn,应用净化 - DOMAIN-SUFFIX,gw5.push.mcp.weibo.cn,应用净化 - DOMAIN-SUFFIX,leju.sina.com.cn,应用净化 - - DOMAIN-SUFFIX,log.mix.sina.com.cn,应用净化 + - DOMAIN-SUFFIX,logger.mix.sina.com.cn,应用净化 - DOMAIN-SUFFIX,mobileads.dx.cn,应用净化 - DOMAIN-SUFFIX,newspush.sinajs.cn,应用净化 - DOMAIN-SUFFIX,pay.mobile.sina.cn,应用净化 @@ -1391,7 +1391,7 @@ rules: - DOMAIN-SUFFIX,cms.ucweb.com,应用净化 - DOMAIN-SUFFIX,dispatcher.upmc.uc.cn,应用净化 - DOMAIN-SUFFIX,huichuan.sm.cn,应用净化 - - DOMAIN-SUFFIX,log.cs.pp.cn,应用净化 + - DOMAIN-SUFFIX,logger.cs.pp.cn,应用净化 - DOMAIN-SUFFIX,m.uczzd.cn,应用净化 - DOMAIN-SUFFIX,patriot.cs.pp.cn,应用净化 - DOMAIN-SUFFIX,puds.ucweb.com,应用净化 @@ -1531,8 +1531,8 @@ rules: - DOMAIN-SUFFIX,click.hunantv.com,应用净化 - DOMAIN-SUFFIX,da.hunantv.com,应用净化 - DOMAIN-SUFFIX,da.mgtv.com,应用净化 - - DOMAIN-SUFFIX,log.hunantv.com,应用净化 - - DOMAIN-SUFFIX,log.v2.hunantv.com,应用净化 + - DOMAIN-SUFFIX,logger.hunantv.com,应用净化 + - DOMAIN-SUFFIX,logger.v2.hunantv.com,应用净化 - DOMAIN-SUFFIX,p2.hunantv.com,应用净化 - DOMAIN-SUFFIX,res.hunantv.com,应用净化 - DOMAIN-SUFFIX,888.tv.sohu.com,应用净化 @@ -1616,10 +1616,10 @@ rules: - DOMAIN-SUFFIX,msg.youku.com,应用净化 - DOMAIN-SUFFIX,myes.youku.com,应用净化 - DOMAIN-SUFFIX,nstat.tudou.com,应用净化 - - DOMAIN-SUFFIX,p-log.ykimg.com,应用净化 + - DOMAIN-SUFFIX,p-logger.ykimg.com,应用净化 - DOMAIN-SUFFIX,p.l.ykimg.com,应用净化 - DOMAIN-SUFFIX,p.l.youku.com,应用净化 - - DOMAIN-SUFFIX,passport-log.youku.com,应用净化 + - DOMAIN-SUFFIX,passport-logger.youku.com,应用净化 - DOMAIN-SUFFIX,push.m.youku.com,应用净化 - DOMAIN-SUFFIX,r.l.youku.com,应用净化 - DOMAIN-SUFFIX,s.p.youku.com,应用净化 @@ -1746,7 +1746,7 @@ rules: - DOMAIN-SUFFIX,iadsdk.apple.com,应用净化 - DOMAIN-SUFFIX,image.gentags.com,应用净化 - DOMAIN-SUFFIX,its-dori.tumblr.com,应用净化 - - DOMAIN-SUFFIX,log.outbrain.com,应用净化 + - DOMAIN-SUFFIX,logger.outbrain.com,应用净化 - DOMAIN-SUFFIX,m.12306media.com,应用净化 - DOMAIN-SUFFIX,media.cheshi-img.com,应用净化 - DOMAIN-SUFFIX,media.cheshi.com,应用净化 diff --git a/utils/os.go b/utils/os.go new file mode 100644 index 0000000..e318160 --- /dev/null +++ b/utils/os.go @@ -0,0 +1,16 @@ +package utils + +import ( + "os" +) + +func MKDir(dir string) error { + if _, err := os.Stat(dir); os.IsNotExist(err) { + err := os.MkdirAll(dir, os.ModePerm) + if err != nil { + + return err + } + } + return nil +} diff --git a/utils/proxy.go b/utils/proxy.go index 7b347a1..dfb54dc 100644 --- a/utils/proxy.go +++ b/utils/proxy.go @@ -34,7 +34,7 @@ var skipGroups = map[string]bool{ "应用进化": true, } -func AddProxy(sub *model.Subscription, proxies ...model.Proxy) { +func AddProxy(sub *model.Subscription, autotest bool, lazy bool, proxies ...model.Proxy) { newCountryGroupNames := make([]string, 0) for _, proxy := range proxies { @@ -57,11 +57,25 @@ func AddProxy(sub *model.Subscription, proxies ...model.Proxy) { } if !haveProxyGroup { - newGroup := model.ProxyGroup{ - Name: countryName, - Type: "select", - Proxies: []string{proxy.Name}, - IsCountryGrop: true, + var newGroup model.ProxyGroup + if !autotest { + newGroup = model.ProxyGroup{ + Name: countryName, + Type: "select", + Proxies: []string{proxy.Name}, + IsCountryGrop: true, + } + } else { + newGroup = model.ProxyGroup{ + Name: countryName, + Type: "url-test", + Proxies: []string{proxy.Name}, + IsCountryGrop: true, + Url: "http://www.gstatic.com/generate_204", + Interval: 300, + Tolerance: 50, + Lazy: lazy, + } } sub.ProxyGroups = append(sub.ProxyGroups, newGroup) newCountryGroupNames = append(newCountryGroupNames, countryName) diff --git a/utils/rule.go b/utils/rule.go index 7a1cbc9..028be53 100644 --- a/utils/rule.go +++ b/utils/rule.go @@ -2,53 +2,49 @@ package utils import ( "fmt" - "gopkg.in/yaml.v3" - "io" + "strings" "sub2clash/model" ) -func AddRulesByUrl(sub *model.Subscription, url string, proxy string) { - get, err := Get(url) - if err != nil { - fmt.Println(err) - return - } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - fmt.Println(err) - } - }(get.Body) - bytes, err := io.ReadAll(get.Body) - if err != nil { - fmt.Println(err) - return - } - var payload model.Payload - err = yaml.Unmarshal(bytes, &payload) - if err != nil { - fmt.Println(err) - return - } - for i := range payload.Rules { - payload.Rules[i] = payload.Rules[i] + "," + proxy - } - AddRules(sub, payload.Rules...) -} - -func AddRuleProvider( - sub *model.Subscription, providerName string, proxy string, provider model.RuleProvider, +func PrependRuleProvider( + sub *model.Subscription, providerName string, group string, provider model.RuleProvider, ) { if sub.RuleProviders == nil { sub.RuleProviders = make(map[string]model.RuleProvider) } sub.RuleProviders[providerName] = provider - AddRules( + PrependRules( sub, - fmt.Sprintf("RULE-SET,%s,%s", providerName, proxy), + fmt.Sprintf("RULE-SET,%s,%s", providerName, group), ) } -func AddRules(sub *model.Subscription, rules ...string) { +func AppenddRuleProvider( + sub *model.Subscription, providerName string, group string, provider model.RuleProvider, +) { + if sub.RuleProviders == nil { + sub.RuleProviders = make(map[string]model.RuleProvider) + } + sub.RuleProviders[providerName] = provider + AppendRules(sub, fmt.Sprintf("RULE-SET,%s,%s", providerName, group)) +} + +func PrependRules(sub *model.Subscription, rules ...string) { + if sub.Rules == nil { + sub.Rules = make([]string, 0) + } sub.Rules = append(rules, sub.Rules...) } + +func AppendRules(sub *model.Subscription, rules ...string) { + if sub.Rules == nil { + sub.Rules = make([]string, 0) + } + matchRule := sub.Rules[len(sub.Rules)-1] + if strings.Contains(matchRule, "MATCH") { + sub.Rules = append(sub.Rules[:len(sub.Rules)-1], rules...) + sub.Rules = append(sub.Rules, matchRule) + return + } + sub.Rules = append(sub.Rules, rules...) +} diff --git a/utils/sub.go b/utils/sub.go index ddec690..f4ef032 100644 --- a/utils/sub.go +++ b/utils/sub.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "sub2clash/config" "sync" "time" ) @@ -20,7 +21,6 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) { } hash := md5.Sum([]byte(url)) fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:])) - const refreshInterval = 500 * 60 // 5分钟 stat, err := os.Stat(fileName) if err != nil { if !os.IsNotExist(err) { @@ -29,16 +29,13 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) { return FetchSubscriptionFromAPI(url) } lastGetTime := stat.ModTime().Unix() // 单位是秒 - if lastGetTime+refreshInterval > time.Now().Unix() { + if lastGetTime+config.Default.CacheExpire > time.Now().Unix() { file, err := os.Open(fileName) if err != nil { return nil, err } defer func(file *os.File) { - err := file.Close() - if err != nil { - fmt.Println(err) - } + _ = file.Close() }(file) fileLock.RLock() defer fileLock.RUnlock() @@ -58,7 +55,9 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) data, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) @@ -68,10 +67,7 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) { return nil, err } defer func(file *os.File) { - err := file.Close() - if err != nil { - fmt.Println(err) - } + _ = file.Close() }(file) fileLock.Lock() defer fileLock.Unlock() diff --git a/utils/template.go b/utils/template.go index 20e53c0..cd90a45 100644 --- a/utils/template.go +++ b/utils/template.go @@ -2,7 +2,6 @@ package utils import ( "errors" - "fmt" "io" "os" "path/filepath" @@ -10,27 +9,24 @@ import ( // LoadTemplate 加载模板 // template 模板文件名 -func LoadTemplate(template string) (string, error) { +func LoadTemplate(template string) ([]byte, error) { tPath := filepath.Join("templates", template) if _, err := os.Stat(tPath); err == nil { file, err := os.Open(tPath) if err != nil { - return "", err + return nil, err } defer func(file *os.File) { - err := file.Close() - if err != nil { - fmt.Println(err) - } + _ = file.Close() }(file) result, err := io.ReadAll(file) if err != nil { - return "", err + return nil, err } if err != nil { - return "", err + return nil, err } - return string(result), nil + return result, nil } - return "", errors.New("模板文件不存在") + return nil, errors.New("模板文件不存在") } diff --git a/validator/sub.go b/validator/sub.go index 71d0219..c903dd5 100644 --- a/validator/sub.go +++ b/validator/sub.go @@ -1,7 +1,136 @@ package validator +import ( + "errors" + "github.com/gin-gonic/gin" + "net/url" + "regexp" + "strings" +) + type SubQuery struct { - Sub string `form:"sub" json:"name" binding:"required"` - Mix bool `form:"mix,default=false" json:"email" binding:""` - Refresh bool `form:"refresh,default=false" json:"age" binding:""` + Sub string `form:"sub" binding:""` + Subs []string `form:"-" binding:""` + Proxy string `form:"proxy" binding:""` + Proxies []string `form:"-" binding:""` + Refresh bool `form:"refresh,default=false" binding:""` + Template string `form:"template" binding:""` + RuleProvider string `form:"ruleProvider" binding:""` + RuleProviders []RuleProviderStruct `form:"-" binding:""` + Rule string `form:"rule" binding:""` + Rules []RuleStruct `form:"-" binding:""` + AutoTest bool `form:"autoTest,default=false" binding:""` + Lazy bool `form:"lazy,default=false" binding:""` +} + +type RuleProviderStruct struct { + Behavior string + Url string + Group string + Prepend bool +} + +type RuleStruct struct { + Rule string + Prepend bool +} + +func ParseQuery(c *gin.Context) (SubQuery, error) { + var query SubQuery + if err := c.ShouldBind(&query); err != nil { + return SubQuery{}, errors.New("参数错误: " + err.Error()) + } + if query.Sub == "" && query.Proxy == "" { + return SubQuery{}, errors.New("参数错误: sub 和 proxy 不能同时为空") + } + if query.Sub != "" { + query.Subs = strings.Split(query.Sub, ",") + for i := range query.Subs { + query.Subs[i], _ = url.QueryUnescape(query.Subs[i]) + if _, err := url.ParseRequestURI(query.Subs[i]); err != nil { + return SubQuery{}, errors.New("参数错误: " + err.Error()) + } + } + } else { + query.Subs = nil + } + if query.Proxy != "" { + query.Proxies = strings.Split(query.Proxy, ",") + for i := range query.Proxies { + query.Proxies[i], _ = url.QueryUnescape(query.Proxies[i]) + if _, err := url.ParseRequestURI(query.Proxies[i]); err != nil { + return SubQuery{}, errors.New("参数错误: " + err.Error()) + } + } + } else { + query.Proxies = nil + } + if query.Template != "" { + unescape, err := url.QueryUnescape(query.Template) + if err != nil { + return SubQuery{}, errors.New("参数错误: " + err.Error()) + } + uri, err := url.ParseRequestURI(unescape) + query.Template = uri.String() + if err != nil { + return SubQuery{}, errors.New("参数错误: " + err.Error()) + } + } + if query.RuleProvider != "" { + var err error + query.RuleProvider, err = url.QueryUnescape(query.RuleProvider) + if err != nil { + return SubQuery{}, errors.New("参数错误: " + err.Error()) + } + reg := regexp.MustCompile(`\[(.*?)\]`) + ruleProviders := reg.FindAllStringSubmatch(query.RuleProvider, -1) + for i := range ruleProviders { + length := len(ruleProviders) + parts := strings.Split(ruleProviders[length-i-1][1], ",") + if len(parts) != 4 { + return SubQuery{}, errors.New("参数错误: ruleProvider 格式错误") + } + u := parts[1] + u, err = url.QueryUnescape(u) + if err != nil { + return SubQuery{}, errors.New("参数错误: " + err.Error()) + } + uri, err := url.ParseRequestURI(u) + u = uri.String() + if err != nil { + return SubQuery{}, errors.New("参数错误: " + err.Error()) + } + query.RuleProviders = append( + query.RuleProviders, RuleProviderStruct{ + Behavior: parts[0], + Url: u, + Group: parts[2], + Prepend: parts[3] == "true", + }, + ) + } + } else { + query.RuleProviders = nil + } + if query.Rule != "" { + reg := regexp.MustCompile(`\[(.*?)\]`) + rules := reg.FindAllStringSubmatch(query.Rule, -1) + for i := range rules { + length := len(rules) + r := rules[length-1-i][1] + strings.LastIndex(r, ",") + parts := [2]string{} + parts[0] = r[:strings.LastIndex(r, ",")] + parts[1] = r[strings.LastIndex(r, ",")+1:] + query.Rules = append( + query.Rules, RuleStruct{ + Rule: parts[0], + Prepend: parts[1] == "true", + }, + ) + } + } else { + query.Rules = nil + } + return query, nil }