16 Commits

Author SHA1 Message Date
35deaa015f 🔥 Deprecate Woodpecker CI in favor of GitHub Actions 2024-04-25 22:23:33 +08:00
ddd297492c ⬆️ Upgrade dependencies 2024-04-24 13:05:10 +08:00
effd22c750 ♻️ Refactor logger
📝 Update README
2024-04-24 13:01:22 +08:00
566965bb6a ♻️ Migrate from gorm/sqlite to boltdb 2024-04-24 12:51:37 +08:00
3d3b4e0bea 🔧 Update workflows 2024-04-23 17:28:05 +08:00
b86aa2559a 🔧 Remove builder for arm 2024-04-23 15:03:21 +08:00
4a2fa21a0a 🔧 Update docker-compose.yml 2024-04-23 14:54:10 +08:00
3b8352a34f 🔧 update docker and goreleaser configrations 2024-04-23 14:49:31 +08:00
ac4ad3c8aa ♻️ Refactor code
🔥 Remove update detection
2024-04-23 14:47:53 +08:00
ebc91d8aad ♻️ Refactor parsers 2024-04-23 14:39:16 +08:00
48dece2a51 🐛 Fix trojan parser missing fields 2024-04-23 13:36:33 +08:00
aa9e102a81 fix: hy2 解析缺少 name 字段 2024-04-21 00:23:29 +08:00
faaf5c366a mod 2024-04-18 00:15:45 +08:00
4384e56cc6 mod 2024-04-17 23:08:26 +08:00
abbd7b8b19 mod 2024-04-17 22:55:04 +08:00
b687acb94c update 2024-04-17 21:52:03 +08:00
51 changed files with 1079 additions and 1049 deletions

View File

@ -5,4 +5,3 @@ REQUEST_RETRY_TIMES=3
REQUEST_MAX_FILE_SIZE=1048576 REQUEST_MAX_FILE_SIZE=1048576
CACHE_EXPIRE=300 CACHE_EXPIRE=300
LOG_LEVEL=info LOG_LEVEL=info
BASE_PATH=/

View File

@ -1,66 +1,53 @@
name: Build and Push Docker name: docker
on: on:
push: push:
branches:
- dev
tags: tags:
- "*" - "v*"
workflow_dispatch:
jobs: jobs:
build-and-push: prepare-and-build:
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@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: | images: |
${{ secrets.DOCKER_HUB_USERNAME }}/sub2clash nite07/sub2clash
ghcr.io/${{ github.repository }} ghcr.io/nitezs/sub2clash
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Prepare args - name: Set up QEMU
id: prep uses: docker/setup-qemu-action@v3
run: |
if [[ "${{ github.event_name }}" == 'workflow_dispatch' ]]; then
VERSION="${{ github.sha }}"
fi
if [[ "$GITHUB_REF" == 'refs/heads/dev' ]]; then
VERSION="${{ github.sha }}"
fi
if [[ "$GITHUB_REF" == 'refs/tags/'* ]]; then
VERSION=$TAG_NAME
fi
echo "version=$VERSION" >> $GITHUB_ENV
- name: Set up Docker buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build and push Docker image to GHCR and Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . build-args: |
file: ./Dockerfile "version=${{ github.ref_name }}"
build-args: version=${{ env.version }}
push: true push: true
platforms: linux/amd64,linux/arm,linux/arm64
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,31 +1,28 @@
name: Build and Release name: release
on: on:
push: push:
tags: tags:
- "*" - "v*"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
packages: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with:
go-version: 1.21.5
- name: Run goreleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5 uses: goreleaser/goreleaser-action@v5
with: with:
distribution: goreleaser distribution: goreleaser
version: latest version: latest
args: release --rm-dist --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@ -1,8 +1,7 @@
.idea .idea
dist dist
subs subs
test
logs logs
sub2clash.db data
.env .env
.vscode/settings.json .vscode/settings.json

View File

@ -9,9 +9,24 @@ builds:
goarch: goarch:
- amd64 - amd64
- arm64 - arm64
- arm
- "386"
goarm:
- "6"
- "7"
ldflags: ldflags:
- -s -w -X sub2clash/config.Version={{ .Version }} - -s -w -X sub2clash/constant.Version={{ .Version }}
no_unique_dist_dir: true flags:
binary: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" - -trimpath
archives: archives:
- format: binary - format: tar.gz
format_overrides:
- format: zip
goos: windows
wrap_in_directory: true
files:
- LICENSE
- README.md
- templates
release:
draft: true

2
.vscode/launch.json vendored
View File

@ -7,7 +7,7 @@
"mode": "debug", "mode": "debug",
"program": "${workspaceFolder}/main.go", "program": "${workspaceFolder}/main.go",
"output": "${workspaceFolder}/dist/main.exe", "output": "${workspaceFolder}/dist/main.exe",
"buildFlags": "-ldflags '-X sub2clash/config.Version=dev'" "buildFlags": "-ldflags '-X sub2clash/constant.Version=dev'"
} }
] ]
} }

View File

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

View File

@ -42,12 +42,12 @@
### API ### API
[API 文档](./API_README.md) [API 文档](./API.md)
### 模板 ### 模板
可以通过变量自定义模板中的策略组代理节点 可以通过变量自定义模板中的策略组代理节点
解释的不太清楚,可以参考下方默认模板 具体参考下方默认模板
- `<all>` 为添加所有节点 - `<all>` 为添加所有节点
- `<countries>` 为添加所有国家策略组 - `<countries>` 为添加所有国家策略组
@ -57,7 +57,3 @@
- [Clash](./templates/template_clash.yaml) - [Clash](./templates/template_clash.yaml)
- [Clash.Meta](./templates/template_meta.yaml) - [Clash.Meta](./templates/template_meta.yaml)
## 已知问题
[代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue

View File

@ -9,10 +9,10 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sub2clash/common"
"sub2clash/logger" "sub2clash/logger"
"sub2clash/model" "sub2clash/model"
"sub2clash/parser" "sub2clash/parser"
"sub2clash/utils"
"sub2clash/validator" "sub2clash/validator"
"go.uber.org/zap" "go.uber.org/zap"
@ -32,7 +32,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
template = query.Template template = query.Template
} }
if strings.HasPrefix(template, "http") { if strings.HasPrefix(template, "http") {
templateBytes, err = utils.LoadSubscription(template, query.Refresh) templateBytes, err = common.LoadSubscription(template, query.Refresh)
if err != nil { if err != nil {
logger.Logger.Debug( logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err), "load template failed", zap.String("template", template), zap.Error(err),
@ -44,7 +44,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
if err != nil { if err != nil {
return nil, errors.New("加载模板失败: " + err.Error()) return nil, errors.New("加载模板失败: " + err.Error())
} }
templateBytes, err = utils.LoadTemplate(unescape) templateBytes, err = common.LoadTemplate(unescape)
if err != nil { if err != nil {
logger.Logger.Debug( logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err), "load template failed", zap.String("template", template), zap.Error(err),
@ -61,7 +61,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
var proxyList []model.Proxy var proxyList []model.Proxy
// 加载订阅 // 加载订阅
for i := range query.Subs { for i := range query.Subs {
data, err := utils.LoadSubscription(query.Subs[i], query.Refresh) data, err := common.LoadSubscription(query.Subs[i], query.Refresh)
subName := "" subName := ""
if strings.Contains(query.Subs[i], "#") { if strings.Contains(query.Subs[i], "#") {
subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:] subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:]
@ -76,9 +76,9 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
err = yaml.Unmarshal(data, &sub) err = yaml.Unmarshal(data, &sub)
var newProxies []model.Proxy var newProxies []model.Proxy
if err != nil { if err != nil {
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria)://") reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria|hy2|hysteria2)://")
if reg.Match(data) { if reg.Match(data) {
p := utils.ParseProxy(strings.Split(string(data), "\n")...) p := common.ParseProxy(strings.Split(string(data), "\n")...)
newProxies = p newProxies = p
} else { } else {
// 如果无法直接解析尝试Base64解码 // 如果无法直接解析尝试Base64解码
@ -91,7 +91,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
) )
return nil, errors.New("加载订阅失败: " + err.Error()) return nil, errors.New("加载订阅失败: " + err.Error())
} }
p := utils.ParseProxy(strings.Split(base64, "\n")...) p := common.ParseProxy(strings.Split(base64, "\n")...)
newProxies = p newProxies = p
} }
} else { } else {
@ -106,7 +106,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
} }
// 添加自定义节点 // 添加自定义节点
if len(query.Proxies) != 0 { if len(query.Proxies) != 0 {
proxyList = append(proxyList, utils.ParseProxy(query.Proxies...)...) proxyList = append(proxyList, common.ParseProxy(query.Proxies...)...)
} }
// 给节点添加订阅名称 // 给节点添加订阅名称
for i := range proxyList { for i := range proxyList {
@ -118,7 +118,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
proxies := make(map[string]*model.Proxy) proxies := make(map[string]*model.Proxy)
newProxies := make([]model.Proxy, 0, len(proxyList)) newProxies := make([]model.Proxy, 0, len(proxyList))
for i := range proxyList { for i := range proxyList {
key := proxyList[i].Server + ":" + strconv.Itoa(proxyList[i].Port) + ":" + proxyList[i].Type key := proxyList[i].Server + strconv.Itoa(proxyList[i].Port) + proxyList[i].Type + proxyList[i].UUID + proxyList[i].Password
if _, exist := proxies[key]; !exist { if _, exist := proxies[key]; !exist {
proxies[key] = &proxyList[i] proxies[key] = &proxyList[i]
newProxies = append(newProxies, proxyList[i]) newProxies = append(newProxies, proxyList[i])
@ -181,7 +181,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
} }
// 将新增节点都添加到临时变量 t 中,防止策略组排序错乱 // 将新增节点都添加到临时变量 t 中,防止策略组排序错乱
var t = &model.Subscription{} var t = &model.Subscription{}
utils.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...) common.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
// 排序策略组 // 排序策略组
switch query.Sort { switch query.Sort {
case "sizeasc": case "sizeasc":
@ -200,9 +200,9 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
// 处理自定义规则 // 处理自定义规则
for _, v := range query.Rules { for _, v := range query.Rules {
if v.Prepend { if v.Prepend {
utils.PrependRules(temp, v.Rule) common.PrependRules(temp, v.Rule)
} else { } else {
utils.AppendRules(temp, v.Rule) common.AppendRules(temp, v.Rule)
} }
} }
// 处理自定义 ruleProvider // 处理自定义 ruleProvider
@ -217,11 +217,11 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
Interval: 3600, Interval: 3600,
} }
if v.Prepend { if v.Prepend {
utils.PrependRuleProvider( common.PrependRuleProvider(
temp, v.Name, v.Group, provider, temp, v.Name, v.Group, provider,
) )
} else { } else {
utils.AppenddRuleProvider( common.AppenddRuleProvider(
temp, v.Name, v.Group, provider, temp, v.Name, v.Group, provider,
) )
} }
@ -266,14 +266,18 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg
case "all": case "all":
newProxies = append(newProxies, proxyNames...) newProxies = append(newProxies, proxyNames...)
case "countries": case "countries":
if !igcg {
newProxies = append(newProxies, countryGroupNames...) newProxies = append(newProxies, countryGroupNames...)
}
default: default:
if !igcg {
if len(key) == 2 { if len(key) == 2 {
newProxies = append( newProxies = append(
newProxies, countryGroupMap[utils.GetContryName(key)].Proxies..., newProxies, countryGroupMap[common.GetContryName(key)].Proxies...,
) )
} }
} }
}
} else { } else {
newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j]) newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j])
} }

