Skip to content

Commit

Permalink
v0.0.5
Browse files Browse the repository at this point in the history
feat: 增加节点去重
feat: 增加节点重命名
feat: 增加节点过滤
feat: 增加短链密码
modify: 修改模板解析逻辑,现在需要添加 <all>,<countries> 来让程序解析模板
modify: 修改短链请求逻辑,不再跳转链接,而是服务器内部请求
modify: 完善 Meta 默认模板
如果你从旧版升级,请务必修改或删除程序目录下的模板
  • Loading branch information
nitezs authored Sep 23, 2023
2 parents 8d06ab3 + 3546396 commit 34b85c8
Show file tree
Hide file tree
Showing 15 changed files with 386 additions and 65 deletions.
36 changes: 36 additions & 0 deletions API_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# `/clash`, `/meta`

获取 Clash/Clash.Meta 配置链接

| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|--------------|--------|-------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| sub | string | sub/proxy 至少有一项存在 | - | 订阅链接(可以输入多个,用 `,` 分隔) |
| proxy | string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个,用 `,` 分隔) |
| refresh | bool || `false` | 强制刷新配置(默认缓存 5 分钟) |
| template | string || - | 外部模板链接或内部模板名称 |
| ruleProvider | string || - | 格式 `[Behavior,Url,Group,Prepend,Name],[Behavior,Url,Group,Prepend,Name]...`,其中 `Group` 是该规则集使用的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) |
| rule | string || - | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) |
| autoTest | bool || `false` | 国家策略组是否自动测速 |
| lazy | bool || `false` | 自动测速是否启用 lazy |
| sort | string || `nameasc` | 国家策略组排序策略,可选值 `nameasc``namedesc``sizeasc``sizedesc` |
| replace | string || - | 通过正则表达式重命名节点,格式 `[<ReplaceKey>,<ReplaceTo>],[<ReplaceKey>,<ReplaceTo>]...` |
| remove | string || - | 通过正则表达式删除节点 |

# `/short`

获取短链,Content-Type 为 `application/json`
具体参考使用可以参考 [api\templates\index.html](./api/templates/index.html)

| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|----------|--------|------|-----|------------------|
| url | string || - | 需要转换的 Query 参数部分 |
| password | string || - | 短链密码 |

# `/s/:hash`

短链跳转
`hash` 为动态路由参数,可以通过 `/short` 接口获取

| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|----------|--------|------|-----|------|
| password | string || - | 短链密码 |
24 changes: 8 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,21 @@

### API

#### `/clash`, `/meta`
[API文档](./API_README.md)

获取 Clash/Clash.Meta 配置链接
### 模板

| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|--------------|--------|-------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| sub | string | sub/proxy 至少有一项存在 | - | 订阅链接(可以输入多个,用 `,` 分隔) |
| proxy | string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个,用 `,` 分隔) |
| refresh | bool || `false` | 强制刷新配置(默认缓存 5 分钟) |
| template | string || - | 外部模板链接或内部模板名称 |
| ruleProvider | string || - | 格式 `[Behavior,Url,Group,Prepend,Name],[Behavior,Url,Group,Prepend,Name]...`,其中 `Group` 是该规则集所走的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) |
| rule | string || - | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) |
| autoTest | bool || `false` | 国家策略组是否自动测速 |
| lazy | bool || `false` | 自动测速是否启用 lazy |
| sort | string || `nameasc` | 国家策略组排序策略,可选值 `nameasc``namedesc``sizeasc``sizedesc` |
可以通过变量自定义模板中的策略组代理节点
解释的不太清楚,可以参考下方默认模板

## 默认模板
- `<all>` 为添加所有节点
- `<countries>` 为添加所有国家策略组

#### 默认模板

- [Clash](./templates/template_clash.yaml)
- [Clash.Meta](./templates/template_meta.yaml)

## 已知问题

