1
0
mirror of https://github.com/nitezs/sub2clash.git synced 2024-12-24 11:14:41 -05:00
feat: 增加节点去重
feat: 增加节点重命名
feat: 增加节点过滤
feat: 增加短链密码
modify: 修改模板解析逻辑,现在需要添加 <all>,<countries> 来让程序解析模板
modify: 修改短链请求逻辑,不再跳转链接,而是服务器内部请求
modify: 完善 Meta 默认模板
如果你从旧版升级,请务必修改或删除程序目录下的模板
This commit is contained in:
Nite07 2023-09-23 09:09:09 +08:00 committed by GitHub
commit 34b85c8d63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 387 additions and 66 deletions

36
API_README.md Normal file
View File

@ -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 | 否 | - | 短链密码 |

View File

@ -39,23 +39,17 @@
### API ### 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](./templates/template_clash.yaml)
- [Clash.Meta](./templates/template_meta.yaml) - [Clash.Meta](./templates/template_meta.yaml)
@ -63,5 +57,3 @@
## 已知问题 ## 已知问题
[代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue [代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue
## TODO

View File

@ -4,11 +4,14 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "errors"
"go.uber.org/zap"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"net/url" "net/url"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"sub2clash/logger"
"sub2clash/model" "sub2clash/model"
"sub2clash/parser" "sub2clash/parser"
"sub2clash/utils" "sub2clash/utils"
@ -31,17 +34,24 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
if err != nil { if err != nil {
templateBytes, err = utils.LoadTemplate(template) templateBytes, err = utils.LoadTemplate(template)
if err != nil { if err != nil {
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, errors.New("加载模板失败: " + err.Error()) return nil, errors.New("加载模板失败: " + err.Error())
} }
} else { } else {
templateBytes, err = utils.LoadSubscription(template, query.Refresh) templateBytes, err = utils.LoadSubscription(template, query.Refresh)
if err != nil { if err != nil {
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, errors.New("加载模板失败: " + err.Error()) return nil, errors.New("加载模板失败: " + err.Error())
} }
} }
// 解析模板 // 解析模板
err = yaml.Unmarshal(templateBytes, &temp) err = yaml.Unmarshal(templateBytes, &temp)
if err != nil { if err != nil {
logger.Logger.Debug("parse template failed", zap.Error(err))
return nil, errors.New("解析模板失败: " + err.Error()) return nil, errors.New("解析模板失败: " + err.Error())
} }
var proxyList []model.Proxy var proxyList []model.Proxy
@ -49,13 +59,15 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
for i := range query.Subs { for i := range query.Subs {
data, err := utils.LoadSubscription(query.Subs[i], query.Refresh) data, err := utils.LoadSubscription(query.Subs[i], query.Refresh)
if err != nil { if err != nil {
logger.Logger.Debug(
"load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
)
return nil, errors.New("加载订阅失败: " + err.Error()) return nil, errors.New("加载订阅失败: " + err.Error())
} }
// 解析订阅 // 解析订阅
err = yaml.Unmarshal(data, &sub) err = yaml.Unmarshal(data, &sub)
if err != nil { if err != nil {
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|http|https)://") reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless)://")
if reg.Match(data) { if reg.Match(data) {
p := utils.ParseProxy(strings.Split(string(data), "\n")...) p := utils.ParseProxy(strings.Split(string(data), "\n")...)
proxyList = append(proxyList, p...) proxyList = append(proxyList, p...)
@ -63,6 +75,11 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
// 如果无法直接解析尝试Base64解码 // 如果无法直接解析尝试Base64解码
base64, err := parser.DecodeBase64(string(data)) base64, err := parser.DecodeBase64(string(data))
if err != nil { 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()) return nil, errors.New("加载订阅失败: " + err.Error())
} }
p := utils.ParseProxy(strings.Split(base64, "\n")...) p := utils.ParseProxy(strings.Split(base64, "\n")...)
@ -72,14 +89,80 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
proxyList = append(proxyList, sub.Proxies...) 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 中,防止策略组排序错乱 // 将新增节点都添加到临时变量 t 中,防止策略组排序错乱
var t = &model.Subscription{} var t = &model.Subscription{}
utils.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...) utils.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
// 处理自定义代理
utils.AddProxy(
t, query.AutoTest, query.Lazy, clashType,
utils.ParseProxy(query.Proxies...)...,
)
// 排序策略组 // 排序策略组
switch query.Sort { switch query.Sort {
case "sizeasc": case "sizeasc":
@ -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...) temp.Proxies = append(temp.Proxies, sub.Proxies...)
// 将订阅中的策略组添加到模板中 // 将订阅中的策略组添加到模板中
skipGroups := []string{"全球直连", "广告拦截", "手动切换"}
for i := range temp.ProxyGroups { for i := range temp.ProxyGroups {
skip := false if temp.ProxyGroups[i].IsCountryGrop {
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 continue
} }
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])
} }
if !skip {
temp.ProxyGroups[i].Proxies = append(temp.ProxyGroups[i].Proxies, countryGroupNames...)
} }
temp.ProxyGroups[i].Proxies = newProxies
} }
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...) temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
} }

