1
0
mirror of https://github.com/nitezs/sub2clash.git synced 2024-12-24 13:14:42 -05:00

Merge pull request #1 from nitezs/dev

Dev
This commit is contained in:
Nite07 2023-09-15 10:39:44 +08:00 committed by GitHub
commit 300bb4931d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 215 additions and 89 deletions

View File

@ -2,6 +2,8 @@ name: Build and Push to GHCR
on: on:
push: push:
branches:
- dev
tags: tags:
- '*' - '*'
workflow_dispatch: workflow_dispatch:
@ -11,21 +13,51 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image - name: Set tag name
uses: docker/build-push-action@v2 id: set_tag
with: run: |
context: . if [[ $GITHUB_REF == refs/heads/* ]]; then
file: ./Dockerfile echo "::set-output name=tag::$(echo $GITHUB_REF | cut -d'/' -f3)"
push: true else
tags: ghcr.io/${{ github.repository }}:latest echo "::set-output name=tag::${{ github.ref_name }}"
fi
- name: Check if triggered by tag
id: check_tag
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "::set-output name=triggered_by_tag::true"
else
echo "::set-output name=triggered_by_tag::false"
fi
- name: Build and push Docker image for dev branch
if: steps.check_tag.outputs.triggered_by_tag == 'false'
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
tags: ghcr.io/${{ github.repository }}:${{ steps.set_tag.outputs.tag }}
- name: Build and push Docker image for tags
if: steps.check_tag.outputs.triggered_by_tag == 'true'
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ steps.set_tag.outputs.tag }}
ghcr.io/${{ github.repository }}:latest

View File

@ -14,6 +14,9 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o sub2clash
FROM alpine:latest FROM alpine:latest
# 设置工作目录
WORKDIR /app
# 从 builder 镜像中复制出编译好的二进制文件 # 从 builder 镜像中复制出编译好的二进制文件
COPY --from=builder /app/sub2clash /app/sub2clash COPY --from=builder /app/sub2clash /app/sub2clash

View File

@ -41,16 +41,17 @@
获取 Clash/Clash.Meta 配置链接 获取 Clash/Clash.Meta 配置链接
| Query 参数 | 类型 | 是否必须 | 说明 | | Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|--------------|--------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------|--------|-------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| sub | string | sub/proxy 至少有一项存在 | 订阅链接(可以输入多个,用 `,` 分隔) | | sub | string | sub/proxy 至少有一项存在 | - | 订阅链接(可以输入多个,用 `,` 分隔) |
| proxy | string | sub/proxy 至少有一项存在 | 节点分享链接(可以输入多个,用 `,` 分隔) | | proxy | string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个,用 `,` 分隔) |
| refresh | bool | 否(默认 `false` | 强制刷新配置(默认缓存 5 分钟) | | refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) |
| template | string | 否 | 外部模板 | | template | string | 否 | - | 外部模板链接或内部模板名称 |
| ruleProvider | string | 否 | 格式 `[Behavior,Url,Group,Prepend],[Behavior,Url,Group,Prepend],...`,其中 `Group` 是该规则集所走的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部否则添加到规则列表底部会调整到MATCH规则之前 | | ruleProvider | string | 否 | - | 格式 `[Behavior,Url,Group,Prepend,Name],[Behavior,Url,Group,Prepend,Name]...`,其中 `Group` 是该规则集所走的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部否则添加到规则列表底部会调整到MATCH规则之前 |
| rule | string | 否 | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部否则添加到规则列表底部会调整到MATCH规则之前 | | rule | string | 否 | - | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部否则添加到规则列表底部会调整到MATCH规则之前 |
| autoTest | bool | 否(默认 `false` | 指定国家策略组是否自动测速 | | autoTest | bool | 否 | `false` | 指定国家策略组是否自动测速 |
| lazy | bool | 否(默认 `false` | 自动测速是否启用 lazy | | lazy | bool | 否 | `false` | 自动测速是否启用 lazy |
| sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc` |
## 默认模板 ## 默认模板
@ -63,3 +64,4 @@
## TODO ## TODO
- [ ] 可视化面板

View File

@ -5,6 +5,7 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"net/url"
"regexp" "regexp"
"strings" "strings"
"sub2clash/model" "sub2clash/model"
@ -17,13 +18,16 @@ 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 err error
var templateBytes []byte var templateBytes []byte
// 加载模板 // 加载模板
if !externalTemplate { if query.Template != "" {
template = query.Template
}
_, err = url.ParseRequestURI(template)
if err != nil {
templateBytes, err = utils.LoadTemplate(template) templateBytes, err = utils.LoadTemplate(template)
if err != nil { if err != nil {
return nil, errors.New("加载模板失败: " + err.Error()) return nil, errors.New("加载模板失败: " + err.Error())
@ -63,10 +67,14 @@ func BuildSub(query validator.SubQuery, template string) (
} else { } else {
proxyList = sub.Proxies proxyList = sub.Proxies
} }
utils.AddProxy(temp, query.AutoTest, query.Lazy, proxyList...) utils.AddProxy(sub, query.AutoTest, query.Lazy, query.Sort, proxyList...)
} }
// 处理自定义代理 // 处理自定义代理
utils.AddProxy(temp, query.AutoTest, query.Lazy, utils.ParseProxy(query.Proxies...)...) utils.AddProxy(
sub, query.AutoTest, query.Lazy, query.Sort,
utils.ParseProxy(query.Proxies...)...,
)
MergeSubAndTemplate(temp, sub)
// 处理自定义规则 // 处理自定义规则
for _, v := range query.Rules { for _, v := range query.Rules {
if v.Prepend { if v.Prepend {
@ -88,13 +96,50 @@ func BuildSub(query validator.SubQuery, template string) (
} }
if v.Prepend { if v.Prepend {
utils.PrependRuleProvider( utils.PrependRuleProvider(
temp, name, v.Group, provider, temp, v.Name, v.Group, provider,
) )
} else { } else {
utils.AppenddRuleProvider( utils.AppenddRuleProvider(
temp, name, v.Group, provider, temp, v.Name, v.Group, provider,
) )
} }
} }
return temp, nil return temp, nil
} }
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
// 只合并节点、策略组
// 统计所有国家策略组名称
var countryGroupNames []string
for _, proxyGroup := range sub.ProxyGroups {
if proxyGroup.IsCountryGrop {
countryGroupNames = append(
countryGroupNames, proxyGroup.Name,
)
}
}
// 将订阅中的节点添加到模板中
temp.Proxies = append(temp.Proxies, sub.Proxies...)
// 将订阅中的策略组添加到模板中
skipGroups := []string{"全球直连", "广告拦截", "手动切换"}
for i := range temp.ProxyGroups {
skip := false
for _, v := range skipGroups {
if strings.Contains(temp.ProxyGroups[i].Name, v) {
if v == "手动切换" {
proxies := make([]string, 0, len(sub.Proxies))
for _, p := range sub.Proxies {
proxies = append(proxies, p.Name)
}
temp.ProxyGroups[i].Proxies = proxies
}
skip = true
continue
}
}
if !skip {
temp.ProxyGroups[i].Proxies = append(temp.ProxyGroups[i].Proxies, countryGroupNames...)
}
}
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
}

58
model/proxy_group.go Normal file
View File

@ -0,0 +1,58 @@
package model
import (
"golang.org/x/text/collate"
"golang.org/x/text/language"
)
type ProxyGroup struct {
Name string `yaml:"name,omitempty"`
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"`
Size int `yaml:"-"`
}
type ProxyGroupsSortByName []ProxyGroup
type ProxyGroupsSortBySize []ProxyGroup
func (p ProxyGroupsSortByName) Len() int {
return len(p)
}
func (p ProxyGroupsSortBySize) Len() int {
return len(p)
}
func (p ProxyGroupsSortByName) Less(i, j int) bool {
// 定义一组备选语言:首选英语,其次中文
tags := []language.Tag{
language.English,
language.Chinese,
}
matcher := language.NewMatcher(tags)
// 假设我们的请求语言是 "zh"(中文),则使用匹配器找到最佳匹配的语言
bestMatch, _, _ := matcher.Match(language.Make("zh"))
// 使用最佳匹配的语言进行排序
c := collate.New(bestMatch)
return c.CompareString(p[i].Name, p[j].Name) < 0
}
func (p ProxyGroupsSortBySize) Less(i, j int) bool {
if p[i].Size == p[j].Size {
return p[i].Name < p[j].Name
}
return p[i].Size < p[j].Size
}
func (p ProxyGroupsSortByName) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
func (p ProxyGroupsSortBySize) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}

View File

@ -13,17 +13,6 @@ type Subscription struct {
RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"` RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"`
} }
type ProxyGroup struct {
Name string `yaml:"name,omitempty"`
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 RuleProvider struct {
Type string `yaml:"type,omitempty"` Type string `yaml:"type,omitempty"`
Behavior string `yaml:"behavior,omitempty"` Behavior string `yaml:"behavior,omitempty"`

View File

@ -50,13 +50,12 @@ proxy-groups:
rules: rules:
- GEOSITE,private,全球直连 - GEOSITE,private,全球直连
- GEOIP,private,全球直连 - GEOIP,private,全球直连
- GEOSITE,category-ads-all,广告拦截
- GEOSITE,CN,全球直连
- GEOIP,CN,全球直连
- GEOSITE,biliintl,哔哩哔哩 - GEOSITE,biliintl,哔哩哔哩
- GEOSITE,bilibili,哔哩哔哩 - GEOSITE,bilibili,哔哩哔哩
- GEOSITE,bahamut,巴哈姆特 - GEOSITE,bahamut,巴哈姆特
- GEOSITE,CN,全球直连
- GEOIP,CN,全球直连
- GEOSITE,category-games,游戏平台 - GEOSITE,category-games,游戏平台
- GEOSITE,geolocation-!cn,节点选择 - GEOSITE,geolocation-!cn,节点选择
- GEOIP,ad,广告拦截
- GEOSITE,category-ads-all,广告拦截
- MATCH,漏网之鱼 - MATCH,漏网之鱼

View File

@ -1,6 +1,7 @@
package utils package utils
import ( import (
"sort"
"strings" "strings"
"sub2clash/model" "sub2clash/model"
"sub2clash/parser" "sub2clash/parser"
@ -27,35 +28,30 @@ func GetContryName(proxy model.Proxy) string {
return "其他地区" return "其他地区"
} }
var skipGroups = map[string]bool{ func AddProxy(
"手动切换": true, sub *model.Subscription, autotest bool, lazy bool, sortStrategy string,
"全球直连": true, proxies ...model.Proxy,
"广告拦截": true, ) {
"应用净化": true,
}
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 {
sub.Proxies = append(sub.Proxies, proxy) sub.Proxies = append(sub.Proxies, proxy)
haveProxyGroup := false haveProxyGroup := false
countryName := GetContryName(proxy) countryName := GetContryName(proxy)
for i := range sub.ProxyGroups { for i := range sub.ProxyGroups {
group := &sub.ProxyGroups[i] group := &sub.ProxyGroups[i]
if group.Name == countryName { if group.Name == countryName {
group.Proxies = append(group.Proxies, proxy.Name) group.Proxies = append(group.Proxies, proxy.Name)
group.Size++
haveProxyGroup = true haveProxyGroup = true
} }
if group.Name == "手动切换" { if group.Name == "手动切换" {
group.Proxies = append(group.Proxies, proxy.Name) group.Proxies = append(group.Proxies, proxy.Name)
group.Size++
} }
} }
if !haveProxyGroup { if !haveProxyGroup {
var newGroup model.ProxyGroup var newGroup model.ProxyGroup
if !autotest { if !autotest {
@ -64,6 +60,7 @@ func AddProxy(sub *model.Subscription, autotest bool, lazy bool, proxies ...mode
Type: "select", Type: "select",
Proxies: []string{proxy.Name}, Proxies: []string{proxy.Name},
IsCountryGrop: true, IsCountryGrop: true,
Size: 1,
} }
} else { } else {
newGroup = model.ProxyGroup{ newGroup = model.ProxyGroup{
@ -75,24 +72,33 @@ func AddProxy(sub *model.Subscription, autotest bool, lazy bool, proxies ...mode
Interval: 300, Interval: 300,
Tolerance: 50, Tolerance: 50,
Lazy: lazy, Lazy: lazy,
Size: 1,
} }
} }
sub.ProxyGroups = append(sub.ProxyGroups, newGroup) sub.ProxyGroups = append(sub.ProxyGroups, newGroup)
newCountryGroupNames = append(newCountryGroupNames, countryName) newCountryGroupNames = append(newCountryGroupNames, countryName)
} }
} }
// 统计国家策略组数量
countryGroupCount := 0
for i := range sub.ProxyGroups { for i := range sub.ProxyGroups {
if sub.ProxyGroups[i].IsCountryGrop { if sub.ProxyGroups[i].IsCountryGrop {
continue countryGroupCount++
}
if !skipGroups[sub.ProxyGroups[i].Name] {
combined := make([]string, len(newCountryGroupNames)+len(sub.ProxyGroups[i].Proxies))
copy(combined, newCountryGroupNames)
copy(combined[len(newCountryGroupNames):], sub.ProxyGroups[i].Proxies)
sub.ProxyGroups[i].Proxies = combined
} }
} }
// 对国家策略组进行排序
switch sortStrategy {
case "sizeasc":
sort.Sort(model.ProxyGroupsSortBySize(sub.ProxyGroups[:countryGroupCount]))
case "sizedesc":
sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(sub.ProxyGroups[:countryGroupCount])))
case "nameasc":
sort.Sort(model.ProxyGroupsSortByName(sub.ProxyGroups[:countryGroupCount]))
case "namedesc":
sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(sub.ProxyGroups[:countryGroupCount])))
default:
sort.Sort(model.ProxyGroupsSortByName(sub.ProxyGroups[:countryGroupCount]))
}
} }
func ParseProxy(proxies ...string) []model.Proxy { func ParseProxy(proxies ...string) []model.Proxy {

View File

@ -1,9 +1,12 @@
package validator package validator
import ( import (
"crypto/md5"
"encoding/hex"
"errors" "errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/url" "net/url"
"os"
"regexp" "regexp"
"strings" "strings"
) )
@ -21,6 +24,7 @@ type SubQuery struct {
Rules []RuleStruct `form:"-" binding:""` Rules []RuleStruct `form:"-" binding:""`
AutoTest bool `form:"autoTest,default=false" binding:""` AutoTest bool `form:"autoTest,default=false" binding:""`
Lazy bool `form:"lazy,default=false" binding:""` Lazy bool `form:"lazy,default=false" binding:""`
Sort string `form:"sort" binding:""`
} }
type RuleProviderStruct struct { type RuleProviderStruct struct {
@ -28,6 +32,7 @@ type RuleProviderStruct struct {
Url string Url string
Group string Group string
Prepend bool Prepend bool
Name string
} }
type RuleStruct struct { type RuleStruct struct {
@ -46,7 +51,6 @@ func ParseQuery(c *gin.Context) (SubQuery, error) {
if query.Sub != "" { if query.Sub != "" {
query.Subs = strings.Split(query.Sub, ",") query.Subs = strings.Split(query.Sub, ",")
for i := range query.Subs { for i := range query.Subs {
query.Subs[i], _ = url.QueryUnescape(query.Subs[i])
if _, err := url.ParseRequestURI(query.Subs[i]); err != nil { if _, err := url.ParseRequestURI(query.Subs[i]); err != nil {
return SubQuery{}, errors.New("参数错误: " + err.Error()) return SubQuery{}, errors.New("参数错误: " + err.Error())
} }
@ -56,49 +60,36 @@ func ParseQuery(c *gin.Context) (SubQuery, error) {
} }
if query.Proxy != "" { if query.Proxy != "" {
query.Proxies = strings.Split(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 { } else {
query.Proxies = nil query.Proxies = nil
} }
if query.Template != "" { if query.Template != "" {
unescape, err := url.QueryUnescape(query.Template) uri, err := url.ParseRequestURI(query.Template)
if err != nil { if err != nil {
return SubQuery{}, errors.New("参数错误: " + err.Error()) if strings.Contains(query.Template, string(os.PathSeparator)) {
return SubQuery{}, err
}
} }
uri, err := url.ParseRequestURI(unescape)
query.Template = uri.String() query.Template = uri.String()
if err != nil {
return SubQuery{}, errors.New("参数错误: " + err.Error())
}
} }
if query.RuleProvider != "" { 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(`\[(.*?)\]`) reg := regexp.MustCompile(`\[(.*?)\]`)
ruleProviders := reg.FindAllStringSubmatch(query.RuleProvider, -1) ruleProviders := reg.FindAllStringSubmatch(query.RuleProvider, -1)
for i := range ruleProviders { for i := range ruleProviders {
length := len(ruleProviders) length := len(ruleProviders)
parts := strings.Split(ruleProviders[length-i-1][1], ",") parts := strings.Split(ruleProviders[length-i-1][1], ",")
if len(parts) != 4 { if len(parts) < 4 {
return SubQuery{}, errors.New("参数错误: ruleProvider 格式错误") return SubQuery{}, errors.New("参数错误: ruleProvider 格式错误")
} }
u := parts[1] u := parts[1]
u, err = url.QueryUnescape(u) uri, err := url.ParseRequestURI(u)
if err != nil { if err != nil {
return SubQuery{}, errors.New("参数错误: " + err.Error()) return SubQuery{}, errors.New("参数错误: " + err.Error())
} }
uri, err := url.ParseRequestURI(u)
u = uri.String() u = uri.String()
if err != nil { if len(parts) == 4 {
return SubQuery{}, errors.New("参数错误: " + err.Error()) hash := md5.Sum([]byte(u))
parts = append(parts, hex.EncodeToString(hash[:]))
} }
query.RuleProviders = append( query.RuleProviders = append(
query.RuleProviders, RuleProviderStruct{ query.RuleProviders, RuleProviderStruct{
@ -106,6 +97,7 @@ func ParseQuery(c *gin.Context) (SubQuery, error) {
Url: u, Url: u,
Group: parts[2], Group: parts[2],
Prepend: parts[3] == "true", Prepend: parts[3] == "true",
Name: parts[4],
}, },
) )
} }