1
0
mirror of https://github.com/nitezs/sub2clash.git synced 2024-12-23 21:24:42 -05:00
This commit is contained in:
Nite07 2023-09-13 13:47:22 +08:00
parent 6e5e999937
commit 2c8e4f7b56
26 changed files with 508 additions and 196 deletions

View File

@ -2,4 +2,6 @@ PORT=8011
META_TEMPLATE=meta_template.json META_TEMPLATE=meta_template.json
CLASH_TEMPLATE=clash_template.json CLASH_TEMPLATE=clash_template.json
REQUEST_RETRY_TIMES=3 REQUEST_RETRY_TIMES=3
REQUEST_MAX_FILE_SIZE=1048576 REQUEST_MAX_FILE_SIZE=1048576
CACHE_EXPIRE=300
LOG_LEVEL=info

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
.idea .idea
dist dist
subs subs
test test
logs

View File

@ -19,26 +19,29 @@
## API ## API
### /clash ### `/clash`,`/meta`
获取 Clash 配置链接 获取 Clash/Clash.Meta 配置链接
| Query 参数 | 类型 | 说明 | | Query 参数 | 类型 | 是否必须 | 说明 |
|----------|--------|-------------------------| |--------------|--------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| sub | string | 订阅链接(可以输入多个订阅,用 `,` 分隔) | | sub | string | sub/proxy 至少有一项存在 | 订阅链接(可以输入多个,用 `,` 分隔) |
| refresh | bool | 强制刷新配置(默认缓存 5 分钟) | | 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 | 订阅链接(可以输入多个订阅,用 `,` 分隔) | [代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue
| refresh | bool | 强制刷新配置(默认缓存 5 分钟) |
## TODO ## TODO
- [ ] 完善日志功能
- [ ] 支持自动测速分组
- [ ] 完善配置模板

View File

@ -1,36 +1,30 @@
package controller package controller
import ( import (
"net/http"
"strings"
"sub2clash/config"
"sub2clash/validator"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"net/http"
"sub2clash/config"
"sub2clash/validator"
) )
func SubmodHandler(c *gin.Context) { func SubmodHandler(c *gin.Context) {
// 从请求中获取参数 // 从请求中获取参数
var query validator.SubQuery query, err := validator.ParseQuery(c)
if err := c.ShouldBind(&query); err != nil { if err != nil {
c.String(http.StatusBadRequest, "参数错误: "+err.Error()) c.String(http.StatusBadRequest, err.Error())
return return
} }
// 混合订阅和模板节点 sub, err := BuildSub(query, config.Default.ClashTemplate)
sub, err := MixinSubsAndTemplate(
strings.Split(query.Sub, ","), query.Refresh, config.Default.ClashTemplate,
)
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, err.Error()) c.String(http.StatusInternalServerError, err.Error())
return return
} }
// 添加自定义节点、规则
// 输出 // 输出
bytes, err := yaml.Marshal(sub) marshal, err := yaml.Marshal(sub)
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, err.Error()) c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return return
} }
c.String(http.StatusOK, string(bytes)) c.String(http.StatusOK, string(marshal))
} }

View File

