mirror of
https://github.com/nitezs/sub2clash.git
synced 2024-12-23 20:34:41 -05:00
update
This commit is contained in:
parent
6e5e999937
commit
2c8e4f7b56
@ -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
|
||||
REQUEST_MAX_FILE_SIZE=1048576
|
||||
CACHE_EXPIRE=300
|
||||
LOG_LEVEL=info
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
.idea
|
||||
dist
|
||||
subs
|
||||
test
|
||||
test
|
||||
logs
|
33
README.md
33
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
|
||||
|
||||
- [ ] 完善日志功能
|
||||
- [ ] 支持自动测速分组
|
||||
- [ ] 完善配置模板
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
6
go.mod
6
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
|
||||
|
10
go.sum
10
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=
|
||||
|
72
logger/logger.go
Normal file
72
logger/logger.go
Normal file
@ -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()
|
||||
}
|
||||
}
|
34
main.go
34
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
|
||||
}
|
||||
}
|
||||
|
39
middleware/logger.go
Normal file
39
middleware/logger.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
model/sub.go
10
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"`
|
||||
}
|
||||
|
@ -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], ":") {
|
||||
// 解码
|
||||
|
@ -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, ":") {
|
||||
|
@ -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)
|
||||
|
@ -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"),
|
||||
|
@ -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"
|
||||
|
@ -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,应用净化
|
||||
|
16
utils/os.go
Normal file
16
utils/os.go
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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...)
|
||||
}
|
||||
|
18
utils/sub.go
18
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()
|
||||
|
@ -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("模板文件不存在")
|
||||
}
|
||||
|
135
validator/sub.go
135
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user