diff --git a/common/sub.go b/common/sub.go index 7e0b7c5..c2192fe 100644 --- a/common/sub.go +++ b/common/sub.go @@ -19,6 +19,7 @@ import ( "github.com/bestnite/sub2clash/model" P "github.com/bestnite/sub2clash/model/proxy" "github.com/bestnite/sub2clash/parser" + "github.com/bestnite/sub2clash/utils" "go.uber.org/zap" "gopkg.in/yaml.v3" ) @@ -92,7 +93,7 @@ func FetchSubscriptionFromAPI(url string, userAgent string, retryTimes int) ([]b return data, nil } -func BuildSub(clashType model.ClashType, query model.SubConfig, template string, cacheExpire int64, retryTimes int) ( +func BuildSub(clashType model.ClashType, query model.ConvertConfig, template string, cacheExpire int64, retryTimes int) ( *model.Subscription, error, ) { var temp = &model.Subscription{} @@ -160,7 +161,7 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string, } newProxies = p } else { - base64, err := parser.DecodeBase64(string(data)) + base64, err := utils.DecodeBase64(string(data), true) if err != nil { logger.Logger.Debug( "parse subscription failed", zap.String("url", query.Subs[i]), @@ -186,7 +187,7 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string, proxyList = append(proxyList, newProxies...) } - if len(query.Proxy) != 0 { + if len(query.Proxies) != 0 { p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, query.Proxies...) if err != nil { return nil, err @@ -236,22 +237,17 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string, } // 替换 - if len(query.ReplaceKeys) != 0 { - replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys)) - for _, v := range query.ReplaceKeys { - replaceReg, err := regexp.Compile(v) + if len(query.Replace) != 0 { + for k, v := range query.Replace { + replaceReg, err := regexp.Compile(k) if err != nil { logger.Logger.Debug("replace regexp compile failed", zap.Error(err)) return nil, NewRegexInvalidError("replace", err) } - replaceRegs = append(replaceRegs, replaceReg) - } - for i := range proxyList { - - for j, v := range replaceRegs { - if v.MatchString(proxyList[i].Name) { - proxyList[i].Name = v.ReplaceAllString( - proxyList[i].Name, query.ReplaceTo[j], + for i := range proxyList { + if replaceReg.MatchString(proxyList[i].Name) { + proxyList[i].Name = replaceReg.ReplaceAllString( + proxyList[i].Name, v, ) } } @@ -276,6 +272,7 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string, var t = &model.Subscription{} AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...) + // 排序 switch query.Sort { case "sizeasc": sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroup)) diff --git a/model/clash.go b/model/clash.go index 401db6d..99bf439 100644 --- a/model/clash.go +++ b/model/clash.go @@ -13,11 +13,12 @@ func GetSupportProxyTypes(clashType ClashType) map[string]bool { supportProxyTypes := make(map[string]bool) for _, parser := range parser.GetAllParsers() { - if clashType == Clash { + switch clashType { + case Clash: if parser.SupportClash() { supportProxyTypes[parser.GetType()] = true } - } else if clashType == ClashMeta { + case ClashMeta: if parser.SupportMeta() { supportProxyTypes[parser.GetType()] = true } diff --git a/model/convert_config.go b/model/convert_config.go new file mode 100644 index 0000000..b5122a1 --- /dev/null +++ b/model/convert_config.go @@ -0,0 +1,92 @@ +package model + +import ( + "encoding/json" + "errors" + "net/url" + "strings" + + "github.com/bestnite/sub2clash/utils" + "github.com/gin-gonic/gin" +) + +type ConvertConfig struct { + ClashType ClashType `json:"clashType" binding:"required"` + Subs []string `json:"subscriptions" binding:""` + Proxies []string `json:"proxies" binding:""` + Refresh bool `json:"refresh" binding:""` + Template string `json:"template" binding:""` + RuleProviders []RuleProviderStruct `json:"ruleProviders" binding:""` + Rules []RuleStruct `json:"rules" binding:""` + AutoTest bool `json:"autoTest" binding:""` + Lazy bool `json:"lazy" binding:""` + Sort string `json:"sort" binding:""` + Remove string `json:"remove" binding:""` + Replace map[string]string `json:"replace" binding:""` + NodeListMode bool `json:"nodeList" binding:""` + IgnoreCountryGrooup bool `json:"ignoreCountryGroup" binding:""` + UserAgent string `json:"userAgent" binding:""` + UseUDP bool `json:"useUDP" binding:""` +} + +type RuleProviderStruct struct { + Behavior string `json:"behavior" binding:""` + Url string `json:"url" binding:""` + Group string `json:"group" binding:""` + Prepend bool `json:"prepend" binding:""` + Name string `json:"name" binding:""` +} + +type RuleStruct struct { + Rule string `json:"rule" binding:""` + Prepend bool `json:"prepend" binding:""` +} + +func ParseConvertQuery(c *gin.Context) (ConvertConfig, error) { + config := c.Param("config") + queryBytes, err := utils.DecodeBase64(config, true) + if err != nil { + return ConvertConfig{}, errors.New("参数错误: " + err.Error()) + } + var query ConvertConfig + err = json.Unmarshal([]byte(queryBytes), &query) + if err != nil { + return ConvertConfig{}, errors.New("参数错误: " + err.Error()) + } + if len(query.Subs) == 0 && len(query.Proxies) == 0 { + return ConvertConfig{}, errors.New("参数错误: sub 和 proxy 不能同时为空") + } + if len(query.Subs) > 0 { + for i := range query.Subs { + if !strings.HasPrefix(query.Subs[i], "http") { + return ConvertConfig{}, errors.New("参数错误: sub 格式错误") + } + if _, err := url.ParseRequestURI(query.Subs[i]); err != nil { + return ConvertConfig{}, errors.New("参数错误: " + err.Error()) + } + } + } else { + query.Subs = nil + } + if query.Template != "" { + if strings.HasPrefix(query.Template, "http") { + uri, err := url.ParseRequestURI(query.Template) + if err != nil { + return ConvertConfig{}, err + } + query.Template = uri.String() + } + } + if len(query.RuleProviders) > 0 { + names := make(map[string]bool) + for _, ruleProvider := range query.RuleProviders { + if _, ok := names[ruleProvider.Name]; ok { + return ConvertConfig{}, errors.New("参数错误: Rule-Provider 名称重复") + } + names[ruleProvider.Name] = true + } + } else { + query.RuleProviders = nil + } + return query, nil +} diff --git a/model/sub_config.go b/model/sub_config.go deleted file mode 100644 index b8d965d..0000000 --- a/model/sub_config.go +++ /dev/null @@ -1,159 +0,0 @@ -package model - -import ( - "crypto/sha256" - "encoding/hex" - "errors" - "net/url" - "regexp" - "strings" - - "github.com/gin-gonic/gin" -) - -type SubConfig struct { - Sub string `form:"sub" binding:""` - Subs []string `form:"-" binding:""` - Proxy string `form:"proxy" binding:""` - Proxies []string `form:"-" binding:""` - Refresh bool `form:"refresh,default=false" binding:""` - Template string `form:"template" binding:""` - RuleProvider string `form:"ruleProvider" binding:""` - RuleProviders []RuleProviderStruct `form:"-" binding:""` - Rule string `form:"rule" binding:""` - Rules []RuleStruct `form:"-" binding:""` - 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:""` - NodeListMode bool `form:"nodeList,default=false" binding:""` - IgnoreCountryGrooup bool `form:"ignoreCountryGroup,default=false" binding:""` - UserAgent string `form:"userAgent" binding:""` - UseUDP bool `form:"useUDP,default=false" binding:""` -} - -type RuleProviderStruct struct { - Behavior string - Url string - Group string - Prepend bool - Name string -} - -type RuleStruct struct { - Rule string - Prepend bool -} - -func ParseSubQuery(c *gin.Context) (SubConfig, error) { - var query SubConfig - if err := c.ShouldBind(&query); err != nil { - return SubConfig{}, errors.New("参数错误: " + err.Error()) - } - if query.Sub == "" && query.Proxy == "" { - return SubConfig{}, errors.New("参数错误: sub 和 proxy 不能同时为空") - } - if query.Sub != "" { - query.Subs = strings.Split(query.Sub, ",") - for i := range query.Subs { - if !strings.HasPrefix(query.Subs[i], "http") { - return SubConfig{}, errors.New("参数错误: sub 格式错误") - } - if _, err := url.ParseRequestURI(query.Subs[i]); err != nil { - return SubConfig{}, errors.New("参数错误: " + err.Error()) - } - } - } else { - query.Subs = nil - } - if query.Proxy != "" { - query.Proxies = strings.Split(query.Proxy, ",") - } else { - query.Proxies = nil - } - if query.Template != "" { - if strings.HasPrefix(query.Template, "http") { - uri, err := url.ParseRequestURI(query.Template) - if err != nil { - return SubConfig{}, err - } - query.Template = uri.String() - } - } - if query.RuleProvider != "" { - reg := regexp.MustCompile(`\[(.*?)\]`) - ruleProviders := reg.FindAllStringSubmatch(query.RuleProvider, -1) - for i := range ruleProviders { - length := len(ruleProviders) - parts := strings.Split(ruleProviders[length-i-1][1], ",") - if len(parts) < 4 { - return SubConfig{}, errors.New("参数错误: ruleProvider 格式错误") - } - u := parts[1] - uri, err := url.ParseRequestURI(u) - if err != nil { - return SubConfig{}, errors.New("参数错误: " + err.Error()) - } - u = uri.String() - if len(parts) == 4 { - hash := sha256.Sum224([]byte(u)) - parts = append(parts, hex.EncodeToString(hash[:])) - } - query.RuleProviders = append( - query.RuleProviders, RuleProviderStruct{ - Behavior: parts[0], - Url: u, - Group: parts[2], - Prepend: parts[3] == "true", - Name: parts[4], - }, - ) - } - - names := make(map[string]bool) - for _, ruleProvider := range query.RuleProviders { - if _, ok := names[ruleProvider.Name]; ok { - return SubConfig{}, errors.New("参数错误: Rule-Provider 名称重复") - } - names[ruleProvider.Name] = true - } - } else { - query.RuleProviders = nil - } - if query.Rule != "" { - reg := regexp.MustCompile(`\[(.*?)\]`) - rules := reg.FindAllStringSubmatch(query.Rule, -1) - for i := range rules { - length := len(rules) - r := rules[length-1-i][1] - strings.LastIndex(r, ",") - parts := [2]string{} - parts[0] = r[:strings.LastIndex(r, ",")] - parts[1] = r[strings.LastIndex(r, ",")+1:] - query.Rules = append( - query.Rules, RuleStruct{ - Rule: parts[0], - Prepend: parts[1] == "true", - }, - ) - } - } 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 SubConfig{}, errors.New("参数错误: replace 格式错误") - } - query.ReplaceKeys = append(query.ReplaceKeys, replaces[i][1]) - query.ReplaceTo = append(query.ReplaceTo, replaces[i][2]) - } - } - return query, nil -} diff --git a/parser/common.go b/parser/common.go index cc80f14..60e961f 100644 --- a/parser/common.go +++ b/parser/common.go @@ -1,13 +1,13 @@ package parser import ( - "encoding/base64" "errors" "strconv" "strings" "unicode/utf8" P "github.com/bestnite/sub2clash/model/proxy" + "github.com/bestnite/sub2clash/utils" ) func hasPrefix(proxy string, prefixes []string) bool { @@ -49,7 +49,7 @@ func isLikelyBase64(s string) bool { } } - decoded, err := DecodeBase64(s) + decoded, err := utils.DecodeBase64(s, true) if err != nil { return false } @@ -60,23 +60,6 @@ func isLikelyBase64(s string) bool { return true } -func DecodeBase64(s string) (string, error) { - s = strings.TrimSpace(s) - - if strings.Contains(s, "-") || strings.Contains(s, "_") { - s = strings.ReplaceAll(s, "-", "+") - s = strings.ReplaceAll(s, "_", "/") - } - if len(s)%4 != 0 { - s += strings.Repeat("=", 4-len(s)%4) - } - decodeStr, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return "", err - } - return string(decodeStr), nil -} - type ParseConfig struct { UseUDP bool } diff --git a/parser/shadowsocks.go b/parser/shadowsocks.go index 8906ced..25a2b5a 100644 --- a/parser/shadowsocks.go +++ b/parser/shadowsocks.go @@ -6,6 +6,7 @@ import ( "strings" P "github.com/bestnite/sub2clash/model/proxy" + "github.com/bestnite/sub2clash/utils" ) // ShadowsocksParser Shadowsocks协议解析器 @@ -43,7 +44,7 @@ func (p *ShadowsocksParser) Parse(config ParseConfig, proxy string) (P.Proxy, er break } } - d, err := DecodeBase64(s[0]) + d, err := utils.DecodeBase64(s[0], true) if err != nil { return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error()) } @@ -76,7 +77,7 @@ func (p *ShadowsocksParser) Parse(config ParseConfig, proxy string) (P.Proxy, er password, hasPassword := link.User.Password() if !hasPassword && isLikelyBase64(method) { - decodedStr, err := DecodeBase64(method) + decodedStr, err := utils.DecodeBase64(method, true) if err == nil { methodAndPass := strings.SplitN(decodedStr, ":", 2) if len(methodAndPass) == 2 { @@ -88,7 +89,7 @@ func (p *ShadowsocksParser) Parse(config ParseConfig, proxy string) (P.Proxy, er } } if password != "" && isLikelyBase64(password) { - password, err = DecodeBase64(password) + password, err = utils.DecodeBase64(password, true) if err != nil { return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error()) } diff --git a/parser/shadowsocksr.go b/parser/shadowsocksr.go index 557a1bf..f4db56e 100644 --- a/parser/shadowsocksr.go +++ b/parser/shadowsocksr.go @@ -7,6 +7,7 @@ import ( "strings" P "github.com/bestnite/sub2clash/model/proxy" + "github.com/bestnite/sub2clash/utils" ) type ShadowsocksRParser struct{} @@ -39,7 +40,7 @@ func (p *ShadowsocksRParser) Parse(config ParseConfig, proxy string) (P.Proxy, e } } - proxy, err := DecodeBase64(proxy) + proxy, err := utils.DecodeBase64(proxy, true) if err != nil { return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidBase64, err.Error()) } @@ -55,7 +56,7 @@ func (p *ShadowsocksRParser) Parse(config ParseConfig, proxy string) (P.Proxy, e protocol := parts[2] method := parts[3] obfs := parts[4] - password, err := DecodeBase64(parts[5]) + password, err := utils.DecodeBase64(parts[5], true) if err != nil { return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error()) } @@ -73,13 +74,13 @@ func (p *ShadowsocksRParser) Parse(config ParseConfig, proxy string) (P.Proxy, e return P.Proxy{}, fmt.Errorf("%w: %s", ErrCannotParseParams, err.Error()) } if params.Get("obfsparam") != "" { - obfsParam, err = DecodeBase64(params.Get("obfsparam")) + obfsParam, err = utils.DecodeBase64(params.Get("obfsparam"), true) } if params.Get("protoparam") != "" { - protoParam, err = DecodeBase64(params.Get("protoparam")) + protoParam, err = utils.DecodeBase64(params.Get("protoparam"), true) } if params.Get("remarks") != "" { - remarks, err = DecodeBase64(params.Get("remarks")) + remarks, err = utils.DecodeBase64(params.Get("remarks"), true) } else { remarks = server + ":" + strconv.Itoa(port) } diff --git a/parser/socks.go b/parser/socks.go index 8a170e5..2a69c76 100644 --- a/parser/socks.go +++ b/parser/socks.go @@ -6,6 +6,7 @@ import ( "strings" P "github.com/bestnite/sub2clash/model/proxy" + "github.com/bestnite/sub2clash/utils" ) type SocksParser struct{} @@ -59,7 +60,7 @@ func (p *SocksParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) { password, hasPassword := link.User.Password() if !hasPassword && isLikelyBase64(username) { - decodedStr, err := DecodeBase64(username) + decodedStr, err := utils.DecodeBase64(username, true) if err == nil { usernameAndPassword := strings.SplitN(decodedStr, ":", 2) if len(usernameAndPassword) == 2 { diff --git a/parser/vmess.go b/parser/vmess.go index 8132f3f..a7afe4a 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -8,6 +8,7 @@ import ( "strings" P "github.com/bestnite/sub2clash/model/proxy" + "github.com/bestnite/sub2clash/utils" ) type VmessJson struct { @@ -57,7 +58,7 @@ func (p *VmessParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) { break } } - base64, err := DecodeBase64(proxy) + base64, err := utils.DecodeBase64(proxy, true) if err != nil { return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidBase64, err.Error()) } diff --git a/server/handler/sub.go b/server/handler/convert.go similarity index 83% rename from server/handler/sub.go rename to server/handler/convert.go index 21dd40a..181fce9 100644 --- a/server/handler/sub.go +++ b/server/handler/convert.go @@ -12,14 +12,14 @@ import ( "gopkg.in/yaml.v3" ) -func SubHandler(model M.ClashType, template string) func(c *gin.Context) { +func ConvertHandler(template string) func(c *gin.Context) { return func(c *gin.Context) { - query, err := M.ParseSubQuery(c) + query, err := M.ParseConvertQuery(c) if err != nil { c.String(http.StatusBadRequest, err.Error()) return } - sub, err := common.BuildSub(model, query, template, config.GlobalConfig.CacheExpire, config.GlobalConfig.RequestRetryTimes) + sub, err := common.BuildSub(query.ClashType, query, template, config.GlobalConfig.CacheExpire, config.GlobalConfig.RequestRetryTimes) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return diff --git a/server/handler/short_link.go b/server/handler/short_link.go index 7fbbfda..9cb8fbf 100644 --- a/server/handler/short_link.go +++ b/server/handler/short_link.go @@ -142,7 +142,6 @@ func UpdateLinkHandler(c *gin.Context) { } func GetRawConfHandler(c *gin.Context) { - hash := c.Param("hash") password := c.Query("password") @@ -176,11 +175,11 @@ func GetRawConfHandler(c *gin.Context) { host := c.Request.Host targetPath := strings.TrimPrefix(shortLink.Url, "/") requestURL := fmt.Sprintf("%s://%s/%s", scheme, host, targetPath) - + client := &http.Client{ Timeout: 30 * time.Second, // 30秒超时 } - + response, err := client.Get(requestURL) if err != nil { respondWithError(c, http.StatusInternalServerError, "请求错误: "+err.Error()) @@ -198,7 +197,6 @@ func GetRawConfHandler(c *gin.Context) { } func GetRawConfUriHandler(c *gin.Context) { - hash := c.Query("hash") password := c.Query("password") diff --git a/server/route.go b/server/route.go index e381a6c..f49b3ca 100644 --- a/server/route.go +++ b/server/route.go @@ -8,7 +8,6 @@ import ( "github.com/bestnite/sub2clash/config" "github.com/bestnite/sub2clash/constant" - "github.com/bestnite/sub2clash/model" "github.com/bestnite/sub2clash/server/handler" "github.com/bestnite/sub2clash/server/middleware" @@ -40,8 +39,7 @@ func SetRoute(r *gin.Engine) { ) }, ) - r.GET("/clash", middleware.ZapLogger(), handler.SubHandler(model.Clash, config.GlobalConfig.ClashTemplate)) - r.GET("/meta", middleware.ZapLogger(), handler.SubHandler(model.ClashMeta, config.GlobalConfig.MetaTemplate)) + r.GET("/convert/:config", middleware.ZapLogger(), handler.ConvertHandler(config.GlobalConfig.ClashTemplate)) r.GET("/s/:hash", middleware.ZapLogger(), handler.GetRawConfHandler) r.POST("/short", middleware.ZapLogger(), handler.GenerateLinkHandler) r.PUT("/short", middleware.ZapLogger(), handler.UpdateLinkHandler) diff --git a/server/static/index.html b/server/static/index.html index 8c70247..3c61452 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -82,8 +82,8 @@
@@ -180,6 +180,7 @@
+ diff --git a/server/static/index.js b/server/static/index.js index 0c99fe0..ca6c9d2 100644 --- a/server/static/index.js +++ b/server/static/index.js @@ -49,141 +49,138 @@ function clearExistingValues() { } function generateURI() { - const queryParams = []; + const config = {}; - // 获取 API Endpoint - const endpoint = document.getElementById("endpoint").value; + config.clashType = parseInt(document.getElementById("endpoint").value); - // 获取并组合订阅链接 let subLines = document .getElementById("sub") .value.split("\n") .filter((line) => line.trim() !== ""); - let noSub = false; - // 去除 subLines 中空元素 - subLines = subLines.map((item) => { - if (item !== "") { - return item; - } - }); if (subLines.length > 0) { - queryParams.push(`sub=${encodeURIComponent(subLines.join(","))}`); - } else { - noSub = true; + config.subscriptions = subLines; } - // 获取并组合节点分享链接 let proxyLines = document .getElementById("proxy") .value.split("\n") .filter((line) => line.trim() !== ""); - let noProxy = false; - // 去除 proxyLines 中空元素 - proxyLines = proxyLines.map((item) => { - if (item !== "") { - return item; - } - }); if (proxyLines.length > 0) { - queryParams.push(`proxy=${encodeURIComponent(proxyLines.join(","))}`); - } else { - noProxy = true; + config.proxies = proxyLines; } - if (noSub && noProxy) { - // alert("订阅链接和节点分享链接不能同时为空!"); + + if ( + (config.subscriptions === undefined || config.subscriptions.length === 0) && + (config.proxies === undefined || config.proxies.length === 0) + ) { return ""; } - // 获取订阅user-agent标识 - const userAgent = document.getElementById("user-agent").value; - queryParams.push(`userAgent=${encodeURIComponent(userAgent)}`); + config.userAgent = document.getElementById("user-agent").value; - // 获取复选框的值 - const refresh = document.getElementById("refresh").checked; - queryParams.push(`refresh=${refresh ? "true" : "false"}`); - const autoTest = document.getElementById("autoTest").checked; - queryParams.push(`autoTest=${autoTest ? "true" : "false"}`); - const lazy = document.getElementById("lazy").checked; - queryParams.push(`lazy=${lazy ? "true" : "false"}`); - const nodeList = document.getElementById("nodeList").checked; - queryParams.push(`nodeList=${nodeList ? "true" : "false"}`); - const igcg = document.getElementById("igcg").checked; - queryParams.push(`ignoreCountryGroup=${igcg ? "true" : "false"}`); - const useUDP = document.getElementById("useUDP").checked; - queryParams.push(`useUDP=${useUDP ? "true" : "false"}`); + config.refresh = document.getElementById("refresh").checked; + config.autoTest = document.getElementById("autoTest").checked; + config.lazy = document.getElementById("lazy").checked; + config.nodeList = document.getElementById("nodeList").checked; + config.ignoreCountryGroup = document.getElementById("igcg").checked; + config.useUDP = document.getElementById("useUDP").checked; - // 获取模板链接或名称(如果存在) const template = document.getElementById("template").value; if (template.trim() !== "") { - queryParams.push(`template=${encodeURIComponent(template)}`); + config.template = template; } - // 获取Rule Provider和规则 - const ruleProviders = document.getElementsByName("ruleProvider"); - const rules = document.getElementsByName("rule"); - let providers = []; - for (let i = 0; i < ruleProviders.length / 5; i++) { - let baseIndex = i * 5; - let behavior = ruleProviders[baseIndex].value; - let url = ruleProviders[baseIndex + 1].value; - let group = ruleProviders[baseIndex + 2].value; - let prepend = ruleProviders[baseIndex + 3].value; - let name = ruleProviders[baseIndex + 4].value; - // 是否存在空值 - if ( - behavior.trim() === "" || - url.trim() === "" || - group.trim() === "" || - prepend.trim() === "" || - name.trim() === "" - ) { - // alert("Rule Provider 中存在空值,请检查后重试!"); - return ""; - } - providers.push(`[${behavior},${url},${group},${prepend},${name}]`); - } - queryParams.push(`ruleProvider=${encodeURIComponent(providers.join(","))}`); - - let ruleList = []; - for (let i = 0; i < rules.length / 2; i++) { - if (rules[i * 2].value.trim() !== "") { - let rule = rules[i * 2].value; - let prepend = rules[i * 2 + 1].value; - // 是否存在空值 - if (rule.trim() === "" || prepend.trim() === "") { - // alert("Rule 中存在空值,请检查后重试!"); + const ruleProvidersElements = document.getElementsByName("ruleProvider"); + if (ruleProvidersElements.length > 0) { + const ruleProviders = []; + for (let i = 0; i < ruleProvidersElements.length / 5; i++) { + let baseIndex = i * 5; + let behavior = ruleProvidersElements[baseIndex].value; + let url = ruleProvidersElements[baseIndex + 1].value; + let group = ruleProvidersElements[baseIndex + 2].value; + let prepend = ruleProvidersElements[baseIndex + 3].value; + let name = ruleProvidersElements[baseIndex + 4].value; + if ( + behavior.trim() === "" || + url.trim() === "" || + group.trim() === "" || + prepend.trim() === "" || + name.trim() === "" + ) { return ""; } - ruleList.push(`[${rule},${prepend}]`); + ruleProviders.push({ + behavior: behavior, + url: url, + group: group, + prepend: prepend.toLowerCase() === "true", + name: name, + }); + } + if (ruleProviders.length > 0) { + config.ruleProviders = ruleProviders; } } - queryParams.push(`rule=${encodeURIComponent(ruleList.join(","))}`); - // 获取排序策略 - const sort = document.getElementById("sort").value; - queryParams.push(`sort=${sort}`); + const rulesElements = document.getElementsByName("rule"); + if (rulesElements.length > 0) { + const rules = []; + for (let i = 0; i < rulesElements.length / 2; i++) { + if (rulesElements[i * 2].value.trim() !== "") { + let rule = rulesElements[i * 2].value; + let prepend = rulesElements[i * 2 + 1].value; + if (rule.trim() === "" || prepend.trim() === "") { + return ""; + } + rules.push({ + rule: rule, + prepend: prepend.toLowerCase() === "true", + }); + } + } + if (rules.length > 0) { + config.rules = rules; + } + } + + config.sort = document.getElementById("sort").value; - // 获取删除节点的正则表达式 const remove = document.getElementById("remove").value; if (remove.trim() !== "") { - queryParams.push(`remove=${encodeURIComponent(remove)}`); + config.remove = 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 ""; + const replacesElements = document.getElementsByName("replace"); + if (replacesElements.length > 0) { + const replace = {}; + for (let i = 0; i < replacesElements.length / 2; i++) { + let replaceStr = replacesElements[i * 2].value; + let replaceTo = replacesElements[i * 2 + 1].value; + if (replaceStr.trim() === "") { + return ""; + } + replace[replaceStr] = replaceTo; + } + if (Object.keys(replace).length > 0) { + config.replace = replace; } - replaceList.push(`[${replaceStr},${replaceTo}]`); } - queryParams.push(`replace=${encodeURIComponent(replaceList.join(","))}`); - return `${endpoint}?${queryParams.join("&")}`; + const jsonString = JSON.stringify(config); + // 解决 btoa 中文报错,使用 TextEncoder 进行 UTF-8 编码再 base64 + function base64EncodeUnicode(str) { + const bytes = new TextEncoder().encode(str); + let binary = ''; + bytes.forEach((b) => binary += String.fromCharCode(b)); + return btoa(binary); + } + const encoded = base64EncodeUnicode(jsonString); + const urlSafeBase64 = encoded + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + + return `convert/${urlSafeBase64}`; } // 将输入框中的 URL 解析为参数 @@ -216,11 +213,11 @@ async function parseInputURL() { q.append("password", password); try { const response = await axios.get("./short?" + q.toString()); - url = new URL(window.location.href + response.data); + url = new URL(response.data, window.location.href); // 回显配置链接 const apiLinkInput = document.querySelector("#apiLink"); - apiLinkInput.value = `${window.location.origin}${window.location.pathname}${response.data}`; + apiLinkInput.value = url.href; setInputReadOnly(apiLinkInput, true); // 回显短链相关信息 @@ -248,99 +245,106 @@ async function parseInputURL() { alert("获取短链失败,请检查密码!"); } } - let params = new URLSearchParams(url.search); - - // 分配值到对应的输入框 const pathSections = url.pathname.split("/"); - const lastSection = pathSections[pathSections.length - 1]; - const clientTypeSelect = document.getElementById("endpoint"); - switch (lastSection.toLowerCase()) { - case "meta": - clientTypeSelect.value = "meta"; - break; - case "clash": - default: - clientTypeSelect.value = "clash"; - break; + const convertIndex = pathSections.findIndex((s) => s === "convert"); + + if (convertIndex === -1 || convertIndex + 1 >= pathSections.length) { + alert("无效的配置链接,请确认链接为新版格式。"); + return; + } + const base64Config = pathSections[convertIndex + 1]; + let config; + try { + const regularBase64 = base64Config.replace(/-/g, "+").replace(/_/g, "/"); + const decodedStr = atob(regularBase64); + config = JSON.parse(decodeURIComponent(escape(decodedStr))); + } catch (e) { + alert("解析配置失败!"); + console.error(e); + return; } - if (params.has("sub")) { - document.getElementById("sub").value = decodeURIComponent(params.get("sub")) - .split(",") - .join("\n"); + document.getElementById("endpoint").value = config.clashType || "1"; + + if (config.subscriptions) { + document.getElementById("sub").value = config.subscriptions.join("\n"); } - if (params.has("proxy")) { - document.getElementById("proxy").value = decodeURIComponent( - params.get("proxy") - ) - .split(",") - .join("\n"); + if (config.proxies) { + document.getElementById("proxy").value = config.proxies.join("\n"); } - if (params.has("refresh")) { - document.getElementById("refresh").checked = - params.get("refresh") === "true"; + if (config.refresh) { + document.getElementById("refresh").checked = config.refresh; } - if (params.has("autoTest")) { - document.getElementById("autoTest").checked = - params.get("autoTest") === "true"; + if (config.autoTest) { + document.getElementById("autoTest").checked = config.autoTest; } - if (params.has("lazy")) { - document.getElementById("lazy").checked = params.get("lazy") === "true"; + if (config.lazy) { + document.getElementById("lazy").checked = config.lazy; } - if (params.has("template")) { - document.getElementById("template").value = decodeURIComponent( - params.get("template") - ); + if (config.template) { + document.getElementById("template").value = config.template; } - if (params.has("sort")) { - document.getElementById("sort").value = params.get("sort"); + if (config.sort) { + document.getElementById("sort").value = config.sort; } - if (params.has("remove")) { - document.getElementById("remove").value = decodeURIComponent( - params.get("remove") - ); + if (config.remove) { + document.getElementById("remove").value = config.remove; } - if (params.has("userAgent")) { - document.getElementById("user-agent").value = decodeURIComponent( - params.get("userAgent") - ); + if (config.userAgent) { + document.getElementById("user-agent").value = config.userAgent; } - if (params.has("ignoreCountryGroup")) { - document.getElementById("igcg").checked = - params.get("ignoreCountryGroup") === "true"; + if (config.ignoreCountryGroup) { + document.getElementById("igcg").checked = config.ignoreCountryGroup; } - if (params.has("replace")) { - parseAndFillReplaceParams(decodeURIComponent(params.get("replace"))); + if (config.replace) { + const replaceGroup = document.getElementById("replaceGroup"); + for (const original in config.replace) { + const div = createReplace(); + div.children[0].value = original; + div.children[1].value = config.replace[original]; + replaceGroup.appendChild(div); + } } - if (params.has("ruleProvider")) { - parseAndFillRuleProviderParams( - decodeURIComponent(params.get("ruleProvider")) - ); + if (config.ruleProviders) { + const ruleProviderGroup = document.getElementById("ruleProviderGroup"); + for (const p of config.ruleProviders) { + const div = createRuleProvider(); + div.children[0].value = p.behavior; + div.children[1].value = p.url; + div.children[2].value = p.group; + div.children[3].value = p.prepend; + div.children[4].value = p.name; + ruleProviderGroup.appendChild(div); + } } - if (params.has("rule")) { - parseAndFillRuleParams(decodeURIComponent(params.get("rule"))); + if (config.rules) { + const ruleGroup = document.getElementById("ruleGroup"); + for (const r of config.rules) { + const div = createRule(); + div.children[0].value = r.rule; + div.children[1].value = r.prepend; + ruleGroup.appendChild(div); + } } - if (params.has("nodeList")) { - document.getElementById("nodeList").checked = - params.get("nodeList") === "true"; + if (config.nodeList) { + document.getElementById("nodeList").checked = config.nodeList; } - if (params.has("useUDP")) { - document.getElementById("useUDP").checked = - params.get("useUDP") === "true"; + if (config.useUDP) { + document.getElementById("useUDP").checked = config.useUDP; } } @@ -352,51 +356,6 @@ function clearInputGroup(groupId) { } } -function parseAndFillReplaceParams(replaceParams) { - const replaceGroup = document.getElementById("replaceGroup"); - let matches; - const regex = /\[(<.*?>),(<.*?>)\]/g; - const str = decodeURIComponent(replaceParams); - while ((matches = regex.exec(str)) !== null) { - const div = createReplace(); - const original = matches[1].slice(1, -1); // Remove < and > - const replacement = matches[2].slice(1, -1); // Remove < and > - - div.children[0].value = original; - div.children[1].value = replacement; - replaceGroup.appendChild(div); - } -} - -function parseAndFillRuleProviderParams(ruleProviderParams) { - const ruleProviderGroup = document.getElementById("ruleProviderGroup"); - let matches; - const regex = /\[(.*?),(.*?),(.*?),(.*?),(.*?)\]/g; - const str = decodeURIComponent(ruleProviderParams); - while ((matches = regex.exec(str)) !== null) { - const div = createRuleProvider(); - div.children[0].value = matches[1]; - div.children[1].value = matches[2]; - div.children[2].value = matches[3]; - div.children[3].value = matches[4]; - div.children[4].value = matches[5]; - ruleProviderGroup.appendChild(div); - } -} - -function parseAndFillRuleParams(ruleParams) { - const ruleGroup = document.getElementById("ruleGroup"); - let matches; - const regex = /\[(.*?),(.*?)\]/g; - const str = decodeURIComponent(ruleParams); - while ((matches = regex.exec(str)) !== null) { - const div = createRule(); - div.children[0].value = matches[1]; - div.children[1].value = matches[2]; - ruleGroup.appendChild(div); - } -} - async function copyToClipboard(elem, e) { const apiLinkInput = document.querySelector(`#${elem}`).value; try { diff --git a/utils/base64.go b/utils/base64.go new file mode 100644 index 0000000..8bf6443 --- /dev/null +++ b/utils/base64.go @@ -0,0 +1,31 @@ +package utils + +import ( + "encoding/base64" + "strings" +) + +func DecodeBase64(s string, urlSafe bool) (string, error) { + s = strings.TrimSpace(s) + if len(s)%4 != 0 { + s += strings.Repeat("=", 4-len(s)%4) + } + var decodeStr []byte + var err error + if urlSafe { + decodeStr, err = base64.URLEncoding.DecodeString(s) + } else { + decodeStr, err = base64.StdEncoding.DecodeString(s) + } + if err != nil { + return "", err + } + return string(decodeStr), nil +} + +func EncodeBase64(s string, urlSafe bool) string { + if urlSafe { + return base64.URLEncoding.EncodeToString([]byte(s)) + } + return base64.StdEncoding.EncodeToString([]byte(s)) +}