mirror of
https://github.com/bestnite/sub2clash.git
synced 2025-10-25 16:51:01 +00:00
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:
@@ -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))
|
||||
|
||||
@@ -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
92
model/convert_config.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
31
utils/base64.go
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user