View File

@ -1,160 +1,137 @@
package handler package handler
import ( import (
"errors"
"io" "io"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"sub2clash/config"
"sub2clash/logger"
"sub2clash/model"
"sub2clash/utils"
"sub2clash/utils/database"
"sub2clash/validator"
"time" "time"
"sub2clash/common"
"sub2clash/common/database"
"sub2clash/config"
"sub2clash/model"
"sub2clash/validator"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
) )
func ShortLinkGenHandler(c *gin.Context) { func respondWithError(c *gin.Context, code int, message string) {
// 从请求中获取参数 c.String(code, message)
c.Abort()
}
func GenerateLinkHandler(c *gin.Context) {
var params validator.ShortLinkGenValidator var params validator.ShortLinkGenValidator
if err := c.ShouldBind(&params); err != nil { if err := c.ShouldBind(&params); err != nil {
c.String(400, "参数错误: "+err.Error()) respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error())
return
} }
if strings.TrimSpace(params.Url) == "" { if strings.TrimSpace(params.Url) == "" {
c.String(400, "参数错误") respondWithError(c, http.StatusBadRequest, "URL 不能为空")
return return
} }
// 生成hash
hash := utils.RandomString(config.Default.ShortLinkLength) hash, err := generateUniqueHash()
var item model.ShortLink if err != nil {
result := database.FindShortLinkByUrl(params.Url, &item) respondWithError(c, http.StatusInternalServerError, "生成短链接失败")
if result.Error == nil {
if item.Password != params.Password {
item.Password = params.Password
database.SaveShortLink(&item)
c.String(200, item.Hash+"?password="+params.Password)
} else {
c.String(200, item.Hash)
}
return
} else {
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
c.String(500, "数据库错误: "+result.Error.Error())
return return
} }
}
// 如果记录存在则重新生成hash直到记录不存在 shortLink := model.ShortLink{
result = database.FindShortLinkByHash(hash, &item)
for result.Error == nil {
hash = utils.RandomString(config.Default.ShortLinkLength)
result = database.FindShortLinkByHash(hash, &item)
}
// 创建记录
database.FirstOrCreateShortLink(
&model.ShortLink{
Hash: hash, Hash: hash,
Url: params.Url, Url: params.Url,
LastRequestTime: -1,
Password: params.Password, Password: params.Password,
}, }
)
// 返回短链接 if err := database.SaveShortLink(&shortLink); err != nil {
respondWithError(c, http.StatusInternalServerError, "数据库错误")
return
}
if params.Password != "" { if params.Password != "" {
hash += "?password=" + params.Password hash += "?password=" + params.Password
} }
c.String(200, hash) c.String(http.StatusOK, hash)
} }
func ShortLinkGetUrlHandler(c *gin.Context) { func generateUniqueHash() (string, error) {
var params validator.ShortLinkGetValidator for {
if err := c.ShouldBindQuery(&params); err != nil { hash := common.RandomString(config.Default.ShortLinkLength)
c.String(400, "参数错误: "+err.Error()) exists, err := database.CheckShortLinkHashExists(hash)
return if err != nil {
return "", err
} }
if strings.TrimSpace(params.Hash) == "" { if !exists {
c.String(400, "参数错误") return hash, nil
return
} }
var shortLink model.ShortLink
result := database.FindShortLinkByHash(params.Hash, &shortLink)
if result.Error != nil {
c.String(404, "未找到短链接")
return
} }
if shortLink.Password != "" && shortLink.Password != params.Password {
c.String(403, "密码错误")
return
}
c.String(200, shortLink.Url)
} }
func ShortLinkGetConfigHandler(c *gin.Context) { func UpdateLinkHandler(c *gin.Context) {
// 获取动态路由 var params validator.ShortLinkUpdateValidator
if err := c.ShouldBindJSON(&params); err != nil {
respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error())
return
}
shortLink := model.ShortLink{
Hash: params.Hash,
Url: params.Url,
Password: params.Password,
}
if err := database.SaveShortLink(&shortLink); err != nil {
respondWithError(c, http.StatusInternalServerError, "数据库错误")
return
}
c.String(http.StatusOK, "短链接更新成功")
}
func GetRawConfHandler(c *gin.Context) {
// 获取动态路由参数
hash := c.Param("hash") hash := c.Param("hash")
password := c.Query("password") password := c.Query("password")
if strings.TrimSpace(hash) == "" { if strings.TrimSpace(hash) == "" {
c.String(400, "参数错误") c.String(http.StatusBadRequest, "参数错误")
return return
} }
// 查询数据库
var shortLink model.ShortLink
result := database.FindShortLinkByHash(hash, &shortLink)
// 重定向
if result.Error != nil {
c.String(404, "未找到短链接或密码错误")
return
}
if shortLink.Password != "" && shortLink.Password != password {
c.String(404, "未找到短链接或密码错误")
return
}
// 更新最后访问时间
shortLink.LastRequestTime = time.Now().Unix()
database.SaveShortLink(&shortLink)
get, err := utils.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url)
if err != nil {
logger.Logger.Debug("get short link data failed", zap.Error(err))
c.String(500, "请求错误: "+err.Error())
return
}
all, err := io.ReadAll(get.Body)
if err != nil {
logger.Logger.Debug("read short link data failed", zap.Error(err))
c.String(500, "读取错误: "+err.Error())
return
}
c.String(http.StatusOK, string(all))
}
func ShortLinkUpdateHandler(c *gin.Context) { // 查询数据库中的短链接
var params validator.ShortLinkUpdateValidator shortLink, err := database.FindShortLinkByHash(hash)
if err := c.ShouldBind(&params); err != nil { if err != nil {
c.String(400, "参数错误: "+err.Error()) c.String(http.StatusNotFound, "未找到短链接或密码错误")
}
if strings.TrimSpace(params.Url) == "" {
c.String(400, "参数错误")
return return
} }
var shortLink model.ShortLink
result := database.FindShortLinkByHash(params.Hash, &shortLink) // 校验密码
if result.Error != nil { if shortLink.Password != "" && shortLink.Password != password {
c.String(404, "未找到短链接") c.String(http.StatusNotFound, "未找到短链接或密码错误")
return return
} }
if shortLink.Password == "" {
c.String(403, "无法修改无密码短链接") // 更新最后访问时间
shortLink.LastRequestTime = time.Now().Unix()
err = database.SaveShortLink(shortLink)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "数据库错误")
return return
} }
if shortLink.Password != params.Password { // 请求短链接指向的URL
c.String(403, "密码错误") response, err := http.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "请求错误: "+err.Error())
return return
} }
shortLink.Url = params.Url defer response.Body.Close()
database.SaveShortLink(&shortLink)
c.String(200, "更新成功") // 读取响应内容
all, err := io.ReadAll(response.Body)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "读取错误: "+err.Error())
return
}
// 返回响应内容
c.String(http.StatusOK, string(all))
} }

