1
0
mirror of https://github.com/nitezs/sub2clash.git synced 2024-12-23 15:24:42 -05:00

️ Improve

This commit is contained in:
Nite07 2024-08-11 23:55:47 +08:00
parent dedbf2bc03
commit 3cfa4bdf24
24 changed files with 204 additions and 304 deletions

View File

@ -11,7 +11,7 @@ import (
) )
func SubmodHandler(c *gin.Context) { func SubmodHandler(c *gin.Context) {
// 从请求中获取参数
query, err := validator.ParseQuery(c) query, err := validator.ParseQuery(c)
if err != nil { if err != nil {
c.String(http.StatusBadRequest, err.Error()) c.String(http.StatusBadRequest, err.Error())
@ -22,7 +22,7 @@ func SubmodHandler(c *gin.Context) {
c.String(http.StatusInternalServerError, err.Error()) c.String(http.StatusInternalServerError, err.Error())
return return
} }
// 输出
if query.NodeListMode { if query.NodeListMode {
nodelist := model.NodeList{} nodelist := model.NodeList{}
nodelist.Proxies = sub.Proxies nodelist.Proxies = sub.Proxies

View File

@ -22,12 +22,12 @@ import (
func BuildSub(clashType model.ClashType, query validator.SubValidator, template string) ( func BuildSub(clashType model.ClashType, query validator.SubValidator, template string) (
*model.Subscription, error, *model.Subscription, error,
) { ) {
// 定义变量
var temp = &model.Subscription{} var temp = &model.Subscription{}
var sub = &model.Subscription{} var sub = &model.Subscription{}
var err error var err error
var templateBytes []byte var templateBytes []byte
// 加载模板
if query.Template != "" { if query.Template != "" {
template = query.Template template = query.Template
} }
@ -52,14 +52,14 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
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)) 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
// 加载订阅
for i := range query.Subs { for i := range query.Subs {
data, err := common.LoadSubscription(query.Subs[i], query.Refresh, query.UserAgent) data, err := common.LoadSubscription(query.Subs[i], query.Refresh, query.UserAgent)
subName := "" subName := ""
@ -72,7 +72,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
) )
return nil, errors.New("加载订阅失败: " + err.Error()) return nil, errors.New("加载订阅失败: " + err.Error())
} }
// 解析订阅
err = yaml.Unmarshal(data, &sub) err = yaml.Unmarshal(data, &sub)
var newProxies []model.Proxy var newProxies []model.Proxy
if err != nil { if err != nil {
@ -81,7 +81,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
p := common.ParseProxy(strings.Split(string(data), "\n")...) p := common.ParseProxy(strings.Split(string(data), "\n")...)
newProxies = p newProxies = p
} else { } else {
// 如果无法直接解析尝试Base64解码
base64, err := parser.DecodeBase64(string(data)) base64, err := parser.DecodeBase64(string(data))
if err != nil { if err != nil {
logger.Logger.Debug( logger.Logger.Debug(
@ -104,17 +104,17 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
} }
proxyList = append(proxyList, newProxies...) proxyList = append(proxyList, newProxies...)
} }
// 添加自定义节点
if len(query.Proxies) != 0 { if len(query.Proxies) != 0 {
proxyList = append(proxyList, common.ParseProxy(query.Proxies...)...) proxyList = append(proxyList, common.ParseProxy(query.Proxies...)...)
} }
// 给节点添加订阅名称
for i := range proxyList { for i := range proxyList {
if proxyList[i].SubName != "" { if proxyList[i].SubName != "" {
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name) proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
} }
} }
// 去掉配置相同的节点
proxies := make(map[string]*model.Proxy) proxies := make(map[string]*model.Proxy)
newProxies := make([]model.Proxy, 0, len(proxyList)) newProxies := make([]model.Proxy, 0, len(proxyList))
for i := range proxyList { for i := range proxyList {
@ -125,7 +125,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
} }
} }
proxyList = newProxies proxyList = newProxies
// 删除节点
if strings.TrimSpace(query.Remove) != "" { if strings.TrimSpace(query.Remove) != "" {
newProxyList := make([]model.Proxy, 0, len(proxyList)) newProxyList := make([]model.Proxy, 0, len(proxyList))
for i := range proxyList { for i := range proxyList {
@ -134,17 +134,17 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
logger.Logger.Debug("remove regexp compile failed", zap.Error(err)) logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
return nil, errors.New("remove 参数非法: " + err.Error()) return nil, errors.New("remove 参数非法: " + err.Error())
} }
// 删除匹配到的节点
if removeReg.MatchString(proxyList[i].Name) { if removeReg.MatchString(proxyList[i].Name) {
continue // 如果匹配到要删除的元素,跳过该元素,不添加到新切片中 continue
} }
newProxyList = append(newProxyList, proxyList[i]) // 将要保留的元素添加到新切片中 newProxyList = append(newProxyList, proxyList[i])
} }
proxyList = newProxyList proxyList = newProxyList
} }
// 重命名
if len(query.ReplaceKeys) != 0 { if len(query.ReplaceKeys) != 0 {
// 创建重命名正则表达式
replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys)) replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys))
for _, v := range query.ReplaceKeys { for _, v := range query.ReplaceKeys {
replaceReg, err := regexp.Compile(v) replaceReg, err := regexp.Compile(v)
@ -155,7 +155,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
replaceRegs = append(replaceRegs, replaceReg) replaceRegs = append(replaceRegs, replaceReg)
} }
for i := range proxyList { for i := range proxyList {
// 重命名匹配到的节点
for j, v := range replaceRegs { for j, v := range replaceRegs {
if v.MatchString(proxyList[i].Name) { if v.MatchString(proxyList[i].Name) {
proxyList[i].Name = v.ReplaceAllString( proxyList[i].Name = v.ReplaceAllString(
@ -165,7 +165,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
} }
} }
} }
// 重名检测
names := make(map[string]int) names := make(map[string]int)
for i := range proxyList { for i := range proxyList {
if _, exist := names[proxyList[i].Name]; exist { if _, exist := names[proxyList[i].Name]; exist {
@ -175,14 +175,14 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
names[proxyList[i].Name] = 0 names[proxyList[i].Name] = 0
} }
} }
// trim
for i := range proxyList { for i := range proxyList {
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name) proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
} }
// 将新增节点都添加到临时变量 t 中,防止策略组排序错乱
var t = &model.Subscription{} var t = &model.Subscription{}
common.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...) common.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
// 排序策略组
switch query.Sort { switch query.Sort {
case "sizeasc": case "sizeasc":
sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups)) sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups))
@ -195,9 +195,9 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
default: default:
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups)) sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
} }
// 合并新节点和模板
MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup) MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup)
// 处理自定义规则
for _, v := range query.Rules { for _, v := range query.Rules {
if v.Prepend { if v.Prepend {
common.PrependRules(temp, v.Rule) common.PrependRules(temp, v.Rule)
@ -205,7 +205,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
common.AppendRules(temp, v.Rule) common.AppendRules(temp, v.Rule)
} }
} }
// 处理自定义 ruleProvider
for _, v := range query.RuleProviders { for _, v := range query.RuleProviders {
hash := sha256.Sum224([]byte(v.Url)) hash := sha256.Sum224([]byte(v.Url))
name := hex.EncodeToString(hash[:]) name := hex.EncodeToString(hash[:])
@ -230,8 +230,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
} }
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) { func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {
// 只合并节点、策略组
// 统计所有国家策略组名称
var countryGroupNames []string var countryGroupNames []string
for _, proxyGroup := range sub.ProxyGroups { for _, proxyGroup := range sub.ProxyGroups {
if proxyGroup.IsCountryGrop { if proxyGroup.IsCountryGrop {
@ -244,9 +243,9 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg
for _, proxy := range sub.Proxies { for _, proxy := range sub.Proxies {
proxyNames = append(proxyNames, proxy.Name) proxyNames = append(proxyNames, proxy.Name)
} }
// 将订阅中的节点添加到模板中
temp.Proxies = append(temp.Proxies, sub.Proxies...) temp.Proxies = append(temp.Proxies, sub.Proxies...)
// 将订阅中的策略组添加到模板中
for i := range temp.ProxyGroups { for i := range temp.ProxyGroups {
if temp.ProxyGroups[i].IsCountryGrop { if temp.ProxyGroups[i].IsCountryGrop {
continue continue

View File

@ -12,7 +12,7 @@ import (
) )
func SubHandler(c *gin.Context) { func SubHandler(c *gin.Context) {
// 从请求中获取参数
query, err := validator.ParseQuery(c) query, err := validator.ParseQuery(c)
if err != nil { if err != nil {
c.String(http.StatusBadRequest, err.Error()) c.String(http.StatusBadRequest, err.Error())
@ -23,7 +23,7 @@ func SubHandler(c *gin.Context) {
c.String(http.StatusInternalServerError, err.Error()) c.String(http.StatusInternalServerError, err.Error())
return return
} }
// 输出
if query.NodeListMode { if query.NodeListMode {
nodelist := model.NodeList{} nodelist := model.NodeList{}
nodelist.Proxies = sub.Proxies nodelist.Proxies = sub.Proxies

View File

@ -88,7 +88,7 @@ func UpdateLinkHandler(c *gin.Context) {
} }
func GetRawConfHandler(c *gin.Context) { func GetRawConfHandler(c *gin.Context) {
// 获取动态路由参数
hash := c.Param("hash") hash := c.Param("hash")
password := c.Query("password") password := c.Query("password")
@ -97,27 +97,24 @@ func GetRawConfHandler(c *gin.Context) {
return return
} }
// 查询数据库中的短链接
shortLink, err := database.FindShortLinkByHash(hash) shortLink, err := database.FindShortLinkByHash(hash)
if err != nil { if err != nil {
c.String(http.StatusNotFound, "未找到短链接或密码错误") c.String(http.StatusNotFound, "未找到短链接或密码错误")
return return
} }
// 校验密码
if shortLink.Password != "" && shortLink.Password != password { if shortLink.Password != "" && shortLink.Password != password {
c.String(http.StatusNotFound, "未找到短链接或密码错误") c.String(http.StatusNotFound, "未找到短链接或密码错误")
return return
} }
// 更新最后访问时间
shortLink.LastRequestTime = time.Now().Unix() shortLink.LastRequestTime = time.Now().Unix()
err = database.SaveShortLink(shortLink) err = database.SaveShortLink(shortLink)
if err != nil { if err != nil {
respondWithError(c, http.StatusInternalServerError, "数据库错误") respondWithError(c, http.StatusInternalServerError, "数据库错误")
return return
} }
// 请求短链接指向的URL
response, err := http.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url) response, err := http.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url)
if err != nil { if err != nil {
respondWithError(c, http.StatusInternalServerError, "请求错误: "+err.Error()) respondWithError(c, http.StatusInternalServerError, "请求错误: "+err.Error())
@ -125,19 +122,17 @@ func GetRawConfHandler(c *gin.Context) {
} }
defer response.Body.Close() defer response.Body.Close()
// 读取响应内容
all, err := io.ReadAll(response.Body) all, err := io.ReadAll(response.Body)
if err != nil { if err != nil {
respondWithError(c, http.StatusInternalServerError, "读取错误: "+err.Error()) respondWithError(c, http.StatusInternalServerError, "读取错误: "+err.Error())
return return
} }
// 返回响应内容
c.String(http.StatusOK, string(all)) c.String(http.StatusOK, string(all))
} }
func GetRawConfUriHandler(c *gin.Context) { func GetRawConfUriHandler(c *gin.Context) {
// 获取动态路由参数
hash := c.Query("hash") hash := c.Query("hash")
password := c.Query("password") password := c.Query("password")
@ -146,14 +141,12 @@ func GetRawConfUriHandler(c *gin.Context) {
return return
} }
// 查询数据库中的短链接
shortLink, err := database.FindShortLinkByHash(hash) shortLink, err := database.FindShortLinkByHash(hash)
if err != nil { if err != nil {
c.String(http.StatusNotFound, "未找到短链接或密码错误") c.String(http.StatusNotFound, "未找到短链接或密码错误")
return return
} }
// 校验密码
if shortLink.Password != "" && shortLink.Password != password { if shortLink.Password != "" && shortLink.Password != password {
c.String(http.StatusNotFound, "未找到短链接或密码错误") c.String(http.StatusNotFound, "未找到短链接或密码错误")
return return

View File

@ -13,16 +13,14 @@ import (
var DB *bbolt.DB var DB *bbolt.DB
func ConnectDB() error { func ConnectDB() error {
// 确保数据目录存在
path := filepath.Join("data", "sub2clash.db") path := filepath.Join("data", "sub2clash.db")
// 打开或创建数据库文件
db, err := bbolt.Open(path, 0600, nil) db, err := bbolt.Open(path, 0600, nil)
if err != nil { if err != nil {
return err return err
} }
DB = db DB = db
// 确保存储桶存在
return db.Update(func(tx *bbolt.Tx) error { return db.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte("ShortLinks")) _, err := tx.CreateBucketIfNotExists([]byte("ShortLinks"))
return err return err

View File

@ -22,7 +22,7 @@ func WithUserAgent(userAgent string) GetOption {
func Get(url string, options ...GetOption) (resp *http.Response, err error) { func Get(url string, options ...GetOption) (resp *http.Response, err error) {
retryTimes := config.Default.RequestRetryTimes retryTimes := config.Default.RequestRetryTimes
haveTried := 0 haveTried := 0
retryDelay := time.Second // 延迟1秒再重试 retryDelay := time.Second
getConfig := GetConfig{} getConfig := GetConfig{}
for _, option := range options { for _, option := range options {
option(&getConfig) option(&getConfig)
@ -45,7 +45,7 @@ func Get(url string, options ...GetOption) (resp *http.Response, err error) {
time.Sleep(retryDelay) time.Sleep(retryDelay)
continue continue
} else { } else {
// 如果文件大小大于设定,直接返回错误
if get != nil && get.ContentLength > config.Default.RequestMaxFileSize { if get != nil && get.ContentLength > config.Default.RequestMaxFileSize {
return nil, errors.New("文件过大") return nil, errors.New("文件过大")
} }

View File

@ -11,7 +11,7 @@ import (
) )
func GetContryName(countryKey string) string { func GetContryName(countryKey string) string {
// 创建一个切片包含所有的国家映射
countryMaps := []map[string]string{ countryMaps := []map[string]string{
model.CountryFlag, model.CountryFlag,
model.CountryChineseName, model.CountryChineseName,
@ -19,11 +19,9 @@ func GetContryName(countryKey string) string {
model.CountryEnglishName, model.CountryEnglishName,
} }
// 对每一个映射进行检查
for i, countryMap := range countryMaps { for i, countryMap := range countryMaps {
if i == 2 { if i == 2 {
// 对ISO匹配做特殊处理
// 根据常用分割字符分割字符串
splitChars := []string{"-", "_", " "} splitChars := []string{"-", "_", " "}
key := make([]string, 0) key := make([]string, 0)
for _, splitChar := range splitChars { for _, splitChar := range splitChars {
@ -34,9 +32,9 @@ func GetContryName(countryKey string) string {
} }
} }
} }
// 对每一个分割后的字符串进行检查
for _, v := range key { for _, v := range key {
// 如果匹配到了国家
if country, ok := countryMap[strings.ToUpper(v)]; ok { if country, ok := countryMap[strings.ToUpper(v)]; ok {
return country return country
} }
@ -56,7 +54,7 @@ func AddProxy(
lazy bool, clashType model.ClashType, proxies ...model.Proxy, lazy bool, clashType model.ClashType, proxies ...model.Proxy,
) { ) {
proxyTypes := model.GetSupportProxyTypes(clashType) proxyTypes := model.GetSupportProxyTypes(clashType)
// 添加节点
for _, proxy := range proxies { for _, proxy := range proxies {
if !proxyTypes[proxy.Type] { if !proxyTypes[proxy.Type] {
continue continue
@ -106,7 +104,7 @@ func ParseProxy(proxies ...string) []model.Proxy {
if proxy != "" { if proxy != "" {
var proxyItem model.Proxy var proxyItem model.Proxy
var err error var err error
// 解析节点
if strings.HasPrefix(proxy, constant.ShadowsocksPrefix) { if strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
proxyItem, err = parser.ParseShadowsocks(proxy) proxyItem, err = parser.ParseShadowsocks(proxy)
} }

View File

@ -3,7 +3,7 @@ package common
import "math/rand" import "math/rand"
func RandomString(length int) string { func RandomString(length int) string {
// 生成随机字符串
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var result []byte var result []byte
for i := 0; i < length; i++ { for i := 0; i < length; i++ {

View File

@ -29,7 +29,7 @@ func LoadSubscription(url string, refresh bool, userAgent string) ([]byte, error
} }
return FetchSubscriptionFromAPI(url, userAgent) return FetchSubscriptionFromAPI(url, userAgent)
} }
lastGetTime := stat.ModTime().Unix() // 单位是秒 lastGetTime := stat.ModTime().Unix()
if lastGetTime+config.Default.CacheExpire > time.Now().Unix() { if lastGetTime+config.Default.CacheExpire > time.Now().Unix() {
file, err := os.Open(fileName) file, err := os.Open(fileName)
if err != nil { if err != nil {

View File

@ -7,8 +7,6 @@ import (
"path/filepath" "path/filepath"
) )
// LoadTemplate 加载模板
// templates 模板文件名
func LoadTemplate(template string) ([]byte, error) { func LoadTemplate(template string) ([]byte, error) {
tPath := filepath.Join("templates", template) tPath := filepath.Join("templates", template)
if _, err := os.Stat(tPath); err == nil { if _, err := os.Stat(tPath); err == nil {

18
main.go
View File

@ -22,24 +22,24 @@ var templateClash string
func init() { func init() {
var err error var err error
// 创建文件夹
err = common.MkEssentialDir() err = common.MkEssentialDir()
if err != nil { if err != nil {
logger.Logger.Panic("create essential dir failed", zap.Error(err)) logger.Logger.Panic("create essential dir failed", zap.Error(err))
} }
// 加载配置
err = config.LoadConfig() err = config.LoadConfig()
// 初始化日志
logger.InitLogger(config.Default.LogLevel) logger.InitLogger(config.Default.LogLevel)
if err != nil { if err != nil {
logger.Logger.Panic("load config failed", zap.Error(err)) logger.Logger.Panic("load config failed", zap.Error(err))
} }
// 写入默认模板
err = common.WriteDefalutTemplate(templateMeta, templateClash) err = common.WriteDefalutTemplate(templateMeta, templateClash)
if err != nil { if err != nil {
logger.Logger.Panic("write default template failed", zap.Error(err)) logger.Logger.Panic("write default template failed", zap.Error(err))
} }
// 连接数据库
err = database.ConnectDB() err = database.ConnectDB()
if err != nil { if err != nil {
logger.Logger.Panic("database connect failed", zap.Error(err)) logger.Logger.Panic("database connect failed", zap.Error(err))
@ -48,13 +48,13 @@ func init() {
} }
func main() { func main() {
// 设置运行模式
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
// 关闭 Gin 的日志输出
gin.DefaultWriter = io.Discard gin.DefaultWriter = io.Discard
// 创建路由
r := gin.Default() r := gin.Default()
// 设置路由
api.SetRoute(r) api.SetRoute(r)
logger.Logger.Info("server is running at http://localhost:" + strconv.Itoa(config.Default.Port)) logger.Logger.Info("server is running at http://localhost:" + strconv.Itoa(config.Default.Port))
err := r.Run(":" + strconv.Itoa(config.Default.Port)) err := r.Run(":" + strconv.Itoa(config.Default.Port))

View File

@ -1,8 +1,5 @@
package model 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{ var CountryEnglishName = map[string]string{
"Andorra": "安道尔(AD)", "Andorra": "安道尔(AD)",
"United Arab Emirates": "阿联酋(AE)", "United Arab Emirates": "阿联酋(AE)",

View File

@ -9,12 +9,12 @@ type ProxyGroup struct {
Type string `yaml:"type,omitempty"` Type string `yaml:"type,omitempty"`
Name string `yaml:"name,omitempty"` Name string `yaml:"name,omitempty"`
Proxies []string `yaml:"proxies,omitempty"` Proxies []string `yaml:"proxies,omitempty"`
IsCountryGrop bool `yaml:"-"` // 是否是国家分组 IsCountryGrop bool `yaml:"-"`
Url string `yaml:"url,omitempty"` Url string `yaml:"url,omitempty"`
Interval int `yaml:"interval,omitempty"` Interval int `yaml:"interval,omitempty"`
Tolerance int `yaml:"tolerance,omitempty"` Tolerance int `yaml:"tolerance,omitempty"`
Lazy bool `yaml:"lazy"` Lazy bool `yaml:"lazy"`
Size int `yaml:"-"` // 代理数量 Size int `yaml:"-"`
DisableUDP bool `yaml:"disable-udp,omitempty"` DisableUDP bool `yaml:"disable-udp,omitempty"`
Strategy string `yaml:"strategy,omitempty"` Strategy string `yaml:"strategy,omitempty"`
Icon string `yaml:"icon,omitempty"` Icon string `yaml:"icon,omitempty"`
@ -42,16 +42,15 @@ func (p ProxyGroupsSortBySize) Len() int {
} }
func (p ProxyGroupsSortByName) Less(i, j int) bool { func (p ProxyGroupsSortByName) Less(i, j int) bool {
// 定义一组备选语言:首选英语,其次中文
tags := []language.Tag{ tags := []language.Tag{
language.English, language.English,
language.Chinese, language.Chinese,
} }
matcher := language.NewMatcher(tags) matcher := language.NewMatcher(tags)
// 假设我们的请求语言是 "zh"(中文),则使用匹配器找到最佳匹配的语言
bestMatch, _, _ := matcher.Match(language.Make("zh")) bestMatch, _, _ := matcher.Match(language.Make("zh"))
// 使用最佳匹配的语言进行排序
c := collate.New(bestMatch) c := collate.New(bestMatch)
return c.CompareString(p[i].Name, p[j].Name) < 0 return c.CompareString(p[i].Name, p[j].Name) < 0
} }

View File

@ -7,11 +7,11 @@ type Hysteria struct {
Port int `yaml:"port,omitempty"` Port int `yaml:"port,omitempty"`
Ports string `yaml:"ports,omitempty"` Ports string `yaml:"ports,omitempty"`
Protocol string `yaml:"protocol,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"` Up string `yaml:"up"`
UpSpeed int `yaml:"up-speed,omitempty"` // compatible with Stash UpSpeed int `yaml:"up-speed,omitempty"`
Down string `yaml:"down"` Down string `yaml:"down"`
DownSpeed int `yaml:"down-speed,omitempty"` // compatible with Stash DownSpeed int `yaml:"down-speed,omitempty"`
Auth string `yaml:"auth,omitempty"` Auth string `yaml:"auth,omitempty"`
AuthStringOLD string `yaml:"auth_str,omitempty"` AuthStringOLD string `yaml:"auth_str,omitempty"`
AuthString string `yaml:"auth-str,omitempty"` AuthString string `yaml:"auth-str,omitempty"`

View File

@ -103,7 +103,6 @@ type IPTables struct {
DnsRedirect bool `yaml:"dns-redirect,omitempty" json:"dns-redirect"` DnsRedirect bool `yaml:"dns-redirect,omitempty" json:"dns-redirect"`
} }
// EBpf config
type EBpf struct { type EBpf struct {
RedirectToTun []string `yaml:"redirect-to-tun,omitempty" json:"redirect-to-tun"` RedirectToTun []string `yaml:"redirect-to-tun,omitempty" json:"redirect-to-tun"`
AutoRedir []string `yaml:"auto-redir,omitempty" json:"auto-redir"` AutoRedir []string `yaml:"auto-redir,omitempty" json:"auto-redir"`

View File

@ -7,7 +7,7 @@ import (
func DecodeBase64(s string) (string, error) { func DecodeBase64(s string) (string, error) {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
// url safe
if strings.Contains(s, "-") || strings.Contains(s, "_") { if strings.Contains(s, "-") || strings.Contains(s, "_") {
s = strings.Replace(s, "-", "+", -1) s = strings.Replace(s, "-", "+", -1)
s = strings.Replace(s, "_", "/", -1) s = strings.Replace(s, "_", "/", -1)

View File

@ -1,6 +1,7 @@
package parser package parser
import ( import (
"fmt"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
@ -13,25 +14,31 @@ func ParseHysteria(proxy string) (model.Proxy, error) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
} }
proxy = strings.TrimPrefix(proxy, constant.HysteriaPrefix) link, err := url.Parse(proxy)
urlParts := strings.SplitN(proxy, "?", 2) if err != nil {
if len(urlParts) != 2 {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct, Type: ErrInvalidStruct,
Message: "missing character '?' in url", Message: "url parse error",
Raw: proxy,
}
}
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host",
Raw: proxy, Raw: proxy,
} }
} }
serverInfo := strings.SplitN(urlParts[0], ":", 2) portStr := link.Port()
if len(serverInfo) != 2 { if portStr == "" {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct, Type: ErrInvalidStruct,
Message: "missing server host or port", Message: "missing server port",
Raw: proxy, Raw: proxy,
} }
} }
server, portStr := serverInfo[0], serverInfo[1]
port, err := ParsePort(portStr) port, err := ParsePort(portStr)
if err != nil { if err != nil {
@ -42,16 +49,9 @@ func ParseHysteria(proxy string) (model.Proxy, error) {
} }
} }
params, err := url.ParseQuery(urlParts[1]) query := link.Query()
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
}
}
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) insecureBool, err := strconv.ParseBool(insecure)
if err != nil { if err != nil {
insecureBool = false insecureBool = false
@ -63,10 +63,11 @@ func ParseHysteria(proxy string) (model.Proxy, error) {
alpn = strings.Split(alpnStr, ",") alpn = strings.Split(alpnStr, ",")
} }
remarks := server + ":" + portStr remarks := link.Fragment
if params.Get("remarks") != "" { if remarks == "" {
remarks = params.Get("remarks") remarks = fmt.Sprintf("%s:%s", server, portStr)
} }
remarks = strings.TrimSpace(remarks)
result := model.Proxy{ result := model.Proxy{
Type: "hysteria", Type: "hysteria",
@ -77,7 +78,7 @@ func ParseHysteria(proxy string) (model.Proxy, error) {
Down: downmbps, Down: downmbps,
Auth: auth, Auth: auth,
Obfs: obfs, Obfs: obfs,
SkipCertVerify: insecure == "1", SkipCertVerify: insecureBool,
Alpn: alpn, Alpn: alpn,
Protocol: protocol, Protocol: protocol,
AllowInsecure: insecureBool, AllowInsecure: insecureBool,

View File

@ -1,6 +1,7 @@
package parser package parser
import ( import (
"fmt"
"net/url" "net/url"
"strings" "strings"
"sub2clash/constant" "sub2clash/constant"
@ -13,72 +14,53 @@ func ParseHysteria2(proxy string) (model.Proxy, error) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
} }
proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix1) link, err := url.Parse(proxy)
proxy = strings.TrimPrefix(proxy, constant.Hysteria2Prefix2) if err != nil {
urlParts := strings.SplitN(proxy, "@", 2)
if len(urlParts) != 2 {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct, Type: ErrInvalidStruct,
Message: "missing character '@' in url", Message: "url parse error",
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",
Raw: proxy, Raw: proxy,
} }
} }
username := link.User.Username()
password, exist := link.User.Password()
if !exist {
password = username
}
query := link.Query()
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
}
portStr := link.Port()
if portStr == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
}
port, err := ParsePort(portStr) port, err := ParsePort(portStr)
if err != nil { if err != nil {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrInvalidPort, Type: ErrInvalidPort,
Message: err.Error(), Raw: portStr,
Raw: proxy,
} }
} }
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")
params, err := url.ParseQuery(paramStr) enableTLS := pinSHA256 != "" || sni != ""
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 != ""
insecureBool := insecure == "1" insecureBool := insecure == "1"
remarks := link.Fragment
if remarks == "" { if remarks == "" {
remarksIndex := strings.LastIndex(proxy, "#") remarks = fmt.Sprintf("%s:%s", server, portStr)
if remarksIndex != -1 {
remarks = proxy[remarksIndex:]
remarks, _ = url.QueryUnescape(remarks)
}
} }
remarks = strings.TrimSpace(remarks)
result := model.Proxy{ result := model.Proxy{
Type: "hysteria2", Type: "hysteria2",

View File

@ -1,6 +1,7 @@
package parser package parser
import ( import (
"fmt"
"net/url" "net/url"
"strings" "strings"
"sub2clash/constant" "sub2clash/constant"
@ -12,71 +13,72 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
} }
proxy = strings.TrimPrefix(proxy, constant.ShadowsocksPrefix) link, err := url.Parse(proxy)
urlParts := strings.SplitN(proxy, "@", 2)
if len(urlParts) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing character '@' in url",
Raw: proxy,
}
}
var serverAndPort []string
if !strings.Contains(urlParts[0], ":") {
decoded, err := DecodeBase64(urlParts[0])
if err != nil { if err != nil {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct, Type: ErrInvalidStruct,
Message: "invalid base64 encoded", Message: "url parse error",
Raw: proxy, Raw: proxy,
} }
} }
urlParts[0] = decoded
}
credentials := strings.SplitN(urlParts[0], ":", 2)
if len(credentials) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host or port",
Raw: proxy,
}
}
method, password := credentials[0], credentials[1]
serverInfo := strings.SplitN(urlParts[1], "#", 2) server := link.Hostname()
serverAndPort = strings.SplitN(serverInfo[0], ":", 2) if server == "" {
server, portStr := serverAndPort[0], serverAndPort[1]
if len(serverInfo) != 2 {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct, Type: ErrInvalidStruct,
Message: "missing server host or port", Message: "missing server host",
Raw: proxy,
}
}
portStr := link.Port()
if portStr == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server port",
Raw: proxy, Raw: proxy,
} }
} }
port, err := ParsePort(portStr) port, err := ParsePort(portStr)
if err != nil { if err != nil {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrInvalidPort, Type: ErrInvalidStruct,
Message: err.Error(),
Raw: proxy, Raw: proxy,
} }
} }
var remarks string user, err := DecodeBase64(link.User.Username())
if len(serverInfo) == 2 {
unescape, err := url.QueryUnescape(serverInfo[1])
if err != nil { if err != nil {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct, Type: ErrInvalidStruct,
Message: "cannot unescape remarks", Message: "missing method and password",
Raw: proxy, Raw: proxy,
} }
} }
remarks = strings.TrimSpace(unescape)
} else { if user == "" {
remarks = strings.TrimSpace(server + ":" + portStr) return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing method and password",
Raw: proxy,
} }
}
methodAndPass := strings.SplitN(user, ":", 2)
if len(methodAndPass) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing method and password",
Raw: proxy,
}
}
method := methodAndPass[0]
password := methodAndPass[1]
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
remarks = strings.TrimSpace(remarks)
result := model.Proxy{ result := model.Proxy{
Type: "ss", Type: "ss",

View File

@ -1,55 +1,42 @@
package parser package parser
import ( import (
"fmt"
"net/url" "net/url"
"strings" "strings"
"sub2clash/constant" "sub2clash/constant"
"sub2clash/model" "sub2clash/model"
) )
// ParseTrojan 解析给定的Trojan代理URL并返回Proxy结构。
func ParseTrojan(proxy string) (model.Proxy, error) { func ParseTrojan(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.TrojanPrefix) { if !strings.HasPrefix(proxy, constant.TrojanPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
} }
proxy = strings.TrimPrefix(proxy, constant.TrojanPrefix) link, err := url.Parse(proxy)
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])
if err != nil { if err != nil {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrCannotParseParams, Type: ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
}
password := link.User.Username()
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
}
portStr := link.Port()
if portStr == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server port",
Raw: proxy, Raw: proxy,
Message: err.Error(),
} }
} }
@ -62,14 +49,14 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
} }
} }
remarks := "" remarks := link.Fragment
if len(serverInfo) == 2 { if remarks == "" {
remarks, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1])) remarks = fmt.Sprintf("%s:%s", server, portStr)
} else {
remarks = serverAndPort[0]
} }
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 var alpn []string
if strings.Contains(alpnStr, ",") { if strings.Contains(alpnStr, ",") {
@ -78,9 +65,6 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
alpn = nil alpn = nil
} }
// enableUTLS := fp != ""
// 构建Proxy结构体
result := model.Proxy{ result := model.Proxy{
Type: "trojan", Type: "trojan",
Server: server, Server: server,
@ -116,14 +100,6 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
} }
} }
// if network == "http" {
// // 未查到相关支持文档
// }
// if network == "quic" {
// // 未查到相关支持文档
// }
if network == "grpc" { if network == "grpc" {
result.GrpcOpts = model.GrpcOptions{ result.GrpcOpts = model.GrpcOptions{
GrpcServiceName: serviceName, GrpcServiceName: serviceName,

View File

@ -1,6 +1,7 @@
package parser package parser
import ( import (
"fmt"
"net/url" "net/url"
"strings" "strings"
"sub2clash/constant" "sub2clash/constant"
@ -12,34 +13,24 @@ func ParseVless(proxy string) (model.Proxy, error) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
} }
urlParts := strings.SplitN(strings.TrimPrefix(proxy, constant.VLESSPrefix), "@", 2) link, err := url.Parse(proxy)
if len(urlParts) != 2 { if err != nil {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct, Type: ErrInvalidStruct,
Message: "missing character '@' in url", Message: "url parse error",
Raw: proxy, Raw: proxy,
} }
} }
serverInfo := strings.SplitN(urlParts[1], "#", 2) server := link.Hostname()
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) if server == "" {
if len(serverAndPortAndParams) != 2 {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct, Type: ErrInvalidStruct,
Message: "missing character '?' in url", Message: "missing server host",
Raw: proxy, Raw: proxy,
} }
} }
portStr := link.Port()
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
if len(serverAndPort) != 2 {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host or port",
Raw: proxy,
}
}
server, portStr := serverAndPort[0], serverAndPort[1]
port, err := ParsePort(portStr) port, err := ParsePort(portStr)
if err != nil { if err != nil {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
@ -49,40 +40,10 @@ func ParseVless(proxy string) (model.Proxy, error) {
} }
} }
params, err := url.ParseQuery(serverAndPortAndParams[1]) query := link.Query()
if err != nil { uuid := link.User.Username()
return model.Proxy{}, &ParseError{ 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")
Type: ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
}
}
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" insecureBool := insecure == "1"
var alpn []string var alpn []string
if strings.Contains(alpnStr, ",") { if strings.Contains(alpnStr, ",") {
@ -90,6 +51,11 @@ func ParseVless(proxy string) (model.Proxy, error) {
} else { } else {
alpn = nil alpn = nil
} }
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
remarks = strings.TrimSpace(remarks)
result := model.Proxy{ result := model.Proxy{
Type: "vless", Type: "vless",
@ -129,10 +95,6 @@ func ParseVless(proxy string) (model.Proxy, error) {
} }
} }
// if _type == "quic" {
// // 未查到相关支持文档
// }
if _type == "grpc" { if _type == "grpc" {
result.Network = "grpc" result.Network = "grpc"
result.GrpcOpts = model.GrpcOptions{ result.GrpcOpts = model.GrpcOptions{

View File

@ -100,10 +100,6 @@ func ParseVmess(proxy string) (model.Proxy, error) {
} }
} }
// if vmess.Net == "quic" {
// // 未查到相关支持文档
// }
if vmess.Net == "grpc" { if vmess.Net == "grpc" {
result.GrpcOpts = model.GrpcOptions{ result.GrpcOpts = model.GrpcOptions{
GrpcServiceName: vmess.Path, GrpcServiceName: vmess.Path,

View File

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

View File

@ -111,7 +111,7 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
}, },
) )
} }
// 校验 Rule-Provider 是否有重名
names := make(map[string]bool) names := make(map[string]bool)
for _, ruleProvider := range query.RuleProviders { for _, ruleProvider := range query.RuleProviders {
if _, ok := names[ruleProvider.Name]; ok { if _, ok := names[ruleProvider.Name]; ok {