1
0
mirror of https://github.com/nitezs/sub2clash.git synced 2024-12-23 21:14:43 -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

@ -3,3 +3,5 @@ META_TEMPLATE=meta_template.json
CLASH_TEMPLATE=clash_template.json
REQUEST_RETRY_TIMES=3
REQUEST_MAX_FILE_SIZE=1048576
CACHE_EXPIRE=300
LOG_LEVEL=info

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
dist
subs
test
logs

View File

@ -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
- [ ] 完善日志功能
- [ ] 支持自动测速分组
- [ ] 完善配置模板

View File

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

View File

@ -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 !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 {
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")...)
}
} 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
}

View File

@ -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 {

View File

@ -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)

View File

@ -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
View File

@ -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
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 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
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 (
_ "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
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 {
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"`
}

View File

@ -8,16 +8,16 @@ import (
"sub2clash/model"
)
// ParseSS 解析 SSShadowsocksURL
// ParseSS 解析 SSShadowsocksUrl
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], ":") {
// 解码

View File

@ -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, ":") {

View File

@ -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)

View File

@ -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"),

View File

@ -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"

View File

@ -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
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,
}
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,12 +57,26 @@ func AddProxy(sub *model.Subscription, proxies ...model.Proxy) {
}
if !haveProxyGroup {
newGroup := model.ProxyGroup{
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)
}

View File

@ -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...)
}

View File

@ -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()

View File

@ -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("模板文件不存在")
}

View File

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