17 Commits

Author SHA1 Message Date
300bb4931d Merge pull request #1 from nitezs/dev
Dev
2023-09-15 10:39:44 +08:00
8e53183685 fix: 修复规则列表顺序错误,导致无法拦截广告的问题 2023-09-15 10:29:10 +08:00
1f38125e85 update: 修改 docker.yml 镜像构建规则 2023-09-15 09:59:21 +08:00
4e7d84499f fix: 修改 Dockerfile 工作目录 2023-09-15 09:15:24 +08:00
282649aca0 fix 2023-09-15 08:51:22 +08:00
fe2f47865e fix 2023-09-15 08:26:48 +08:00
f6273947e7 mod 2023-09-15 00:31:57 +08:00
0f2dba21eb mod: 增加 dev 分支的 docker 镜像构建 2023-09-15 00:17:32 +08:00
72635ce0fc feat: 增加国家策略组排序策略
feat: 支持指定本地模板
mod: Rule-Provider 可以自定义名称
2023-09-15 00:13:45 +08:00
fd1ff69711 Update README.md 2023-09-14 18:02:04 +08:00
52e609298a add:goreleaser.yaml
mod:docker.yml
mod:go.yml
mod:docker-compose.yml
mod:README.md
2023-09-14 17:59:17 +08:00
d6539a0707 update 2023-09-13 15:52:37 +08:00
070d8b5eb5 update 2023-09-13 15:44:48 +08:00
5c71c54276 Rename docker to docker.yaml 2023-09-13 15:27:23 +08:00
83ce5349c9 Create docker 2023-09-13 15:26:56 +08:00
f1bdeddfba Update go.yml 2023-09-13 15:15:46 +08:00
1866e6bf67 update 2023-09-13 15:06:44 +08:00
15 changed files with 466 additions and 187 deletions