View File

@ -6,7 +6,7 @@ import (
"log" "log"
"net/http" "net/http"
"sub2clash/api/handler" "sub2clash/api/handler"
"sub2clash/config" "sub2clash/constant"
"sub2clash/middleware" "sub2clash/middleware"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -32,9 +32,9 @@ func SetRoute(r *gin.Engine) {
) )
r.GET( r.GET(
"/", func(c *gin.Context) { "/", func(c *gin.Context) {
version := config.Version version := constant.Version
if len(config.Version) > 7 { if len(constant.Version) > 7 {
version = config.Version[:7] version = constant.Version[:7]
} }
c.HTML( c.HTML(
200, "index.html", gin.H{ 200, "index.html", gin.H{
@ -43,33 +43,9 @@ func SetRoute(r *gin.Engine) {
) )
}, },
) )
r.GET( r.GET("/clash", handler.SubmodHandler)
"/clash", func(c *gin.Context) { r.GET("/meta", handler.SubHandler)
handler.SubmodHandler(c) r.GET("/s/:hash", handler.GetRawConfHandler)
}, r.POST("/short", handler.GenerateLinkHandler)
) r.PUT("/short", handler.UpdateLinkHandler)
r.GET(
"/meta", func(c *gin.Context) {
handler.SubHandler(c)
},
)
r.GET(
"/s/:hash", func(c *gin.Context) {
handler.ShortLinkGetConfigHandler(c)
},
)
r.GET(
"/short", func(c *gin.Context) {
handler.ShortLinkGetUrlHandler(c)
})
r.POST(
"/short", func(c *gin.Context) {
handler.ShortLinkGenHandler(c)
},
)
r.PUT(
"/short", func(c *gin.Context) {
handler.ShortLinkUpdateHandler(c)
},
)
} }

View File

@ -0,0 +1,71 @@
package database
import (
"encoding/json"
"errors"
"path/filepath"
"sub2clash/model"
"go.etcd.io/bbolt"
)
var DB *bbolt.DB
func ConnectDB() error {
// 确保数据目录存在
path := filepath.Join("data", "sub2clash.db")
// 打开或创建数据库文件
db, err := bbolt.Open(path, 0600, nil)
if err != nil {
return err
}
DB = db
// 确保存储桶存在
return db.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte("ShortLinks"))
return err
})
}
func FindShortLinkByHash(hash string) (*model.ShortLink, error) {
var shortLink model.ShortLink
err := DB.View(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte("ShortLinks"))
v := b.Get([]byte(hash))
if v == nil {
return errors.New("ShortLink not found")
}
return json.Unmarshal(v, &shortLink)
})
if err != nil {
return nil, err
}
return &shortLink, nil
}
func SaveShortLink(shortLink *model.ShortLink) error {
return DB.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte("ShortLinks"))
encoded, err := json.Marshal(shortLink)
if err != nil {
return err
}
return b.Put([]byte(shortLink.Hash), encoded)
})
}
func CheckShortLinkHashExists(hash string) (bool, error) {
exists := false
err := DB.View(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte("ShortLinks"))
v := b.Get([]byte(hash))
exists = v != nil
return nil
})
if err != nil {
return false, err
}
return exists, nil
}

View File

