mirror of
https://github.com/nitezs/sub2clash.git
synced 2024-12-24 11:24:41 -05:00
update
This commit is contained in:
parent
6e5e999937
commit
2c8e4f7b56
@ -3,3 +3,5 @@ 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
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
dist
|
dist
|
||||||
subs
|
subs
|
||||||
test
|
test
|
||||||
|
logs
|
33
README.md
33
README.md
@ -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
|
||||||
|
|
||||||
- [ ] 完善日志功能
|
|
||||||
- [ ] 支持自动测速分组
|
|
||||||
- [ ] 完善配置模板
|
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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
6
go.mod
@ -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
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-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
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 (
|
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
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 {
|
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"`
|
||||||
}
|
}
|
||||||
|
@ -8,16 +8,16 @@ import (
|
|||||||
"sub2clash/model"
|
"sub2clash/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseSS 解析 SS(Shadowsocks)URL
|
// ParseSS 解析 SS(Shadowsocks)Url
|
||||||
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], ":") {
|
||||||
// 解码
|
// 解码
|
||||||
|
@ -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, ":") {
|
||||||
|
@ -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)
|
||||||
|
@ -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"),
|
||||||
|
@ -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"
|
||||||
|
@ -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
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,
|
"应用进化": 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)
|
||||||
|
@ -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...)
|
||||||
|
}
|
||||||
|
18
utils/sub.go
18
utils/sub.go
@ -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()
|
||||||
|
@ -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("模板文件不存在")
|
||||||
}
|
}
|
||||||
|
135
validator/sub.go
135
validator/sub.go
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user