63
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,63 @@
name: Build and Push to GHCR
on:
push:
branches:
- dev
tags:
- '*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set tag name
id: set_tag
run: |
if [[ $GITHUB_REF == refs/heads/* ]]; then
echo "::set-output name=tag::$(echo $GITHUB_REF | cut -d'/' -f3)"
else
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

@ -7,114 +7,148 @@ on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Build
run: |
- name: Build
run: |
LDFLAGS="-s -w"
# Linux
GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-amd64 main.go
GOOS=linux GOARCH=arm go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-arm main.go
GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-arm64 main.go
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-386 main.go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-amd64 main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-arm main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-arm64 main.go
# Darwin
GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-amd64 main.go
GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-arm64 main.go # For Apple Silicon
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-amd64 main.go
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-arm64 main.go
# Windows
GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-amd64.exe main.go
GOOS=windows GOARCH=arm go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-arm.exe main.go
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-386.exe main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-amd64.exe main.go
CGO_ENABLED=0 GOOS=windows GOARCH=arm go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-arm.exe main.go
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-arm64.exe main.go
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
draft: false
prerelease: false
- name: Upload Release Asset (Linux amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-amd64
asset_name: sub2clash-linux-amd64
asset_content_type: application/octet-stream
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
draft: false
prerelease: false
- name: Upload Release Asset (Linux arm)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-arm
asset_name: sub2clash-linux-arm
asset_content_type: application/octet-stream
- name: Upload Release Asset (Linux 386)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-386
asset_name: sub2clash-linux-386
asset_content_type: application/octet-stream
- name: Upload Release Asset (Linux arm64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-arm64
asset_name: sub2clash-linux-arm64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Linux amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-amd64
asset_name: sub2clash-linux-amd64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Darwin amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-darwin-amd64
asset_name: sub2clash-darwin-amd64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Linux arm)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-arm
asset_name: sub2clash-linux-arm
asset_content_type: application/octet-stream
- name: Upload Release Asset (Darwin arm64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-darwin-arm64
asset_name: sub2clash-darwin-arm64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Linux arm64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-arm64
asset_name: sub2clash-linux-arm64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-windows-amd64.exe
asset_name: sub2clash-windows-amd64.exe
asset_content_type: application/octet-stream
- name: Upload Release Asset (Darwin amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-darwin-amd64
asset_name: sub2clash-darwin-amd64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows arm)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-windows-arm.exe
asset_name: sub2clash-windows-arm.exe
asset_content_type: application/octet-stream
- name: Upload Release Asset (Darwin arm64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-darwin-arm64
asset_name: sub2clash-darwin-arm64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows 386)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.up
asset_path: ./output/sub2clash-windows-386.exe
asset_name: sub2clash-windows-386.exe
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-windows-amd64.exe
asset_name: sub2clash-windows-amd64.exe
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows arm)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-windows-arm.exe
asset_name: sub2clash-windows-arm.exe
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows arm64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.up
asset_path: ./output/sub2clash-windows-arm64.exe
asset_name: sub2clash-windows-arm64.exe
asset_content_type: application/octet-stream

3
.gitignore vendored
View File

@ -2,4 +2,5 @@
dist
subs
test
logs
logs
dist/

19
.goreleaser.yaml Normal file
View File

@ -0,0 +1,19 @@
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
- 386
ldflags:
- -s -w
no_unique_dist_dir: true
binary: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"

24
Dockerfile Normal file
View File

@ -0,0 +1,24 @@
# 使用官方 Golang 镜像作为构建环境
FROM golang:1.21-alpine as builder
LABEL authors="nite07"
# 设置工作目录
WORKDIR /app
# 复制源代码到工作目录
COPY . .
RUN go mod download
# 使用 -ldflags 参数进行编译
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o sub2clash main.go
FROM alpine:latest
# 设置工作目录
WORKDIR /app
# 从 builder 镜像中复制出编译好的二进制文件
COPY --from=builder /app/sub2clash /app/sub2clash
# 设置容器的默认启动命令
ENTRYPOINT ["/app/sub2clash"]

View File

@ -8,31 +8,50 @@
- 自动根据节点名称按国家划分策略组
- 支持多订阅合并
- 支持多种协议
- [x] Shadowsocks
- [x] ShadowsocksR
- [x] Vmess
- [x] Vless
- [x] Trojan
- [ ] Hysteria
- [ ] TUIC
- [ ] WireGuard
- Shadowsocks
- ShadowsocksR
- Vmess
- Vless
- Trojan
## API
## 使用
### `/clash`,`/meta`
### 运行
- [docker compose](./docker-compose.yml)
- 运行[二进制文件](https://github.com/nitezs/sub2clash/releases/latest)
### 配置
可以通过编辑 .env 文件来修改默认配置docker 直接添加环境变量
| 变量名 | 说明 | 默认值 |
|-----------------------|----------------------------------------|-----------------------|
| PORT | 端口 | `8011` |
| META_TEMPLATE | meta 模板文件名 | `template_meta.yaml` |
| CLASH_TEMPLATE | clash 模板文件名 | `template_clash.yaml` |
| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` |
| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小byte | `1048576` |
| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
| LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` |
### API
#### `/clash`, `/meta`
获取 Clash/Clash.Meta 配置链接
| 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 |
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|--------------|--------|-------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| sub | string | sub/proxy 至少有一项存在 | - | 订阅链接(可以输入多个,用 `,` 分隔) |
| proxy | string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个,用 `,` 分隔) |
| refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) |
| template | string | 否 | - | 外部模板链接或内部模板名称 |
| 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规则之前 |
| autoTest | bool | 否 | `false` | 指定国家策略组是否自动测速 |
| lazy | bool | 否 | `false` | 自动测速是否启用 lazy |
| sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc``namedesc``sizeasc``sizedesc` |
## 默认模板
@ -45,3 +64,4 @@
## TODO
- [ ] 可视化面板

View File

@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"gopkg.in/yaml.v3"
"net/url"
"regexp"
"strings"
"sub2clash/model"
@ -17,13 +18,16 @@ 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
// 加载模板
if !externalTemplate {
if query.Template != "" {
template = query.Template
}
_, err = url.ParseRequestURI(template)
if err != nil {
templateBytes, err = utils.LoadTemplate(template)
if err != nil {
return nil, errors.New("加载模板失败: " + err.Error())
@ -63,10 +67,14 @@ func BuildSub(query validator.SubQuery, template string) (
} else {
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 {
if v.Prepend {
@ -88,13 +96,50 @@ func BuildSub(query validator.SubQuery, template string) (
}
if v.Prepend {
utils.PrependRuleProvider(
temp, name, v.Group, provider,
temp, v.Name, v.Group, provider,
)
} else {
utils.AppenddRuleProvider(
temp, name, v.Group, provider,
temp, v.Name, v.Group, provider,
)
}
}
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...)
}

20
docker-compose.yml Normal file
View File

@ -0,0 +1,20 @@
version: '3'
services:
sub2clash:
container_name: sub2clash
restart: unless-stopped
image: ghcr.io/nitezs/sub2clash:latest
ports:
- "8011:8011"
volumes:
- ./templates:/app/templates
- ./logs:/app/logs
# environment:
# - PORT=8011
# - META_TEMPLATE=template_meta.yaml
# - PROXY_TEMPLATE=template_clash.yaml
# - REQUEST_RETRY_TIMES=3
# - REQUEST_MAX_FILE_SIZE=1048576
# - CACHE_EXPIRE=300
# - LOG_LEVEL=info

11
go.mod
View File

@ -2,18 +2,23 @@ module sub2clash
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/joho/godotenv v1.5.1
go.uber.org/zap v1.25.0
gopkg.in/yaml.v3 v3.0.1
)
require (
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
github.com/gin-contrib/sse v0.1.0 // indirect
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.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
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
@ -24,12 +29,10 @@ require (
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
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

18
go.sum
View File

@ -1,7 +1,7 @@
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
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=
@ -11,6 +11,7 @@ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpV
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
@ -18,17 +19,18 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
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=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@ -48,10 +50,9 @@ 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@ -62,11 +63,14 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
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/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
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=
@ -84,10 +88,12 @@ golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

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"`
}
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 string `yaml:"type,omitempty"`
Behavior string `yaml:"behavior,omitempty"`

View File

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

View File

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

View File

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