diff --git a/API_README.md b/API_README.md new file mode 100644 index 0000000..0462aa8 --- /dev/null +++ b/API_README.md @@ -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 | 否 | - | 通过正则表达式重命名节点,格式 `[,],[,]...` | +| 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 | 否 | - | 短链密码 | diff --git a/README.md b/README.md index ba8b5d5..e178a88 100644 --- a/README.md +++ b/README.md @@ -39,23 +39,17 @@ ### 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` | +可以通过变量自定义模板中的策略组代理节点 +解释的不太清楚,可以参考下方默认模板 -## 默认模板 +- `` 为添加所有节点 +- `` 为添加所有国家策略组 + +#### 默认模板 - [Clash](./templates/template_clash.yaml) - [Clash.Meta](./templates/template_meta.yaml) @@ -63,5 +57,3 @@ ## 已知问题 [代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue - -## TODO diff --git a/api/controller/default.go b/api/controller/default.go index 0b58d97..e0eed50 100644 --- a/api/controller/default.go +++ b/api/controller/default.go @@ -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" @@ -31,17 +34,24 @@ 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 @@ -49,13 +59,15 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template 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...) @@ -63,6 +75,11 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template // 如果无法直接解析,尝试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")...) @@ -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": @@ -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 + } + newProxies := make([]string, 0, len(temp.ProxyGroups[i].Proxies)) + for j := range temp.ProxyGroups[i].Proxies { + if temp.ProxyGroups[i].Proxies[j] == "" { + newProxies = append(newProxies, proxyNames...) + } else if temp.ProxyGroups[i].Proxies[j] == "" { + 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...) } diff --git a/api/controller/short_link.go b/api/controller/short_link.go index 1eea508..9c39619 100644 --- a/api/controller/short_link.go +++ b/api/controller/short_link.go @@ -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" @@ -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) { @@ -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 @@ -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)) } diff --git a/api/templates/index.html b/api/templates/index.html index 4b3bf5e..3c6d4e7 100644 --- a/api/templates/index.html +++ b/api/templates/index.html @@ -168,6 +168,30 @@ + + +
+ + +
+ + +
+ + +
@@ -188,6 +212,12 @@
+ + `; + return div; + } + function createRule() { const div = document.createElement("div"); div.classList.add("input-group", "mb-2"); @@ -268,6 +309,11 @@ document.getElementById("ruleGroup").appendChild(div); } + function addReplace() { + const div = createReplace(); + document.getElementById("replaceGroup").appendChild(div); + } + function removeElement(button) { button.parentElement.remove(); } @@ -382,6 +428,29 @@ // 获取排序策略 const sort = document.getElementById("sort").value; 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("&")}`; } @@ -396,6 +465,7 @@ function generateShortLink() { const apiShortLink = document.getElementById("apiShortLink"); + const password = document.getElementById("password"); let uri = generateURI(); if (uri === "") { return; @@ -405,6 +475,7 @@ "./short", { url: uri, + password: password.value.trim(), }, { headers: { diff --git a/model/short_link.go b/model/short_link.go index b47a6c0..f18b11f 100644 --- a/model/short_link.go +++ b/model/short_link.go @@ -3,5 +3,6 @@ package model type ShortLink struct { Hash string `gorm:"primary_key"` Url string + Password string LastRequestTime int64 } diff --git a/templates/template_clash.yaml b/templates/template_clash.yaml index fbbefff..40748e6 100644 --- a/templates/template_clash.yaml +++ b/templates/template_clash.yaml @@ -8,88 +8,104 @@ proxy-groups: - name: 节点选择 type: select proxies: + - - 手动切换 - DIRECT - name: 手动切换 type: select proxies: + - - name: 电报消息 type: select proxies: + - - 节点选择 - 手动切换 - DIRECT - name: OpenAi type: select proxies: + - - 节点选择 - 手动切换 - DIRECT - name: 油管视频 type: select proxies: + - - 节点选择 - 手动切换 - DIRECT - name: 巴哈姆特 type: select proxies: + - - 节点选择 - 手动切换 - DIRECT - name: 哔哩哔哩 type: select proxies: + - - 全球直连 - name: 国外媒体 type: select proxies: + - - 节点选择 - 手动切换 - DIRECT - name: 国内媒体 type: select proxies: + - - DIRECT - 手动切换 - name: 谷歌FCM type: select proxies: + - - DIRECT - 节点选择 - 手动切换 - name: 微软云盘 type: select proxies: + - - DIRECT - 节点选择 - 手动切换 - name: 微软服务 type: select proxies: + - - DIRECT - 节点选择 - 手动切换 - name: 苹果服务 type: select proxies: + - - DIRECT - 节点选择 - 手动切换 - name: 游戏平台 type: select proxies: + - - DIRECT - 节点选择 - 手动切换 - name: 网易音乐 type: select proxies: + - - DIRECT - 节点选择 - name: 全球直连 type: select proxies: + - - DIRECT - 节点选择 - name: 广告拦截 @@ -105,6 +121,7 @@ proxy-groups: - name: 漏网之鱼 type: select proxies: + - - 节点选择 - DIRECT - 手动切换 @@ -9579,4 +9596,4 @@ rules: - PROCESS-NAME,Weiyun.exe,全球直连 - PROCESS-NAME,baidunetdisk.exe,全球直连 - GEOIP,CN,全球直连 - - MATCH,漏网之鱼 \ No newline at end of file + - MATCH,漏网之鱼 diff --git a/templates/template_meta.yaml b/templates/template_meta.yaml index ec264c5..8885bf4 100644 --- a/templates/template_meta.yaml +++ b/templates/template_meta.yaml @@ -8,40 +8,90 @@ proxy-groups: - name: 节点选择 type: select proxies: + - - 手动切换 - DIRECT - name: 手动切换 type: select proxies: - - name: 微软服务 + - + - name: 游戏平台(中国) type: select proxies: - 节点选择 + - - 手动切换 - DIRECT - - name: 游戏平台 + - name: 游戏平台(全球) type: select proxies: - 节点选择 + - - 手动切换 - DIRECT - name: 巴哈姆特 type: select proxies: - 节点选择 + - - 手动切换 - DIRECT - name: 哔哩哔哩 type: select proxies: - 节点选择 + - - 手动切换 - DIRECT - - name: 全球直连 + - name: Telegram type: select proxies: - - DIRECT - 节点选择 + - + - 手动切换 + - DIRECT + - name: OpenAI + type: select + proxies: + - 节点选择 + - + - 手动切换 + - DIRECT + - name: Youtube + type: select + proxies: + - 节点选择 + - + - 手动切换 + - DIRECT + - name: Microsoft + type: select + proxies: + - 节点选择 + - + - 手动切换 + - DIRECT + - name: Onedrive + type: select + proxies: + - 节点选择 + - + - 手动切换 + - DIRECT + - name: Apple + type: select + proxies: + - 节点选择 + - + - 手动切换 + - DIRECT + - name: Netflix + type: select + proxies: + - 节点选择 + - + - 手动切换 + - DIRECT - name: 广告拦截 type: select proxies: @@ -51,17 +101,25 @@ proxy-groups: type: select proxies: - 节点选择 + - - 手动切换 - DIRECT rules: - - GEOSITE,private,全球直连,no-resolve - - GEOIP,private,全球直连 + - GEOSITE,private,DIRECT,no-resolve + - GEOIP,private,DIRECT - 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,bahamut,巴哈姆特 - - GEOSITE,category-games,游戏平台 + - GEOSITE,category-games@cn,游戏平台(中国) + - GEOSITE,category-games,游戏平台(全球) - GEOSITE,geolocation-!cn,节点选择 - - GEOSITE,CN,全球直连 - - GEOIP,CN,全球直连 - - MATCH,漏网之鱼 \ No newline at end of file + - GEOSITE,CN,DIRECT + - GEOIP,CN,DIRECT + - MATCH,漏网之鱼 diff --git a/utils/get.go b/utils/get.go index 90302c9..a7fbc96 100644 --- a/utils/get.go +++ b/utils/get.go @@ -12,7 +12,15 @@ func Get(url string) (resp *http.Response, err error) { haveTried := 0 retryDelay := time.Second // 延迟1秒再重试 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 { haveTried++ time.Sleep(retryDelay) diff --git a/utils/proxy.go b/utils/proxy.go index c51b720..7868791 100644 --- a/utils/proxy.go +++ b/utils/proxy.go @@ -31,7 +31,6 @@ func AddProxy( sub *model.Subscription, autotest bool, lazy bool, clashType model.ClashType, proxies ...model.Proxy, ) { - newCountryGroupNames := make([]string, 0) proxyTypes := model.GetSupportProxyTypes(clashType) // 添加节点 for _, proxy := range proxies { @@ -79,7 +78,6 @@ func AddProxy( } } sub.ProxyGroups = append(sub.ProxyGroups, newGroup) - newCountryGroupNames = append(newCountryGroupNames, countryName) } } } diff --git a/utils/sub.go b/utils/sub.go index 00bf437..d7442b1 100644 --- a/utils/sub.go +++ b/utils/sub.go @@ -35,7 +35,9 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) { return nil, err } defer func(file *os.File) { - _ = file.Close() + if file != nil { + _ = file.Close() + } }(file) fileLock.RLock() defer fileLock.RUnlock() @@ -56,7 +58,9 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) { return nil, err } defer func(Body io.ReadCloser) { - _ = Body.Close() + if Body != nil { + _ = Body.Close() + } }(resp.Body) data, err := io.ReadAll(resp.Body) if err != nil { @@ -67,7 +71,9 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) { return nil, err } defer func(file *os.File) { - _ = file.Close() + if file != nil { + _ = file.Close() + } }(file) fileLock.Lock() defer fileLock.Unlock() diff --git a/utils/template.go b/utils/template.go index 781a099..45bf68a 100644 --- a/utils/template.go +++ b/utils/template.go @@ -17,7 +17,9 @@ func LoadTemplate(template string) ([]byte, error) { return nil, err } defer func(file *os.File) { - _ = file.Close() + if file != nil { + _ = file.Close() + } }(file) result, err := io.ReadAll(file) if err != nil { diff --git a/utils/write_default_template.go b/utils/write_default_template.go index 07a60ae..9fbb192 100644 --- a/utils/write_default_template.go +++ b/utils/write_default_template.go @@ -16,7 +16,9 @@ func writeTemplate(path string, template string) error { return err } defer func(file *os.File) { - _ = file.Close() + if file != nil { + _ = file.Close() + } }(file) _, err = file.WriteString(template) if err != nil { diff --git a/validator/short_link.go b/validator/short_link.go index fb68190..8945f0f 100644 --- a/validator/short_link.go +++ b/validator/short_link.go @@ -1,9 +1,11 @@ package validator type ShortLinkGenValidator struct { - Url string `form:"url" binding:"required"` + Url string `form:"url" binding:"required"` + Password string `form:"password"` } type ShortLinkGetValidator struct { - Hash string `form:"hash" binding:"required"` + Hash string `form:"hash" binding:"required"` + Password string `form:"password"` } diff --git a/validator/sub.go b/validator/sub.go index e175dee..aca9116 100644 --- a/validator/sub.go +++ b/validator/sub.go @@ -25,6 +25,10 @@ type SubValidator struct { AutoTest bool `form:"autoTest,default=false" binding:""` Lazy bool `form:"lazy,default=false" 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 { @@ -135,5 +139,17 @@ func ParseQuery(c *gin.Context) (SubValidator, error) { } else { 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 }