@ -1,4 +1,4 @@
package utils package common
import ( import (
"errors" "errors"

View File

@ -1,4 +1,4 @@
package utils package common
import ( import (
"errors" "errors"
@ -26,5 +26,8 @@ func MkEssentialDir() error {
if err := MKDir("logs"); err != nil { if err := MKDir("logs"); err != nil {
return errors.New("create logs dir failed" + err.Error()) return errors.New("create logs dir failed" + err.Error())
} }
if err := MKDir("data"); err != nil {
return errors.New("create data dir failed" + err.Error())
}
return nil return nil
} }

View File

@ -1,7 +1,8 @@
package utils package common
import ( import (
"strings" "strings"
"sub2clash/constant"
"sub2clash/logger" "sub2clash/logger"
"sub2clash/model" "sub2clash/model"
"sub2clash/parser" "sub2clash/parser"
@ -106,25 +107,25 @@ func ParseProxy(proxies ...string) []model.Proxy {
var proxyItem model.Proxy var proxyItem model.Proxy
var err error var err error
// 解析节点 // 解析节点
if strings.HasPrefix(proxy, "ss://") { if strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
proxyItem, err = parser.ParseSS(proxy) proxyItem, err = parser.ParseShadowsocks(proxy)
} }
if strings.HasPrefix(proxy, "trojan://") { if strings.HasPrefix(proxy, constant.TrojanPrefix) {
proxyItem, err = parser.ParseTrojan(proxy) proxyItem, err = parser.ParseTrojan(proxy)
} }
if strings.HasPrefix(proxy, "vmess://") { if strings.HasPrefix(proxy, constant.VMessPrefix) {
proxyItem, err = parser.ParseVmess(proxy) proxyItem, err = parser.ParseVmess(proxy)
} }
if strings.HasPrefix(proxy, "vless://") { if strings.HasPrefix(proxy, constant.VLESSPrefix) {
proxyItem, err = parser.ParseVless(proxy) proxyItem, err = parser.ParseVless(proxy)
} }
if strings.HasPrefix(proxy, "ssr://") { if strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) {
proxyItem, err = parser.ParseShadowsocksR(proxy) proxyItem, err = parser.ParseShadowsocksR(proxy)
} }
if strings.HasPrefix(proxy, "hysteria2://") || strings.HasPrefix(proxy, "hy2://") { if strings.HasPrefix(proxy, constant.Hysteria2Prefix1) || strings.HasPrefix(proxy, constant.Hysteria2Prefix2) {
proxyItem, err = parser.ParseHysteria2(proxy) proxyItem, err = parser.ParseHysteria2(proxy)
} }
if strings.HasPrefix(proxy, "hysteria://") { if strings.HasPrefix(proxy, constant.HysteriaPrefix) {
proxyItem, err = parser.ParseHysteria(proxy) proxyItem, err = parser.ParseHysteria(proxy)
} }
if err == nil { if err == nil {

View File

@ -1,4 +1,4 @@
package utils package common
import "math/rand" import "math/rand"

View File

@ -1,4 +1,4 @@
package utils package common
import ( import (
"fmt" "fmt"

View File

@ -1,10 +1,11 @@
package utils package common
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"sub2clash/config" "sub2clash/config"
@ -57,11 +58,11 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(Body io.ReadCloser) { defer func(resp *http.Response) {
if Body != nil { if resp != nil && resp.Body != nil {
_ = Body.Close() _ = resp.Body.Close()
} }
}(resp.Body) }(resp)
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)

View File

@ -1,4 +1,4 @@
package utils package common
import ( import (
"errors" "errors"

View File

@ -1,4 +1,4 @@
package utils package common
import ( import (
"os" "os"

View File

@ -3,7 +3,6 @@ package config
import ( import (
"errors" "errors"
"os" "os"
"regexp"
"strconv" "strconv"
"github.com/joho/godotenv" "github.com/joho/godotenv"
@ -22,18 +21,8 @@ type Config struct {
} }
var Default *Config var Default *Config
var Version string
var Dev string var Dev string
func init() {
reg := regexp.MustCompile(`^v\d+\.\d+\.\d+$`)
if reg.MatchString(Version) {
Dev = "false"
} else {
Dev = "true"
}
}
func LoadConfig() error { func LoadConfig() error {
Default = &Config{ Default = &Config{
MetaTemplate: "template_meta.yaml", MetaTemplate: "template_meta.yaml",

12
constant/prefix.go Normal file
View File

@ -0,0 +1,12 @@
package constant
const (
HysteriaPrefix string = "hysteria://"
Hysteria2Prefix1 string = "hysteria2://"
Hysteria2Prefix2 string = "hy2://"
ShadowsocksPrefix string = "ss://"
ShadowsocksRPrefix string = "ssr://"
TrojanPrefix string = "trojan://"
VLESSPrefix string = "vless://"
VMessPrefix string = "vmess://"
)

3
constant/version.go Normal file
View File

@ -0,0 +1,3 @@
package constant
var Version = "dev"

View File

@ -4,7 +4,7 @@ services:
sub2clash: sub2clash:
container_name: sub2clash container_name: sub2clash
restart: unless-stopped restart: unless-stopped
image: ghcr.io/nitezs/sub2clash:latest image: nite07/sub2clash:latest
ports: ports:
- "8011:8011" - "8011:8011"
volumes: volumes:

35
go.mod
View File

@ -4,54 +4,39 @@ go 1.21
require ( require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.10.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
go.etcd.io/bbolt v1.3.9
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/text v0.14.0 golang.org/x/text v0.14.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.7
) )
require ( require (
github.com/bytedance/sonic v1.11.3 // indirect github.com/bytedance/sonic v1.11.5 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/cloudwego/base64x v0.1.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // 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.19.0 // indirect github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/knz/go-libedit v1.10.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pelletier/go-toml/v2 v2.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.7.0 // indirect golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.21.0 // indirect golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.22.0 // indirect golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/sys v0.19.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.44.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.29.4 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
) )

141
go.sum
View File

@ -1,86 +1,44 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw=
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY=
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 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-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 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 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/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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
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.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
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/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -88,102 +46,57 @@ 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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
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.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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.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/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 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/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=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 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/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
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/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/libc v1.44.0 h1:71bbnKgb0mCg7GOOI/PHlzz7Bv6obELGNKnIEeowX8c=
modernc.org/libc v1.44.0/go.mod h1:RRqfGVjvILF5AdNP3RPCiihj7+Dn2pIBrdlU60lA9vs=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
modernc.org/sqlite v1.29.4 h1:mbvQTJ3Tl5Vz+wLA6z8hdBFSeNQ0XXQ+KVwn8NkUliw=
modernc.org/sqlite v1.29.4/go.mod h1:MjUIBKZ+tU/lqjNLbVAAMjsQPdWdA/ciwdhsT9kBwk8=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,70 +1,52 @@
package logger package logger
import ( import (
"path/filepath" "os"
"sync" "strings"
"time"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
) )
var ( var Logger *zap.Logger
Logger *zap.Logger
lock sync.Mutex
logLevel string
)
func InitLogger(level string) { func InitLogger(logLevel string) {
logLevel = level logger := zap.New(buildZapCore(getZapLogLevel(logLevel)))
buildLogger() Logger = logger
go rotateLogs()
} }
func buildLogger() { func buildZapCore(logLevel zapcore.Level) zapcore.Core {
lock.Lock() fileWriter := zapcore.AddSync(&lumberjack.Logger{
defer lock.Unlock() Filename: "logs/app.log",
var level zapcore.Level MaxSize: 500,
switch logLevel { MaxBackups: 3,
case "error": MaxAge: 28,
level = zap.ErrorLevel Compress: true,
})
consoleWriter := zapcore.AddSync(os.Stdout)
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
fileCore := zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), fileWriter, logLevel)
consoleCore := zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), consoleWriter, logLevel)
combinedCore := zapcore.NewTee(fileCore, consoleCore)
return combinedCore
}
func getZapLogLevel(logLevel string) zapcore.Level {
switch strings.ToLower(logLevel) {
case "debug": case "debug":
level = zap.DebugLevel return zap.DebugLevel
case "warn": case "warn":
level = zap.WarnLevel return zap.WarnLevel
case "error":
return zap.ErrorLevel
case "info": case "info":
level = zap.InfoLevel return zap.InfoLevel
default: default:
level = zap.InfoLevel return zap.InfoLevel
}
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)
var err error
Logger, err = zapConfig.Build()
if err != nil {
panic("log failed" + 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()
} }
} }

22
main.go
View File

@ -5,10 +5,10 @@ import (
"io" "io"
"strconv" "strconv"
"sub2clash/api" "sub2clash/api"
"sub2clash/common"
"sub2clash/common/database"
"sub2clash/config" "sub2clash/config"
"sub2clash/logger" "sub2clash/logger"
"sub2clash/utils"
"sub2clash/utils/database"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
@ -23,7 +23,7 @@ var templateClash string
func init() { func init() {
var err error var err error
// 创建文件夹 // 创建文件夹
err = utils.MkEssentialDir() err = common.MkEssentialDir()
if err != nil { if err != nil {
logger.Logger.Panic("create essential dir failed", zap.Error(err)) logger.Logger.Panic("create essential dir failed", zap.Error(err))
} }
@ -34,22 +34,8 @@ func init() {
if err != nil { if err != nil {
logger.Logger.Panic("load config failed", zap.Error(err)) logger.Logger.Panic("load config failed", zap.Error(err))
} }
// 检查更新
if config.Dev != "true" {
go func() {
update, newVersion, err := utils.CheckUpdate()
if err != nil {
logger.Logger.Warn("check update failed", zap.Error(err))
}
if update {
logger.Logger.Info("new version is available", zap.String("version", newVersion))
}
}()
} else {
logger.Logger.Info("running in dev mode")
}
// 写入默认模板 // 写入默认模板
err = utils.WriteDefalutTemplate(templateMeta, templateClash) err = common.WriteDefalutTemplate(templateMeta, templateClash)
if err != nil { if err != nil {
logger.Logger.Panic("write default template failed", zap.Error(err)) logger.Logger.Panic("write default template failed", zap.Error(err))
} }

View File

@ -6,108 +6,29 @@ import (
) )
type ProxyGroup struct { type ProxyGroup struct {
Name string `yaml:"name,omitempty"`
Type string `yaml:"type,omitempty"` Type string `yaml:"type,omitempty"`
Name string `yaml:"name,omitempty"`
Proxies []string `yaml:"proxies,omitempty"` Proxies []string `yaml:"proxies,omitempty"`
IsCountryGrop bool `yaml:"-"` IsCountryGrop bool `yaml:"-"` // 是否是国家分组
Url string `yaml:"url,omitempty"` Url string `yaml:"url,omitempty"`
Interval int `yaml:"interval,omitempty"` Interval int `yaml:"interval,omitempty"`
Tolerance int `yaml:"tolerance,omitempty"` Tolerance int `yaml:"tolerance,omitempty"`
Lazy bool `yaml:"lazy"` Lazy bool `yaml:"lazy"`
Size int `yaml:"-"` Size int `yaml:"-"` // 代理数量
DisableUDP bool `yaml:"disable-udp,omitempty"` DisableUDP bool `yaml:"disable-udp,omitempty"`
Strategy string `yaml:"strategy,omitempty"` Strategy string `yaml:"strategy,omitempty"`
Icon string `yaml:"icon,omitempty"` Icon string `yaml:"icon,omitempty"`
} Timeout int `yaml:"timeout,omitempty"`
Use []string `yaml:"use,omitempty"`
type SelectProxyGroup struct { InterfaceName string `yaml:"interface-name,omitempty"`
Name string `yaml:"name,omitempty"` RoutingMark int `yaml:"routing-mark,omitempty"`
Type string `yaml:"type,omitempty"` IncludeAll bool `yaml:"include-all,omitempty"`
Proxies []string `yaml:"proxies,omitempty"` IncludeAllProxies bool `yaml:"include-all-proxies,omitempty"`
DisableUDP bool `yaml:"disable-udp,omitempty"` IncludeAllProviders bool `yaml:"include-all-providers,omitempty"`
Icon string `yaml:"icon,omitempty"` Filter string `yaml:"filter,omitempty"`
} ExcludeFilter string `yaml:"exclude-filter,omitempty"`
ExpectedStatus int `yaml:"expected-status,omitempty"`
type UrlTestProxyGroup struct { Hidden bool `yaml:"hidden,omitempty"`
Name string `yaml:"name,omitempty"`
Type string `yaml:"type,omitempty"`
Proxies []string `yaml:"proxies,omitempty"`
Url string `yaml:"url,omitempty"`
Interval int `yaml:"interval,omitempty"`
Tolerance int `yaml:"tolerance,omitempty"`
Lazy bool `yaml:"lazy"`
DisableUDP bool `yaml:"disable-udp,omitempty"`
Icon string `yaml:"icon,omitempty"`
}
type LoadBalanceProxyGroup struct {
Name string `yaml:"name,omitempty"`
Type string `yaml:"type,omitempty"`
Proxies []string `yaml:"proxies,omitempty"`
DisableUDP bool `yaml:"disable-udp,omitempty"`
Url string `yaml:"url,omitempty"`
Interval int `yaml:"interval,omitempty"`
Lazy bool `yaml:"lazy"`
Strategy string `yaml:"strategy,omitempty"`
Icon string `yaml:"icon,omitempty"`
}
type RelayProxyGroup struct {
Name string `yaml:"name,omitempty"`
Type string `yaml:"type,omitempty"`
Proxies []string `yaml:"proxies,omitempty"`
Icon string `yaml:"icon,omitempty"`
}
func (p ProxyGroup) MarshalYAML() (interface{}, error) {
switch p.Type {
case "select":
return SelectProxyGroup{
Name: p.Name,
Type: p.Type,
Proxies: p.Proxies,
DisableUDP: p.DisableUDP,
Icon: p.Icon,
}, nil
case "url-test", "fallback":
return UrlTestProxyGroup{
Name: p.Name,
Type: p.Type,
Proxies: p.Proxies,
Url: p.Url,
Interval: p.Interval,
Tolerance: p.Tolerance,
Lazy: p.Lazy,
DisableUDP: p.DisableUDP,
Icon: p.Icon,
}, nil
case "load-balance":
return LoadBalanceProxyGroup{
Name: p.Name,
Type: p.Type,
Proxies: p.Proxies,
DisableUDP: p.DisableUDP,
Url: p.Url,
Interval: p.Interval,
Lazy: p.Lazy,
Strategy: p.Strategy,
Icon: p.Icon,
}, nil
case "relay":
return RelayProxyGroup{
Name: p.Name,
Type: p.Type,
Proxies: p.Proxies,
Icon: p.Icon,
}, nil
default:
return SelectProxyGroup{
Name: p.Name,
Type: p.Type,
Proxies: p.Proxies,
Icon: p.Icon,
}, nil
}
} }
type ProxyGroupsSortByName []ProxyGroup type ProxyGroupsSortByName []ProxyGroup

View File

@ -1,7 +1,7 @@
package model package model
type ShortLink struct { type ShortLink struct {
Hash string `gorm:"primary_key"` Hash string
Url string Url string
Password string Password string
LastRequestTime int64 LastRequestTime int64

View File

@ -1,18 +1,5 @@
package model package model
// type Subscription struct {
// Port int `yaml:"port,omitempty"`
// SocksPort int `yaml:"socks-port,omitempty"`
// AllowLan bool `yaml:"allow-lan"`
// Mode string `yaml:"mode,omitempty"`
// LogLevel string `yaml:"logger-level,omitempty"`
// ExternalController string `yaml:"external-controller,omitempty"`
// Proxies []Proxy `yaml:"proxies,omitempty"`
// ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"`
// Rules []string `yaml:"rules,omitempty"`
// RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"`
// }
type NodeList struct { type NodeList struct {
Proxies []Proxy `yaml:"proxies,omitempty"` Proxies []Proxy `yaml:"proxies,omitempty"`
} }

View File

@ -7,6 +7,11 @@ import (
func DecodeBase64(s string) (string, error) { func DecodeBase64(s string) (string, error) {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
// url safe
if strings.Contains(s, "-") || strings.Contains(s, "_") {
s = strings.Replace(s, "-", "+", -1)
s = strings.Replace(s, "_", "/", -1)
}
if len(s)%4 != 0 { if len(s)%4 != 0 {
s += strings.Repeat("=", 4-len(s)%4) s += strings.Repeat("=", 4-len(s)%4)
} }

24
parser/error.go Normal file
View File

@ -0,0 +1,24 @@
package parser
type ParseError struct {
Type ParseErrorType
Message string
Raw string
}
type ParseErrorType string
const (
ErrInvalidPrefix ParseErrorType = "invalid url prefix"
ErrInvalidStruct ParseErrorType = "invalid struct"
ErrInvalidPort ParseErrorType = "invalid port number"
ErrCannotParseParams ParseErrorType = "cannot parse query parameters"
ErrInvalidBase64 ParseErrorType = "invalid base64"
)
func (e *ParseError) Error() string {
if e.Message != "" {
return string(e.Type) + ": " + e.Message + " \"" + e.Raw + "\""
}
return string(e.Type)
}

View File

@ -1,79 +1,86 @@
package parser package parser
import ( import (
"errors"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sub2clash/constant"
"sub2clash/model" "sub2clash/model"
) )
//hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks
//
//- host: hostname or IP address of the server to connect to (required)
//- port: port of the server to connect to (required)
//- protocol: protocol to use ("udp", "wechat-video", "faketcp") (optional, default: "udp")
//- auth: authentication payload (string) (optional)
//- peer: SNI for TLS (optional)
//- insecure: ignore certificate errors (optional)
//- upmbps: upstream bandwidth in Mbps (required)
//- downmbps: downstream bandwidth in Mbps (required)
//- alpn: QUIC ALPN (optional)
//- obfs: Obfuscation mode (optional, empty or "xplus")
//- obfsParam: Obfuscation password (optional)
//- remarks: remarks (optional)
func ParseHysteria(proxy string) (model.Proxy, error) { func ParseHysteria(proxy string) (model.Proxy, error) {
// 判断是否以 hysteria:// 开头 if !strings.HasPrefix(proxy, constant.HysteriaPrefix) {
if !strings.HasPrefix(proxy, "hysteria://") { return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
return model.Proxy{}, errors.New("invalid hysteria Url")
} }
// 分割
parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria://"), "?", 2) proxy = strings.TrimPrefix(proxy, constant.HysteriaPrefix)
serverInfo := strings.SplitN(parts[0], ":", 2) urlParts := strings.SplitN(proxy, "?", 2)
if len(urlParts) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing character '?' in url",
Raw: proxy,
}
}
serverInfo := strings.SplitN(urlParts[0], ":", 2)
if len(serverInfo) != 2 { if len(serverInfo) != 2 {
return model.Proxy{}, errors.New("invalid hysteria Url") return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host or port",
Raw: proxy,
} }
params, err := url.ParseQuery(parts[1]) }
server, portStr := serverInfo[0], serverInfo[1]
port, err := ParsePort(portStr)
if err != nil { if err != nil {
return model.Proxy{}, errors.New("invalid hysteria Url") return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
} }
host := serverInfo[0] }
port, err := strconv.Atoi(serverInfo[1])
params, err := url.ParseQuery(urlParts[1])
if err != nil { if err != nil {
return model.Proxy{}, errors.New("invalid hysteria Url") return model.Proxy{}, &ParseError{
Type: ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
} }
protocol := params.Get("protocol")
auth := params.Get("auth")
peer := params.Get("peer")
insecure := params.Get("insecure")
upmbps := params.Get("upmbps")
downmbps := params.Get("downmbps")
alpn := params.Get("alpn")
obfs := params.Get("obfs")
obfsParam := params.Get("obfsParam")
remarks := ""
if strings.Contains(parts[1], "#") {
r := strings.Split(parts[1], "#")
remarks = r[len(r)-1]
} else {
remarks = serverInfo[0] + ":" + serverInfo[1]
} }
// 返回结果
protocol, auth, insecure, upmbps, downmbps, obfs, alpnStr := params.Get("protocol"), params.Get("auth"), params.Get("insecure"), params.Get("upmbps"), params.Get("downmbps"), params.Get("obfs"), params.Get("alpn")
insecureBool, err := strconv.ParseBool(insecure)
if err != nil {
insecureBool = false
}
var alpn []string
alpnStr = strings.TrimSpace(alpnStr)
if alpnStr != "" {
alpn = strings.Split(alpnStr, ",")
}
remarks := server + ":" + portStr
if params.Get("remarks") != "" {
remarks = params.Get("remarks")
}
result := model.Proxy{ result := model.Proxy{
Type: "hysteria", Type: "hysteria",
Name: remarks, Name: remarks,
Server: host, Server: server,
Port: port, Port: port,
Up: upmbps, Up: upmbps,
Down: downmbps, Down: downmbps,
Auth: auth, Auth: auth,
Obfs: obfs, Obfs: obfs,
Sni: peer,
SkipCertVerify: insecure == "1", SkipCertVerify: insecure == "1",
Alpn: strings.Split(alpn, ","), Alpn: alpn,
ObfsParam: obfsParam,
Protocol: protocol, Protocol: protocol,
AllowInsecure: insecureBool,
} }
return result, nil return result, nil
} }

View File

@ -1,50 +1,89 @@
package parser package parser
import ( import (
"errors"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sub2clash/constant"
"sub2clash/model" "sub2clash/model"
) )
// hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com
func ParseHysteria2(proxy string) (model.Proxy, error) { func ParseHysteria2(proxy string) (model.Proxy, error) {
// 判断是否以 hysteria2:// 开头 if !strings.HasPrefix(proxy, constant.Hysteria2Prefix1) &&
if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") { !strings.HasPrefix(proxy, constant.Hysteria2Prefix2) {
return model.Proxy{}, errors.New("invalid hysteria2 Url") return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
} }
// 分割
parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2) proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix1)
// 分割 proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix2)
serverInfo := strings.SplitN(parts[1], "/?", 2) urlParts := strings.SplitN(proxy, "@", 2)
if len(urlParts) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing character '@' in url",
Raw: proxy,
}
}
password := urlParts[0]
serverInfo := strings.SplitN(urlParts[1], "/?", 2)
if len(serverInfo) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing params in url",
Raw: proxy,
}
}
paramStr := serverInfo[1]
serverAndPort := strings.SplitN(serverInfo[0], ":", 2) serverAndPort := strings.SplitN(serverInfo[0], ":", 2)
var server string
var portStr string
if len(serverAndPort) == 1 { if len(serverAndPort) == 1 {
serverAndPort = append(serverAndPort, "443") portStr = "443"
} else if len(serverAndPort) != 2 { } else if len(serverAndPort) == 2 {
return model.Proxy{}, errors.New("invalid hysteria2 Url") server, portStr = serverAndPort[0], serverAndPort[1]
} else {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host or port",
Raw: proxy,
} }
params, err := url.ParseQuery(serverInfo[1]) }
port, err := ParsePort(portStr)
if err != nil { if err != nil {
return model.Proxy{}, errors.New("invalid hysteria2 Url") return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
} }
// 获取端口 }
port, err := strconv.Atoi(serverAndPort[1])
params, err := url.ParseQuery(paramStr)
if err != nil { if err != nil {
return model.Proxy{}, errors.New("invalid hysteria2 Url") return model.Proxy{}, &ParseError{
Type: ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
} }
// 返回结果 }
remarks, network, obfs, obfsPassword, pinSHA256, insecure, sni := params.Get("name"), params.Get("network"), params.Get("obfs"), params.Get("obfs-password"), params.Get("pinSHA256"), params.Get("insecure"), params.Get("sni")
enableTLS := pinSHA256 != ""
insecureBool := insecure == "1"
result := model.Proxy{ result := model.Proxy{
Type: "hysteria2", Type: "hysteria2",
Name: params.Get("name"), Name: remarks,
Server: serverAndPort[0], Server: server,
Port: port, Port: port,
Password: parts[0], Password: password,
Obfs: params.Get("obfs"), Obfs: obfs,
ObfsParam: params.Get("obfs-password"), ObfsParam: obfsPassword,
Sni: params.Get("sni"), Sni: sni,
SkipCertVerify: params.Get("insecure") == "1", SkipCertVerify: insecureBool,
TLS: enableTLS,
Network: network,
} }
return result, nil return result, nil
} }

18
parser/port.go Normal file
View File

@ -0,0 +1,18 @@
package parser
import (
"errors"
"strconv"
)
func ParsePort(portStr string) (int, error) {
port, err := strconv.Atoi(portStr)
if err != nil {
return 0, err
}
if port < 1 || port > 65535 {
return 0, errors.New("invaild port range")
}
return port, nil
}

91
parser/shadowsocks.go Normal file
View File

@ -0,0 +1,91 @@
package parser
import (
"net/url"
"strings"
"sub2clash/constant"
"sub2clash/model"
)
func ParseShadowsocks(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
}
proxy = strings.TrimPrefix(proxy, constant.ShadowsocksPrefix)
urlParts := strings.SplitN(proxy, "@", 2)
if len(urlParts) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing character '@' in url",
Raw: proxy,
}
}
var serverAndPort []string
if !strings.Contains(urlParts[0], ":") {
decoded, err := DecodeBase64(urlParts[0])
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "invalid base64 encoded",
Raw: proxy,
}
}
urlParts[0] = decoded
}
credentials := strings.SplitN(urlParts[0], ":", 2)
if len(credentials) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host or port",
Raw: proxy,
}
}
method, password := credentials[0], credentials[1]
serverInfo := strings.SplitN(urlParts[1], "#", 2)
serverAndPort = strings.SplitN(serverInfo[0], ":", 2)
server, portStr := serverAndPort[0], serverAndPort[1]
if len(serverInfo) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host or port",
Raw: proxy,
}
}
port, err := ParsePort(portStr)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
}
var remarks string
if len(serverInfo) == 2 {
unescape, err := url.QueryUnescape(serverInfo[1])
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "cannot unescape remarks",
Raw: proxy,
}
}
remarks = strings.TrimSpace(unescape)
} else {
remarks = strings.TrimSpace(server + ":" + portStr)
}
result := model.Proxy{
Type: "ss",
Cipher: method,
Password: password,
Server: server,
Port: port,
Name: remarks,
}
return result, nil
}

