mirror of
https://github.com/bestnite/sub2clash.git
synced 2025-07-05 04:12:34 +08:00
Compare commits
35 Commits
v0.1.0-alp
...
v0.0.12-be
Author | SHA1 | Date | |
---|---|---|---|
a2e97aaa01 | |||
4f3c2bb280 | |||
1b662de245 | |||
4a9297f4a3 | |||
fcb1358846 | |||
da9a17201b | |||
5b7a94e65c | |||
b5fcbab1a5 | |||
0b8299f432 | |||
84c5a6e5f4 | |||
06887d91ac | |||
db00433931 | |||
5b3a12f00d | |||
d4d7010d8f | |||
2331cd4d18 | |||
88d8653ab5 | |||
cc0b73d7a4 | |||
b57e4cf49f | |||
cc80e237d6 | |||
fefb4b895a | |||
66f214ae10 | |||
f7dc78aabc | |||
6bb2d16e4b | |||
98ef93c7bb | |||
6e09c44d17 | |||
42fd251eb5 | |||
e504a6cca4 | |||
3cfa4bdf24 | |||
dedbf2bc03 | |||
7158ad467a | |||
fd22cd1499 | |||
0946412ea7 | |||
98bca0a7ac | |||
036907fba4 | |||
3e720ec14d |
@ -1,7 +0,0 @@
|
||||
PORT=8011
|
||||
META_TEMPLATE=meta_template.json
|
||||
CLASH_TEMPLATE=clash_template.json
|
||||
REQUEST_RETRY_TIMES=3
|
||||
REQUEST_MAX_FILE_SIZE=1048576
|
||||
CACHE_EXPIRE=300
|
||||
LOG_LEVEL=info
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
images: |
|
||||
nite07/sub2clash
|
||||
ghcr.io/nitezs/sub2clash
|
||||
ghcr.io/bestnite/sub2clash
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,4 +5,5 @@ logs
|
||||
data
|
||||
.env
|
||||
.vscode/settings.json
|
||||
test
|
||||
test
|
||||
*test.go
|
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -5,7 +5,7 @@
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"program": "${workspaceFolder}",
|
||||
"output": "${workspaceFolder}/dist/main.exe",
|
||||
"buildFlags": "-ldflags '-X sub2clash/constant.Version=dev'"
|
||||
}
|
||||
|
56
README.md
56
README.md
@ -1,9 +1,7 @@
|
||||
# sub2clash
|
||||
|
||||
> Sing-box 用户?看看另一个项目 [sub2sing-box](https://github.com/nitezs/sub2sing-box)
|
||||
|
||||
将订阅链接转换为 Clash、Clash.Meta 配置
|
||||
[预览](https://www.nite07.com/sub)
|
||||
[预览](https://clash.nite07.com/)
|
||||
|
||||
## 特性
|
||||
|
||||
@ -19,28 +17,45 @@
|
||||
- Trojan
|
||||
- Hysteria (Clash.Meta)
|
||||
- Hysteria2 (Clash.Meta)
|
||||
- Socks5
|
||||
- Anytls (Clash.Meta)
|
||||
|
||||
## 使用
|
||||
|
||||
### 部署
|
||||
|
||||
- [docker compose](./docker-compose.yml)
|
||||
- 运行[二进制文件](https://github.com/nitezs/sub2clash/releases/latest)
|
||||
- [docker compose](./compose.yml)
|
||||
- 运行[二进制文件](https://github.com/bestnite/sub2clash/releases/latest)
|
||||
|
||||
### 配置
|
||||
|
||||
可以通过编辑 .env 文件来修改默认配置,docker 直接添加环境变量
|
||||
支持多种配置方式,按优先级排序:
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
| --------------------- | ---------------------------------------------- | --------------------- |
|
||||
| PORT | 端口 | `8011` |
|
||||
| META_TEMPLATE | 默认 meta 模板文件名 | `template_meta.yaml` |
|
||||
| CLASH_TEMPLATE | 默认 clash 模板文件名 | `template_clash.yaml` |
|
||||
| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` |
|
||||
| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小(byte) | `1048576` |
|
||||
| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
|
||||
| LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` |
|
||||
| SHORT_LINK_LENGTH | 短链长度 | `6` |
|
||||
1. **配置文件**:支持多种格式(YAML、JSON),按以下优先级搜索:
|
||||
- `config.yaml` / `config.yml`
|
||||
- `config.json`
|
||||
- `sub2clash.yaml` / `sub2clash.yml`
|
||||
- `sub2clash.json`
|
||||
2. **环境变量**:使用 `SUB2CLASH_` 前缀,例如 `SUB2CLASH_ADDRESS=0.0.0.0:8011`
|
||||
3. **默认值**:内置默认配置
|
||||
|
||||
| 配置项 | 环境变量 | 说明 | 默认值 |
|
||||
| --------------------- | ------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| address | SUB2CLASH_ADDRESS | 服务监听地址 | `0.0.0.0:8011` |
|
||||
| meta_template | SUB2CLASH_META_TEMPLATE | 默认 meta 模板 URL | `https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml` |
|
||||
| clash_template | SUB2CLASH_CLASH_TEMPLATE | 默认 clash 模板 URL | `https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml` |
|
||||
| request_retry_times | SUB2CLASH_REQUEST_RETRY_TIMES | 请求重试次数 | `3` |
|
||||
| request_max_file_size | SUB2CLASH_REQUEST_MAX_FILE_SIZE | 请求文件最大大小(byte) | `1048576` |
|
||||
| cache_expire | SUB2CLASH_CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
|
||||
| log_level | SUB2CLASH_LOG_LEVEL | 日志等级:`debug`,`info`,`warn`,`error` | `info` |
|
||||
| short_link_length | SUB2CLASH_SHORT_LINK_LENGTH | 短链长度 | `6` |
|
||||
|
||||
#### 配置文件示例
|
||||
|
||||
参考示例文件:
|
||||
|
||||
- [config.example.yaml](./config.example.yaml) - YAML 格式
|
||||
- [config.example.json](./config.example.json) - JSON 格式
|
||||
|
||||
### API
|
||||
|
||||
@ -59,3 +74,12 @@
|
||||
|
||||
- [Clash](./templates/template_clash.yaml)
|
||||
- [Clash.Meta](./templates/template_meta.yaml)
|
||||
|
||||
## 开发
|
||||
|
||||
### 添加新协议支持
|
||||
|
||||
添加新协议支持需要实现以下组件:
|
||||
|
||||
1. 在 `parser` 目录下实现协议解析器,用于解析订阅链接
|
||||
2. 在 `model/proxy` 目录下定义协议结构体和对应的序列化器,如有新增属性需在 `model/proxy/proxy.go` 中声明
|
||||
|
@ -1,43 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sub2clash/config"
|
||||
"sub2clash/model"
|
||||
"sub2clash/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func SubmodHandler(c *gin.Context) {
|
||||
// 从请求中获取参数
|
||||
query, err := validator.ParseQuery(c)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
sub, err := BuildSub(model.Clash, query, config.Default.ClashTemplate)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
// 输出
|
||||
if query.NodeListMode {
|
||||
nodelist := model.NodeList{}
|
||||
nodelist.Proxies = sub.Proxies
|
||||
marshal, err := yaml.Marshal(nodelist)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
return
|
||||
}
|
||||
marshal, err := yaml.Marshal(sub)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
}
|
@ -1,290 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/common"
|
||||
"sub2clash/logger"
|
||||
"sub2clash/model"
|
||||
"sub2clash/parser"
|
||||
"sub2clash/validator"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func BuildSub(clashType model.ClashType, query validator.SubValidator, template string) (
|
||||
*model.Subscription, error,
|
||||
) {
|
||||
// 定义变量
|
||||
var temp = &model.Subscription{}
|
||||
var sub = &model.Subscription{}
|
||||
var err error
|
||||
var templateBytes []byte
|
||||
// 加载模板
|
||||
if query.Template != "" {
|
||||
template = query.Template
|
||||
}
|
||||
if strings.HasPrefix(template, "http") {
|
||||
templateBytes, err = common.LoadSubscription(template, query.Refresh)
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load template failed", zap.String("template", template), zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载模板失败: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
unescape, err := url.QueryUnescape(template)
|
||||
if err != nil {
|
||||
return nil, errors.New("加载模板失败: " + err.Error())
|
||||
}
|
||||
templateBytes, err = common.LoadTemplate(unescape)
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load template failed", zap.String("template", template), zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载模板失败: " + err.Error())
|
||||
}
|
||||
}
|
||||
// 解析模板
|
||||
err = yaml.Unmarshal(templateBytes, &temp)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("parse template failed", zap.Error(err))
|
||||
return nil, errors.New("解析模板失败: " + err.Error())
|
||||
}
|
||||
var proxyList []model.Proxy
|
||||
// 加载订阅
|
||||
for i := range query.Subs {
|
||||
data, err := common.LoadSubscription(query.Subs[i], query.Refresh)
|
||||
subName := ""
|
||||
if strings.Contains(query.Subs[i], "#") {
|
||||
subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:]
|
||||
}
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载订阅失败: " + err.Error())
|
||||
}
|
||||
// 解析订阅
|
||||
err = yaml.Unmarshal(data, &sub)
|
||||
var newProxies []model.Proxy
|
||||
if err != nil {
|
||||
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria|hy2|hysteria2)://")
|
||||
if reg.Match(data) {
|
||||
p := common.ParseProxy(strings.Split(string(data), "\n")...)
|
||||
newProxies = p
|
||||
} else {
|
||||
// 如果无法直接解析,尝试Base64解码
|
||||
base64, err := parser.DecodeBase64(string(data))
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"parse subscription failed", zap.String("url", query.Subs[i]),
|
||||
zap.String("data", string(data)),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载订阅失败: " + err.Error())
|
||||
}
|
||||
p := common.ParseProxy(strings.Split(base64, "\n")...)
|
||||
newProxies = p
|
||||
}
|
||||
} else {
|
||||
newProxies = sub.Proxies
|
||||
}
|
||||
if subName != "" {
|
||||
for i := range newProxies {
|
||||
newProxies[i].SubName = subName
|
||||
}
|
||||
}
|
||||
proxyList = append(proxyList, newProxies...)
|
||||
}
|
||||
// 添加自定义节点
|
||||
if len(query.Proxies) != 0 {
|
||||
proxyList = append(proxyList, common.ParseProxy(query.Proxies...)...)
|
||||
}
|
||||
// 给节点添加订阅名称
|
||||
for i := range proxyList {
|
||||
if proxyList[i].SubName != "" {
|
||||
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
|
||||
}
|
||||
}
|
||||
// 去掉配置相同的节点
|
||||
proxies := make(map[string]*model.Proxy)
|
||||
newProxies := make([]model.Proxy, 0, len(proxyList))
|
||||
for i := range proxyList {
|
||||
key := proxyList[i].Server + strconv.Itoa(proxyList[i].Port) + proxyList[i].Type + proxyList[i].UUID + proxyList[i].Password
|
||||
if _, exist := proxies[key]; !exist {
|
||||
proxies[key] = &proxyList[i]
|
||||
newProxies = append(newProxies, proxyList[i])
|
||||
}
|
||||
}
|
||||
proxyList = newProxies
|
||||
// 删除节点
|
||||
if strings.TrimSpace(query.Remove) != "" {
|
||||
newProxyList := make([]model.Proxy, 0, len(proxyList))
|
||||
for i := range proxyList {
|
||||
removeReg, err := regexp.Compile(query.Remove)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
|
||||
return nil, errors.New("remove 参数非法: " + err.Error())
|
||||
}
|
||||
// 删除匹配到的节点
|
||||
if removeReg.MatchString(proxyList[i].Name) {
|
||||
continue // 如果匹配到要删除的元素,跳过该元素,不添加到新切片中
|
||||
}
|
||||
newProxyList = append(newProxyList, proxyList[i]) // 将要保留的元素添加到新切片中
|
||||
}
|
||||
proxyList = newProxyList
|
||||
}
|
||||
// 重命名
|
||||
if len(query.ReplaceKeys) != 0 {
|
||||
// 创建重命名正则表达式
|
||||
replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys))
|
||||
for _, v := range query.ReplaceKeys {
|
||||
replaceReg, err := regexp.Compile(v)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
|
||||
return nil, errors.New("replace 参数非法: " + err.Error())
|
||||
}
|
||||
replaceRegs = append(replaceRegs, replaceReg)
|
||||
}
|
||||
for i := range proxyList {
|
||||
// 重命名匹配到的节点
|
||||
for j, v := range replaceRegs {
|
||||
if v.MatchString(proxyList[i].Name) {
|
||||
proxyList[i].Name = v.ReplaceAllString(
|
||||
proxyList[i].Name, query.ReplaceTo[j],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 重名检测
|
||||
names := make(map[string]int)
|
||||
for i := range proxyList {
|
||||
if _, exist := names[proxyList[i].Name]; exist {
|
||||
names[proxyList[i].Name] = names[proxyList[i].Name] + 1
|
||||
proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
|
||||
} else {
|
||||
names[proxyList[i].Name] = 0
|
||||
}
|
||||
}
|
||||
// trim
|
||||
for i := range proxyList {
|
||||
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
|
||||
}
|
||||
// 将新增节点都添加到临时变量 t 中,防止策略组排序错乱
|
||||
var t = &model.Subscription{}
|
||||
common.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
|
||||
// 排序策略组
|
||||
switch query.Sort {
|
||||
case "sizeasc":
|
||||
sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups))
|
||||
case "sizedesc":
|
||||
sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroups)))
|
||||
case "nameasc":
|
||||
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
|
||||
case "namedesc":
|
||||
sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroups)))
|
||||
default:
|
||||
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
|
||||
}
|
||||
// 合并新节点和模板
|
||||
MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup)
|
||||
// 处理自定义规则
|
||||
for _, v := range query.Rules {
|
||||
if v.Prepend {
|
||||
common.PrependRules(temp, v.Rule)
|
||||
} else {
|
||||
common.AppendRules(temp, v.Rule)
|
||||
}
|
||||
}
|
||||
// 处理自定义 ruleProvider
|
||||
for _, v := range query.RuleProviders {
|
||||
hash := sha256.Sum224([]byte(v.Url))
|
||||
name := hex.EncodeToString(hash[:])
|
||||
provider := model.RuleProvider{
|
||||
Type: "http",
|
||||
Behavior: v.Behavior,
|
||||
Url: v.Url,
|
||||
Path: "./" + name + ".yaml",
|
||||
Interval: 3600,
|
||||
}
|
||||
if v.Prepend {
|
||||
common.PrependRuleProvider(
|
||||
temp, v.Name, v.Group, provider,
|
||||
)
|
||||
} else {
|
||||
common.AppenddRuleProvider(
|
||||
temp, v.Name, v.Group, provider,
|
||||
)
|
||||
}
|
||||
}
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {
|
||||
// 只合并节点、策略组
|
||||
// 统计所有国家策略组名称
|
||||
var countryGroupNames []string
|
||||
for _, proxyGroup := range sub.ProxyGroups {
|
||||
if proxyGroup.IsCountryGrop {
|
||||
countryGroupNames = append(
|
||||
countryGroupNames, proxyGroup.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
var proxyNames []string
|
||||
for _, proxy := range sub.Proxies {
|
||||
proxyNames = append(proxyNames, proxy.Name)
|
||||
}
|
||||
// 将订阅中的节点添加到模板中
|
||||
temp.Proxies = append(temp.Proxies, sub.Proxies...)
|
||||
// 将订阅中的策略组添加到模板中
|
||||
for i := range temp.ProxyGroups {
|
||||
if temp.ProxyGroups[i].IsCountryGrop {
|
||||
continue
|
||||
}
|
||||
newProxies := make([]string, 0)
|
||||
countryGroupMap := make(map[string]model.ProxyGroup)
|
||||
for _, v := range sub.ProxyGroups {
|
||||
if v.IsCountryGrop {
|
||||
countryGroupMap[v.Name] = v
|
||||
}
|
||||
}
|
||||
for j := range temp.ProxyGroups[i].Proxies {
|
||||
reg := regexp.MustCompile("<(.*?)>")
|
||||
if reg.Match([]byte(temp.ProxyGroups[i].Proxies[j])) {
|
||||
key := reg.FindStringSubmatch(temp.ProxyGroups[i].Proxies[j])[1]
|
||||
switch key {
|
||||
case "all":
|
||||
newProxies = append(newProxies, proxyNames...)
|
||||
case "countries":
|
||||
if !igcg {
|
||||
newProxies = append(newProxies, countryGroupNames...)
|
||||
}
|
||||
default:
|
||||
if !igcg {
|
||||
if len(key) == 2 {
|
||||
newProxies = append(
|
||||
newProxies, countryGroupMap[common.GetContryName(key)].Proxies...,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j])
|
||||
}
|
||||
}
|
||||
temp.ProxyGroups[i].Proxies = newProxies
|
||||
}
|
||||
if !igcg {
|
||||
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"sub2clash/config"
|
||||
"sub2clash/model"
|
||||
"sub2clash/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func SubHandler(c *gin.Context) {
|
||||
// 从请求中获取参数
|
||||
query, err := validator.ParseQuery(c)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
sub, err := BuildSub(model.ClashMeta, query, config.Default.MetaTemplate)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
// 输出
|
||||
if query.NodeListMode {
|
||||
nodelist := model.NodeList{}
|
||||
nodelist.Proxies = sub.Proxies
|
||||
marshal, err := yaml.Marshal(nodelist)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
return
|
||||
}
|
||||
marshal, err := yaml.Marshal(sub)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sub2clash/common"
|
||||
"sub2clash/common/database"
|
||||
"sub2clash/config"
|
||||
"sub2clash/model"
|
||||
"sub2clash/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func respondWithError(c *gin.Context, code int, message string) {
|
||||
c.String(code, message)
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
func GenerateLinkHandler(c *gin.Context) {
|
||||
var params validator.ShortLinkGenValidator
|
||||
if err := c.ShouldBind(¶ms); err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(params.Url) == "" {
|
||||
respondWithError(c, http.StatusBadRequest, "URL 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := generateUniqueHash()
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "生成短链接失败")
|
||||
return
|
||||
}
|
||||
|
||||
shortLink := model.ShortLink{
|
||||
Hash: hash,
|
||||
Url: params.Url,
|
||||
Password: params.Password,
|
||||
}
|
||||
|
||||
if err := database.SaveShortLink(&shortLink); err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "数据库错误")
|
||||
return
|
||||
}
|
||||
|
||||
if params.Password != "" {
|
||||
hash += "?password=" + params.Password
|
||||
}
|
||||
c.String(http.StatusOK, hash)
|
||||
}
|
||||
|
||||
func generateUniqueHash() (string, error) {
|
||||
for {
|
||||
hash := common.RandomString(config.Default.ShortLinkLength)
|
||||
exists, err := database.CheckShortLinkHashExists(hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !exists {
|
||||
return hash, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateLinkHandler(c *gin.Context) {
|
||||
var params validator.ShortLinkUpdateValidator
|
||||
if err := c.ShouldBindJSON(¶ms); 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")
|
||||
password := c.Query("password")
|
||||
|
||||
if strings.TrimSpace(hash) == "" {
|
||||
c.String(http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 查询数据库中的短链接
|
||||
shortLink, err := database.FindShortLinkByHash(hash)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "未找到短链接或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 校验密码
|
||||
if shortLink.Password != "" && shortLink.Password != password {
|
||||
c.String(http.StatusNotFound, "未找到短链接或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新最后访问时间
|
||||
shortLink.LastRequestTime = time.Now().Unix()
|
||||
err = database.SaveShortLink(shortLink)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "数据库错误")
|
||||
return
|
||||
}
|
||||
// 请求短链接指向的URL
|
||||
response, err := http.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "请求错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// 读取响应内容
|
||||
all, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "读取错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应内容
|
||||
c.String(http.StatusOK, string(all))
|
||||
}
|
@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
|
||||
"sub2clash/model"
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
@ -13,16 +13,14 @@ import (
|
||||
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
|
||||
|
@ -1,38 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"sub2clash/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Get(url string) (resp *http.Response, err error) {
|
||||
retryTimes := config.Default.RequestRetryTimes
|
||||
haveTried := 0
|
||||
retryDelay := time.Second // 延迟1秒再重试
|
||||
for haveTried < retryTimes {
|
||||
client := &http.Client{}
|
||||
//client.Timeout = time.Second * 10
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
haveTried++
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
}
|
||||
get, err := client.Do(req)
|
||||
if err != nil {
|
||||
haveTried++
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
} else {
|
||||
// 如果文件大小大于设定,直接返回错误
|
||||
if get != nil && get.ContentLength > config.Default.RequestMaxFileSize {
|
||||
return nil, errors.New("文件过大")
|
||||
}
|
||||
return get, nil
|
||||
}
|
||||
|
||||
}
|
||||
return nil, err
|
||||
}
|
@ -20,9 +20,6 @@ func MkEssentialDir() error {
|
||||
if err := MKDir("subs"); err != nil {
|
||||
return errors.New("create subs dir failed" + err.Error())
|
||||
}
|
||||
if err := MKDir("templates"); err != nil {
|
||||
return errors.New("create templates dir failed" + err.Error())
|
||||
}
|
||||
if err := MKDir("logs"); err != nil {
|
||||
return errors.New("create logs dir failed" + err.Error())
|
||||
}
|
||||
|
@ -2,16 +2,13 @@ package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sub2clash/constant"
|
||||
"sub2clash/logger"
|
||||
"sub2clash/model"
|
||||
"sub2clash/parser"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
"github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
func GetContryName(countryKey string) string {
|
||||
// 创建一个切片包含所有的国家映射
|
||||
|
||||
countryMaps := []map[string]string{
|
||||
model.CountryFlag,
|
||||
model.CountryChineseName,
|
||||
@ -19,11 +16,9 @@ func GetContryName(countryKey string) string {
|
||||
model.CountryEnglishName,
|
||||
}
|
||||
|
||||
// 对每一个映射进行检查
|
||||
for i, countryMap := range countryMaps {
|
||||
if i == 2 {
|
||||
// 对ISO匹配做特殊处理
|
||||
// 根据常用分割字符分割字符串
|
||||
|
||||
splitChars := []string{"-", "_", " "}
|
||||
key := make([]string, 0)
|
||||
for _, splitChar := range splitChars {
|
||||
@ -34,9 +29,9 @@ func GetContryName(countryKey string) string {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 对每一个分割后的字符串进行检查
|
||||
|
||||
for _, v := range key {
|
||||
// 如果匹配到了国家
|
||||
|
||||
if country, ok := countryMap[strings.ToUpper(v)]; ok {
|
||||
return country
|
||||
}
|
||||
@ -53,10 +48,10 @@ func GetContryName(countryKey string) string {
|
||||
|
||||
func AddProxy(
|
||||
sub *model.Subscription, autotest bool,
|
||||
lazy bool, clashType model.ClashType, proxies ...model.Proxy,
|
||||
lazy bool, clashType model.ClashType, proxies ...proxy.Proxy,
|
||||
) {
|
||||
proxyTypes := model.GetSupportProxyTypes(clashType)
|
||||
// 添加节点
|
||||
|
||||
for _, proxy := range proxies {
|
||||
if !proxyTypes[proxy.Type] {
|
||||
continue
|
||||
@ -99,43 +94,3 @@ func AddProxy(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ParseProxy(proxies ...string) []model.Proxy {
|
||||
var result []model.Proxy
|
||||
for _, proxy := range proxies {
|
||||
if proxy != "" {
|
||||
var proxyItem model.Proxy
|
||||
var err error
|
||||
// 解析节点
|
||||
if strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
|
||||
proxyItem, err = parser.ParseShadowsocks(proxy)
|
||||
}
|
||||
if strings.HasPrefix(proxy, constant.TrojanPrefix) {
|
||||
proxyItem, err = parser.ParseTrojan(proxy)
|
||||
}
|
||||
if strings.HasPrefix(proxy, constant.VMessPrefix) {
|
||||
proxyItem, err = parser.ParseVmess(proxy)
|
||||
}
|
||||
if strings.HasPrefix(proxy, constant.VLESSPrefix) {
|
||||
proxyItem, err = parser.ParseVless(proxy)
|
||||
}
|
||||
if strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) {
|
||||
proxyItem, err = parser.ParseShadowsocksR(proxy)
|
||||
}
|
||||
if strings.HasPrefix(proxy, constant.Hysteria2Prefix1) || strings.HasPrefix(proxy, constant.Hysteria2Prefix2) {
|
||||
proxyItem, err = parser.ParseHysteria2(proxy)
|
||||
}
|
||||
if strings.HasPrefix(proxy, constant.HysteriaPrefix) {
|
||||
proxyItem, err = parser.ParseHysteria(proxy)
|
||||
}
|
||||
if err == nil {
|
||||
result = append(result, proxyItem)
|
||||
} else {
|
||||
logger.Logger.Debug(
|
||||
"parse proxy failed", zap.String("proxy", proxy), zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package common
|
||||
import "math/rand"
|
||||
|
||||
func RandomString(length int) string {
|
||||
// 生成随机字符串
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var result []byte
|
||||
for i := 0; i < length; i++ {
|
||||
|
12
common/request.go
Normal file
12
common/request.go
Normal file
@ -0,0 +1,12 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
func Request(retryTimes int) *resty.Client {
|
||||
client := resty.New()
|
||||
client.
|
||||
SetRetryCount(retryTimes)
|
||||
return client
|
||||
}
|
@ -3,7 +3,8 @@ package common
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sub2clash/model"
|
||||
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
)
|
||||
|
||||
func PrependRuleProvider(
|
||||
|
324
common/sub.go
324
common/sub.go
@ -3,22 +3,33 @@ package common
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sub2clash/config"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bestnite/sub2clash/logger"
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
"github.com/bestnite/sub2clash/parser"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var subsDir = "subs"
|
||||
var fileLock sync.RWMutex
|
||||
|
||||
func LoadSubscription(url string, refresh bool) ([]byte, error) {
|
||||
func LoadSubscription(url string, refresh bool, userAgent string, cacheExpire int64, retryTimes int) ([]byte, error) {
|
||||
if refresh {
|
||||
return FetchSubscriptionFromAPI(url)
|
||||
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
|
||||
}
|
||||
hash := sha256.Sum224([]byte(url))
|
||||
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
|
||||
@ -27,10 +38,10 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
return FetchSubscriptionFromAPI(url)
|
||||
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
|
||||
}
|
||||
lastGetTime := stat.ModTime().Unix() // 单位是秒
|
||||
if lastGetTime+config.Default.CacheExpire > time.Now().Unix() {
|
||||
lastGetTime := stat.ModTime().Unix()
|
||||
if lastGetTime+cacheExpire > time.Now().Unix() {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -48,21 +59,18 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) {
|
||||
}
|
||||
return subContent, nil
|
||||
}
|
||||
return FetchSubscriptionFromAPI(url)
|
||||
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
|
||||
}
|
||||
|
||||
func FetchSubscriptionFromAPI(url string) ([]byte, error) {
|
||||
func FetchSubscriptionFromAPI(url string, userAgent string, retryTimes int) ([]byte, error) {
|
||||
hash := sha256.Sum224([]byte(url))
|
||||
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
|
||||
resp, err := Get(url)
|
||||
client := Request(retryTimes)
|
||||
defer client.Close()
|
||||
resp, err := client.R().SetHeader("User-Agent", userAgent).Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(resp *http.Response) {
|
||||
if resp != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
}(resp)
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
@ -84,3 +92,289 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func BuildSub(clashType model.ClashType, query model.SubConfig, template string, cacheExpire int64, retryTimes int) (
|
||||
*model.Subscription, error,
|
||||
) {
|
||||
var temp = &model.Subscription{}
|
||||
var sub = &model.Subscription{}
|
||||
var err error
|
||||
var templateBytes []byte
|
||||
|
||||
if query.Template != "" {
|
||||
template = query.Template
|
||||
}
|
||||
if strings.HasPrefix(template, "http") {
|
||||
templateBytes, err = LoadSubscription(template, query.Refresh, query.UserAgent, cacheExpire, retryTimes)
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load template failed", zap.String("template", template), zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载模板失败: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
unescape, err := url.QueryUnescape(template)
|
||||
if err != nil {
|
||||
return nil, errors.New("加载模板失败: " + err.Error())
|
||||
}
|
||||
templateBytes, err = LoadTemplate(unescape)
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load template failed", zap.String("template", template), zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载模板失败: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(templateBytes, &temp)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("parse template failed", zap.Error(err))
|
||||
return nil, errors.New("解析模板失败: " + err.Error())
|
||||
}
|
||||
var proxyList []P.Proxy
|
||||
|
||||
for i := range query.Subs {
|
||||
data, err := LoadSubscription(query.Subs[i], query.Refresh, query.UserAgent, cacheExpire, retryTimes)
|
||||
subName := ""
|
||||
if strings.Contains(query.Subs[i], "#") {
|
||||
subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:]
|
||||
}
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载订阅失败: " + err.Error())
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(data, &sub)
|
||||
var newProxies []P.Proxy
|
||||
if err != nil {
|
||||
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria|hy2|hysteria2)://")
|
||||
if reg.Match(data) {
|
||||
p := parser.ParseProxies(strings.Split(string(data), "\n")...)
|
||||
newProxies = p
|
||||
} else {
|
||||
base64, err := parser.DecodeBase64(string(data))
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"parse subscription failed", zap.String("url", query.Subs[i]),
|
||||
zap.String("data", string(data)),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载订阅失败: " + err.Error())
|
||||
}
|
||||
p := parser.ParseProxies(strings.Split(base64, "\n")...)
|
||||
newProxies = p
|
||||
}
|
||||
} else {
|
||||
newProxies = sub.Proxies
|
||||
}
|
||||
if subName != "" {
|
||||
for i := range newProxies {
|
||||
newProxies[i].SubName = subName
|
||||
}
|
||||
}
|
||||
proxyList = append(proxyList, newProxies...)
|
||||
}
|
||||
|
||||
if len(query.Proxies) != 0 {
|
||||
proxyList = append(proxyList, parser.ParseProxies(query.Proxies...)...)
|
||||
}
|
||||
|
||||
for i := range proxyList {
|
||||
if proxyList[i].SubName != "" {
|
||||
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
|
||||
}
|
||||
}
|
||||
|
||||
proxies := make(map[string]*P.Proxy)
|
||||
newProxies := make([]P.Proxy, 0, len(proxyList))
|
||||
for i := range proxyList {
|
||||
key := proxyList[i].Server + strconv.Itoa(proxyList[i].Port) + proxyList[i].Type + proxyList[i].UUID + proxyList[i].Password
|
||||
if proxyList[i].Network == "ws" {
|
||||
key += proxyList[i].WSOpts.Path + proxyList[i].WSOpts.Headers["Host"]
|
||||
}
|
||||
if _, exist := proxies[key]; !exist {
|
||||
proxies[key] = &proxyList[i]
|
||||
newProxies = append(newProxies, proxyList[i])
|
||||
}
|
||||
}
|
||||
proxyList = newProxies
|
||||
|
||||
if strings.TrimSpace(query.Remove) != "" {
|
||||
newProxyList := make([]P.Proxy, 0, len(proxyList))
|
||||
for i := range proxyList {
|
||||
removeReg, err := regexp.Compile(query.Remove)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
|
||||
return nil, errors.New("remove 参数非法: " + err.Error())
|
||||
}
|
||||
|
||||
if removeReg.MatchString(proxyList[i].Name) {
|
||||
continue
|
||||
}
|
||||
newProxyList = append(newProxyList, proxyList[i])
|
||||
}
|
||||
proxyList = newProxyList
|
||||
}
|
||||
|
||||
if len(query.ReplaceKeys) != 0 {
|
||||
|
||||
replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys))
|
||||
for _, v := range query.ReplaceKeys {
|
||||
replaceReg, err := regexp.Compile(v)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
|
||||
return nil, errors.New("replace 参数非法: " + err.Error())
|
||||
}
|
||||
replaceRegs = append(replaceRegs, replaceReg)
|
||||
}
|
||||
for i := range proxyList {
|
||||
|
||||
for j, v := range replaceRegs {
|
||||
if v.MatchString(proxyList[i].Name) {
|
||||
proxyList[i].Name = v.ReplaceAllString(
|
||||
proxyList[i].Name, query.ReplaceTo[j],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
names := make(map[string]int)
|
||||
for i := range proxyList {
|
||||
if _, exist := names[proxyList[i].Name]; exist {
|
||||
names[proxyList[i].Name] = names[proxyList[i].Name] + 1
|
||||
proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
|
||||
} else {
|
||||
names[proxyList[i].Name] = 0
|
||||
}
|
||||
}
|
||||
|
||||
for i := range proxyList {
|
||||
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
|
||||
}
|
||||
|
||||
var t = &model.Subscription{}
|
||||
AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
|
||||
|
||||
switch query.Sort {
|
||||
case "sizeasc":
|
||||
sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups))
|
||||
case "sizedesc":
|
||||
sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroups)))
|
||||
case "nameasc":
|
||||
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
|
||||
case "namedesc":
|
||||
sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroups)))
|
||||
default:
|
||||
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
|
||||
}
|
||||
|
||||
MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup)
|
||||
|
||||
for _, v := range query.Rules {
|
||||
if v.Prepend {
|
||||
PrependRules(temp, v.Rule)
|
||||
} else {
|
||||
AppendRules(temp, v.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range query.RuleProviders {
|
||||
hash := sha256.Sum224([]byte(v.Url))
|
||||
name := hex.EncodeToString(hash[:])
|
||||
provider := model.RuleProvider{
|
||||
Type: "http",
|
||||
Behavior: v.Behavior,
|
||||
Url: v.Url,
|
||||
Path: "./" + name + ".yaml",
|
||||
Interval: 3600,
|
||||
}
|
||||
if v.Prepend {
|
||||
PrependRuleProvider(
|
||||
temp, v.Name, v.Group, provider,
|
||||
)
|
||||
} else {
|
||||
AppenddRuleProvider(
|
||||
temp, v.Name, v.Group, provider,
|
||||
)
|
||||
}
|
||||
}
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
func FetchSubscriptionUserInfo(url string, userAgent string, retryTimes int) (string, error) {
|
||||
client := Request(retryTimes)
|
||||
defer client.Close()
|
||||
resp, err := client.R().SetHeader("User-Agent", userAgent).Head(url)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if userInfo := resp.Header().Get("subscription-userinfo"); userInfo != "" {
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
logger.Logger.Debug("目标 URL 未返回 subscription-userinfo 头", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {
|
||||
var countryGroupNames []string
|
||||
for _, proxyGroup := range sub.ProxyGroups {
|
||||
if proxyGroup.IsCountryGrop {
|
||||
countryGroupNames = append(
|
||||
countryGroupNames, proxyGroup.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
var proxyNames []string
|
||||
for _, proxy := range sub.Proxies {
|
||||
proxyNames = append(proxyNames, proxy.Name)
|
||||
}
|
||||
|
||||
temp.Proxies = append(temp.Proxies, sub.Proxies...)
|
||||
|
||||
for i := range temp.ProxyGroups {
|
||||
if temp.ProxyGroups[i].IsCountryGrop {
|
||||
continue
|
||||
}
|
||||
newProxies := make([]string, 0)
|
||||
countryGroupMap := make(map[string]model.ProxyGroup)
|
||||
for _, v := range sub.ProxyGroups {
|
||||
if v.IsCountryGrop {
|
||||
countryGroupMap[v.Name] = v
|
||||
}
|
||||
}
|
||||
for j := range temp.ProxyGroups[i].Proxies {
|
||||
reg := regexp.MustCompile("<(.*?)>")
|
||||
if reg.Match([]byte(temp.ProxyGroups[i].Proxies[j])) {
|
||||
key := reg.FindStringSubmatch(temp.ProxyGroups[i].Proxies[j])[1]
|
||||
switch key {
|
||||
case "all":
|
||||
newProxies = append(newProxies, proxyNames...)
|
||||
case "countries":
|
||||
if !igcg {
|
||||
newProxies = append(newProxies, countryGroupNames...)
|
||||
}
|
||||
default:
|
||||
if !igcg {
|
||||
if len(key) == 2 {
|
||||
newProxies = append(
|
||||
newProxies, countryGroupMap[GetContryName(key)].Proxies...,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j])
|
||||
}
|
||||
}
|
||||
temp.ProxyGroups[i].Proxies = newProxies
|
||||
}
|
||||
if !igcg {
|
||||
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
|
||||
}
|
||||
}
|
||||
|
@ -4,15 +4,11 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// LoadTemplate 加载模板
|
||||
// templates 模板文件名
|
||||
func LoadTemplate(template string) ([]byte, error) {
|
||||
tPath := filepath.Join("templates", template)
|
||||
if _, err := os.Stat(tPath); err == nil {
|
||||
file, err := os.Open(tPath)
|
||||
func LoadTemplate(templatePath string) ([]byte, error) {
|
||||
if _, err := os.Stat(templatePath); err == nil {
|
||||
file, err := os.Open(templatePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sub2clash/config"
|
||||
)
|
||||
|
||||
func writeTemplate(path string, template string) error {
|
||||
tPath := filepath.Join(
|
||||
"templates", path,
|
||||
)
|
||||
if _, err := os.Stat(tPath); os.IsNotExist(err) {
|
||||
file, err := os.Create(tPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
if file != nil {
|
||||
_ = file.Close()
|
||||
}
|
||||
}(file)
|
||||
_, err = file.WriteString(template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteDefalutTemplate(templateMeta string, templateClash string) error {
|
||||
if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeTemplate(config.Default.ClashTemplate, templateClash); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
11
compose.yml
Normal file
11
compose.yml
Normal file
@ -0,0 +1,11 @@
|
||||
name: sub2clash
|
||||
services:
|
||||
sub2clash:
|
||||
restart: unless-stopped
|
||||
image: nite07/sub2clash:latest
|
||||
ports:
|
||||
- "8011:8011"
|
||||
volumes:
|
||||
# - ./logs:/app/logs
|
||||
# - ./templates:/app/templates
|
||||
- ./data:/app/data
|
10
config.example.json
Normal file
10
config.example.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"address": "0.0.0.0:8011",
|
||||
"meta_template": "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml",
|
||||
"clash_template": "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml",
|
||||
"request_retry_times": 3,
|
||||
"request_max_file_size": 1048576,
|
||||
"cache_expire": 300,
|
||||
"log_level": "info",
|
||||
"short_link_length": 6
|
||||
}
|
22
config.example.yaml
Normal file
22
config.example.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
# Sub2Clash 配置文件示例
|
||||
# 复制此文件为 config.yaml 并根据需要修改配置
|
||||
|
||||
# 服务端口
|
||||
address: "0.0.0.0:8011"
|
||||
|
||||
# 模板配置
|
||||
meta_template: "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml"
|
||||
clash_template: "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml"
|
||||
|
||||
# 请求配置
|
||||
request_retry_times: 3
|
||||
request_max_file_size: 1048576 # 1MB in bytes
|
||||
|
||||
# 缓存配置 (秒)
|
||||
cache_expire: 300 # 5 minutes
|
||||
|
||||
# 日志级别 (debug, info, warn, error)
|
||||
log_level: "info"
|
||||
|
||||
# 短链接长度
|
||||
short_link_length: 6
|
130
config/config.go
130
config/config.go
@ -1,90 +1,76 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port int
|
||||
MetaTemplate string
|
||||
ClashTemplate string
|
||||
RequestRetryTimes int
|
||||
RequestMaxFileSize int64
|
||||
CacheExpire int64
|
||||
LogLevel string
|
||||
//BasePath string
|
||||
ShortLinkLength int
|
||||
Address string `mapstructure:"address"`
|
||||
MetaTemplate string `mapstructure:"meta_template"`
|
||||
ClashTemplate string `mapstructure:"clash_template"`
|
||||
RequestRetryTimes int `mapstructure:"request_retry_times"`
|
||||
RequestMaxFileSize int64 `mapstructure:"request_max_file_size"`
|
||||
CacheExpire int64 `mapstructure:"cache_expire"`
|
||||
LogLevel string `mapstructure:"log_level"`
|
||||
ShortLinkLength int `mapstructure:"short_link_length"`
|
||||
}
|
||||
|
||||
var Default *Config
|
||||
var GlobalConfig *Config
|
||||
var Dev string
|
||||
|
||||
func LoadConfig() error {
|
||||
Default = &Config{
|
||||
MetaTemplate: "template_meta.yaml",
|
||||
ClashTemplate: "template_clash.yaml",
|
||||
RequestRetryTimes: 3,
|
||||
RequestMaxFileSize: 1024 * 1024 * 1,
|
||||
Port: 8011,
|
||||
CacheExpire: 60 * 5,
|
||||
LogLevel: "info",
|
||||
//BasePath: "/",
|
||||
ShortLinkLength: 6,
|
||||
}
|
||||
_ = godotenv.Load()
|
||||
if os.Getenv("PORT") != "" {
|
||||
atoi, err := strconv.Atoi(os.Getenv("PORT"))
|
||||
if err != nil {
|
||||
return errors.New("PORT invalid")
|
||||
v := viper.New()
|
||||
|
||||
// 添加配置文件搜索路径
|
||||
v.AddConfigPath(".")
|
||||
v.AddConfigPath("./config")
|
||||
v.AddConfigPath("/etc/sub2clash/")
|
||||
|
||||
// 设置默认值
|
||||
setDefaults(v)
|
||||
|
||||
// 设置环境变量前缀和自动绑定
|
||||
v.SetEnvPrefix("SUB2CLASH")
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
// 尝试按优先级加载不同格式的配置文件
|
||||
configLoaded := false
|
||||
configNames := []string{"config", "sub2clash"}
|
||||
configExts := []string{"yaml", "yml", "json"}
|
||||
|
||||
for _, name := range configNames {
|
||||
for _, ext := range configExts {
|
||||
v.SetConfigName(name)
|
||||
v.SetConfigType(ext)
|
||||
if err := v.ReadInConfig(); err == nil {
|
||||
configLoaded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
Default.Port = atoi
|
||||
}
|
||||
if os.Getenv("META_TEMPLATE") != "" {
|
||||
Default.MetaTemplate = os.Getenv("META_TEMPLATE")
|
||||
}
|
||||
if os.Getenv("CLASH_TEMPLATE") != "" {
|
||||
Default.ClashTemplate = os.Getenv("CLASH_TEMPLATE")
|
||||
}
|
||||
if os.Getenv("REQUEST_RETRY_TIMES") != "" {
|
||||
atoi, err := strconv.Atoi(os.Getenv("REQUEST_RETRY_TIMES"))
|
||||
if err != nil {
|
||||
return errors.New("REQUEST_RETRY_TIMES invalid")
|
||||
if configLoaded {
|
||||
break
|
||||
}
|
||||
Default.RequestRetryTimes = atoi
|
||||
}
|
||||
if os.Getenv("REQUEST_MAX_FILE_SIZE") != "" {
|
||||
atoi, err := strconv.Atoi(os.Getenv("REQUEST_MAX_FILE_SIZE"))
|
||||
if err != nil {
|
||||
return errors.New("REQUEST_MAX_FILE_SIZE invalid")
|
||||
}
|
||||
Default.RequestMaxFileSize = int64(atoi)
|
||||
}
|
||||
if os.Getenv("CACHE_EXPIRE") != "" {
|
||||
atoi, err := strconv.Atoi(os.Getenv("CACHE_EXPIRE"))
|
||||
if err != nil {
|
||||
return errors.New("CACHE_EXPIRE invalid")
|
||||
}
|
||||
Default.CacheExpire = int64(atoi)
|
||||
}
|
||||
if os.Getenv("LOG_LEVEL") != "" {
|
||||
Default.LogLevel = os.Getenv("LOG_LEVEL")
|
||||
}
|
||||
//if os.Getenv("BASE_PATH") != "" {
|
||||
// Default.BasePath = os.Getenv("BASE_PATH")
|
||||
// if Default.BasePath[len(Default.BasePath)-1] != '/' {
|
||||
// Default.BasePath += "/"
|
||||
// }
|
||||
//}
|
||||
if os.Getenv("SHORT_LINK_LENGTH") != "" {
|
||||
atoi, err := strconv.Atoi(os.Getenv("SHORT_LINK_LENGTH"))
|
||||
if err != nil {
|
||||
return errors.New("SHORT_LINK_LENGTH invalid")
|
||||
}
|
||||
Default.ShortLinkLength = atoi
|
||||
|
||||
// 将配置解析到结构体
|
||||
GlobalConfig = &Config{}
|
||||
if err := v.Unmarshal(GlobalConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setDefaults(v *viper.Viper) {
|
||||
v.SetDefault("address", "0.0.0.0:8011")
|
||||
v.SetDefault("meta_template", "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml")
|
||||
v.SetDefault("clash_template", "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml")
|
||||
v.SetDefault("request_retry_times", 3)
|
||||
v.SetDefault("request_max_file_size", 1024*1024*1)
|
||||
v.SetDefault("cache_expire", 60*5)
|
||||
v.SetDefault("log_level", "info")
|
||||
v.SetDefault("short_link_length", 6)
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
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://"
|
||||
)
|
@ -1,21 +0,0 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
sub2clash:
|
||||
container_name: sub2clash
|
||||
restart: unless-stopped
|
||||
image: nite07/sub2clash:latest
|
||||
ports:
|
||||
- "8011:8011"
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./templates:/app/templates
|
||||
- ./data:/app/data
|
||||
# environment:
|
||||
# - PORT=8011
|
||||
# - META_TEMPLATE=template_meta.yaml
|
||||
# - PROXY_TEMPLATE=template_clash.yaml
|
||||
# - REQUEST_RETRY_TIMES=3
|
||||
# - REQUEST_MAX_FILE_SIZE=1048576
|
||||
# - CACHE_EXPIRE=300
|
||||
# - LOG_LEVEL=info
|
@ -1,4 +1,4 @@
|
||||
package parser
|
||||
package error
|
||||
|
||||
type ParseError struct {
|
||||
Type ParseErrorType
|
40
go.mod
40
go.mod
@ -1,42 +1,52 @@
|
||||
module sub2clash
|
||||
module github.com/bestnite/sub2clash
|
||||
|
||||
go 1.21
|
||||
go 1.21.0
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/spf13/viper v1.20.1
|
||||
go.etcd.io/bbolt v1.3.9
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/text v0.21.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
resty.dev/v3 v3.0.0-beta.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.5 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.19.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // 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/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
)
|
||||
|
88
go.sum
88
go.sum
@ -1,42 +1,49 @@
|
||||
github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k=
|
||||
github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw=
|
||||
github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg=
|
||||
github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
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/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
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/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
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/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.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@ -46,22 +53,36 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
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.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.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.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.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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
@ -75,28 +96,31 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
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.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
|
||||
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
46
main.go
46
main.go
@ -3,43 +3,32 @@ package main
|
||||
import (
|
||||
_ "embed"
|
||||
"io"
|
||||
"strconv"
|
||||
"sub2clash/api"
|
||||
"sub2clash/common"
|
||||
"sub2clash/common/database"
|
||||
"sub2clash/config"
|
||||
"sub2clash/logger"
|
||||
|
||||
"github.com/bestnite/sub2clash/common"
|
||||
"github.com/bestnite/sub2clash/common/database"
|
||||
"github.com/bestnite/sub2clash/config"
|
||||
"github.com/bestnite/sub2clash/logger"
|
||||
"github.com/bestnite/sub2clash/server"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
//go:embed templates/template_meta.yaml
|
||||
var templateMeta string
|
||||
|
||||
//go:embed templates/template_clash.yaml
|
||||
var templateClash string
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
// 创建文件夹
|
||||
|
||||
err = common.MkEssentialDir()
|
||||
if err != nil {
|
||||
logger.Logger.Panic("create essential dir failed", zap.Error(err))
|
||||
}
|
||||
// 加载配置
|
||||
|
||||
err = config.LoadConfig()
|
||||
// 初始化日志
|
||||
logger.InitLogger(config.Default.LogLevel)
|
||||
|
||||
logger.InitLogger(config.GlobalConfig.LogLevel)
|
||||
if err != nil {
|
||||
logger.Logger.Panic("load config failed", zap.Error(err))
|
||||
}
|
||||
// 写入默认模板
|
||||
err = common.WriteDefalutTemplate(templateMeta, templateClash)
|
||||
if err != nil {
|
||||
logger.Logger.Panic("write default template failed", zap.Error(err))
|
||||
}
|
||||
// 连接数据库
|
||||
|
||||
err = database.ConnectDB()
|
||||
if err != nil {
|
||||
logger.Logger.Panic("database connect failed", zap.Error(err))
|
||||
@ -48,16 +37,15 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 设置运行模式
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
// 关闭 Gin 的日志输出
|
||||
|
||||
gin.DefaultWriter = io.Discard
|
||||
// 创建路由
|
||||
|
||||
r := gin.Default()
|
||||
// 设置路由
|
||||
api.SetRoute(r)
|
||||
logger.Logger.Info("server is running at http://localhost:" + strconv.Itoa(config.Default.Port))
|
||||
err := r.Run(":" + strconv.Itoa(config.Default.Port))
|
||||
|
||||
server.SetRoute(r)
|
||||
logger.Logger.Info("server is running at " + config.GlobalConfig.Address)
|
||||
err := r.Run(config.GlobalConfig.Address)
|
||||
if err != nil {
|
||||
logger.Logger.Error("server running failed", zap.Error(err))
|
||||
return
|
||||
|
@ -1,5 +1,7 @@
|
||||
package model
|
||||
|
||||
import "github.com/bestnite/sub2clash/parser"
|
||||
|
||||
type ClashType int
|
||||
|
||||
const (
|
||||
@ -8,24 +10,19 @@ const (
|
||||
)
|
||||
|
||||
func GetSupportProxyTypes(clashType ClashType) map[string]bool {
|
||||
if clashType == Clash {
|
||||
return map[string]bool{
|
||||
"ss": true,
|
||||
"ssr": true,
|
||||
"vmess": true,
|
||||
"trojan": true,
|
||||
supportProxyTypes := make(map[string]bool)
|
||||
|
||||
for _, parser := range parser.GetAllParsers() {
|
||||
if clashType == Clash {
|
||||
if parser.SupportClash() {
|
||||
supportProxyTypes[parser.GetType()] = true
|
||||
}
|
||||
} else if clashType == ClashMeta {
|
||||
if parser.SupportMeta() {
|
||||
supportProxyTypes[parser.GetType()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if clashType == ClashMeta {
|
||||
return map[string]bool{
|
||||
"ss": true,
|
||||
"ssr": true,
|
||||
"vmess": true,
|
||||
"trojan": true,
|
||||
"vless": true,
|
||||
"hysteria": true,
|
||||
"hysteria2": true,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return supportProxyTypes
|
||||
}
|
||||
|
@ -1,8 +1,5 @@
|
||||
package model
|
||||
|
||||
// https://zh.wikipedia.org/wiki/%E5%8C%BA%E5%9F%9F%E6%8C%87%E7%A4%BA%E7%AC%A6
|
||||
// https://zh.wikipedia.org/zh-sg/ISO_3166-1%E4%BA%8C%E4%BD%8D%E5%AD%97%E6%AF%8D%E4%BB%A3%E7%A0%81
|
||||
|
||||
var CountryEnglishName = map[string]string{
|
||||
"Andorra": "安道尔(AD)",
|
||||
"United Arab Emirates": "阿联酋(AE)",
|
||||
|
@ -1,12 +0,0 @@
|
||||
package model
|
||||
|
||||
type Tags []struct {
|
||||
Name string `json:"name"`
|
||||
ZipballUrl string `json:"zipball_url"`
|
||||
TarballUrl string `json:"tarball_url"`
|
||||
Commit struct {
|
||||
Sha string `json:"sha"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
NodeId string `json:"node_id"`
|
||||
}
|
@ -9,12 +9,12 @@ type ProxyGroup struct {
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Proxies []string `yaml:"proxies,omitempty"`
|
||||
IsCountryGrop bool `yaml:"-"` // 是否是国家分组
|
||||
IsCountryGrop bool `yaml:"-"`
|
||||
Url string `yaml:"url,omitempty"`
|
||||
Interval int `yaml:"interval,omitempty"`
|
||||
Tolerance int `yaml:"tolerance,omitempty"`
|
||||
Lazy bool `yaml:"lazy"`
|
||||
Size int `yaml:"-"` // 代理数量
|
||||
Size int `yaml:"-"`
|
||||
DisableUDP bool `yaml:"disable-udp,omitempty"`
|
||||
Strategy string `yaml:"strategy,omitempty"`
|
||||
Icon string `yaml:"icon,omitempty"`
|
||||
@ -42,16 +42,15 @@ func (p ProxyGroupsSortBySize) Len() int {
|
||||
}
|
||||
|
||||
func (p ProxyGroupsSortByName) Less(i, j int) bool {
|
||||
// 定义一组备选语言:首选英语,其次中文
|
||||
|
||||
tags := []language.Tag{
|
||||
language.English,
|
||||
language.Chinese,
|
||||
}
|
||||
matcher := language.NewMatcher(tags)
|
||||
|
||||
// 假设我们的请求语言是 "zh"(中文),则使用匹配器找到最佳匹配的语言
|
||||
bestMatch, _, _ := matcher.Match(language.Make("zh"))
|
||||
// 使用最佳匹配的语言进行排序
|
||||
|
||||
c := collate.New(bestMatch)
|
||||
return c.CompareString(p[i].Name, p[j].Name) < 0
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package model
|
||||
|
||||
type SmuxStruct struct {
|
||||
Enabled bool `yaml:"enable"`
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Server string `yaml:"server,omitempty"`
|
||||
Port int `yaml:"port,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Cipher string `yaml:"cipher,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
UUID string `yaml:"uuid,omitempty"`
|
||||
Network string `yaml:"network,omitempty"`
|
||||
Flow string `yaml:"flow,omitempty"`
|
||||
TLS bool `yaml:"tls,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
Plugin string `yaml:"plugin,omitempty"`
|
||||
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
|
||||
Smux SmuxStruct `yaml:"smux,omitempty"`
|
||||
Sni string `yaml:"sni,omitempty"`
|
||||
AllowInsecure bool `yaml:"allow-insecure,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Alpn []string `yaml:"alpn,omitempty"`
|
||||
XUDP bool `yaml:"xudp,omitempty"`
|
||||
Servername string `yaml:"servername,omitempty"`
|
||||
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
|
||||
AlterID int `yaml:"alterId,omitempty"`
|
||||
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
|
||||
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
|
||||
Protocol string `yaml:"protocol,omitempty"`
|
||||
Obfs string `yaml:"obfs,omitempty"`
|
||||
ObfsParam string `yaml:"obfs-param,omitempty"`
|
||||
ProtocolParam string `yaml:"protocol-param,omitempty"`
|
||||
Remarks []string `yaml:"remarks,omitempty"`
|
||||
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
|
||||
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
|
||||
PacketAddr bool `yaml:"packet-addr,omitempty"`
|
||||
PacketEncoding string `yaml:"packet-encoding,omitempty"`
|
||||
GlobalPadding bool `yaml:"global-padding,omitempty"`
|
||||
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
|
||||
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
|
||||
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
|
||||
SubName string `yaml:"-"`
|
||||
Up string `yaml:"up,omitempty"`
|
||||
Down string `yaml:"down,omitempty"`
|
||||
CustomCA string `yaml:"ca,omitempty"`
|
||||
CustomCAString string `yaml:"ca-str,omitempty"`
|
||||
CWND int `yaml:"cwnd,omitempty"`
|
||||
Auth string `yaml:"auth,omitempty"`
|
||||
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
|
||||
ReceiveWindow int `yaml:"recv-window,omitempty"`
|
||||
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
|
||||
FastOpen bool `yaml:"fast-open,omitempty"`
|
||||
HopInterval int `yaml:"hop-interval,omitempty"`
|
||||
Ports string `yaml:"ports,omitempty"`
|
||||
AuthStringOLD string `yaml:"auth_str,omitempty"`
|
||||
AuthString string `yaml:"auth-str,omitempty"`
|
||||
}
|
||||
|
||||
func (p Proxy) MarshalYAML() (interface{}, error) {
|
||||
switch p.Type {
|
||||
case "vmess":
|
||||
return ProxyToVmess(p), nil
|
||||
case "ss":
|
||||
return ProxyToShadowSocks(p), nil
|
||||
case "ssr":
|
||||
return ProxyToShadowSocksR(p), nil
|
||||
case "vless":
|
||||
return ProxyToVless(p), nil
|
||||
case "trojan":
|
||||
return ProxyToTrojan(p), nil
|
||||
case "hysteria":
|
||||
return ProxyToHysteria(p), nil
|
||||
case "hysteria2":
|
||||
return ProxyToHysteria2(p), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
51
model/proxy/anytls.go
Normal file
51
model/proxy/anytls.go
Normal file
@ -0,0 +1,51 @@
|
||||
package proxy
|
||||
|
||||
type Anytls struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
Alpn []string `yaml:"alpn,omitempty"`
|
||||
SNI string `yaml:"sni,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
IdleSessionCheckInterval int `yaml:"idle-session-check-interval,omitempty"`
|
||||
IdleSessionTimeout int `yaml:"idle-session-timeout,omitempty"`
|
||||
MinIdleSession int `yaml:"min-idle-session,omitempty"`
|
||||
}
|
||||
|
||||
func ProxyToAnytls(p Proxy) Anytls {
|
||||
return Anytls{
|
||||
Type: "anytls",
|
||||
Name: p.Name,
|
||||
Server: p.Server,
|
||||
Port: p.Port,
|
||||
Password: p.Password,
|
||||
Alpn: p.Alpn,
|
||||
SNI: p.Sni,
|
||||
ClientFingerprint: p.ClientFingerprint,
|
||||
SkipCertVerify: p.SkipCertVerify,
|
||||
Fingerprint: p.Fingerprint,
|
||||
UDP: p.UDP,
|
||||
IdleSessionCheckInterval: p.IdleSessionCheckInterval,
|
||||
IdleSessionTimeout: p.IdleSessionTimeout,
|
||||
MinIdleSession: p.MinIdleSession,
|
||||
}
|
||||
}
|
||||
|
||||
type AnytlsMarshaler struct{}
|
||||
|
||||
func (m *AnytlsMarshaler) GetType() string {
|
||||
return "anytls"
|
||||
}
|
||||
|
||||
func (m *AnytlsMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
|
||||
return ProxyToAnytls(p), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterMarshaler(&AnytlsMarshaler{})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package model
|
||||
package proxy
|
||||
|
||||
type Hysteria struct {
|
||||
Type string `yaml:"type"`
|
||||
@ -7,11 +7,11 @@ type Hysteria struct {
|
||||
Port int `yaml:"port,omitempty"`
|
||||
Ports string `yaml:"ports,omitempty"`
|
||||
Protocol string `yaml:"protocol,omitempty"`
|
||||
ObfsProtocol string `yaml:"obfs-protocol,omitempty"` // compatible with Stash
|
||||
ObfsProtocol string `yaml:"obfs-protocol,omitempty"`
|
||||
Up string `yaml:"up"`
|
||||
UpSpeed int `yaml:"up-speed,omitempty"` // compatible with Stash
|
||||
UpSpeed int `yaml:"up-speed,omitempty"`
|
||||
Down string `yaml:"down"`
|
||||
DownSpeed int `yaml:"down-speed,omitempty"` // compatible with Stash
|
||||
DownSpeed int `yaml:"down-speed,omitempty"`
|
||||
Auth string `yaml:"auth,omitempty"`
|
||||
AuthStringOLD string `yaml:"auth_str,omitempty"`
|
||||
AuthString string `yaml:"auth-str,omitempty"`
|
||||
@ -56,3 +56,13 @@ func ProxyToHysteria(p Proxy) Hysteria {
|
||||
HopInterval: p.HopInterval,
|
||||
}
|
||||
}
|
||||
|
||||
type HysteriaMarshaler struct{}
|
||||
|
||||
func (m *HysteriaMarshaler) GetType() string {
|
||||
return "hysteria"
|
||||
}
|
||||
|
||||
func (m *HysteriaMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
|
||||
return ProxyToHysteria(p), nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package model
|
||||
package proxy
|
||||
|
||||
type Hysteria2 struct {
|
||||
Type string `yaml:"type"`
|
||||
@ -39,3 +39,13 @@ func ProxyToHysteria2(p Proxy) Hysteria2 {
|
||||
CWND: p.CWND,
|
||||
}
|
||||
}
|
||||
|
||||
type Hysteria2Marshaler struct{}
|
||||
|
||||
func (m *Hysteria2Marshaler) GetType() string {
|
||||
return "hysteria2"
|
||||
}
|
||||
|
||||
func (m *Hysteria2Marshaler) MarshalProxy(p Proxy) (interface{}, error) {
|
||||
return ProxyToHysteria2(p), nil
|
||||
}
|
115
model/proxy/proxy.go
Normal file
115
model/proxy/proxy.go
Normal file
@ -0,0 +1,115 @@
|
||||
package proxy
|
||||
|
||||
type SmuxStruct struct {
|
||||
Enabled bool `yaml:"enable"`
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Server string `yaml:"server,omitempty"`
|
||||
Port int `yaml:"port,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Cipher string `yaml:"cipher,omitempty"`
|
||||
Username string `yaml:"username,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
UUID string `yaml:"uuid,omitempty"`
|
||||
Network string `yaml:"network,omitempty"`
|
||||
Flow string `yaml:"flow,omitempty"`
|
||||
TLS bool `yaml:"tls,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
Plugin string `yaml:"plugin,omitempty"`
|
||||
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
|
||||
Smux SmuxStruct `yaml:"smux,omitempty"`
|
||||
Sni string `yaml:"sni,omitempty"`
|
||||
AllowInsecure bool `yaml:"allow-insecure,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Alpn []string `yaml:"alpn,omitempty"`
|
||||
XUDP bool `yaml:"xudp,omitempty"`
|
||||
Servername string `yaml:"servername,omitempty"`
|
||||
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
|
||||
AlterID int `yaml:"alterId,omitempty"`
|
||||
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
|
||||
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
|
||||
Protocol string `yaml:"protocol,omitempty"`
|
||||
Obfs string `yaml:"obfs,omitempty"`
|
||||
ObfsParam string `yaml:"obfs-param,omitempty"`
|
||||
ProtocolParam string `yaml:"protocol-param,omitempty"`
|
||||
Remarks []string `yaml:"remarks,omitempty"`
|
||||
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
|
||||
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
|
||||
PacketAddr bool `yaml:"packet-addr,omitempty"`
|
||||
PacketEncoding string `yaml:"packet-encoding,omitempty"`
|
||||
GlobalPadding bool `yaml:"global-padding,omitempty"`
|
||||
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
|
||||
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
|
||||
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
|
||||
SubName string `yaml:"-"`
|
||||
Up string `yaml:"up,omitempty"`
|
||||
Down string `yaml:"down,omitempty"`
|
||||
CustomCA string `yaml:"ca,omitempty"`
|
||||
CustomCAString string `yaml:"ca-str,omitempty"`
|
||||
CWND int `yaml:"cwnd,omitempty"`
|
||||
Auth string `yaml:"auth,omitempty"`
|
||||
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
|
||||
ReceiveWindow int `yaml:"recv-window,omitempty"`
|
||||
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
|
||||
FastOpen bool `yaml:"fast-open,omitempty"`
|
||||
HopInterval int `yaml:"hop-interval,omitempty"`
|
||||
Ports string `yaml:"ports,omitempty"`
|
||||
AuthStringOLD string `yaml:"auth_str,omitempty"`
|
||||
AuthString string `yaml:"auth-str,omitempty"`
|
||||
Ip string `yaml:"ip,omitempty"`
|
||||
Ipv6 string `yaml:"ipv6,omitempty"`
|
||||
PrivateKey string `yaml:"private-key,omitempty"`
|
||||
Workers int `yaml:"workers,omitempty"`
|
||||
MTU int `yaml:"mtu,omitempty"`
|
||||
PersistentKeepalive int `yaml:"persistent-keepalive,omitempty"`
|
||||
Peers []WireGuardPeerOption `yaml:"peers,omitempty"`
|
||||
RemoteDnsResolve bool `yaml:"remote-dns-resolve,omitempty"`
|
||||
Dns []string `yaml:"dns,omitempty"`
|
||||
IdleSessionCheckInterval int `yaml:"idle-session-check-interval,omitempty"`
|
||||
IdleSessionTimeout int `yaml:"idle-session-timeout,omitempty"`
|
||||
MinIdleSession int `yaml:"min-idle-session,omitempty"`
|
||||
}
|
||||
|
||||
type WireGuardPeerOption struct {
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port"`
|
||||
PublicKey string `yaml:"public-key,omitempty"`
|
||||
PreSharedKey string `yaml:"pre-shared-key,omitempty"`
|
||||
Reserved []uint8 `yaml:"reserved,omitempty"`
|
||||
AllowedIPs []string `yaml:"allowed-ips,omitempty"`
|
||||
}
|
||||
|
||||
type _Proxy Proxy
|
||||
|
||||
func (p Proxy) MarshalYAML() (interface{}, error) {
|
||||
// 尝试使用注册的序列化器
|
||||
if marshaler, exists := GetMarshaler(p.Type); exists {
|
||||
return marshaler.MarshalProxy(p)
|
||||
}
|
||||
|
||||
// 保持向后兼容,对于未注册的类型使用原有逻辑
|
||||
switch p.Type {
|
||||
case "vmess":
|
||||
return ProxyToVmess(p), nil
|
||||
case "ss":
|
||||
return ProxyToShadowSocks(p), nil
|
||||
case "ssr":
|
||||
return ProxyToShadowSocksR(p), nil
|
||||
case "vless":
|
||||
return ProxyToVless(p), nil
|
||||
case "trojan":
|
||||
return ProxyToTrojan(p), nil
|
||||
case "hysteria":
|
||||
return ProxyToHysteria(p), nil
|
||||
case "hysteria2":
|
||||
return ProxyToHysteria2(p), nil
|
||||
case "anytls":
|
||||
return ProxyToAnytls(p), nil
|
||||
default:
|
||||
return _Proxy(p), nil
|
||||
}
|
||||
}
|
64
model/proxy/registry.go
Normal file
64
model/proxy/registry.go
Normal file
@ -0,0 +1,64 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ProxyMarshaler 代理YAML序列化接口
|
||||
type ProxyMarshaler interface {
|
||||
// MarshalProxy 将通用Proxy对象序列化为特定协议的YAML结构
|
||||
MarshalProxy(p Proxy) (interface{}, error)
|
||||
// GetType 返回支持的协议类型
|
||||
GetType() string
|
||||
}
|
||||
|
||||
// marshalerRegistry YAML序列化器注册中心
|
||||
type marshalerRegistry struct {
|
||||
mu sync.RWMutex
|
||||
marshalers map[string]ProxyMarshaler // type -> marshaler
|
||||
}
|
||||
|
||||
var yamlRegistry = &marshalerRegistry{
|
||||
marshalers: make(map[string]ProxyMarshaler),
|
||||
}
|
||||
|
||||
// RegisterMarshaler 注册YAML序列化器
|
||||
func RegisterMarshaler(marshaler ProxyMarshaler) {
|
||||
yamlRegistry.mu.Lock()
|
||||
defer yamlRegistry.mu.Unlock()
|
||||
|
||||
yamlRegistry.marshalers[marshaler.GetType()] = marshaler
|
||||
}
|
||||
|
||||
// GetMarshaler 根据协议类型获取序列化器
|
||||
func GetMarshaler(proxyType string) (ProxyMarshaler, bool) {
|
||||
yamlRegistry.mu.RLock()
|
||||
defer yamlRegistry.mu.RUnlock()
|
||||
|
||||
marshaler, exists := yamlRegistry.marshalers[proxyType]
|
||||
return marshaler, exists
|
||||
}
|
||||
|
||||
// GetAllMarshalers 获取所有注册的序列化器
|
||||
func GetAllMarshalers() map[string]ProxyMarshaler {
|
||||
yamlRegistry.mu.RLock()
|
||||
defer yamlRegistry.mu.RUnlock()
|
||||
|
||||
result := make(map[string]ProxyMarshaler)
|
||||
for k, v := range yamlRegistry.marshalers {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetSupportedTypes 获取所有支持的协议类型
|
||||
func GetSupportedTypes() []string {
|
||||
yamlRegistry.mu.RLock()
|
||||
defer yamlRegistry.mu.RUnlock()
|
||||
|
||||
types := make([]string, 0, len(yamlRegistry.marshalers))
|
||||
for proxyType := range yamlRegistry.marshalers {
|
||||
types = append(types, proxyType)
|
||||
}
|
||||
return types
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package model
|
||||
package proxy
|
||||
|
||||
type ShadowSocks struct {
|
||||
Type string `yaml:"type"`
|
||||
@ -31,3 +31,21 @@ func ProxyToShadowSocks(p Proxy) ShadowSocks {
|
||||
ClientFingerprint: p.ClientFingerprint,
|
||||
}
|
||||
}
|
||||
|
||||
// ShadowsocksMarshaler Shadowsocks协议的YAML序列化器
|
||||
type ShadowsocksMarshaler struct{}
|
||||
|
||||
// GetType 返回协议类型
|
||||
func (m *ShadowsocksMarshaler) GetType() string {
|
||||
return "ss"
|
||||
}
|
||||
|
||||
// MarshalProxy 序列化Shadowsocks代理
|
||||
func (m *ShadowsocksMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
|
||||
return ProxyToShadowSocks(p), nil
|
||||
}
|
||||
|
||||
// 注册序列化器
|
||||
func init() {
|
||||
RegisterMarshaler(&ShadowsocksMarshaler{})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package model
|
||||
package proxy
|
||||
|
||||
type ShadowSocksR struct {
|
||||
Type string `yaml:"type"`
|
||||
@ -29,3 +29,13 @@ func ProxyToShadowSocksR(p Proxy) ShadowSocksR {
|
||||
UDP: p.UDP,
|
||||
}
|
||||
}
|
||||
|
||||
type ShadowsocksRMarshaler struct{}
|
||||
|
||||
func (m *ShadowsocksRMarshaler) GetType() string {
|
||||
return "ssr"
|
||||
}
|
||||
|
||||
func (m *ShadowsocksRMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
|
||||
return ProxyToShadowSocksR(p), nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package model
|
||||
package proxy
|
||||
|
||||
type Trojan struct {
|
||||
Type string `yaml:"type"`
|
||||
@ -37,3 +37,13 @@ func ProxyToTrojan(p Proxy) Trojan {
|
||||
ClientFingerprint: p.ClientFingerprint,
|
||||
}
|
||||
}
|
||||
|
||||
type TrojanMarshaler struct{}
|
||||
|
||||
func (m *TrojanMarshaler) GetType() string {
|
||||
return "trojan"
|
||||
}
|
||||
|
||||
func (m *TrojanMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
|
||||
return ProxyToTrojan(p), nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package model
|
||||
package proxy
|
||||
|
||||
type Vless struct {
|
||||
Type string `yaml:"type"`
|
||||
@ -55,3 +55,13 @@ func ProxyToVless(p Proxy) Vless {
|
||||
ClientFingerprint: p.ClientFingerprint,
|
||||
}
|
||||
}
|
||||
|
||||
type VlessMarshaler struct{}
|
||||
|
||||
func (m *VlessMarshaler) GetType() string {
|
||||
return "vless"
|
||||
}
|
||||
|
||||
func (m *VlessMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
|
||||
return ProxyToVless(p), nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package model
|
||||
package proxy
|
||||
|
||||
type HTTPOptions struct {
|
||||
Method string `yaml:"method,omitempty"`
|
||||
@ -102,3 +102,13 @@ func ProxyToVmess(p Proxy) Vmess {
|
||||
ClientFingerprint: p.ClientFingerprint,
|
||||
}
|
||||
}
|
||||
|
||||
type VmessMarshaler struct{}
|
||||
|
||||
func (m *VmessMarshaler) GetType() string {
|
||||
return "vmess"
|
||||
}
|
||||
|
||||
func (m *VmessMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
|
||||
return ProxyToVmess(p), nil
|
||||
}
|
@ -6,6 +6,7 @@ type RuleProvider struct {
|
||||
Url string `yaml:"url,omitempty"`
|
||||
Path string `yaml:"path,omitempty"`
|
||||
Interval int `yaml:"interval,omitempty"`
|
||||
Format string `yaml:"format,omitempty"`
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package validator
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@ -11,7 +11,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SubValidator struct {
|
||||
type SubConfig struct {
|
||||
Sub string `form:"sub" binding:""`
|
||||
Subs []string `form:"-" binding:""`
|
||||
Proxy string `form:"proxy" binding:""`
|
||||
@ -31,6 +31,7 @@ type SubValidator struct {
|
||||
ReplaceTo []string `form:"-" binding:""`
|
||||
NodeListMode bool `form:"nodeList,default=false" binding:""`
|
||||
IgnoreCountryGrooup bool `form:"ignoreCountryGroup,default=false" binding:""`
|
||||
UserAgent string `form:"userAgent" binding:""`
|
||||
}
|
||||
|
||||
type RuleProviderStruct struct {
|
||||
@ -46,22 +47,22 @@ type RuleStruct struct {
|
||||
Prepend bool
|
||||
}
|
||||
|
||||
func ParseQuery(c *gin.Context) (SubValidator, error) {
|
||||
var query SubValidator
|
||||
func ParseSubQuery(c *gin.Context) (SubConfig, error) {
|
||||
var query SubConfig
|
||||
if err := c.ShouldBind(&query); err != nil {
|
||||
return SubValidator{}, errors.New("参数错误: " + err.Error())
|
||||
return SubConfig{}, errors.New("参数错误: " + err.Error())
|
||||
}
|
||||
if query.Sub == "" && query.Proxy == "" {
|
||||
return SubValidator{}, errors.New("参数错误: sub 和 proxy 不能同时为空")
|
||||
return SubConfig{}, errors.New("参数错误: sub 和 proxy 不能同时为空")
|
||||
}
|
||||
if query.Sub != "" {
|
||||
query.Subs = strings.Split(query.Sub, ",")
|
||||
for i := range query.Subs {
|
||||
if !strings.HasPrefix(query.Subs[i], "http") {
|
||||
return SubValidator{}, errors.New("参数错误: sub 格式错误")
|
||||
return SubConfig{}, errors.New("参数错误: sub 格式错误")
|
||||
}
|
||||
if _, err := url.ParseRequestURI(query.Subs[i]); err != nil {
|
||||
return SubValidator{}, errors.New("参数错误: " + err.Error())
|
||||
return SubConfig{}, errors.New("参数错误: " + err.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -76,7 +77,7 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
|
||||
if strings.HasPrefix(query.Template, "http") {
|
||||
uri, err := url.ParseRequestURI(query.Template)
|
||||
if err != nil {
|
||||
return SubValidator{}, err
|
||||
return SubConfig{}, err
|
||||
}
|
||||
query.Template = uri.String()
|
||||
}
|
||||
@ -88,12 +89,12 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
|
||||
length := len(ruleProviders)
|
||||
parts := strings.Split(ruleProviders[length-i-1][1], ",")
|
||||
if len(parts) < 4 {
|
||||
return SubValidator{}, errors.New("参数错误: ruleProvider 格式错误")
|
||||
return SubConfig{}, errors.New("参数错误: ruleProvider 格式错误")
|
||||
}
|
||||
u := parts[1]
|
||||
uri, err := url.ParseRequestURI(u)
|
||||
if err != nil {
|
||||
return SubValidator{}, errors.New("参数错误: " + err.Error())
|
||||
return SubConfig{}, errors.New("参数错误: " + err.Error())
|
||||
}
|
||||
u = uri.String()
|
||||
if len(parts) == 4 {
|
||||
@ -110,11 +111,11 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
|
||||
},
|
||||
)
|
||||
}
|
||||
// 校验 Rule-Provider 是否有重名
|
||||
|
||||
names := make(map[string]bool)
|
||||
for _, ruleProvider := range query.RuleProviders {
|
||||
if _, ok := names[ruleProvider.Name]; ok {
|
||||
return SubValidator{}, errors.New("参数错误: Rule-Provider 名称重复")
|
||||
return SubConfig{}, errors.New("参数错误: Rule-Provider 名称重复")
|
||||
}
|
||||
names[ruleProvider.Name] = true
|
||||
}
|
||||
@ -147,7 +148,7 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
|
||||
for i := range replaces {
|
||||
length := len(replaces[i])
|
||||
if length != 3 {
|
||||
return SubValidator{}, errors.New("参数错误: replace 格式错误")
|
||||
return SubConfig{}, errors.New("参数错误: replace 格式错误")
|
||||
}
|
||||
query.ReplaceKeys = append(query.ReplaceKeys, replaces[i][1])
|
||||
query.ReplaceTo = append(query.ReplaceTo, replaces[i][2])
|
@ -1,7 +1,9 @@
|
||||
package model
|
||||
|
||||
import "github.com/bestnite/sub2clash/model/proxy"
|
||||
|
||||
type NodeList struct {
|
||||
Proxies []Proxy `yaml:"proxies,omitempty"`
|
||||
Proxies []proxy.Proxy `yaml:"proxies,omitempty"`
|
||||
}
|
||||
|
||||
type Subscription struct {
|
||||
@ -57,7 +59,7 @@ type Subscription struct {
|
||||
Experimental Experimental `yaml:"experimental,omitempty"`
|
||||
Profile Profile `yaml:"profile,omitempty"`
|
||||
GeoXUrl GeoXUrl `yaml:"geox-url,omitempty"`
|
||||
Proxies []Proxy `yaml:"proxies,omitempty"`
|
||||
Proxies []proxy.Proxy `yaml:"proxies,omitempty"`
|
||||
ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"`
|
||||
Rules []string `yaml:"rules,omitempty"`
|
||||
SubRules map[string][]string `yaml:"sub-rules,omitempty"`
|
||||
@ -103,7 +105,6 @@ type IPTables struct {
|
||||
DnsRedirect bool `yaml:"dns-redirect,omitempty" json:"dns-redirect"`
|
||||
}
|
||||
|
||||
// EBpf config
|
||||
type EBpf struct {
|
||||
RedirectToTun []string `yaml:"redirect-to-tun,omitempty" json:"redirect-to-tun"`
|
||||
AutoRedir []string `yaml:"auto-redir,omitempty" json:"auto-redir"`
|
96
parser/anytls.go
Normal file
96
parser/anytls.go
Normal file
@ -0,0 +1,96 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
E "github.com/bestnite/sub2clash/error"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
type AnytlsParser struct{}
|
||||
|
||||
func (p *AnytlsParser) SupportClash() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *AnytlsParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *AnytlsParser) GetPrefixes() []string {
|
||||
return []string{"anytls://"}
|
||||
}
|
||||
|
||||
func (p *AnytlsParser) GetType() string {
|
||||
return "anytls"
|
||||
}
|
||||
|
||||
func (p *AnytlsParser) Parse(proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "url parse error",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
username := link.User.Username()
|
||||
password, exist := link.User.Password()
|
||||
if !exist {
|
||||
password = username
|
||||
}
|
||||
|
||||
query := link.Query()
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server host",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidPort,
|
||||
Raw: portStr,
|
||||
}
|
||||
}
|
||||
insecure, sni := query.Get("insecure"), query.Get("sni")
|
||||
insecureBool := insecure == "1"
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
Server: server,
|
||||
Port: port,
|
||||
Password: password,
|
||||
Sni: sni,
|
||||
SkipCertVerify: insecureBool,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&AnytlsParser{})
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DecodeBase64(s string) (string, error) {
|
||||
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 {
|
||||
s += strings.Repeat("=", 4-len(s)%4)
|
||||
}
|
||||
decodeStr, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decodeStr), nil
|
||||
}
|
87
parser/common.go
Normal file
87
parser/common.go
Normal file
@ -0,0 +1,87 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bestnite/sub2clash/logger"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func hasPrefix(proxy string, prefixes []string) bool {
|
||||
hasPrefix := false
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(proxy, prefix) {
|
||||
hasPrefix = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return hasPrefix
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func isLikelyBase64(s string) bool {
|
||||
if len(s)%4 == 0 && strings.HasSuffix(s, "=") && !strings.Contains(strings.TrimSuffix(s, "="), "=") {
|
||||
s = strings.TrimSuffix(s, "=")
|
||||
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
for _, c := range s {
|
||||
if !strings.ContainsRune(chars, c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func DecodeBase64(s string) (string, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
if strings.Contains(s, "-") || strings.Contains(s, "_") {
|
||||
s = strings.Replace(s, "-", "+", -1)
|
||||
s = strings.Replace(s, "_", "/", -1)
|
||||
}
|
||||
if len(s)%4 != 0 {
|
||||
s += strings.Repeat("=", 4-len(s)%4)
|
||||
}
|
||||
decodeStr, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decodeStr), nil
|
||||
}
|
||||
|
||||
func ParseProxies(proxies ...string) []P.Proxy {
|
||||
var result []P.Proxy
|
||||
for _, proxy := range proxies {
|
||||
if proxy != "" {
|
||||
var proxyItem P.Proxy
|
||||
var err error
|
||||
|
||||
proxyItem, err = ParseProxyWithRegistry(proxy)
|
||||
|
||||
if err == nil {
|
||||
result = append(result, proxyItem)
|
||||
} else {
|
||||
logger.Logger.Debug(
|
||||
"parse proxy failed", zap.String("proxy", proxy), zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
@ -1,57 +1,76 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/constant"
|
||||
"sub2clash/model"
|
||||
|
||||
E "github.com/bestnite/sub2clash/error"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
func ParseHysteria(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, constant.HysteriaPrefix) {
|
||||
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
type HysteriaParser struct{}
|
||||
|
||||
func (p *HysteriaParser) SupportClash() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *HysteriaParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *HysteriaParser) GetPrefixes() []string {
|
||||
return []string{"hysteria://"}
|
||||
}
|
||||
|
||||
func (p *HysteriaParser) GetType() string {
|
||||
return "hysteria"
|
||||
}
|
||||
|
||||
func (p *HysteriaParser) Parse(proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
proxy = strings.TrimPrefix(proxy, constant.HysteriaPrefix)
|
||||
urlParts := strings.SplitN(proxy, "?", 2)
|
||||
if len(urlParts) != 2 {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '?' in url",
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "url parse error",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server host",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
serverInfo := strings.SplitN(urlParts[0], ":", 2)
|
||||
if len(serverInfo) != 2 {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing server host or port",
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
server, portStr := serverInfo[0], serverInfo[1]
|
||||
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidPort,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidPort,
|
||||
Message: err.Error(),
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
params, err := url.ParseQuery(urlParts[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
query := link.Query()
|
||||
|
||||
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")
|
||||
protocol, auth, insecure, upmbps, downmbps, obfs, alpnStr := query.Get("protocol"), query.Get("auth"), query.Get("insecure"), query.Get("upmbps"), query.Get("downmbps"), query.Get("obfs"), query.Get("alpn")
|
||||
insecureBool, err := strconv.ParseBool(insecure)
|
||||
if err != nil {
|
||||
insecureBool = false
|
||||
@ -63,13 +82,14 @@ func ParseHysteria(proxy string) (model.Proxy, error) {
|
||||
alpn = strings.Split(alpnStr, ",")
|
||||
}
|
||||
|
||||
remarks := server + ":" + portStr
|
||||
if params.Get("remarks") != "" {
|
||||
remarks = params.Get("remarks")
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
result := model.Proxy{
|
||||
Type: "hysteria",
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
Server: server,
|
||||
Port: port,
|
||||
@ -77,10 +97,14 @@ func ParseHysteria(proxy string) (model.Proxy, error) {
|
||||
Down: downmbps,
|
||||
Auth: auth,
|
||||
Obfs: obfs,
|
||||
SkipCertVerify: insecure == "1",
|
||||
SkipCertVerify: insecureBool,
|
||||
Alpn: alpn,
|
||||
Protocol: protocol,
|
||||
AllowInsecure: insecureBool,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&HysteriaParser{})
|
||||
}
|
||||
|
@ -1,79 +1,87 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sub2clash/constant"
|
||||
"sub2clash/model"
|
||||
|
||||
E "github.com/bestnite/sub2clash/error"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
func ParseHysteria2(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, constant.Hysteria2Prefix1) &&
|
||||
!strings.HasPrefix(proxy, constant.Hysteria2Prefix2) {
|
||||
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
type Hysteria2Parser struct{}
|
||||
|
||||
func (p *Hysteria2Parser) SupportClash() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Hysteria2Parser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Hysteria2Parser) GetPrefixes() []string {
|
||||
return []string{"hysteria2://", "hy2://"}
|
||||
}
|
||||
|
||||
func (p *Hysteria2Parser) GetType() string {
|
||||
return "hysteria2"
|
||||
}
|
||||
|
||||
func (p *Hysteria2Parser) Parse(proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix1)
|
||||
proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix2)
|
||||
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)
|
||||
var server string
|
||||
var portStr string
|
||||
if len(serverAndPort) == 1 {
|
||||
portStr = "443"
|
||||
} else if len(serverAndPort) == 2 {
|
||||
server, portStr = serverAndPort[0], serverAndPort[1]
|
||||
} else {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing server host or port",
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "url parse error",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
username := link.User.Username()
|
||||
password, exist := link.User.Password()
|
||||
if !exist {
|
||||
password = username
|
||||
}
|
||||
|
||||
query := link.Query()
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server host",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidPort,
|
||||
Message: err.Error(),
|
||||
Raw: proxy,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidPort,
|
||||
Raw: portStr,
|
||||
}
|
||||
}
|
||||
|
||||
params, err := url.ParseQuery(paramStr)
|
||||
if err != nil {
|
||||
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 != ""
|
||||
network, obfs, obfsPassword, pinSHA256, insecure, sni := query.Get("network"), query.Get("obfs"), query.Get("obfs-password"), query.Get("pinSHA256"), query.Get("insecure"), query.Get("sni")
|
||||
enableTLS := pinSHA256 != "" || sni != ""
|
||||
insecureBool := insecure == "1"
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
result := model.Proxy{
|
||||
Type: "hysteria2",
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
Server: server,
|
||||
Port: port,
|
||||
@ -87,3 +95,7 @@ func ParseHysteria2(proxy string) (model.Proxy, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&Hysteria2Parser{})
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
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
|
||||
}
|
79
parser/registry.go
Normal file
79
parser/registry.go
Normal file
@ -0,0 +1,79 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
E "github.com/bestnite/sub2clash/error"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
// ProxyParser 定义代理解析器接口
|
||||
type ProxyParser interface {
|
||||
// Parse 解析代理字符串并返回Proxy对象
|
||||
Parse(proxy string) (P.Proxy, error)
|
||||
// GetPrefix 返回支持的协议前缀(可以返回多个)
|
||||
GetPrefixes() []string
|
||||
// GetType 返回协议类型名称
|
||||
GetType() string
|
||||
SupportClash() bool
|
||||
SupportMeta() bool
|
||||
}
|
||||
|
||||
// parserRegistry 解析器注册中心
|
||||
type parserRegistry struct {
|
||||
mu sync.RWMutex
|
||||
parsers map[string]ProxyParser // prefix -> parser
|
||||
}
|
||||
|
||||
var registry = &parserRegistry{
|
||||
parsers: make(map[string]ProxyParser),
|
||||
}
|
||||
|
||||
// RegisterParser 注册解析器
|
||||
func RegisterParser(parser ProxyParser) {
|
||||
registry.mu.Lock()
|
||||
defer registry.mu.Unlock()
|
||||
|
||||
for _, prefix := range parser.GetPrefixes() {
|
||||
registry.parsers[prefix] = parser
|
||||
}
|
||||
}
|
||||
|
||||
// GetParser 根据前缀获取解析器
|
||||
func GetParser(prefix string) (ProxyParser, bool) {
|
||||
registry.mu.RLock()
|
||||
defer registry.mu.RUnlock()
|
||||
|
||||
parser, exists := registry.parsers[prefix]
|
||||
return parser, exists
|
||||
}
|
||||
|
||||
// GetAllParsers 获取所有注册的解析器
|
||||
func GetAllParsers() map[string]ProxyParser {
|
||||
registry.mu.RLock()
|
||||
defer registry.mu.RUnlock()
|
||||
|
||||
result := make(map[string]ProxyParser)
|
||||
for k, v := range registry.parsers {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseProxyWithRegistry 使用注册机制解析代理
|
||||
func ParseProxyWithRegistry(proxy string) (P.Proxy, error) {
|
||||
proxy = strings.TrimSpace(proxy)
|
||||
if proxy == "" {
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: "empty proxy string"}
|
||||
}
|
||||
|
||||
// 查找匹配的解析器
|
||||
for prefix, parser := range registry.parsers {
|
||||
if strings.HasPrefix(proxy, prefix) {
|
||||
return parser.Parse(proxy)
|
||||
}
|
||||
}
|
||||
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy, Message: "unsupported protocol"}
|
||||
}
|
@ -1,85 +1,129 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sub2clash/constant"
|
||||
"sub2clash/model"
|
||||
|
||||
E "github.com/bestnite/sub2clash/error"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
func ParseShadowsocks(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
|
||||
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
// ShadowsocksParser Shadowsocks协议解析器
|
||||
type ShadowsocksParser struct{}
|
||||
|
||||
func (p *ShadowsocksParser) SupportClash() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ShadowsocksParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetPrefixes 返回支持的协议前缀
|
||||
func (p *ShadowsocksParser) GetPrefixes() []string {
|
||||
return []string{"ss://"}
|
||||
}
|
||||
|
||||
// GetType 返回协议类型
|
||||
func (p *ShadowsocksParser) GetType() string {
|
||||
return "ss"
|
||||
}
|
||||
|
||||
// Parse 解析Shadowsocks代理
|
||||
func (p *ShadowsocksParser) Parse(proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, &E.ParseError{Type: E.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,
|
||||
if !strings.Contains(proxy, "@") {
|
||||
s := strings.SplitN(proxy, "#", 2)
|
||||
for _, prefix := range p.GetPrefixes() {
|
||||
if strings.HasPrefix(s[0], prefix) {
|
||||
s[0] = strings.TrimPrefix(s[0], prefix)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var serverAndPort []string
|
||||
if !strings.Contains(urlParts[0], ":") {
|
||||
decoded, err := DecodeBase64(urlParts[0])
|
||||
d, err := DecodeBase64(s[0])
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "invalid base64 encoded",
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "url parse error",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
urlParts[0] = decoded
|
||||
if len(s) == 2 {
|
||||
proxy = "ss://" + d + "#" + s[1]
|
||||
} else {
|
||||
proxy = "ss://" + d
|
||||
}
|
||||
}
|
||||
credentials := strings.SplitN(urlParts[0], ":", 2)
|
||||
if len(credentials) != 2 {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing server host or port",
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "url parse error",
|
||||
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",
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server host",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidPort,
|
||||
Message: err.Error(),
|
||||
Raw: proxy,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
var remarks string
|
||||
if len(serverInfo) == 2 {
|
||||
unescape, err := url.QueryUnescape(serverInfo[1])
|
||||
method := link.User.Username()
|
||||
password, _ := link.User.Password()
|
||||
|
||||
if password == "" {
|
||||
user, err := DecodeBase64(method)
|
||||
if err == nil {
|
||||
methodAndPass := strings.SplitN(user, ":", 2)
|
||||
if len(methodAndPass) == 2 {
|
||||
method = methodAndPass[0]
|
||||
password = methodAndPass[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if isLikelyBase64(password) {
|
||||
password, err = DecodeBase64(password)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "cannot unescape remarks",
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "password decode error",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
remarks = strings.TrimSpace(unescape)
|
||||
} else {
|
||||
remarks = strings.TrimSpace(server + ":" + portStr)
|
||||
}
|
||||
|
||||
result := model.Proxy{
|
||||
Type: "ss",
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Cipher: method,
|
||||
Password: password,
|
||||
Server: server,
|
||||
@ -89,3 +133,8 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 注册解析器
|
||||
func init() {
|
||||
RegisterParser(&ShadowsocksParser{})
|
||||
}
|
||||
|
@ -4,20 +4,45 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/constant"
|
||||
"sub2clash/model"
|
||||
|
||||
E "github.com/bestnite/sub2clash/error"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
func ParseShadowsocksR(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) {
|
||||
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
type ShadowsocksRParser struct{}
|
||||
|
||||
func (p *ShadowsocksRParser) SupportClash() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ShadowsocksRParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ShadowsocksRParser) GetPrefixes() []string {
|
||||
return []string{"ssr://"}
|
||||
}
|
||||
|
||||
func (p *ShadowsocksRParser) GetType() string {
|
||||
return "ssr"
|
||||
}
|
||||
|
||||
func (p *ShadowsocksRParser) Parse(proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
for _, prefix := range p.GetPrefixes() {
|
||||
if strings.HasPrefix(proxy, prefix) {
|
||||
proxy = strings.TrimPrefix(proxy, prefix)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
proxy = strings.TrimPrefix(proxy, constant.ShadowsocksRPrefix)
|
||||
proxy, err := DecodeBase64(proxy)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidBase64,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidBase64,
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
@ -27,11 +52,18 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) {
|
||||
protocol := parts[2]
|
||||
method := parts[3]
|
||||
obfs := parts[4]
|
||||
password := parts[5]
|
||||
password, err := DecodeBase64(parts[5])
|
||||
if err != nil {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
port, err := ParsePort(parts[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidPort,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidPort,
|
||||
Message: err.Error(),
|
||||
Raw: proxy,
|
||||
}
|
||||
@ -43,8 +75,8 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) {
|
||||
if len(serverInfoAndParams) == 2 {
|
||||
params, err := url.ParseQuery(serverInfoAndParams[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrCannotParseParams,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
@ -61,17 +93,17 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) {
|
||||
remarks = server + ":" + strconv.Itoa(port)
|
||||
}
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := model.Proxy{
|
||||
result := P.Proxy{
|
||||
Name: remarks,
|
||||
Type: "ssr",
|
||||
Type: p.GetType(),
|
||||
Server: server,
|
||||
Port: port,
|
||||
Protocol: protocol,
|
||||
@ -84,3 +116,7 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) {
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&ShadowsocksRParser{})
|
||||
}
|
||||
|
102
parser/socks.go
Normal file
102
parser/socks.go
Normal file
@ -0,0 +1,102 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
E "github.com/bestnite/sub2clash/error"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
type SocksParser struct{}
|
||||
|
||||
func (p *SocksParser) SupportClash() bool {
|
||||
return true
|
||||
}
|
||||
func (p *SocksParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *SocksParser) GetPrefixes() []string {
|
||||
return []string{"socks://"}
|
||||
}
|
||||
|
||||
func (p *SocksParser) GetType() string {
|
||||
return "socks5"
|
||||
}
|
||||
|
||||
func (p *SocksParser) Parse(proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "url parse error",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server host",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server port",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidPort,
|
||||
Raw: portStr,
|
||||
}
|
||||
}
|
||||
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
encodeStr := link.User.Username()
|
||||
var username, password string
|
||||
if encodeStr != "" {
|
||||
decodeStr, err := DecodeBase64(encodeStr)
|
||||
splitStr := strings.Split(decodeStr, ":")
|
||||
if err != nil {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "url parse error",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
username = splitStr[0]
|
||||
if len(splitStr) == 2 {
|
||||
password = splitStr[1]
|
||||
}
|
||||
}
|
||||
return P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
Server: server,
|
||||
Port: port,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&SocksParser{})
|
||||
}
|
129
parser/trojan.go
129
parser/trojan.go
@ -1,75 +1,81 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sub2clash/constant"
|
||||
"sub2clash/model"
|
||||
|
||||
E "github.com/bestnite/sub2clash/error"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
// ParseTrojan 解析给定的Trojan代理URL并返回Proxy结构。
|
||||
func ParseTrojan(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, constant.TrojanPrefix) {
|
||||
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
type TrojanParser struct{}
|
||||
|
||||
func (p *TrojanParser) SupportClash() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *TrojanParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *TrojanParser) GetPrefixes() []string {
|
||||
return []string{"trojan://"}
|
||||
}
|
||||
|
||||
func (p *TrojanParser) GetType() string {
|
||||
return "trojan"
|
||||
}
|
||||
|
||||
func (p *TrojanParser) Parse(proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
proxy = strings.TrimPrefix(proxy, constant.TrojanPrefix)
|
||||
urlParts := strings.SplitN(proxy, "@", 2)
|
||||
if len(urlParts) != 2 {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '@' in url",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
password := strings.TrimSpace(urlParts[0])
|
||||
|
||||
serverInfo := strings.SplitN(urlParts[1], "#", 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)
|
||||
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])
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "url parse error",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
password := link.User.Username()
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server host",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server port",
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidPort,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidPort,
|
||||
Message: err.Error(),
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
remarks := ""
|
||||
if len(serverInfo) == 2 {
|
||||
remarks, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1]))
|
||||
} else {
|
||||
remarks = serverAndPort[0]
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
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")
|
||||
query := link.Query()
|
||||
network, security, alpnStr, sni, pbk, sid, fp, path, host, serviceName := query.Get("type"), query.Get("security"), query.Get("alpn"), query.Get("sni"), query.Get("pbk"), query.Get("sid"), query.Get("fp"), query.Get("path"), query.Get("host"), query.Get("serviceName")
|
||||
|
||||
var alpn []string
|
||||
if strings.Contains(alpnStr, ",") {
|
||||
@ -78,11 +84,8 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
|
||||
alpn = nil
|
||||
}
|
||||
|
||||
// enableUTLS := fp != ""
|
||||
|
||||
// 构建Proxy结构体
|
||||
result := model.Proxy{
|
||||
Type: "trojan",
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Server: server,
|
||||
Port: port,
|
||||
Password: password,
|
||||
@ -99,7 +102,7 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
|
||||
if security == "reality" {
|
||||
result.TLS = true
|
||||
result.Sni = sni
|
||||
result.RealityOpts = model.RealityOptions{
|
||||
result.RealityOpts = P.RealityOptions{
|
||||
PublicKey: pbk,
|
||||
ShortID: sid,
|
||||
}
|
||||
@ -108,7 +111,7 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
|
||||
|
||||
if network == "ws" {
|
||||
result.Network = "ws"
|
||||
result.WSOpts = model.WSOptions{
|
||||
result.WSOpts = P.WSOptions{
|
||||
Path: path,
|
||||
Headers: map[string]string{
|
||||
"Host": host,
|
||||
@ -116,19 +119,15 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// if network == "http" {
|
||||
// // 未查到相关支持文档
|
||||
// }
|
||||
|
||||
// if network == "quic" {
|
||||
// // 未查到相关支持文档
|
||||
// }
|
||||
|
||||
if network == "grpc" {
|
||||
result.GrpcOpts = model.GrpcOptions{
|
||||
result.GrpcOpts = P.GrpcOptions{
|
||||
GrpcServiceName: serviceName,
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&TrojanParser{})
|
||||
}
|
||||
|
142
parser/vless.go
142
parser/vless.go
@ -1,88 +1,68 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sub2clash/constant"
|
||||
"sub2clash/model"
|
||||
|
||||
E "github.com/bestnite/sub2clash/error"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
func ParseVless(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, constant.VLESSPrefix) {
|
||||
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
type VlessParser struct{}
|
||||
|
||||
func (p *VlessParser) SupportClash() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *VlessParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *VlessParser) GetPrefixes() []string {
|
||||
return []string{"vless://"}
|
||||
}
|
||||
|
||||
func (p *VlessParser) GetType() string {
|
||||
return "vless"
|
||||
}
|
||||
|
||||
func (p *VlessParser) Parse(proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
urlParts := strings.SplitN(strings.TrimPrefix(proxy, constant.VLESSPrefix), "@", 2)
|
||||
if len(urlParts) != 2 {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '@' in url",
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "url parse error",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
serverInfo := strings.SplitN(urlParts[1], "#", 2)
|
||||
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2)
|
||||
if len(serverAndPortAndParams) != 2 {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidStruct,
|
||||
Message: "missing character '?' in url",
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidStruct,
|
||||
Message: "missing server host",
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
portStr := link.Port()
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidPort,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidPort,
|
||||
Message: err.Error(),
|
||||
Raw: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
params, err := url.ParseQuery(serverAndPortAndParams[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
query := link.Query()
|
||||
uuid := link.User.Username()
|
||||
flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName, _type := query.Get("flow"), query.Get("security"), query.Get("alpn"), query.Get("sni"), query.Get("allowInsecure"), query.Get("fp"), query.Get("pbk"), query.Get("sid"), query.Get("path"), query.Get("host"), query.Get("serviceName"), query.Get("type")
|
||||
|
||||
remarks := ""
|
||||
if len(serverInfo) == 2 {
|
||||
if strings.Contains(serverInfo[1], "|") {
|
||||
remarks = strings.SplitN(serverInfo[1], "|", 2)[1]
|
||||
} else {
|
||||
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 {
|
||||
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, ",") {
|
||||
@ -90,9 +70,14 @@ func ParseVless(proxy string) (model.Proxy, error) {
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
result := model.Proxy{
|
||||
Type: "vless",
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Server: server,
|
||||
Name: remarks,
|
||||
Port: port,
|
||||
@ -105,21 +90,22 @@ func ParseVless(proxy string) (model.Proxy, error) {
|
||||
result.Alpn = alpn
|
||||
result.Sni = sni
|
||||
result.AllowInsecure = insecureBool
|
||||
result.Fingerprint = fp
|
||||
result.ClientFingerprint = fp
|
||||
}
|
||||
|
||||
if security == "reality" {
|
||||
result.TLS = true
|
||||
result.Servername = sni
|
||||
result.RealityOpts = model.RealityOptions{
|
||||
result.RealityOpts = P.RealityOptions{
|
||||
PublicKey: pbk,
|
||||
ShortID: sid,
|
||||
}
|
||||
result.ClientFingerprint = fp
|
||||
}
|
||||
|
||||
if _type == "ws" {
|
||||
result.Network = "ws"
|
||||
result.WSOpts = model.WSOptions{
|
||||
result.WSOpts = P.WSOptions{
|
||||
Path: path,
|
||||
}
|
||||
if host != "" {
|
||||
@ -128,32 +114,36 @@ func ParseVless(proxy string) (model.Proxy, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// if _type == "quic" {
|
||||
// // 未查到相关支持文档
|
||||
// }
|
||||
|
||||
if _type == "grpc" {
|
||||
result.Network = "grpc"
|
||||
result.GrpcOpts = model.GrpcOptions{
|
||||
result.GrpcOpts = P.GrpcOptions{
|
||||
GrpcServiceName: serviceName,
|
||||
}
|
||||
}
|
||||
|
||||
if _type == "http" {
|
||||
result.HTTPOpts = P.HTTPOptions{}
|
||||
result.HTTPOpts.Headers = map[string][]string{}
|
||||
|
||||
result.HTTPOpts.Path = strings.Split(path, ",")
|
||||
|
||||
hosts, err := url.QueryUnescape(host)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrCannotParseParams,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrCannotParseParams,
|
||||
Raw: proxy,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
result.Network = "http"
|
||||
result.HTTPOpts = model.HTTPOptions{
|
||||
Headers: map[string][]string{"Host": strings.Split(hosts, ",")},
|
||||
if hosts != "" {
|
||||
result.HTTPOpts.Headers["host"] = strings.Split(host, ",")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&VlessParser{})
|
||||
}
|
||||
|
@ -5,25 +5,49 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/constant"
|
||||
"sub2clash/model"
|
||||
|
||||
E "github.com/bestnite/sub2clash/error"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
func ParseVmess(proxy string) (model.Proxy, error) {
|
||||
if !strings.HasPrefix(proxy, constant.VMessPrefix) {
|
||||
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||
type VmessParser struct{}
|
||||
|
||||
func (p *VmessParser) SupportClash() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *VmessParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *VmessParser) GetPrefixes() []string {
|
||||
return []string{"vmess://"}
|
||||
}
|
||||
|
||||
func (p *VmessParser) GetType() string {
|
||||
return "vmess"
|
||||
}
|
||||
|
||||
func (p *VmessParser) Parse(proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
|
||||
}
|
||||
|
||||
proxy = strings.TrimPrefix(proxy, constant.VMessPrefix)
|
||||
for _, prefix := range p.GetPrefixes() {
|
||||
if strings.HasPrefix(proxy, prefix) {
|
||||
proxy = strings.TrimPrefix(proxy, prefix)
|
||||
break
|
||||
}
|
||||
}
|
||||
base64, err := DecodeBase64(proxy)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{Type: ErrInvalidBase64, Raw: proxy, Message: err.Error()}
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidBase64, Raw: proxy, Message: err.Error()}
|
||||
}
|
||||
|
||||
var vmess model.VmessJson
|
||||
var vmess P.VmessJson
|
||||
err = json.Unmarshal([]byte(base64), &vmess)
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()}
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: err.Error()}
|
||||
}
|
||||
|
||||
var port int
|
||||
@ -31,8 +55,8 @@ func ParseVmess(proxy string) (model.Proxy, error) {
|
||||
case string:
|
||||
port, err = ParsePort(vmess.Port.(string))
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{
|
||||
Type: ErrInvalidPort,
|
||||
return P.Proxy{}, &E.ParseError{
|
||||
Type: E.ErrInvalidPort,
|
||||
Message: err.Error(),
|
||||
Raw: proxy,
|
||||
}
|
||||
@ -46,7 +70,7 @@ func ParseVmess(proxy string) (model.Proxy, error) {
|
||||
case string:
|
||||
aid, err = strconv.Atoi(vmess.Aid.(string))
|
||||
if err != nil {
|
||||
return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()}
|
||||
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: err.Error()}
|
||||
}
|
||||
case float64:
|
||||
aid = int(vmess.Aid.(float64))
|
||||
@ -61,9 +85,9 @@ func ParseVmess(proxy string) (model.Proxy, error) {
|
||||
name = vmess.Ps
|
||||
}
|
||||
|
||||
result := model.Proxy{
|
||||
result := P.Proxy{
|
||||
Name: name,
|
||||
Type: "vmess",
|
||||
Type: p.GetType(),
|
||||
Server: vmess.Add,
|
||||
Port: port,
|
||||
UUID: vmess.Id,
|
||||
@ -92,7 +116,7 @@ func ParseVmess(proxy string) (model.Proxy, error) {
|
||||
vmess.Host = vmess.Add
|
||||
}
|
||||
result.Network = "ws"
|
||||
result.WSOpts = model.WSOptions{
|
||||
result.WSOpts = P.WSOptions{
|
||||
Path: vmess.Path,
|
||||
Headers: map[string]string{
|
||||
"Host": vmess.Host,
|
||||
@ -100,19 +124,15 @@ func ParseVmess(proxy string) (model.Proxy, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// if vmess.Net == "quic" {
|
||||
// // 未查到相关支持文档
|
||||
// }
|
||||
|
||||
if vmess.Net == "grpc" {
|
||||
result.GrpcOpts = model.GrpcOptions{
|
||||
result.GrpcOpts = P.GrpcOptions{
|
||||
GrpcServiceName: vmess.Path,
|
||||
}
|
||||
result.Network = "grpc"
|
||||
}
|
||||
|
||||
if vmess.Net == "h2" {
|
||||
result.HTTP2Opts = model.HTTP2Options{
|
||||
result.HTTP2Opts = P.HTTP2Options{
|
||||
Host: strings.Split(vmess.Host, ","),
|
||||
Path: vmess.Path,
|
||||
}
|
||||
@ -121,3 +141,7 @@ func ParseVmess(proxy string) (model.Proxy, error) {
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&VmessParser{})
|
||||
}
|
||||
|
209
server/handler/short_link.go
Normal file
209
server/handler/short_link.go
Normal file
@ -0,0 +1,209 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bestnite/sub2clash/common"
|
||||
"github.com/bestnite/sub2clash/common/database"
|
||||
"github.com/bestnite/sub2clash/config"
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type shortLinkGenRequset struct {
|
||||
Url string `form:"url" binding:"required"`
|
||||
Password string `form:"password"`
|
||||
CustomID string `form:"customId"`
|
||||
}
|
||||
|
||||
type shortLinkUpdateRequest struct {
|
||||
Hash string `form:"hash" binding:"required"`
|
||||
Url string `form:"url" binding:"required"`
|
||||
Password string `form:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func respondWithError(c *gin.Context, code int, message string) {
|
||||
c.String(code, message)
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
func GenerateLinkHandler(c *gin.Context) {
|
||||
var params shortLinkGenRequset
|
||||
if err := c.ShouldBind(¶ms); err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(params.Url) == "" {
|
||||
respondWithError(c, http.StatusBadRequest, "URL 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var hash string
|
||||
var password string
|
||||
var err error
|
||||
|
||||
if params.CustomID != "" {
|
||||
// 检查自定义ID是否已存在
|
||||
exists, err := database.CheckShortLinkHashExists(params.CustomID)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "数据库错误")
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
respondWithError(c, http.StatusBadRequest, "短链已存在")
|
||||
return
|
||||
}
|
||||
hash = params.CustomID
|
||||
password = params.Password
|
||||
} else {
|
||||
// 自动生成短链ID和密码
|
||||
hash, err = generateUniqueHash(config.GlobalConfig.ShortLinkLength)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "生成短链接失败")
|
||||
return
|
||||
}
|
||||
if params.Password == "" {
|
||||
password = common.RandomString(8) // 生成8位随机密码
|
||||
} else {
|
||||
password = params.Password
|
||||
}
|
||||
}
|
||||
|
||||
shortLink := model.ShortLink{
|
||||
Hash: hash,
|
||||
Url: params.Url,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
if err := database.SaveShortLink(&shortLink); err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "数据库错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回生成的短链ID和密码
|
||||
response := map[string]string{
|
||||
"hash": hash,
|
||||
"password": password,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func generateUniqueHash(length int) (string, error) {
|
||||
for {
|
||||
hash := common.RandomString(length)
|
||||
exists, err := database.CheckShortLinkHashExists(hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !exists {
|
||||
return hash, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateLinkHandler(c *gin.Context) {
|
||||
var params shortLinkUpdateRequest
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 先获取原有的短链接
|
||||
existingLink, err := database.FindShortLinkByHash(params.Hash)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "未找到短链接")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if existingLink.Password != params.Password {
|
||||
respondWithError(c, http.StatusUnauthorized, "密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新URL,但保持原密码不变
|
||||
shortLink := model.ShortLink{
|
||||
Hash: params.Hash,
|
||||
Url: params.Url,
|
||||
Password: existingLink.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")
|
||||
password := c.Query("password")
|
||||
|
||||
if strings.TrimSpace(hash) == "" {
|
||||
c.String(http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
shortLink, err := database.FindShortLinkByHash(hash)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "未找到短链接或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if shortLink.Password != "" && shortLink.Password != password {
|
||||
c.String(http.StatusNotFound, "未找到短链接或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
shortLink.LastRequestTime = time.Now().Unix()
|
||||
err = database.SaveShortLink(shortLink)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "数据库错误")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := http.Get(strings.TrimSuffix(config.GlobalConfig.Address, "/") + "/" + shortLink.Url)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "请求错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
all, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "读取错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, string(all))
|
||||
}
|
||||
|
||||
func GetRawConfUriHandler(c *gin.Context) {
|
||||
|
||||
hash := c.Query("hash")
|
||||
password := c.Query("password")
|
||||
|
||||
if strings.TrimSpace(hash) == "" {
|
||||
c.String(http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
shortLink, err := database.FindShortLinkByHash(hash)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "未找到短链接或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if shortLink.Password != "" && shortLink.Password != password {
|
||||
c.String(http.StatusNotFound, "未找到短链接或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, shortLink.Url)
|
||||
}
|
54
server/handler/sub.go
Normal file
54
server/handler/sub.go
Normal file
@ -0,0 +1,54 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
|
||||
"github.com/bestnite/sub2clash/common"
|
||||
"github.com/bestnite/sub2clash/config"
|
||||
M "github.com/bestnite/sub2clash/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func SubHandler(model M.ClashType, template string) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
query, err := M.ParseSubQuery(c)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
sub, err := common.BuildSub(model, query, template, config.GlobalConfig.CacheExpire, config.GlobalConfig.RequestRetryTimes)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(query.Subs) == 1 {
|
||||
userInfoHeader, err := common.FetchSubscriptionUserInfo(query.Subs[0], "clash", config.GlobalConfig.RequestRetryTimes)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
c.Header("subscription-userinfo", userInfoHeader)
|
||||
}
|
||||
|
||||
if query.NodeListMode {
|
||||
nodelist := M.NodeList{}
|
||||
nodelist.Proxies = sub.Proxies
|
||||
marshal, err := yaml.Marshal(nodelist)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
return
|
||||
}
|
||||
marshal, err := yaml.Marshal(sub)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
}
|
||||
}
|
@ -2,9 +2,10 @@ package middleware
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sub2clash/logger"
|
||||
"time"
|
||||
|
||||
"github.com/bestnite/sub2clash/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
@ -1,13 +1,16 @@
|
||||
package api
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"sub2clash/api/handler"
|
||||
"sub2clash/constant"
|
||||
"sub2clash/middleware"
|
||||
|
||||
"github.com/bestnite/sub2clash/config"
|
||||
"github.com/bestnite/sub2clash/constant"
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
"github.com/bestnite/sub2clash/server/handler"
|
||||
"github.com/bestnite/sub2clash/server/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@ -39,9 +42,10 @@ func SetRoute(r *gin.Engine) {
|
||||
)
|
||||
},
|
||||
)
|
||||
r.GET("/clash", handler.SubmodHandler)
|
||||
r.GET("/meta", handler.SubHandler)
|
||||
r.GET("/clash", handler.SubHandler(model.Clash, config.GlobalConfig.ClashTemplate))
|
||||
r.GET("/meta", handler.SubHandler(model.ClashMeta, config.GlobalConfig.MetaTemplate))
|
||||
r.GET("/s/:hash", handler.GetRawConfHandler)
|
||||
r.POST("/short", handler.GenerateLinkHandler)
|
||||
r.PUT("/short", handler.UpdateLinkHandler)
|
||||
r.GET("/short", handler.GetRawConfUriHandler)
|
||||
}
|
@ -38,7 +38,7 @@
|
||||
<div class="mb-4">
|
||||
<h2>sub2clash</h2>
|
||||
<span class="text-muted fst-italic">通用订阅链接转 Clash(Meta) 配置工具
|
||||
<a href="https://github.com/nitezs/sub2clash#clash-meta" target="_blank">使用文档</a></span><br /><span
|
||||
<a href="https://github.com/bestnite/sub2clash#clash-meta" target="_blank">使用文档</a></span><br /><span
|
||||
class="text-muted fst-italic">注意:本程序非纯前端程序,输入的订阅将被后端缓存,请确保您信任当前站点</span>
|
||||
</div>
|
||||
|
||||
@ -75,6 +75,12 @@
|
||||
<label for="proxy">节点分享链接:</label>
|
||||
<textarea class="form-control" id="proxy" name="proxy" placeholder="每行输入一个节点分享链接" rows="5"></textarea>
|
||||
</div>
|
||||
<!-- User Agent -->
|
||||
<div class="form-group mb-3">
|
||||
<label for="user-agent">ua标识:</label>
|
||||
<textarea class="form-control" id="user-agent" name="user-agent"
|
||||
placeholder="用于获取订阅的http请求中的user-agent标识(可选)" rows="3"></textarea>
|
||||
</div>
|
||||
<!-- Refresh -->
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" id="refresh" name="refresh" type="checkbox" />
|
||||
@ -141,35 +147,39 @@
|
||||
<div class="form-group mb-5">
|
||||
<label for="apiLink">配置链接:</label>
|
||||
<div class="input-group mb-2">
|
||||
<input class="form-control" id="apiLink" type="text" placeholder="链接" />
|
||||
<input class="form-control bg-light" id="apiLink" type="text" placeholder="链接" readonly style="cursor: not-allowed;" />
|
||||
<button class="btn btn-primary" onclick="copyToClipboard('apiLink',this)" type="button">
|
||||
复制链接
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input class="form-control" id="apiShortLink" type="text" placeholder="链接" />
|
||||
<input class="form-control" id="password" type="text" placeholder="密码" />
|
||||
<div class="input-group mb-2">
|
||||
<input class="form-control" id="customId" type="text" placeholder="短链ID(可选)" />
|
||||
<input class="form-control" id="password" type="text" placeholder="密码(可选)" />
|
||||
<button class="btn btn-primary" onclick="generateShortLink()" type="button">
|
||||
生成短链
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="updateShortLink()" type="button">
|
||||
更新短链
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="copyToClipboard('apiShortLink',this)" type="button">
|
||||
复制短链
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input class="form-control bg-light" id="apiShortLink" type="text" placeholder="短链接" readonly style="cursor: not-allowed;" />
|
||||
<button class="btn btn-primary" onclick="updateShortLink()" type="button">
|
||||
更新短链
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- footer-->
|
||||
<footer>
|
||||
<p class="text-center">
|
||||
Powered by
|
||||
<a class="link-primary" href="https://github.com/nitezs/sub2clash">sub2clash</a>
|
||||
<a class="link-primary" href="https://github.com/bestnite/sub2clash">sub2clash</a>
|
||||
</p>
|
||||
<p class="text-center">Version {{.Version}}</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
<script src="./static/index.js"></script>
|
||||
|
||||
</html>
|
@ -1,3 +1,15 @@
|
||||
function setInputReadOnly(input, readonly) {
|
||||
if (readonly) {
|
||||
input.readOnly = true;
|
||||
input.classList.add('bg-light');
|
||||
input.style.cursor = 'not-allowed';
|
||||
} else {
|
||||
input.readOnly = false;
|
||||
input.classList.remove('bg-light');
|
||||
input.style.cursor = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
function clearExistingValues() {
|
||||
// 清除简单输入框和复选框的值
|
||||
document.getElementById("endpoint").value = "clash";
|
||||
@ -12,7 +24,23 @@ function clearExistingValues() {
|
||||
document.getElementById("remove").value = "";
|
||||
document.getElementById("apiLink").value = "";
|
||||
document.getElementById("apiShortLink").value = "";
|
||||
document.getElementById("password").value = "";
|
||||
|
||||
// 恢复短链ID和密码输入框状态
|
||||
const customIdInput = document.getElementById("customId");
|
||||
const passwordInput = document.getElementById("password");
|
||||
const generateButton = document.querySelector('button[onclick="generateShortLink()"]');
|
||||
|
||||
customIdInput.value = "";
|
||||
setInputReadOnly(customIdInput, false);
|
||||
|
||||
passwordInput.value = "";
|
||||
setInputReadOnly(passwordInput, false);
|
||||
|
||||
// 恢复生成短链按钮状态
|
||||
generateButton.disabled = false;
|
||||
generateButton.classList.remove('btn-secondary');
|
||||
generateButton.classList.add('btn-primary');
|
||||
|
||||
document.getElementById("nodeList").checked = false;
|
||||
|
||||
// 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组
|
||||
@ -66,6 +94,11 @@ function generateURI() {
|
||||
// alert("订阅链接和节点分享链接不能同时为空!");
|
||||
return "";
|
||||
}
|
||||
|
||||
// 获取订阅user-agent标识
|
||||
const userAgent = document.getElementById("user-agent").value;
|
||||
queryParams.push(`userAgent=${encodeURIComponent(userAgent)}`);
|
||||
|
||||
// 获取复选框的值
|
||||
const refresh = document.getElementById("refresh").checked;
|
||||
queryParams.push(`refresh=${refresh ? "true" : "false"}`);
|
||||
@ -111,17 +144,16 @@ function generateURI() {
|
||||
queryParams.push(`ruleProvider=${encodeURIComponent(providers.join(","))}`);
|
||||
|
||||
let ruleList = [];
|
||||
for (let i = 0; i < rules.length / 3; i++) {
|
||||
if (rules[i * 3].value.trim() !== "") {
|
||||
let rule = rules[i * 3].value;
|
||||
let prepend = rules[i * 3 + 1].value;
|
||||
let group = rules[i * 3 + 2].value;
|
||||
for (let i = 0; i < rules.length / 2; i++) {
|
||||
if (rules[i * 2].value.trim() !== "") {
|
||||
let rule = rules[i * 2].value;
|
||||
let prepend = rules[i * 2 + 1].value;
|
||||
// 是否存在空值
|
||||
if (rule.trim() === "" || prepend.trim() === "" || group.trim() === "") {
|
||||
if (rule.trim() === "" || prepend.trim() === "") {
|
||||
// alert("Rule 中存在空值,请检查后重试!");
|
||||
return "";
|
||||
}
|
||||
ruleList.push(`[${rule},${prepend},${group}]`);
|
||||
ruleList.push(`[${rule},${prepend}]`);
|
||||
}
|
||||
}
|
||||
queryParams.push(`rule=${encodeURIComponent(ruleList.join(","))}`);
|
||||
@ -175,13 +207,41 @@ async function parseInputURL() {
|
||||
let hash = url.pathname.substring(url.pathname.lastIndexOf("/s/") + 3);
|
||||
let q = new URLSearchParams();
|
||||
let password = url.searchParams.get("password");
|
||||
if (password === null) {
|
||||
alert("仅可解析加密短链");
|
||||
return;
|
||||
}
|
||||
q.append("hash", hash);
|
||||
q.append("password", password);
|
||||
try {
|
||||
const response = await axios.get("./short?" + q.toString());
|
||||
url = new URL(window.location.href + response.data);
|
||||
document.querySelector("#apiShortLink").value = inputURL;
|
||||
document.querySelector("#password").value = password;
|
||||
|
||||
// 回显配置链接
|
||||
const apiLinkInput = document.querySelector("#apiLink");
|
||||
apiLinkInput.value = `${window.location.origin}${window.location.pathname}${response.data}`;
|
||||
setInputReadOnly(apiLinkInput, true);
|
||||
|
||||
// 回显短链相关信息
|
||||
const apiShortLinkInput = document.querySelector("#apiShortLink");
|
||||
apiShortLinkInput.value = inputURL;
|
||||
setInputReadOnly(apiShortLinkInput, true);
|
||||
|
||||
// 设置短链ID和密码,并设置为只读
|
||||
const customIdInput = document.querySelector("#customId");
|
||||
const passwordInput = document.querySelector("#password");
|
||||
const generateButton = document.querySelector('button[onclick="generateShortLink()"]');
|
||||
|
||||
customIdInput.value = hash;
|
||||
setInputReadOnly(customIdInput, true);
|
||||
|
||||
passwordInput.value = password;
|
||||
setInputReadOnly(passwordInput, true);
|
||||
|
||||
// 禁用生成短链按钮
|
||||
generateButton.disabled = true;
|
||||
generateButton.classList.add('btn-secondary');
|
||||
generateButton.classList.remove('btn-primary');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
alert("获取短链失败,请检查密码!");
|
||||
@ -247,6 +307,17 @@ async function parseInputURL() {
|
||||
);
|
||||
}
|
||||
|
||||
if (params.has("userAgent")) {
|
||||
document.getElementById("user-agent").value = decodeURIComponent(
|
||||
params.get("userAgent")
|
||||
);
|
||||
}
|
||||
|
||||
if (params.has("ignoreCountryGroup")) {
|
||||
document.getElementById("igcg").checked =
|
||||
params.get("ignoreCountryGroup") === "true";
|
||||
}
|
||||
|
||||
if (params.has("replace")) {
|
||||
parseAndFillReplaceParams(decodeURIComponent(params.get("replace")));
|
||||
}
|
||||
@ -310,13 +381,12 @@ function parseAndFillRuleProviderParams(ruleProviderParams) {
|
||||
function parseAndFillRuleParams(ruleParams) {
|
||||
const ruleGroup = document.getElementById("ruleGroup");
|
||||
let matches;
|
||||
const regex = /\[(.*?),(.*?),(.*?)\]/g;
|
||||
const regex = /\[(.*?),(.*?)\]/g;
|
||||
const str = decodeURIComponent(ruleParams);
|
||||
while ((matches = regex.exec(str)) !== null) {
|
||||
const div = createRule();
|
||||
div.children[0].value = matches[1];
|
||||
div.children[1].value = matches[2];
|
||||
div.children[2].value = matches[3];
|
||||
ruleGroup.appendChild(div);
|
||||
}
|
||||
}
|
||||
@ -366,7 +436,6 @@ function createRule() {
|
||||
div.innerHTML = `
|
||||
<input type="text" class="form-control" name="rule" placeholder="Rule">
|
||||
<input type="text" class="form-control" name="rule" placeholder="Prepend">
|
||||
<input type="text" class="form-control" name="rule" placeholder="Group">
|
||||
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
|
||||
`;
|
||||
return div;
|
||||
@ -422,21 +491,25 @@ function generateURL() {
|
||||
return;
|
||||
}
|
||||
apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`;
|
||||
setInputReadOnly(apiLink, true);
|
||||
}
|
||||
|
||||
function generateShortLink() {
|
||||
const apiShortLink = document.getElementById("apiShortLink");
|
||||
const password = document.getElementById("password");
|
||||
const customId = document.getElementById("customId");
|
||||
let uri = generateURI();
|
||||
if (uri === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
axios
|
||||
.post(
|
||||
"./short",
|
||||
{
|
||||
url: uri,
|
||||
password: password.value.trim(),
|
||||
customId: customId.value.trim()
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@ -445,11 +518,20 @@ function generateShortLink() {
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
apiShortLink.value = `${window.location.origin}${window.location.pathname}s/${response.data}`;
|
||||
// 设置返回的短链ID和密码
|
||||
customId.value = response.data.hash;
|
||||
password.value = response.data.password;
|
||||
// 生成完整的短链接
|
||||
const shortLink = `${window.location.origin}${window.location.pathname}s/${response.data.hash}?password=${response.data.password}`;
|
||||
apiShortLink.value = shortLink;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
alert("生成短链失败,请重试!");
|
||||
if (error.response && error.response.data) {
|
||||
alert(error.response.data);
|
||||
} else {
|
||||
alert("生成短链失败,请重试!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -462,7 +544,7 @@ function updateShortLink() {
|
||||
hash = u.pathname.substring(u.pathname.lastIndexOf("/s/") + 3);
|
||||
}
|
||||
if (password.value.trim() === "") {
|
||||
alert("请输入密码!");
|
||||
alert("请输入原密码进行验证!");
|
||||
return;
|
||||
}
|
||||
let uri = generateURI();
|
||||
@ -484,11 +566,17 @@ function updateShortLink() {
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
alert("更新短链成功!");
|
||||
alert(`短链 ${hash} 更新成功!`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
alert(error.response.data);
|
||||
if (error.response && error.response.status === 401) {
|
||||
alert("密码错误,请输入正确的原密码!");
|
||||
} else if (error.response && error.response.data) {
|
||||
alert(error.response.data);
|
||||
} else {
|
||||
alert("更新短链失败,请重试!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
package validator
|
||||
|
||||
type ShortLinkGenValidator struct {
|
||||
Url string `form:"url" binding:"required"`
|
||||
Password string `form:"password"`
|
||||
}
|
||||
|
||||
type GetUrlValidator struct {
|
||||
Hash string `form:"hash" binding:"required"` // Hash: 短链接
|
||||
Password string `form:"password"`
|
||||
}
|
||||
|
||||
type ShortLinkUpdateValidator struct {
|
||||
Hash string `form:"hash" binding:"required"`
|
||||
Url string `form:"url" binding:"required"`
|
||||
Password string `form:"password" binding:"required"`
|
||||
}
|
Reference in New Issue
Block a user