[代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue

## TODO
129 changes: 106 additions & 23 deletions api/controller/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"sub2clash/logger"
"sub2clash/model"
"sub2clash/parser"
"sub2clash/utils"
Expand All @@ -31,38 +34,52 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
if err != nil {
templateBytes, err = utils.LoadTemplate(template)
if err != nil {
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, errors.New("加载模板失败: " + err.Error())
}
} else {
templateBytes, err = utils.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())
}
}
// 解析模板
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 := utils.LoadSubscription(query.Subs[i], query.Refresh)
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)
if err != nil {
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|http|https)://")
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless)://")
if reg.Match(data) {
p := utils.ParseProxy(strings.Split(string(data), "\n")...)
proxyList = append(proxyList, 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 := utils.ParseProxy(strings.Split(base64, "\n")...)
Expand All @@ -72,14 +89,80 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
proxyList = append(proxyList, sub.Proxies...)
}
}
// 添加自定义节点
if len(query.Proxies) != 0 {
proxyList = append(proxyList, utils.ParseProxy(query.Proxies...)...)
}
// 去掉配置相同的节点
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
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 err != nil {
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
return nil, errors.New("replaceName 参数非法: " + err.Error())
}
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 {
proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
}
names[proxyList[i].Name] = names[proxyList[i].Name] + 1
}
// trim
for i := range proxyList {
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
}
// 将新增节点都添加到临时变量 t 中,防止策略组排序错乱
var t = &model.Subscription{}
utils.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
// 处理自定义代理
utils.AddProxy(
t, query.AutoTest, query.Lazy, clashType,
utils.ParseProxy(query.Proxies...)...,
)
// 排序策略组
switch query.Sort {
case "sizeasc":
Expand Down Expand Up @@ -138,28 +221,28 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
)
}
}
var proxyNames []string
for _, proxy := range sub.Proxies {
proxyNames = append(proxyNames, proxy.Name)
}
// 将订阅中的节点添加到模板中
temp.Proxies = append(temp.Proxies, sub.Proxies...)
// 将订阅中的策略组添加到模板中
skipGroups := []string{"全球直连", "广告拦截", "手动切换"}
for i := range temp.ProxyGroups {
skip := false
for _, v := range skipGroups {
if strings.Contains(temp.ProxyGroups[i].Name, v) {
if v == "手动切换" {
proxies := make([]string, 0, len(sub.Proxies))
for _, p := range sub.Proxies {
proxies = append(proxies, p.Name)
}
temp.ProxyGroups[i].Proxies = proxies
}
skip = true
continue
}
if temp.ProxyGroups[i].IsCountryGrop {
continue
}
if !skip {
temp.ProxyGroups[i].Proxies = append(temp.ProxyGroups[i].Proxies, countryGroupNames...)
newProxies := make([]string, 0, len(temp.ProxyGroups[i].Proxies))
for j := range temp.ProxyGroups[i].Proxies {
if temp.ProxyGroups[i].Proxies[j] == "<all>" {
newProxies = append(newProxies, proxyNames...)
} else if temp.ProxyGroups[i].Proxies[j] == "<countries>" {
newProxies = append(newProxies, countryGroupNames...)
} else {
newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j])
}
}
temp.ProxyGroups[i].Proxies = newProxies
}
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
}
39 changes: 34 additions & 5 deletions api/controller/short_link.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ package controller
import (
"errors"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
"io"
"net/http"
"strconv"
"strings"
"sub2clash/config"
"sub2clash/logger"
"sub2clash/model"
"sub2clash/utils"
"sub2clash/utils/database"
Expand All @@ -26,11 +30,16 @@ func ShortLinkGenHandler(c *gin.Context) {
}
// 生成hash
hash := utils.RandomString(config.Default.ShortLinkLength)
// 存入数据库
var item model.ShortLink
result := database.FindShortLinkByUrl(params.Url, &item)
if result.Error == nil {
c.String(200, item.Hash)
if item.Password != params.Password {
item.Password = params.Password
database.SaveShortLink(&item)
c.String(200, item.Hash+"?password="+params.Password)
} else {
c.String(200, item.Hash)
}
return
} else {
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
Expand All @@ -50,15 +59,20 @@ func ShortLinkGenHandler(c *gin.Context) {
Hash: hash,
Url: params.Url,
LastRequestTime: -1,
Password: params.Password,
},
)
// 返回短链接
if params.Password != "" {
hash += "?password=" + params.Password
}
c.String(200, hash)
}

func ShortLinkGetHandler(c *gin.Context) {
// 获取动态路由
hash := c.Param("hash")
password := c.Query("password")
if strings.TrimSpace(hash) == "" {
c.String(400, "参数错误")
return
Expand All @@ -68,12 +82,27 @@ func ShortLinkGetHandler(c *gin.Context) {
result := database.FindShortLinkByHash(hash, &shortLink)
// 重定向
if result.Error != nil {
c.String(404, "未找到短链接")
c.String(404, "未找到短链接或密码错误")
return
}
if shortLink.Password != "" && shortLink.Password != password {
c.String(404, "未找到短链接或密码错误")
return
}
// 更新最后访问时间
shortLink.LastRequestTime = time.Now().Unix()
database.SaveShortLink(&shortLink)
uri := config.Default.BasePath + shortLink.Url
c.Redirect(http.StatusTemporaryRedirect, uri)
get, err := utils.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url)
if err != nil {
logger.Logger.Debug("get short link data failed", zap.Error(err))
c.String(500, "请求错误: "+err.Error())
return
}
all, err := io.ReadAll(get.Body)
if err != nil {
logger.Logger.Debug("read short link data failed", zap.Error(err))
c.String(500, "读取错误: "+err.Error())
return
}
c.String(http.StatusOK, string(all))
}
Loading

0 comments on commit 34b85c8

Please sign in to comment.