86
parser/shadowsocksr.go Normal file
View File

@ -0,0 +1,86 @@
package parser
import (
"net/url"
"strconv"
"strings"
"sub2clash/constant"
"sub2clash/model"
)
func ParseShadowsocksR(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
}
proxy = strings.TrimPrefix(proxy, constant.ShadowsocksRPrefix)
proxy, err := DecodeBase64(proxy)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidBase64,
Raw: proxy,
}
}
serverInfoAndParams := strings.SplitN(proxy, "/?", 2)
parts := strings.Split(serverInfoAndParams[0], ":")
server := parts[0]
protocol := parts[2]
method := parts[3]
obfs := parts[4]
password := parts[5]
port, err := ParsePort(parts[1])
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
}
var obfsParam string
var protoParam string
var remarks string
if len(serverInfoAndParams) == 2 {
params, err := url.ParseQuery(serverInfoAndParams[1])
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
}
}
if params.Get("obfsparam") != "" {
obfsParam, err = DecodeBase64(params.Get("obfsparam"))
}
if params.Get("protoparam") != "" {
protoParam, err = DecodeBase64(params.Get("protoparam"))
}
if params.Get("remarks") != "" {
remarks, err = DecodeBase64(params.Get("remarks"))
} else {
remarks = server + ":" + strconv.Itoa(port)
}
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Raw: proxy,
Message: err.Error(),
}
}
}
result := model.Proxy{
Name: remarks,
Type: "ssr",
Server: server,
Port: port,
Protocol: protocol,
Cipher: method,
Obfs: obfs,
Password: password,
ObfsParam: obfsParam,
ProtocolParam: protoParam,
}
return result, nil
}