View File

@ -3,10 +3,14 @@ package controller
import ( import (
"errors" "errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
"io"
"net/http" "net/http"
"strconv"
"strings" "strings"
"sub2clash/config" "sub2clash/config"
"sub2clash/logger"
"sub2clash/model" "sub2clash/model"
"sub2clash/utils" "sub2clash/utils"
"sub2clash/utils/database" "sub2clash/utils/database"
@ -26,11 +30,16 @@ func ShortLinkGenHandler(c *gin.Context) {
} }
// 生成hash // 生成hash
hash := utils.RandomString(config.Default.ShortLinkLength) hash := utils.RandomString(config.Default.ShortLinkLength)
// 存入数据库
var item model.ShortLink var item model.ShortLink
result := database.FindShortLinkByUrl(params.Url, &item) result := database.FindShortLinkByUrl(params.Url, &item)
if result.Error == nil { if result.Error == nil {
if item.Password != params.Password {
item.Password = params.Password
database.SaveShortLink(&item)
c.String(200, item.Hash+"?password="+params.Password)
} else {
c.String(200, item.Hash) c.String(200, item.Hash)
}
return return
} else { } else {
if !errors.Is(result.Error, gorm.ErrRecordNotFound) { if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
@ -50,15 +59,20 @@ func ShortLinkGenHandler(c *gin.Context) {
Hash: hash, Hash: hash,
Url: params.Url, Url: params.Url,
LastRequestTime: -1, LastRequestTime: -1,
Password: params.Password,
}, },
) )
// 返回短链接 // 返回短链接
if params.Password != "" {
hash += "?password=" + params.Password
}
c.String(200, hash) c.String(200, hash)
} }
func ShortLinkGetHandler(c *gin.Context) { func ShortLinkGetHandler(c *gin.Context) {
// 获取动态路由 // 获取动态路由
hash := c.Param("hash") hash := c.Param("hash")
password := c.Query("password")
if strings.TrimSpace(hash) == "" { if strings.TrimSpace(hash) == "" {
c.String(400, "参数错误") c.String(400, "参数错误")
return return
@ -68,12 +82,27 @@ func ShortLinkGetHandler(c *gin.Context) {
result := database.FindShortLinkByHash(hash, &shortLink) result := database.FindShortLinkByHash(hash, &shortLink)
// 重定向 // 重定向
if result.Error != nil { if result.Error != nil {
c.String(404, "未找到短链接") c.String(404, "未找到短链接或密码错误")
return
}
if shortLink.Password != "" && shortLink.Password != password {
c.String(404, "未找到短链接或密码错误")
return return
} }
// 更新最后访问时间 // 更新最后访问时间
shortLink.LastRequestTime = time.Now().Unix() shortLink.LastRequestTime = time.Now().Unix()
database.SaveShortLink(&shortLink) database.SaveShortLink(&shortLink)
uri := config.Default.BasePath + shortLink.Url get, err := utils.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url)
c.Redirect(http.StatusTemporaryRedirect, uri) 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))
} }

View File