@ -1,42 +1,47 @@
package controller package controller
import ( import (
"crypto/md5"
"encoding/hex"
"errors" "errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"net/url" "regexp"
"strings" "strings"
"sub2clash/model" "sub2clash/model"
"sub2clash/parser" "sub2clash/parser"
"sub2clash/utils" "sub2clash/utils"
"sub2clash/validator"
) )
func MixinSubsAndTemplate(subs []string, refresh bool, template string) ( func BuildSub(query validator.SubQuery, template string) (
*model.Subscription, error, *model.Subscription, error,
) { ) {
// 定义变量 // 定义变量
var externalTemplate = query.Template != ""
var temp *model.Subscription var temp *model.Subscription
var sub *model.Subscription var sub *model.Subscription
var err error
var templateBytes []byte
// 加载模板 // 加载模板
template, err := utils.LoadTemplate(template) if !externalTemplate {
if err != nil { templateBytes, err = utils.LoadTemplate(template)
return nil, errors.New("加载模板失败: " + err.Error()) 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 { if err != nil {
return nil, errors.New("解析模板失败: " + err.Error()) return nil, errors.New("解析模板失败: " + err.Error())
} }
var proxies []model.Proxy
// 加载订阅 // 加载订阅
for i := range subs { for i := range query.Subs {
subs[i], _ = url.QueryUnescape(subs[i]) data, err := utils.LoadSubscription(query.Subs[i], query.Refresh)
if _, err := url.ParseRequestURI(subs[i]); err != nil {
return nil, errors.New("订阅地址错误: " + err.Error())
}
data, err := utils.LoadSubscription(
subs[i],
refresh,
)
if err != nil { if err != nil {
return nil, errors.New("加载订阅失败: " + err.Error()) return nil, errors.New("加载订阅失败: " + err.Error())
} }
@ -44,18 +49,52 @@ func MixinSubsAndTemplate(subs []string, refresh bool, template string) (
var proxyList []model.Proxy var proxyList []model.Proxy
err = yaml.Unmarshal(data, &sub) err = yaml.Unmarshal(data, &sub)
if err != nil { if err != nil {
// 如果无法直接解析尝试Base64解码 reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|http|https)://")
base64, err := parser.DecodeBase64(string(data)) if reg.Match(data) {
if err != nil { proxyList = utils.ParseProxy(strings.Split(string(data), "\n")...)
return nil, errors.New("加载订阅失败: " + err.Error()) } 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 { } else {
proxyList = sub.Proxies 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 return temp, nil
} }

View File

@ -2,31 +2,25 @@ package controller
import ( import (
_ "embed" _ "embed"
"net/http"
"strings"
"sub2clash/config"
"sub2clash/validator"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"net/http"
"sub2clash/config"
"sub2clash/validator"
) )
func SubHandler(c *gin.Context) { func SubHandler(c *gin.Context) {
// 从请求中获取参数 // 从请求中获取参数
var query validator.SubQuery query, err := validator.ParseQuery(c)
if err := c.ShouldBind(&query); err != nil { if err != nil {
c.String(http.StatusBadRequest, "参数错误: "+err.Error()) c.String(http.StatusBadRequest, err.Error())
return return
} }
// 混合订阅和模板节点 sub, err := BuildSub(query, config.Default.MetaTemplate)
sub, err := MixinSubsAndTemplate(
strings.Split(query.Sub, ","), query.Refresh, config.Default.MetaTemplate,
)
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, err.Error()) c.String(http.StatusInternalServerError, err.Error())
return return
} }
// 添加自定义节点、规则
// 输出 // 输出
marshal, err := yaml.Marshal(sub) marshal, err := yaml.Marshal(sub)
if err != nil { if err != nil {

View File

@ -3,9 +3,11 @@ package api
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"sub2clash/api/controller" "sub2clash/api/controller"
"sub2clash/middleware"
) )
func SetRoute(r *gin.Engine) { func SetRoute(r *gin.Engine) {
r.Use(middleware.ZapLogger())
r.GET( r.GET(
"/clash", func(c *gin.Context) { "/clash", func(c *gin.Context) {
controller.SubmodHandler(c) controller.SubmodHandler(c)

View File

@ -1,7 +1,6 @@
package config package config
import ( import (
"fmt"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"os" "os"
"strconv" "strconv"
@ -13,6 +12,8 @@ type Config struct {
ClashTemplate string ClashTemplate string
RequestRetryTimes int RequestRetryTimes int
RequestMaxFileSize int64 RequestMaxFileSize int64
CacheExpire int64
LogLevel string
} }
var Default *Config var Default *Config
@ -24,6 +25,8 @@ func init() {
RequestRetryTimes: 3, RequestRetryTimes: 3,
RequestMaxFileSize: 1024 * 1024 * 1, RequestMaxFileSize: 1024 * 1024 * 1,
Port: 8011, Port: 8011,
CacheExpire: 60 * 5,
LogLevel: "info",
} }
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
@ -32,7 +35,7 @@ func init() {
if os.Getenv("PORT") != "" { if os.Getenv("PORT") != "" {
atoi, err := strconv.Atoi(os.Getenv("PORT")) atoi, err := strconv.Atoi(os.Getenv("PORT"))
if err != nil { if err != nil {
fmt.Println("PORT 不合法") panic("PORT invalid")
} }
Default.Port = atoi Default.Port = atoi
} }
@ -45,15 +48,25 @@ func init() {
if os.Getenv("REQUEST_RETRY_TIMES") != "" { if os.Getenv("REQUEST_RETRY_TIMES") != "" {
atoi, err := strconv.Atoi(os.Getenv("REQUEST_RETRY_TIMES")) atoi, err := strconv.Atoi(os.Getenv("REQUEST_RETRY_TIMES"))
if err != nil { if err != nil {
fmt.Println("REQUEST_RETRY_TIMES 不合法") panic("REQUEST_RETRY_TIMES invalid")
} }
Default.RequestRetryTimes = atoi Default.RequestRetryTimes = atoi
} }
if os.Getenv("REQUEST_MAX_FILE_SIZE") != "" { if os.Getenv("REQUEST_MAX_FILE_SIZE") != "" {
atoi, err := strconv.Atoi(os.Getenv("REQUEST_MAX_FILE_SIZE")) atoi, err := strconv.Atoi(os.Getenv("REQUEST_MAX_FILE_SIZE"))
if err != nil { if err != nil {
fmt.Println("REQUEST_MAX_FILE_SIZE 不合法") panic("REQUEST_MAX_FILE_SIZE invalid")
} }
Default.RequestMaxFileSize = int64(atoi) 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
View File

@ -3,7 +3,7 @@ module sub2clash
go 1.21 go 1.21
require ( 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/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // 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/gin-gonic/gin v1.9.1 // indirect
github.com/go-playground/locales v0.14.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/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/goccy/go-json v0.10.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // 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/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // 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/arch v0.5.0 // indirect
golang.org/x/crypto v0.13.0 // indirect golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect golang.org/x/net v0.15.0 // indirect

10
go.sum
View File

@ -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-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk= 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.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-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-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= 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/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 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.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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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= 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/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 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= 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/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 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 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.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 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=

72
logger/logger.go Normal file
View 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
View File

@ -2,14 +2,16 @@ package main
import ( import (
_ "embed" _ "embed"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap"
"io"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"sub2clash/api" "sub2clash/api"
"sub2clash/config" "sub2clash/config"
_ "sub2clash/config" "sub2clash/logger"
"sub2clash/utils"
) )
//go:embed templates/template_meta.yaml //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) { if _, err := os.Stat(tPath); os.IsNotExist(err) {
file, err := os.Create(tPath) file, err := os.Create(tPath)
if err != nil { if err != nil {
fmt.Println(err)
return err return err
} }
defer func(file *os.File) { defer func(file *os.File) {
err := file.Close() _ = file.Close()
if err != nil {
fmt.Println(err)
}
}(file) }(file)
_, err = file.WriteString(template) _, err = file.WriteString(template)
if err != nil { 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 return err
} }
} }
@ -55,10 +41,10 @@ func mkDir(dir string) error {
} }
func init() { func init() {
if err := mkDir("subs"); err != nil { if err := utils.MKDir("subs"); err != nil {
os.Exit(1) os.Exit(1)
} }
if err := mkDir("templates"); err != nil { if err := utils.MKDir("templates"); err != nil {
os.Exit(1) os.Exit(1)
} }
if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil { if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil {
@ -72,14 +58,16 @@ func init() {
func main() { func main() {
// 设置运行模式 // 设置运行模式
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
// 关闭 Gin 的日志输出
gin.DefaultWriter = io.Discard
// 创建路由 // 创建路由
r := gin.Default() r := gin.Default()
// 设置路由 // 设置路由
api.SetRoute(r) 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)) err := r.Run(":" + strconv.Itoa(config.Default.Port))
if err != nil { if err != nil {
fmt.Println(err) logger.Logger.Error("Server run error", zap.Error(err))
return return
} }
} }

39
middleware/logger.go Normal file
View 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)
}
}
}
}

View File

@ -3,9 +3,9 @@ package model
type Subscription struct { type Subscription struct {
Port int `yaml:"port,omitempty"` Port int `yaml:"port,omitempty"`
SocksPort int `yaml:"socks-port,omitempty"` SocksPort int `yaml:"socks-port,omitempty"`
AllowLan bool `yaml:"allow-lan,omitempty"` AllowLan bool `yaml:"allow-lan"`
Mode string `yaml:"mode,omitempty"` Mode string `yaml:"mode,omitempty"`
LogLevel string `yaml:"log-level,omitempty"` LogLevel string `yaml:"logger-level,omitempty"`
ExternalController string `yaml:"external-controller,omitempty"` ExternalController string `yaml:"external-controller,omitempty"`
Proxies []Proxy `yaml:"proxies,omitempty"` Proxies []Proxy `yaml:"proxies,omitempty"`
ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"` ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"`
@ -18,12 +18,16 @@ type ProxyGroup struct {
Type string `yaml:"type,omitempty"` Type string `yaml:"type,omitempty"`
Proxies []string `yaml:"proxies,omitempty"` Proxies []string `yaml:"proxies,omitempty"`
IsCountryGrop bool `yaml:"-"` 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 RuleProvider struct {
Type string `yaml:"type,omitempty"` Type string `yaml:"type,omitempty"`
Behavior string `yaml:"behavior,omitempty"` Behavior string `yaml:"behavior,omitempty"`
URL string `yaml:"url,omitempty"` Url string `yaml:"url,omitempty"`
Path string `yaml:"path,omitempty"` Path string `yaml:"path,omitempty"`
Interval int `yaml:"interval,omitempty"` Interval int `yaml:"interval,omitempty"`
} }

View File

@ -8,16 +8,16 @@ import (
"sub2clash/model" "sub2clash/model"
) )
// ParseSS 解析 SSShadowsocksURL // ParseSS 解析 SSShadowsocksUrl
func ParseSS(proxy string) (model.Proxy, error) { func ParseSS(proxy string) (model.Proxy, error) {
// 判断是否以 ss:// 开头 // 判断是否以 ss:// 开头
if !strings.HasPrefix(proxy, "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) parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2)
if len(parts) != 2 { if len(parts) != 2 {
return model.Proxy{}, fmt.Errorf("无效的 ss URL") return model.Proxy{}, fmt.Errorf("无效的 ss Url")
} }
if !strings.Contains(parts[0], ":") { if !strings.Contains(parts[0], ":") {
// 解码 // 解码

View File

@ -11,7 +11,7 @@ import (
func ParseShadowsocksR(proxy string) (model.Proxy, error) { func ParseShadowsocksR(proxy string) (model.Proxy, error) {
// 判断是否以 ssr:// 开头 // 判断是否以 ssr:// 开头
if !strings.HasPrefix(proxy, "ssr://") { if !strings.HasPrefix(proxy, "ssr://") {
return model.Proxy{}, fmt.Errorf("无效的 ssr URL") return model.Proxy{}, fmt.Errorf("无效的 ssr Url")
} }
var err error var err error
if !strings.Contains(proxy, ":") { if !strings.Contains(proxy, ":") {

View File

@ -11,12 +11,12 @@ import (
func ParseTrojan(proxy string) (model.Proxy, error) { func ParseTrojan(proxy string) (model.Proxy, error) {
// 判断是否以 trojan:// 开头 // 判断是否以 trojan:// 开头
if !strings.HasPrefix(proxy, "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) parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2)
if len(parts) != 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) serverInfo := strings.SplitN(parts[1], "#", 2)

View File

@ -11,12 +11,12 @@ import (
func ParseVless(proxy string) (model.Proxy, error) { func ParseVless(proxy string) (model.Proxy, error) {
// 判断是否以 vless:// 开头 // 判断是否以 vless:// 开头
if !strings.HasPrefix(proxy, "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) parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2)
if len(parts) != 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) serverInfo := strings.SplitN(parts[1], "#", 2)
@ -46,12 +46,14 @@ func ParseVless(proxy string) (model.Proxy, error) {
TLS: params.Get("security") == "tls", TLS: params.Get("security") == "tls",
Flow: params.Get("flow"), Flow: params.Get("flow"),
Fingerprint: params.Get("fp"), Fingerprint: params.Get("fp"),
Alpn: strings.Split(params.Get("alpn"), ","),
Servername: params.Get("sni"), Servername: params.Get("sni"),
RealityOpts: model.RealityOptsStruct{ RealityOpts: model.RealityOptsStruct{
PublicKey: params.Get("pbk"), PublicKey: params.Get("pbk"),
}, },
} }
if params.Get("alpn") != "" {
result.Alpn = strings.Split(params.Get("alpn"), ",")
}
if params.Get("type") == "ws" { if params.Get("type") == "ws" {
result.WSOpts = model.WSOptsStruct{ result.WSOpts = model.WSOptsStruct{
Path: params.Get("path"), Path: params.Get("path"),

View File

@ -12,23 +12,23 @@ import (
func ParseVmess(proxy string) (model.Proxy, error) { func ParseVmess(proxy string) (model.Proxy, error) {
// 判断是否以 vmess:// 开头 // 判断是否以 vmess:// 开头
if !strings.HasPrefix(proxy, "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://")) base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://"))
if err != nil { if err != nil {
return model.Proxy{}, errors.New("无效的 vmess URL") return model.Proxy{}, errors.New("无效的 vmess Url")
} }
// 解析 // 解析
var vmess model.Vmess var vmess model.Vmess
err = json.Unmarshal([]byte(base64), &vmess) err = json.Unmarshal([]byte(base64), &vmess)
if err != nil { 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)) port, err := strconv.Atoi(strings.TrimSpace(vmess.Port))
if err != nil { if err != nil {
return model.Proxy{}, errors.New("无效的 vmess URL") return model.Proxy{}, errors.New("无效的 vmess Url")
} }
if vmess.Scy == "" { if vmess.Scy == "" {
vmess.Scy = "auto" vmess.Scy = "auto"

View File

@ -999,8 +999,8 @@ rules:
- DOMAIN-SUFFIX,itsdata.map.baidu.com,应用净化 - DOMAIN-SUFFIX,itsdata.map.baidu.com,应用净化
- DOMAIN-SUFFIX,j.br.baidu.com,应用净化 - DOMAIN-SUFFIX,j.br.baidu.com,应用净化
- DOMAIN-SUFFIX,kstj.baidu.com,应用净化 - DOMAIN-SUFFIX,kstj.baidu.com,应用净化
- DOMAIN-SUFFIX,log.music.baidu.com,应用净化 - DOMAIN-SUFFIX,logger.music.baidu.com,应用净化
- DOMAIN-SUFFIX,log.nuomi.com,应用净化 - DOMAIN-SUFFIX,logger.nuomi.com,应用净化
- DOMAIN-SUFFIX,m1.baidu.com,应用净化 - DOMAIN-SUFFIX,m1.baidu.com,应用净化
- DOMAIN-SUFFIX,ma.baidu.cn,应用净化 - DOMAIN-SUFFIX,ma.baidu.cn,应用净化
- DOMAIN-SUFFIX,ma.baidu.com,应用净化 - DOMAIN-SUFFIX,ma.baidu.com,应用净化
@ -1130,7 +1130,7 @@ rules:
- DOMAIN-SUFFIX,ad.toutiao.com,应用净化 - DOMAIN-SUFFIX,ad.toutiao.com,应用净化
- DOMAIN-SUFFIX,dsp.toutiao.com,应用净化 - DOMAIN-SUFFIX,dsp.toutiao.com,应用净化
- DOMAIN-SUFFIX,ic.snssdk.com,应用净化 - DOMAIN-SUFFIX,ic.snssdk.com,应用净化
- DOMAIN-SUFFIX,log.snssdk.com,应用净化 - DOMAIN-SUFFIX,logger.snssdk.com,应用净化
- DOMAIN-SUFFIX,nativeapp.toutiao.com,应用净化 - DOMAIN-SUFFIX,nativeapp.toutiao.com,应用净化
- DOMAIN-SUFFIX,pangolin-sdk-toutiao-b.com,应用净化 - DOMAIN-SUFFIX,pangolin-sdk-toutiao-b.com,应用净化
- DOMAIN-SUFFIX,pangolin-sdk-toutiao.com,应用净化 - DOMAIN-SUFFIX,pangolin-sdk-toutiao.com,应用净化
@ -1192,8 +1192,8 @@ rules:
- DOMAIN-SUFFIX,install2.kugou.com,应用净化 - DOMAIN-SUFFIX,install2.kugou.com,应用净化
- DOMAIN-SUFFIX,kgmobilestat.kugou.com,应用净化 - DOMAIN-SUFFIX,kgmobilestat.kugou.com,应用净化
- DOMAIN-SUFFIX,kuaikaiapp.com,应用净化 - DOMAIN-SUFFIX,kuaikaiapp.com,应用净化
- DOMAIN-SUFFIX,log.stat.kugou.com,应用净化 - DOMAIN-SUFFIX,logger.stat.kugou.com,应用净化
- DOMAIN-SUFFIX,log.web.kugou.com,应用净化 - DOMAIN-SUFFIX,logger.web.kugou.com,应用净化
- DOMAIN-SUFFIX,minidcsc.kugou.com,应用净化 - DOMAIN-SUFFIX,minidcsc.kugou.com,应用净化
- DOMAIN-SUFFIX,mo.kugou.com,应用净化 - DOMAIN-SUFFIX,mo.kugou.com,应用净化
- DOMAIN-SUFFIX,mobilelog.kugou.com,应用净化 - DOMAIN-SUFFIX,mobilelog.kugou.com,应用净化
@ -1210,7 +1210,7 @@ rules:
- DOMAIN-SUFFIX,g.koowo.com,应用净化 - DOMAIN-SUFFIX,g.koowo.com,应用净化
- DOMAIN-SUFFIX,g.kuwo.cn,应用净化 - DOMAIN-SUFFIX,g.kuwo.cn,应用净化
- DOMAIN-SUFFIX,kwmsg.kuwo.cn,应用净化 - DOMAIN-SUFFIX,kwmsg.kuwo.cn,应用净化
- DOMAIN-SUFFIX,log.kuwo.cn,应用净化 - DOMAIN-SUFFIX,logger.kuwo.cn,应用净化
- DOMAIN-SUFFIX,mobilead.kuwo.cn,应用净化 - DOMAIN-SUFFIX,mobilead.kuwo.cn,应用净化
- DOMAIN-SUFFIX,msclick2.kuwo.cn,应用净化 - DOMAIN-SUFFIX,msclick2.kuwo.cn,应用净化
- DOMAIN-SUFFIX,msphoneclick.kuwo.cn,应用净化 - DOMAIN-SUFFIX,msphoneclick.kuwo.cn,应用净化
@ -1279,7 +1279,7 @@ rules:
- DOMAIN-SUFFIX,cdn.moji002.com,应用净化 - DOMAIN-SUFFIX,cdn.moji002.com,应用净化
- DOMAIN-SUFFIX,cdn2.moji002.com,应用净化 - DOMAIN-SUFFIX,cdn2.moji002.com,应用净化
- DOMAIN-SUFFIX,fds.api.moji.com,应用净化 - DOMAIN-SUFFIX,fds.api.moji.com,应用净化
- DOMAIN-SUFFIX,log.moji.com,应用净化 - DOMAIN-SUFFIX,logger.moji.com,应用净化
- DOMAIN-SUFFIX,stat.moji.com,应用净化 - DOMAIN-SUFFIX,stat.moji.com,应用净化
- DOMAIN-SUFFIX,ugc.moji001.com,应用净化 - DOMAIN-SUFFIX,ugc.moji001.com,应用净化
- DOMAIN-SUFFIX,ad.qingting.fm,应用净化 - DOMAIN-SUFFIX,ad.qingting.fm,应用净化
@ -1331,7 +1331,7 @@ rules:
- DOMAIN-SUFFIX,game.weibo.com.cn,应用净化 - DOMAIN-SUFFIX,game.weibo.com.cn,应用净化
- DOMAIN-SUFFIX,gw5.push.mcp.weibo.cn,应用净化 - DOMAIN-SUFFIX,gw5.push.mcp.weibo.cn,应用净化
- DOMAIN-SUFFIX,leju.sina.com.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,mobileads.dx.cn,应用净化
- DOMAIN-SUFFIX,newspush.sinajs.cn,应用净化 - DOMAIN-SUFFIX,newspush.sinajs.cn,应用净化
- DOMAIN-SUFFIX,pay.mobile.sina.cn,应用净化 - DOMAIN-SUFFIX,pay.mobile.sina.cn,应用净化
@ -1391,7 +1391,7 @@ rules:
- DOMAIN-SUFFIX,cms.ucweb.com,应用净化 - DOMAIN-SUFFIX,cms.ucweb.com,应用净化
- DOMAIN-SUFFIX,dispatcher.upmc.uc.cn,应用净化 - DOMAIN-SUFFIX,dispatcher.upmc.uc.cn,应用净化
- DOMAIN-SUFFIX,huichuan.sm.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,m.uczzd.cn,应用净化
- DOMAIN-SUFFIX,patriot.cs.pp.cn,应用净化 - DOMAIN-SUFFIX,patriot.cs.pp.cn,应用净化
- DOMAIN-SUFFIX,puds.ucweb.com,应用净化 - DOMAIN-SUFFIX,puds.ucweb.com,应用净化
@ -1531,8 +1531,8 @@ rules:
- DOMAIN-SUFFIX,click.hunantv.com,应用净化 - DOMAIN-SUFFIX,click.hunantv.com,应用净化
- DOMAIN-SUFFIX,da.hunantv.com,应用净化 - DOMAIN-SUFFIX,da.hunantv.com,应用净化
- DOMAIN-SUFFIX,da.mgtv.com,应用净化 - DOMAIN-SUFFIX,da.mgtv.com,应用净化
- DOMAIN-SUFFIX,log.hunantv.com,应用净化 - DOMAIN-SUFFIX,logger.hunantv.com,应用净化
- DOMAIN-SUFFIX,log.v2.hunantv.com,应用净化 - DOMAIN-SUFFIX,logger.v2.hunantv.com,应用净化
- DOMAIN-SUFFIX,p2.hunantv.com,应用净化 - DOMAIN-SUFFIX,p2.hunantv.com,应用净化
- DOMAIN-SUFFIX,res.hunantv.com,应用净化 - DOMAIN-SUFFIX,res.hunantv.com,应用净化
- DOMAIN-SUFFIX,888.tv.sohu.com,应用净化 - DOMAIN-SUFFIX,888.tv.sohu.com,应用净化
@ -1616,10 +1616,10 @@ rules:
- DOMAIN-SUFFIX,msg.youku.com,应用净化 - DOMAIN-SUFFIX,msg.youku.com,应用净化
- DOMAIN-SUFFIX,myes.youku.com,应用净化 - DOMAIN-SUFFIX,myes.youku.com,应用净化
- DOMAIN-SUFFIX,nstat.tudou.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.ykimg.com,应用净化
- DOMAIN-SUFFIX,p.l.youku.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,push.m.youku.com,应用净化
- DOMAIN-SUFFIX,r.l.youku.com,应用净化 - DOMAIN-SUFFIX,r.l.youku.com,应用净化
- DOMAIN-SUFFIX,s.p.youku.com,应用净化 - DOMAIN-SUFFIX,s.p.youku.com,应用净化
@ -1746,7 +1746,7 @@ rules:
- DOMAIN-SUFFIX,iadsdk.apple.com,应用净化 - DOMAIN-SUFFIX,iadsdk.apple.com,应用净化
- DOMAIN-SUFFIX,image.gentags.com,应用净化 - DOMAIN-SUFFIX,image.gentags.com,应用净化
- DOMAIN-SUFFIX,its-dori.tumblr.com,应用净化 - DOMAIN-SUFFIX,its-dori.tumblr.com,应用净化
- DOMAIN-SUFFIX,log.outbrain.com,应用净化 - DOMAIN-SUFFIX,logger.outbrain.com,应用净化
- DOMAIN-SUFFIX,m.12306media.com,应用净化 - DOMAIN-SUFFIX,m.12306media.com,应用净化
- DOMAIN-SUFFIX,media.cheshi-img.com,应用净化 - DOMAIN-SUFFIX,media.cheshi-img.com,应用净化
- DOMAIN-SUFFIX,media.cheshi.com,应用净化 - DOMAIN-SUFFIX,media.cheshi.com,应用净化

16
utils/os.go Normal file
View 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
}

View File

@ -34,7 +34,7 @@ var skipGroups = map[string]bool{
"应用进化": true, "应用进化": 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) newCountryGroupNames := make([]string, 0)
for _, proxy := range proxies { for _, proxy := range proxies {
@ -57,11 +57,25 @@ func AddProxy(sub *model.Subscription, proxies ...model.Proxy) {
} }
if !haveProxyGroup { if !haveProxyGroup {
newGroup := model.ProxyGroup{ var newGroup model.ProxyGroup
Name: countryName, if !autotest {
Type: "select", newGroup = model.ProxyGroup{
Proxies: []string{proxy.Name}, Name: countryName,
IsCountryGrop: true, 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) sub.ProxyGroups = append(sub.ProxyGroups, newGroup)
newCountryGroupNames = append(newCountryGroupNames, countryName) newCountryGroupNames = append(newCountryGroupNames, countryName)

View File

@ -2,53 +2,49 @@ package utils
import ( import (
"fmt" "fmt"
"gopkg.in/yaml.v3" "strings"
"io"
"sub2clash/model" "sub2clash/model"
) )
func AddRulesByUrl(sub *model.Subscription, url string, proxy string) { func PrependRuleProvider(
get, err := Get(url) sub *model.Subscription, providerName string, group string, provider model.RuleProvider,
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,
) { ) {
if sub.RuleProviders == nil { if sub.RuleProviders == nil {
sub.RuleProviders = make(map[string]model.RuleProvider) sub.RuleProviders = make(map[string]model.RuleProvider)
} }
sub.RuleProviders[providerName] = provider sub.RuleProviders[providerName] = provider
AddRules( PrependRules(
sub, 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...) 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...)
}

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"sub2clash/config"
"sync" "sync"
"time" "time"
) )
@ -20,7 +21,6 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) {
} }
hash := md5.Sum([]byte(url)) hash := md5.Sum([]byte(url))
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:])) fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
const refreshInterval = 500 * 60 // 5分钟
stat, err := os.Stat(fileName) stat, err := os.Stat(fileName)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
@ -29,16 +29,13 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) {
return FetchSubscriptionFromAPI(url) return FetchSubscriptionFromAPI(url)
} }
lastGetTime := stat.ModTime().Unix() // 单位是秒 lastGetTime := stat.ModTime().Unix() // 单位是秒
if lastGetTime+refreshInterval > time.Now().Unix() { if lastGetTime+config.Default.CacheExpire > time.Now().Unix() {
file, err := os.Open(fileName) file, err := os.Open(fileName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(file *os.File) { defer func(file *os.File) {
err := file.Close() _ = file.Close()
if err != nil {
fmt.Println(err)
}
}(file) }(file)
fileLock.RLock() fileLock.RLock()
defer fileLock.RUnlock() defer fileLock.RUnlock()
@ -58,7 +55,9 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err) return nil, fmt.Errorf("failed to read response body: %w", err)
@ -68,10 +67,7 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
return nil, err return nil, err
} }
defer func(file *os.File) { defer func(file *os.File) {
err := file.Close() _ = file.Close()
if err != nil {
fmt.Println(err)
}
}(file) }(file)
fileLock.Lock() fileLock.Lock()
defer fileLock.Unlock() defer fileLock.Unlock()

View File

@ -2,7 +2,6 @@ package utils
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@ -10,27 +9,24 @@ import (
// LoadTemplate 加载模板 // LoadTemplate 加载模板
// template 模板文件名 // template 模板文件名
func LoadTemplate(template string) (string, error) { func LoadTemplate(template string) ([]byte, error) {
tPath := filepath.Join("templates", template) tPath := filepath.Join("templates", template)
if _, err := os.Stat(tPath); err == nil { if _, err := os.Stat(tPath); err == nil {
file, err := os.Open(tPath) file, err := os.Open(tPath)
if err != nil { if err != nil {
return "", err return nil, err
} }
defer func(file *os.File) { defer func(file *os.File) {
err := file.Close() _ = file.Close()
if err != nil {
fmt.Println(err)
}
}(file) }(file)
result, err := io.ReadAll(file) result, err := io.ReadAll(file)
if err != nil { if err != nil {
return "", err return nil, err
} }
if err != nil { if err != nil {
return "", err return nil, err
} }
return string(result), nil return result, nil
} }
return "", errors.New("模板文件不存在") return nil, errors.New("模板文件不存在")
} }

View File

@ -1,7 +1,136 @@
package validator package validator
import (
"errors"
"github.com/gin-gonic/gin"
"net/url"
"regexp"
"strings"
)
type SubQuery struct { type SubQuery struct {
Sub string `form:"sub" json:"name" binding:"required"` Sub string `form:"sub" binding:""`
Mix bool `form:"mix,default=false" json:"email" binding:""` Subs []string `form:"-" binding:""`
Refresh bool `form:"refresh,default=false" json:"age" 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
} }