View File

@ -1,67 +0,0 @@
package parser
import (
"errors"
"net/url"
"strconv"
"strings"
"sub2clash/model"
)
// ParseSS 解析 SSShadowsocksUrl
func ParseSS(proxy string) (model.Proxy, error) {
// 判断是否以 ss:// 开头
if !strings.HasPrefix(proxy, "ss://") {
return model.Proxy{}, errors.New("invalid ss Url")
}
// 分割
parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2)
if len(parts) != 2 {
return model.Proxy{}, errors.New("invalid ss Url")
}
if !strings.Contains(parts[0], ":") {
// 解码
decoded, err := DecodeBase64(parts[0])
if err != nil {
return model.Proxy{}, errors.New("invalid ss Url" + err.Error())
}
parts[0] = decoded
}
credentials := strings.SplitN(parts[0], ":", 2)
if len(credentials) != 2 {
return model.Proxy{}, errors.New("invalid ss Url")
}
// 分割
serverInfo := strings.SplitN(parts[1], "#", 2)
serverAndPort := strings.SplitN(serverInfo[0], ":", 2)
if len(serverAndPort) != 2 {
return model.Proxy{}, errors.New("invalid ss Url")
}
// 转换端口字符串为数字
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
if err != nil {
return model.Proxy{}, errors.New("invalid ss Url" + err.Error())
}
// 返回结果
result := model.Proxy{
Type: "ss",
Cipher: strings.TrimSpace(credentials[0]),
Password: strings.TrimSpace(credentials[1]),
Server: strings.TrimSpace(serverAndPort[0]),
Port: port,
UDP: true,
Name: serverAndPort[0],
}
// 如果有节点名称
if len(serverInfo) == 2 {
unescape, err := url.QueryUnescape(serverInfo[1])
if err != nil {
return model.Proxy{}, errors.New("invalid ss Url" + err.Error())
}
result.Name = strings.TrimSpace(unescape)
} else {
result.Name = strings.TrimSpace(serverAndPort[0])
}
return result, nil
}

View File

@ -1,71 +0,0 @@
package parser
import (
"fmt"
"net/url"
"strconv"
"strings"
"sub2clash/model"
)
func ParseShadowsocksR(proxy string) (model.Proxy, error) {
// 判断是否以 ssr:// 开头
if !strings.HasPrefix(proxy, "ssr://") {
return model.Proxy{}, fmt.Errorf("invalid ssr Url")
}
var err error
proxy = strings.TrimPrefix(proxy, "ssr://")
if !strings.Contains(proxy, ":") {
proxy, err = DecodeBase64(strings.TrimPrefix(proxy, "ssr://"))
if err != nil {
return model.Proxy{}, err
}
}
// 分割
detailsAndParams := strings.SplitN(proxy, "/?", 2)
parts := strings.Split(detailsAndParams[0], ":")
params, err := url.ParseQuery(detailsAndParams[1])
if err != nil {
return model.Proxy{}, err
}
// 处理端口
port, err := strconv.Atoi(parts[1])
if err != nil {
return model.Proxy{}, err
}
var obfsParam string
var protoParam string
var remarks string
if params.Get("obfsparam") != "" {
obfsParam, err = DecodeBase64(params.Get("obfsparam"))
}
if params.Get("protoparam") != "" {
protoParam, err = DecodeBase64(params.Get("protoparam"))
}
if params.Get("remarks") != "" {
remarks, err = DecodeBase64(params.Get("remarks"))
}
if err != nil {
return model.Proxy{}, err
}
result := model.Proxy{
Name: remarks,
Type: "ssr",
Server: parts[0],
Port: port,
Protocol: parts[2],
Cipher: parts[3],
Obfs: parts[4],
Password: parts[5],
ObfsParam: obfsParam,
ProtocolParam: protoParam,
}
if result.Name == "" {
result.Name = result.Server
}
return result, nil
}