@ -168,6 +168,30 @@
<option value="sizedesc">节点数量(降序)</option> <option value="sizedesc">节点数量(降序)</option>
</select> </select>
</div> </div>
<!-- Remove -->
<div class="form-group mb-3">
<label for="remove">删除节点:</label>
<input
class="form-control"
type="text"
name="remove"
id="remove"
placeholder="正则表达式"
/>
</div>
<!-- Rename -->
<div class="form-group mb-3" id="replaceGroup">
<label>节点名称替换:</label>
<button
class="btn btn-primary mb-1 btn-xs"
onclick="addReplace()"
type="button"
>
+
</button>
</div>
</form> </form>
<!-- Display the API Link --> <!-- Display the API Link -->
@ -188,6 +212,12 @@
</div> </div>
<div class="input-group"> <div class="input-group">
<input class="form-control" id="apiShortLink" readonly type="text" /> <input class="form-control" id="apiShortLink" readonly type="text" />
<input
class="form-control"
id="password"
type="text"
placeholder="密码"
/>
<button <button
class="btn btn-primary" class="btn btn-primary"
onclick="generateShortLink()" onclick="generateShortLink()"
@ -246,6 +276,17 @@
return div; return div;
} }
function createReplace() {
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
<input type="text" class="form-control" name="replace" placeholder="原字符串(正则表达式)">
<input type="text" class="form-control" name="replace" placeholder="替换为(可为空)">
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
`;
return div;
}
function createRule() { function createRule() {
const div = document.createElement("div"); const div = document.createElement("div");
div.classList.add("input-group", "mb-2"); div.classList.add("input-group", "mb-2");
@ -268,6 +309,11 @@
document.getElementById("ruleGroup").appendChild(div); document.getElementById("ruleGroup").appendChild(div);
} }
function addReplace() {
const div = createReplace();
document.getElementById("replaceGroup").appendChild(div);
}
function removeElement(button) { function removeElement(button) {
button.parentElement.remove(); button.parentElement.remove();
} }
@ -382,6 +428,29 @@
// 获取排序策略 // 获取排序策略
const sort = document.getElementById("sort").value; const sort = document.getElementById("sort").value;
queryParams.push(`sort=${sort}`); queryParams.push(`sort=${sort}`);
// 获取删除节点的正则表达式
const remove = document.getElementById("remove").value;
if (remove.trim() !== "") {
queryParams.push(`remove=${encodeURIComponent(remove)}`);
}
// 获取替换节点名称的正则表达式
let replaceList = [];
const replaces = document.getElementsByName("replace");
for (let i = 0; i < replaces.length / 2; i++) {
let replaceStr = `<${replaces[i * 2].value}>`;
let replaceTo = `<${replaces[i * 2 + 1].value}>`;
if (replaceStr.trim() === "") {
alert("重命名设置中存在空值,请检查后重试!");
return "";
}
replaceList.push(`[${replaceStr},${replaceTo}]`);
}
queryParams.push(
`replace=${encodeURIComponent(replaceList.join(","))}`,
);
return `${endpoint}?${queryParams.join("&")}`; return `${endpoint}?${queryParams.join("&")}`;
} }
@ -396,6 +465,7 @@
function generateShortLink() { function generateShortLink() {
const apiShortLink = document.getElementById("apiShortLink"); const apiShortLink = document.getElementById("apiShortLink");
const password = document.getElementById("password");
let uri = generateURI(); let uri = generateURI();
if (uri === "") { if (uri === "") {
return; return;
@ -405,6 +475,7 @@
"./short", "./short",
{ {
url: uri, url: uri,
password: password.value.trim(),
}, },
{ {
headers: { headers: {

View File

@ -3,5 +3,6 @@ package model
type ShortLink struct { type ShortLink struct {
Hash string `gorm:"primary_key"` Hash string `gorm:"primary_key"`
Url string Url string
Password string
LastRequestTime int64 LastRequestTime int64
} }

View File

@ -8,88 +8,104 @@ proxy-groups:
- name: 节点选择 - name: 节点选择
type: select type: select
proxies: proxies:
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 手动切换 - name: 手动切换
type: select type: select
proxies: proxies:
- <all>
- name: 电报消息 - name: 电报消息
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: OpenAi - name: OpenAi
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 油管视频 - name: 油管视频
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 巴哈姆特 - name: 巴哈姆特
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 哔哩哔哩 - name: 哔哩哔哩
type: select type: select
proxies: proxies:
- <countries>
- 全球直连 - 全球直连
- name: 国外媒体 - name: 国外媒体
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 国内媒体 - name: 国内媒体
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 手动切换 - 手动切换
- name: 谷歌FCM - name: 谷歌FCM
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- name: 微软云盘 - name: 微软云盘
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- name: 微软服务 - name: 微软服务
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- name: 苹果服务 - name: 苹果服务
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- name: 游戏平台 - name: 游戏平台
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- name: 网易音乐 - name: 网易音乐
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- name: 全球直连 - name: 全球直连
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- name: 广告拦截 - name: 广告拦截
@ -105,6 +121,7 @@ proxy-groups:
- name: 漏网之鱼 - name: 漏网之鱼
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- DIRECT - DIRECT
- 手动切换 - 手动切换

View File

@ -8,40 +8,90 @@ proxy-groups:
- name: 节点选择 - name: 节点选择
type: select type: select
proxies: proxies:
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 手动切换 - name: 手动切换
type: select type: select
proxies: proxies:
- name: 微软服务 - <all>
- name: 游戏平台(中国)
type: select type: select
proxies: proxies:
- 节点选择 - 节点选择
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 游戏平台 - name: 游戏平台(全球)
type: select type: select
proxies: proxies:
- 节点选择 - 节点选择
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 巴哈姆特 - name: 巴哈姆特
type: select type: select
proxies: proxies:
- 节点选择 - 节点选择
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 哔哩哔哩 - name: 哔哩哔哩
type: select type: select
proxies: proxies:
- 节点选择 - 节点选择
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 全球直连 - name: Telegram
type: select type: select
proxies: proxies:
- DIRECT
- 节点选择 - 节点选择
- <countries>
- 手动切换
- DIRECT
- name: OpenAI
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Youtube
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Microsoft
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Onedrive
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Apple
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Netflix
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 广告拦截 - name: 广告拦截
type: select type: select
proxies: proxies:
@ -51,17 +101,25 @@ proxy-groups:
type: select type: select
proxies: proxies:
- 节点选择 - 节点选择
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
rules: rules:
- GEOSITE,private,全球直连,no-resolve - GEOSITE,private,DIRECT,no-resolve
- GEOIP,private,全球直连 - GEOIP,private,DIRECT
- GEOSITE,category-ads-all,广告拦截 - GEOSITE,category-ads-all,广告拦截
- GEOSITE,microsoft,微软服务 - GEOSITE,microsoft,Microsoft
- GEOSITE,apple,Apple
- GEOSITE,netflix,Netflix
- GEOSITE,onedrive,Onedrive
- GEOSITE,youtube,Youtube
- GEOSITE,telegram,Telegram
- GEOSITE,openai,OpenAI
- GEOSITE,bilibili,哔哩哔哩 - GEOSITE,bilibili,哔哩哔哩
- GEOSITE,bahamut,巴哈姆特 - GEOSITE,bahamut,巴哈姆特
- GEOSITE,category-games,游戏平台 - GEOSITE,category-games@cn,游戏平台(中国)
- GEOSITE,category-games,游戏平台(全球)
- GEOSITE,geolocation-!cn,节点选择 - GEOSITE,geolocation-!cn,节点选择
- GEOSITE,CN,全球直连 - GEOSITE,CN,DIRECT
- GEOIP,CN,全球直连 - GEOIP,CN,DIRECT
- MATCH,漏网之鱼 - MATCH,漏网之鱼

View File

@ -12,7 +12,15 @@ func Get(url string) (resp *http.Response, err error) {
haveTried := 0 haveTried := 0
retryDelay := time.Second // 延迟1秒再重试 retryDelay := time.Second // 延迟1秒再重试
for haveTried < retryTimes { for haveTried < retryTimes {
get, err := http.Get(url) 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 { if err != nil {
haveTried++ haveTried++
time.Sleep(retryDelay) time.Sleep(retryDelay)

View File

@ -31,7 +31,6 @@ func AddProxy(
sub *model.Subscription, autotest bool, sub *model.Subscription, autotest bool,
lazy bool, clashType model.ClashType, proxies ...model.Proxy, lazy bool, clashType model.ClashType, proxies ...model.Proxy,
) { ) {
newCountryGroupNames := make([]string, 0)
proxyTypes := model.GetSupportProxyTypes(clashType) proxyTypes := model.GetSupportProxyTypes(clashType)
// 添加节点 // 添加节点
for _, proxy := range proxies { for _, proxy := range proxies {
@ -79,7 +78,6 @@ func AddProxy(
} }
} }
sub.ProxyGroups = append(sub.ProxyGroups, newGroup) sub.ProxyGroups = append(sub.ProxyGroups, newGroup)
newCountryGroupNames = append(newCountryGroupNames, countryName)
} }
} }
} }

View File

@ -35,7 +35,9 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) {
return nil, err return nil, err
} }
defer func(file *os.File) { defer func(file *os.File) {
if file != nil {
_ = file.Close() _ = file.Close()
}
}(file) }(file)
fileLock.RLock() fileLock.RLock()
defer fileLock.RUnlock() defer fileLock.RUnlock()
@ -56,7 +58,9 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
return nil, err return nil, err
} }
defer func(Body io.ReadCloser) { defer func(Body io.ReadCloser) {
if Body != nil {
_ = Body.Close() _ = Body.Close()
}
}(resp.Body) }(resp.Body)
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
@ -67,7 +71,9 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
return nil, err return nil, err
} }
defer func(file *os.File) { defer func(file *os.File) {
if file != nil {
_ = file.Close() _ = file.Close()
}
}(file) }(file)
fileLock.Lock() fileLock.Lock()
defer fileLock.Unlock() defer fileLock.Unlock()

View File

@ -17,7 +17,9 @@ func LoadTemplate(template string) ([]byte, error) {
return nil, err return nil, err
} }
defer func(file *os.File) { defer func(file *os.File) {
if file != nil {
_ = file.Close() _ = file.Close()
}
}(file) }(file)
result, err := io.ReadAll(file) result, err := io.ReadAll(file)
if err != nil { if err != nil {

View File

@ -16,7 +16,9 @@ func writeTemplate(path string, template string) error {
return err return err
} }
defer func(file *os.File) { defer func(file *os.File) {
if file != nil {
_ = file.Close() _ = file.Close()
}
}(file) }(file)
_, err = file.WriteString(template) _, err = file.WriteString(template)
if err != nil { if err != nil {

View File

@ -2,8 +2,10 @@ package validator
type ShortLinkGenValidator struct { type ShortLinkGenValidator struct {
Url string `form:"url" binding:"required"` Url string `form:"url" binding:"required"`
Password string `form:"password"`
} }
type ShortLinkGetValidator struct { type ShortLinkGetValidator struct {
Hash string `form:"hash" binding:"required"` Hash string `form:"hash" binding:"required"`
Password string `form:"password"`
} }

View File

@ -25,6 +25,10 @@ type SubValidator struct {
AutoTest bool `form:"autoTest,default=false" binding:""` AutoTest bool `form:"autoTest,default=false" binding:""`
Lazy bool `form:"lazy,default=false" binding:""` Lazy bool `form:"lazy,default=false" binding:""`
Sort string `form:"sort" binding:""` Sort string `form:"sort" binding:""`
Remove string `form:"remove" binding:""`
Replace string `form:"replace" binding:""`
ReplaceKeys []string `form:"-" binding:""`
ReplaceTo []string `form:"-" binding:""`
} }
type RuleProviderStruct struct { type RuleProviderStruct struct {
@ -135,5 +139,17 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
} else { } else {
query.Rules = nil query.Rules = nil
} }
if strings.TrimSpace(query.Replace) != "" {
reg := regexp.MustCompile(`\[<(.*?)>,<(.*?)>\]`)
replaces := reg.FindAllStringSubmatch(query.Replace, -1)
for i := range replaces {
length := len(replaces[i])
if length != 3 {
return SubValidator{}, errors.New("参数错误: replace 格式错误")
}
query.ReplaceKeys = append(query.ReplaceKeys, replaces[i][1])
query.ReplaceTo = append(query.ReplaceTo, replaces[i][2])
}
}
return query, nil return query, nil
} }