Refactor subscription handling by removing SubConfig model, updating BuildSub function to use ConvertConfig, and enhancing Base64 decoding across parsers. Update routes and frontend to support new configuration format.

This commit is contained in:
2025-07-22 04:09:00 +00:00
parent 83a728a415
commit 80d91efca4
15 changed files with 329 additions and 424 deletions

View File

@@ -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))

View File

@@ -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
}

92
model/convert_config.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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())
}

View File

@@ -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

View File

@@ -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")

View File

@@ -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)

View File

@@ -82,8 +82,8 @@
<div class="form-group mb-3">
<label for="endpoint">客户端类型:</label>
<select class="form-control" id="endpoint" name="endpoint">
<option value="clash">Clash</option>
<option value="meta" selected>Clash.Meta</option>
<option value="1">Clash</option>
<option value="2" selected>Clash.Meta</option>
</select>
</div>
<!-- Template -->
@@ -180,6 +180,7 @@
<div class="input-group mb-2">
<input class="form-control" id="apiLink" type="text" placeholder="链接" readonly
style="cursor: not-allowed;" />
<button class="btn btn-primary" onclick="generateURL()" type="button">生成配置</button>
<button class="btn btn-primary" onclick="copyToClipboard('apiLink',this)" type="button">
复制链接
</button>

View File

@@ -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 {

31
utils/base64.go Normal file
View File

@@ -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))
}