View File

@ -1,53 +1,134 @@
package parser package parser
import ( import (
"fmt"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sub2clash/constant"
"sub2clash/model" "sub2clash/model"
) )
// ParseTrojan 解析给定的Trojan代理URL并返回Proxy结构。
func ParseTrojan(proxy string) (model.Proxy, error) { func ParseTrojan(proxy string) (model.Proxy, error) {
// 判断是否以 trojan:// 开头 if !strings.HasPrefix(proxy, constant.TrojanPrefix) {
if !strings.HasPrefix(proxy, "trojan://") { return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
return model.Proxy{}, fmt.Errorf("invalid trojan Url")
} }
// 分割
parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) proxy = strings.TrimPrefix(proxy, constant.TrojanPrefix)
if len(parts) != 2 { urlParts := strings.SplitN(proxy, "@", 2)
return model.Proxy{}, fmt.Errorf("invalid trojan Url") if len(urlParts) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing character '@' in url",
Raw: proxy,
} }
// 分割 }
serverInfo := strings.SplitN(parts[1], "#", 2) password := strings.TrimSpace(urlParts[0])
serverInfo := strings.SplitN(urlParts[1], "#", 2)
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2)
if len(serverAndPortAndParams) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing character '?' in url",
Raw: proxy,
}
}
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
if len(serverAndPort) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host or port",
Raw: proxy,
}
}
server, portStr := serverAndPort[0], serverAndPort[1]
params, err := url.ParseQuery(serverAndPortAndParams[1]) params, err := url.ParseQuery(serverAndPortAndParams[1])
if err != nil { if err != nil {
return model.Proxy{}, err return model.Proxy{}, &ParseError{
Type: ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
} }
if len(serverAndPort) != 2 {
return model.Proxy{}, fmt.Errorf("invalid trojan")
} }
// 处理端口
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) port, err := ParsePort(portStr)
if err != nil { if err != nil {
return model.Proxy{}, err return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
} }
// 返回结果 }
remarks := ""
if len(serverInfo) == 2 {
remarks, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1]))
} else {
remarks = serverAndPort[0]
}
network, security, alpnStr, sni, pbk, sid, fp, path, host, serviceName := params.Get("type"), params.Get("security"), params.Get("alpn"), params.Get("sni"), params.Get("pbk"), params.Get("sid"), params.Get("fp"), params.Get("path"), params.Get("host"), params.Get("serviceName")
var alpn []string
if strings.Contains(alpnStr, ",") {
alpn = strings.Split(alpnStr, ",")
} else {
alpn = nil
}
// enableUTLS := fp != ""
// 构建Proxy结构体
result := model.Proxy{ result := model.Proxy{
Type: "trojan", Type: "trojan",
Server: strings.TrimSpace(serverAndPort[0]), Server: server,
Port: port, Port: port,
UDP: true, Password: password,
Password: strings.TrimSpace(parts[0]), Name: remarks,
Sni: params.Get("sni"), Network: network,
} }
// 如果有节点名称
if len(serverInfo) == 2 { if security == "xtls" || security == "tls" {
result.Name, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1])) result.Alpn = alpn
} else { result.Sni = sni
result.Name = serverAndPort[0] result.TLS = true
} }
if security == "reality" {
result.TLS = true
result.Sni = sni
result.RealityOpts = model.RealityOptions{
PublicKey: pbk,
ShortID: sid,
}
result.Fingerprint = fp
}
if network == "ws" {
result.Network = "ws"
result.WSOpts = model.WSOptions{
Path: path,
Headers: map[string]string{
"Host": host,
},
}
}
// if network == "http" {
// // 未查到相关支持文档
// }
// if network == "quic" {
// // 未查到相关支持文档
// }
if network == "grpc" {
result.GrpcOpts = model.GrpcOptions{
GrpcServiceName: serviceName,
}
}
return result, nil return result, nil
} }

View File

@ -1,92 +1,156 @@
package parser package parser
import ( import (
"fmt"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sub2clash/constant"
"sub2clash/model" "sub2clash/model"
) )
func ParseVless(proxy string) (model.Proxy, error) { func ParseVless(proxy string) (model.Proxy, error) {
// 判断是否以 vless:// 开头 if !strings.HasPrefix(proxy, constant.VLESSPrefix) {
if !strings.HasPrefix(proxy, "vless://") { return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
return model.Proxy{}, fmt.Errorf("invalid vless Url")
} }
// 分割
parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) urlParts := strings.SplitN(strings.TrimPrefix(proxy, constant.VLESSPrefix), "@", 2)
if len(parts) != 2 { if len(urlParts) != 2 {
return model.Proxy{}, fmt.Errorf("invalid vless Url") return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing character '@' in url",
Raw: proxy,
} }
// 分割 }
serverInfo := strings.SplitN(parts[1], "#", 2)
serverInfo := strings.SplitN(urlParts[1], "#", 2)
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2)
if len(serverAndPortAndParams) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing character '?' in url",
Raw: proxy,
}
}
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
if len(serverAndPort) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host or port",
Raw: proxy,
}
}
server, portStr := serverAndPort[0], serverAndPort[1]
port, err := ParsePort(portStr)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
}
params, err := url.ParseQuery(serverAndPortAndParams[1]) params, err := url.ParseQuery(serverAndPortAndParams[1])
if err != nil { if err != nil {
return model.Proxy{}, err return model.Proxy{}, &ParseError{
} Type: ErrCannotParseParams,
if len(serverAndPort) != 2 { Raw: proxy,
return model.Proxy{}, fmt.Errorf("invalid vless") Message: err.Error(),
}
// 处理端口
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
if err != nil {
return model.Proxy{}, err
}
// 返回结果
result := model.Proxy{
Type: "vless",
Server: strings.TrimSpace(serverAndPort[0]),
Port: port,
UUID: strings.TrimSpace(parts[0]),
UDP: true,
Sni: params.Get("sni"),
Network: params.Get("type"),
Flow: params.Get("flow"),
ClientFingerprint: params.Get("fp"),
Servername: params.Get("sni"),
}
if params.Get("alpn") != "" {
result.Alpn = strings.Split(params.Get("alpn"), ",")
}
if params.Get("security") == "reality" {
result.TLS = true
result.RealityOpts = model.RealityOptions{
PublicKey: params.Get("pbk"),
ShortID: params.Get("sid"),
} }
} }
if params.Get("type") == "ws" {
result.TLS = true remarks := ""
result.WSOpts = model.WSOptions{
Path: params.Get("path"),
Headers: map[string]string{
"Host": params.Get("host"),
},
}
}
if params.Get("type") == "grpc" {
result.TLS = true
result.GrpcOpts = model.GrpcOptions{
GrpcServiceName: params.Get("serviceName"),
}
}
// 如果有节点名称
if len(serverInfo) == 2 { if len(serverInfo) == 2 {
if strings.Contains(serverInfo[1], "|") { if strings.Contains(serverInfo[1], "|") {
result.Name = strings.SplitN(serverInfo[1], "|", 2)[1] remarks = strings.SplitN(serverInfo[1], "|", 2)[1]
} else { } else {
result.Name, err = url.QueryUnescape(serverInfo[1]) remarks, err = url.QueryUnescape(serverInfo[1])
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
}
}
}
} else {
remarks, err = url.QueryUnescape(server)
if err != nil { if err != nil {
return model.Proxy{}, err return model.Proxy{}, err
} }
} }
uuid := strings.TrimSpace(urlParts[0])
flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName, _type := params.Get("flow"), params.Get("security"), params.Get("alpn"), params.Get("sni"), params.Get("allowInsecure"), params.Get("fp"), params.Get("pbk"), params.Get("sid"), params.Get("path"), params.Get("host"), params.Get("serviceName"), params.Get("type")
// enableUTLS := fp != ""
insecureBool := insecure == "1"
var alpn []string
if strings.Contains(alpnStr, ",") {
alpn = strings.Split(alpnStr, ",")
} else { } else {
result.Name, err = url.QueryUnescape(serverAndPort[0]) alpn = nil
}
result := model.Proxy{
Type: "vless",
Server: server,
Name: remarks,
Port: port,
UUID: uuid,
Flow: flow,
}
if security == "tls" {
result.TLS = true
result.Alpn = alpn
result.Sni = sni
result.AllowInsecure = insecureBool
result.Fingerprint = fp
}
if security == "reality" {
result.TLS = true
result.RealityOpts = model.RealityOptions{
PublicKey: pbk,
ShortID: sid,
}
}
if _type == "ws" {
result.Network = "ws"
result.WSOpts = model.WSOptions{
Path: path,
}
if host != "" {
result.WSOpts.Headers = make(map[string]string)
result.WSOpts.Headers["Host"] = host
}
}
// if _type == "quic" {
// // 未查到相关支持文档
// }
if _type == "grpc" {
result.Network = "grpc"
result.Servername = serviceName
}
if _type == "http" {
hosts, err := url.QueryUnescape(host)
if err != nil { if err != nil {
return model.Proxy{}, err return model.Proxy{}, &ParseError{
Type: ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
} }
} }
result.Network = "http"
result.HTTPOpts = model.HTTPOptions{
Headers: map[string][]string{"Host": strings.Split(hosts, ",")},
}
}
return result, nil return result, nil
} }

View File

@ -2,66 +2,65 @@ package parser
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sub2clash/constant"
"sub2clash/model" "sub2clash/model"
) )
func ParseVmess(proxy string) (model.Proxy, error) { func ParseVmess(proxy string) (model.Proxy, error) {
// 判断是否以 vmess:// 开头 if !strings.HasPrefix(proxy, constant.VMessPrefix) {
if !strings.HasPrefix(proxy, "vmess://") { return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
return model.Proxy{}, errors.New("invalid vmess url")
} }
// 解码
base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) proxy = strings.TrimPrefix(proxy, constant.VMessPrefix)
base64, err := DecodeBase64(proxy)
if err != nil { if err != nil {
return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) return model.Proxy{}, &ParseError{Type: ErrInvalidBase64, Raw: proxy, Message: err.Error()}
} }
// 解析
var vmess model.VmessJson var vmess model.VmessJson
err = json.Unmarshal([]byte(base64), &vmess) err = json.Unmarshal([]byte(base64), &vmess)
if err != nil { if err != nil {
return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()}
} }
// 解析端口
port := 0 var port int
switch vmess.Port.(type) { switch vmess.Port.(type) {
case string: case string:
port, err = strconv.Atoi(vmess.Port.(string)) port, err = ParsePort(vmess.Port.(string))
if err != nil { if err != nil {
return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
} }
case float64: case float64:
port = int(vmess.Port.(float64)) port = int(vmess.Port.(float64))
} }
// 解析Aid
aid := 0 aid := 0
switch vmess.Aid.(type) { switch vmess.Aid.(type) {
case string: case string:
aid, err = strconv.Atoi(vmess.Aid.(string)) aid, err = strconv.Atoi(vmess.Aid.(string))
if err != nil { if err != nil {
return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()}
} }
case float64: case float64:
aid = int(vmess.Aid.(float64)) aid = int(vmess.Aid.(float64))
} }
// 设置默认值
if vmess.Scy == "" { if vmess.Scy == "" {
vmess.Scy = "auto" vmess.Scy = "auto"
} }
if vmess.Net == "ws" && vmess.Path == "" {
vmess.Path = "/"
}
if vmess.Net == "ws" && vmess.Host == "" {
vmess.Host = vmess.Add
}
name, err := url.QueryUnescape(vmess.Ps) name, err := url.QueryUnescape(vmess.Ps)
if err != nil { if err != nil {
name = vmess.Ps name = vmess.Ps
} }
// 返回结果
result := model.Proxy{ result := model.Proxy{
Name: name, Name: name,
Type: "vmess", Type: "vmess",
@ -70,15 +69,29 @@ func ParseVmess(proxy string) (model.Proxy, error) {
UUID: vmess.Id, UUID: vmess.Id,
AlterID: aid, AlterID: aid,
Cipher: vmess.Scy, Cipher: vmess.Scy,
UDP: true,
TLS: vmess.Tls == "tls",
Fingerprint: vmess.Fp,
ClientFingerprint: "chrome",
SkipCertVerify: true,
Servername: vmess.Add,
Network: vmess.Net,
} }
if vmess.Tls == "tls" {
var alpn []string
if strings.Contains(vmess.Alpn, ",") {
alpn = strings.Split(vmess.Alpn, ",")
} else {
alpn = nil
}
result.TLS = true
result.Fingerprint = vmess.Fp
result.Alpn = alpn
result.Servername = vmess.Sni
}
if vmess.Net == "ws" { if vmess.Net == "ws" {
if vmess.Path == "" {
vmess.Path = "/"
}
if vmess.Host == "" {
vmess.Host = vmess.Add
}
result.Network = "ws"
result.WSOpts = model.WSOptions{ result.WSOpts = model.WSOptions{
Path: vmess.Path, Path: vmess.Path,
Headers: map[string]string{ Headers: map[string]string{
@ -86,5 +99,25 @@ func ParseVmess(proxy string) (model.Proxy, error) {
}, },
} }
} }
// if vmess.Net == "quic" {
// // 未查到相关支持文档
// }
if vmess.Net == "grpc" {
result.GrpcOpts = model.GrpcOptions{
GrpcServiceName: vmess.Path,
}
result.Network = "grpc"
}
if vmess.Net == "h2" {
result.HTTP2Opts = model.HTTP2Options{
Host: strings.Split(vmess.Host, ","),
Path: vmess.Path,
}
result.Network = "h2"
}
return result, nil return result, nil
} }

View File

@ -1,5 +1,4 @@
port: 7890 mixed-port: 7890
socks-port: 7891
allow-lan: true allow-lan: true
mode: Rule mode: Rule
log-level: info log-level: info

View File

@ -1,5 +1,4 @@
port: 7890 mixed-port: 7890
socks-port: 7891
allow-lan: true allow-lan: true
mode: Rule mode: Rule
log-level: info log-level: info

22
test/parser_test.go Normal file
View File

@ -0,0 +1,22 @@
package test
import (
"strings"
"testing"
)
func TestParser(t *testing.T) {
// res, err := parser.ParseTrojan("trojan://Abse64hhjewrs@test.com:8443?type=ws&path=%2Fx&host=test.com&security=tls&fp=&alpn=http%2F1.1&sni=test.com#test")
// if err != nil {
// t.Log(err.Error())
// t.Fail()
// }
// bytes, err := yaml.Marshal(res)
// if err != nil {
// t.Log(err.Error())
// t.Fail()
// }
// t.Log(string(bytes))
t.Log(strings.SplitN("123456", "/?", 2))
}

View File

@ -1,33 +0,0 @@
package utils
import (
"encoding/json"
"errors"
"io"
"sub2clash/config"
"sub2clash/model"
)
func CheckUpdate() (bool, string, error) {
get, err := Get("https://api.github.com/repos/nitezs/sub2clash/tags")
if err != nil {
return false, "", errors.New("get version info failed" + err.Error())
}
var version model.Tags
all, err := io.ReadAll(get.Body)
if err != nil {
return false, "", errors.New("get version info failed" + err.Error())
}
err = json.Unmarshal(all, &version)
if err != nil {
return false, "", errors.New("get version info failed" + err.Error())
}
if version[0].Name == config.Version {
return false, "", nil
} else {
return true, version[0].Name, nil
}
}

View File

@ -1,56 +0,0 @@
package database
import (
"path/filepath"
"sub2clash/logger"
"sub2clash/model"
"sub2clash/utils"
"github.com/glebarez/sqlite"
"go.uber.org/zap"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDB() error {
// 用上面的数据库连接初始化 gorm
err := utils.MKDir("data")
if err != nil {
return err
}
db, err := gorm.Open(
sqlite.Open(filepath.Join("data", "sub2clash.db")), &gorm.Config{
Logger: nil,
},
)
if err != nil {
return err
}
DB = db
err = db.AutoMigrate(&model.ShortLink{})
if err != nil {
return err
}
return nil
}
func FindShortLinkByUrl(url string, shortLink *model.ShortLink) *gorm.DB {
logger.Logger.Debug("find short link by url", zap.String("url", url))
return DB.Where("url = ?", url).First(&shortLink)
}
func FindShortLinkByHash(hash string, shortLink *model.ShortLink) *gorm.DB {
logger.Logger.Debug("find short link by hash", zap.String("hash", hash))
return DB.Where("hash = ?", hash).First(&shortLink)
}
func SaveShortLink(shortLink *model.ShortLink) {
logger.Logger.Debug("save short link", zap.String("hash", shortLink.Hash))
DB.Save(shortLink)
}
func FirstOrCreateShortLink(shortLink *model.ShortLink) {
logger.Logger.Debug("first or create short link", zap.String("hash", shortLink.Hash))
DB.FirstOrCreate(shortLink)
}

View File

@ -5,7 +5,7 @@ type ShortLinkGenValidator struct {
Password string `form:"password"` Password string `form:"password"`
} }
type ShortLinkGetValidator struct { type GetUrlValidator struct {
Hash string `form:"hash" binding:"required"` // Hash: 短链接 Hash string `form:"hash" binding:"required"` // Hash: 短链接
Password string `form:"password"` Password string `form:"password"`
} }