1
0
mirror of https://github.com/bestnite/sub2clash.git synced 2025-11-03 20:30:35 +00:00

28 Commits

Author SHA1 Message Date
80d91efca4 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. 2025-07-22 04:09:00 +00:00
83a728a415 Implement YAML unmarshalling for various proxy types and update SOCKS parser to support "socks5" prefix. 2025-07-15 20:14:37 +08:00
a178d06248 Merge pull request #67 from HaTiWinter/main
修复 GetRawConfHandler 中短链的构建问题 | Fix URL construction in GetRawConfHandler
2025-07-13 22:49:33 +08:00
HaTiWinter
99d36d93d8 fix URL construction in GetRawConfHandler 2025-07-13 15:57:46 +08:00
0a9892503d u 2025-07-05 22:57:46 +08:00
ff81d03492 u 2025-07-05 22:54:02 +08:00
0fa95888cb Fix URL construction in GetRawConfHandler to ensure proper HTTP scheme is used. 2025-07-01 02:22:46 +08:00
b44703fa0f Enhance Trojan, Vless, and Vmess parsers. 2025-07-01 02:06:33 +08:00
b256c5e809 Enhance Base64 validation in isLikelyBase64 function to include UTF-8 check and improve decoding logic. 2025-06-26 10:58:56 +08:00
307c36aa8d Refactor Base64 validation in isLikelyBase64 function to remove unnecessary suffix check. 2025-06-26 09:51:39 +08:00
06da2e91c2 Update index.html to set default option for Clash.Meta and improve user-agent label and placeholder text for clarity. 2025-06-26 09:04:53 +08:00
33d37e631b Fix subscription user info header handling in SubHandler to only set header on successful fetch 2025-06-26 10:55:21 +10:00
c1012750ff add tests 2025-06-12 19:33:14 +10:00
69deed91df Refactor Shadowsocks and Socks parsers to simplify username and password extraction 2025-06-12 15:48:59 +10:00
6a780a5e27 Refactor Shadowsocks and Socks parsers to improve username and password handling 2025-06-12 15:41:56 +10:00
f88ae52a29 Unify parts of the model with MetaCubeX. 2025-06-12 14:19:00 +10:00
0c8bbac2e6 Update README.md to modify contributor image link format for improved clarity. 2025-06-12 13:44:27 +10:00
c4554d9030 Remove .all-contributorsrc file and update README.md to display contributor statistics using an image link. 2025-06-12 13:43:17 +10:00
8db2c46bf0 update regex to dynamically include all supported prefixes for improved proxy parsing. 2025-06-12 13:40:30 +10:00
8b3ae45623 Merge pull request #64 from beck-8/fix/socks
fix reg socks
2025-06-12 13:35:47 +10:00
cdc8ef7e32 Update README.md to include contributor section 2025-06-12 13:30:46 +10:00
beck-8
62229fba97 fix reg socks 2025-06-12 10:41:59 +08:00
b80afd97f1 Refactor error handling 2025-06-12 11:33:13 +10:00
2a042968d0 Update .gitignore to include .vscode directory and remove .vscode/launch.json file 2025-06-12 10:28:33 +10:00
cdf69ce65f Refactor proxy structure and parser implementations to streamline protocol handling; remove unused marshaler interfaces and improve YAML serialization for various proxy types. 2025-06-12 10:27:22 +10:00
2d8508f390 Move HTTP, gRPC, Reality, and WebSocket options to proxy.go refactor VmessJson type to use 'any' for flexible types 2025-06-12 09:22:23 +10:00
73616c98a3 fix Dockerfile 2025-06-12 03:14:08 +10:00
44163d30e1 Update index.html to use local Bootstrap and Axios files instead of CDN links 2025-06-12 03:04:19 +10:00
59 changed files with 3584 additions and 1664 deletions

6
.gitignore vendored
View File

@@ -1,9 +1,11 @@
.idea
.vscode
dist
subs
logs
data
.env
.vscode/settings.json
test
*test.go
config.yaml
config.yml
config.json

13
.vscode/launch.json vendored
View File

@@ -1,13 +0,0 @@
{
"configurations": [
{
"name": "Debug",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}",
"output": "${workspaceFolder}/dist/main.exe",
"buildFlags": "-ldflags '-X sub2clash/constant.Version=dev'"
}
]
}

View File

@@ -4,7 +4,7 @@ WORKDIR /app
COPY . .
RUN go mod download
ARG version
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X sub2clash/constant.Version=${version}" -o sub2clash .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X github.com/bestnite/sub2clash/constant.Version=${version}" -o sub2clash .
FROM alpine:latest
WORKDIR /app

View File

@@ -81,5 +81,9 @@
添加新协议支持需要实现以下组件:
1.`parser` 目录下实现协议解析器,用于解析订阅链接
2.`model/proxy` 目录下定义协议结构体和对应的序列化器,如有新增属性需在 `model/proxy/proxy.go` 中声明
1.`parser` 目录下实现协议解析器,用于解析节点链接
2.`model/proxy` 目录下定义协议结构体
## 贡献者
[![](https://contrib.rocks/image?repo=bestnite/sub2clash)](https://github.com/bestnite/sub2clash/graphs/contributors)

View File

@@ -2,9 +2,9 @@ package database
import (
"encoding/json"
"errors"
"path/filepath"
"github.com/bestnite/sub2clash/common"
"github.com/bestnite/sub2clash/model"
"go.etcd.io/bbolt"
@@ -17,13 +17,16 @@ func ConnectDB() error {
db, err := bbolt.Open(path, 0600, nil)
if err != nil {
return err
return common.NewDatabaseConnectError(err)
}
DB = db
return db.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte("ShortLinks"))
return err
if err != nil {
return common.NewDatabaseConnectError(err)
}
return nil
})
}
@@ -33,7 +36,7 @@ func FindShortLinkByHash(hash string) (*model.ShortLink, error) {
b := tx.Bucket([]byte("ShortLinks"))
v := b.Get([]byte(hash))
if v == nil {
return errors.New("ShortLink not found")
return common.NewRecordNotFoundError("ShortLink", hash)
}
return json.Unmarshal(v, &shortLink)
})

192
common/errors.go Normal file
View File

@@ -0,0 +1,192 @@
package common
import (
"errors"
"fmt"
)
// CommonError represents a structured error type for the common package
type CommonError struct {
Code ErrorCode
Message string
Cause error
}
// ErrorCode represents different types of errors
type ErrorCode string
const (
// Directory operation errors
ErrDirCreation ErrorCode = "DIRECTORY_CREATION_FAILED"
ErrDirAccess ErrorCode = "DIRECTORY_ACCESS_FAILED"
// File operation errors
ErrFileNotFound ErrorCode = "FILE_NOT_FOUND"
ErrFileRead ErrorCode = "FILE_READ_FAILED"
ErrFileWrite ErrorCode = "FILE_WRITE_FAILED"
ErrFileCreate ErrorCode = "FILE_CREATE_FAILED"
// Network operation errors
ErrNetworkRequest ErrorCode = "NETWORK_REQUEST_FAILED"
ErrNetworkResponse ErrorCode = "NETWORK_RESPONSE_FAILED"
// Template and configuration errors
ErrTemplateLoad ErrorCode = "TEMPLATE_LOAD_FAILED"
ErrTemplateParse ErrorCode = "TEMPLATE_PARSE_FAILED"
ErrConfigInvalid ErrorCode = "CONFIG_INVALID"
// Subscription errors
ErrSubscriptionLoad ErrorCode = "SUBSCRIPTION_LOAD_FAILED"
ErrSubscriptionParse ErrorCode = "SUBSCRIPTION_PARSE_FAILED"
// Regex errors
ErrRegexCompile ErrorCode = "REGEX_COMPILE_FAILED"
ErrRegexInvalid ErrorCode = "REGEX_INVALID"
// Database errors
ErrDatabaseConnect ErrorCode = "DATABASE_CONNECTION_FAILED"
ErrDatabaseQuery ErrorCode = "DATABASE_QUERY_FAILED"
ErrRecordNotFound ErrorCode = "RECORD_NOT_FOUND"
// Validation errors
ErrValidation ErrorCode = "VALIDATION_FAILED"
ErrInvalidInput ErrorCode = "INVALID_INPUT"
)
// Error returns the string representation of the error
func (e *CommonError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// Unwrap returns the underlying error
func (e *CommonError) Unwrap() error {
return e.Cause
}
// Is allows error comparison
func (e *CommonError) Is(target error) bool {
if t, ok := target.(*CommonError); ok {
return e.Code == t.Code
}
return false
}
// NewError creates a new CommonError
func NewError(code ErrorCode, message string, cause error) *CommonError {
return &CommonError{
Code: code,
Message: message,
Cause: cause,
}
}
// NewSimpleError creates a new CommonError without a cause
func NewSimpleError(code ErrorCode, message string) *CommonError {
return &CommonError{
Code: code,
Message: message,
}
}
// Convenience constructors for common error types
// Directory errors
func NewDirCreationError(dirPath string, cause error) *CommonError {
return NewError(ErrDirCreation, fmt.Sprintf("failed to create directory: %s", dirPath), cause)
}
func NewDirAccessError(dirPath string, cause error) *CommonError {
return NewError(ErrDirAccess, fmt.Sprintf("failed to access directory: %s", dirPath), cause)
}
// File errors
func NewFileNotFoundError(filePath string) *CommonError {
return NewSimpleError(ErrFileNotFound, fmt.Sprintf("file not found: %s", filePath))
}
func NewFileReadError(filePath string, cause error) *CommonError {
return NewError(ErrFileRead, fmt.Sprintf("failed to read file: %s", filePath), cause)
}
func NewFileWriteError(filePath string, cause error) *CommonError {
return NewError(ErrFileWrite, fmt.Sprintf("failed to write file: %s", filePath), cause)
}
func NewFileCreateError(filePath string, cause error) *CommonError {
return NewError(ErrFileCreate, fmt.Sprintf("failed to create file: %s", filePath), cause)
}
// Network errors
func NewNetworkRequestError(url string, cause error) *CommonError {
return NewError(ErrNetworkRequest, fmt.Sprintf("network request failed for URL: %s", url), cause)
}
func NewNetworkResponseError(message string, cause error) *CommonError {
return NewError(ErrNetworkResponse, message, cause)
}
// Template errors
func NewTemplateLoadError(template string, cause error) *CommonError {
return NewError(ErrTemplateLoad, fmt.Sprintf("failed to load template: %s", template), cause)
}
func NewTemplateParseError(cause error) *CommonError {
return NewError(ErrTemplateParse, "failed to parse template", cause)
}
// Subscription errors
func NewSubscriptionLoadError(url string, cause error) *CommonError {
return NewError(ErrSubscriptionLoad, fmt.Sprintf("failed to load subscription: %s", url), cause)
}
func NewSubscriptionParseError(cause error) *CommonError {
return NewError(ErrSubscriptionParse, "failed to parse subscription", cause)
}
// Regex errors
func NewRegexCompileError(pattern string, cause error) *CommonError {
return NewError(ErrRegexCompile, fmt.Sprintf("failed to compile regex pattern: %s", pattern), cause)
}
func NewRegexInvalidError(paramName string, cause error) *CommonError {
return NewError(ErrRegexInvalid, fmt.Sprintf("invalid regex in parameter: %s", paramName), cause)
}
// Database errors
func NewDatabaseConnectError(cause error) *CommonError {
return NewError(ErrDatabaseConnect, "failed to connect to database", cause)
}
func NewRecordNotFoundError(recordType string, id string) *CommonError {
return NewSimpleError(ErrRecordNotFound, fmt.Sprintf("%s not found: %s", recordType, id))
}
// Validation errors
func NewValidationError(field string, message string) *CommonError {
return NewSimpleError(ErrValidation, fmt.Sprintf("validation failed for %s: %s", field, message))
}
func NewInvalidInputError(paramName string, value string) *CommonError {
return NewSimpleError(ErrInvalidInput, fmt.Sprintf("invalid input for parameter %s: %s", paramName, value))
}
// IsErrorCode checks if an error has a specific error code
func IsErrorCode(err error, code ErrorCode) bool {
var commonErr *CommonError
if errors.As(err, &commonErr) {
return commonErr.Code == code
}
return false
}
// GetErrorCode extracts the error code from an error
func GetErrorCode(err error) (ErrorCode, bool) {
var commonErr *CommonError
if errors.As(err, &commonErr) {
return commonErr.Code, true
}
return "", false
}

View File

@@ -1,7 +1,6 @@
package common
import (
"errors"
"os"
)
@@ -18,13 +17,13 @@ func MKDir(dir string) error {
func MkEssentialDir() error {
if err := MKDir("subs"); err != nil {
return errors.New("create subs dir failed" + err.Error())
return NewDirCreationError("subs", err)
}
if err := MKDir("logs"); err != nil {
return errors.New("create logs dir failed" + err.Error())
return NewDirCreationError("logs", err)
}
if err := MKDir("data"); err != nil {
return errors.New("create data dir failed" + err.Error())
return NewDirCreationError("data", err)
}
return nil
}

View File

@@ -56,11 +56,11 @@ func AddProxy(
if !proxyTypes[proxy.Type] {
continue
}
sub.Proxies = append(sub.Proxies, proxy)
sub.Proxy = append(sub.Proxy, proxy)
haveProxyGroup := false
countryName := GetContryName(proxy.Name)
for i := range sub.ProxyGroups {
group := &sub.ProxyGroups[i]
for i := range sub.ProxyGroup {
group := &sub.ProxyGroup[i]
if group.Name == countryName {
group.Proxies = append(group.Proxies, proxy.Name)
group.Size++
@@ -90,7 +90,7 @@ func AddProxy(
Size: 1,
}
}
sub.ProxyGroups = append(sub.ProxyGroups, newGroup)
sub.ProxyGroup = append(sub.ProxyGroup, newGroup)
}
}
}

View File

@@ -10,10 +10,10 @@ import (
func PrependRuleProvider(
sub *model.Subscription, providerName string, group string, provider model.RuleProvider,
) {
if sub.RuleProviders == nil {
sub.RuleProviders = make(map[string]model.RuleProvider)
if sub.RuleProvider == nil {
sub.RuleProvider = make(map[string]model.RuleProvider)
}
sub.RuleProviders[providerName] = provider
sub.RuleProvider[providerName] = provider
PrependRules(
sub,
fmt.Sprintf("RULE-SET,%s,%s", providerName, group),
@@ -23,29 +23,29 @@ func PrependRuleProvider(
func AppenddRuleProvider(
sub *model.Subscription, providerName string, group string, provider model.RuleProvider,
) {
if sub.RuleProviders == nil {
sub.RuleProviders = make(map[string]model.RuleProvider)
if sub.RuleProvider == nil {
sub.RuleProvider = make(map[string]model.RuleProvider)
}
sub.RuleProviders[providerName] = provider
sub.RuleProvider[providerName] = provider
AppendRules(sub, fmt.Sprintf("RULE-SET,%s,%s", providerName, group))
}
func PrependRules(sub *model.Subscription, rules ...string) {
if sub.Rules == nil {
sub.Rules = make([]string, 0)
if sub.Rule == nil {
sub.Rule = make([]string, 0)
}
sub.Rules = append(rules, sub.Rules...)
sub.Rule = append(rules, sub.Rule...)
}
func AppendRules(sub *model.Subscription, rules ...string) {
if sub.Rules == nil {
sub.Rules = make([]string, 0)
if sub.Rule == nil {
sub.Rule = make([]string, 0)
}
matchRule := sub.Rules[len(sub.Rules)-1]
matchRule := sub.Rule[len(sub.Rule)-1]
if strings.Contains(matchRule, "MATCH") {
sub.Rules = append(sub.Rules[:len(sub.Rules)-1], rules...)
sub.Rules = append(sub.Rules, matchRule)
sub.Rule = append(sub.Rule[:len(sub.Rule)-1], rules...)
sub.Rule = append(sub.Rule, matchRule)
return
}
sub.Rules = append(sub.Rules, rules...)
sub.Rule = append(sub.Rule, rules...)
}

View File

@@ -3,7 +3,6 @@ package common
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/url"
@@ -20,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"
)
@@ -93,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{}
@@ -110,26 +110,26 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, errors.New("加载模板失败: " + err.Error())
return nil, NewTemplateLoadError(template, err)
}
} else {
unescape, err := url.QueryUnescape(template)
if err != nil {
return nil, errors.New("加载模板失败: " + err.Error())
return nil, NewTemplateLoadError(template, err)
}
templateBytes, err = LoadTemplate(unescape)
if err != nil {
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, errors.New("加载模板失败: " + err.Error())
return nil, NewTemplateLoadError(unescape, err)
}
}
err = yaml.Unmarshal(templateBytes, &temp)
if err != nil {
logger.Logger.Debug("parse template failed", zap.Error(err))
return nil, errors.New("解析模板失败: " + err.Error())
return nil, NewTemplateParseError(err)
}
var proxyList []P.Proxy
@@ -143,31 +143,41 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
logger.Logger.Debug(
"load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
)
return nil, errors.New("加载订阅失败: " + err.Error())
return nil, NewSubscriptionLoadError(query.Subs[i], err)
}
err = yaml.Unmarshal(data, &sub)
var newProxies []P.Proxy
if err != nil {
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria|hy2|hysteria2)://")
reg, err := regexp.Compile("(" + strings.Join(parser.GetAllPrefixes(), "|") + ")://")
if err != nil {
logger.Logger.Debug("compile regex failed", zap.Error(err))
return nil, NewRegexInvalidError("prefix", err)
}
if reg.Match(data) {
p := parser.ParseProxies(strings.Split(string(data), "\n")...)
p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, strings.Split(string(data), "\n")...)
if err != nil {
return nil, err
}
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]),
zap.String("data", string(data)),
zap.Error(err),
)
return nil, errors.New("加载订阅失败: " + err.Error())
return nil, NewSubscriptionParseError(err)
}
p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, strings.Split(base64, "\n")...)
if err != nil {
return nil, err
}
p := parser.ParseProxies(strings.Split(base64, "\n")...)
newProxies = p
}
} else {
newProxies = sub.Proxies
newProxies = sub.Proxy
}
if subName != "" {
for i := range newProxies {
@@ -178,7 +188,11 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
}
if len(query.Proxies) != 0 {
proxyList = append(proxyList, parser.ParseProxies(query.Proxies...)...)
p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, query.Proxies...)
if err != nil {
return nil, err
}
proxyList = append(proxyList, p...)
}
for i := range proxyList {
@@ -187,13 +201,16 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
}
}
// 去重
proxies := make(map[string]*P.Proxy)
newProxies := make([]P.Proxy, 0, len(proxyList))
for i := range proxyList {
key := proxyList[i].Server + strconv.Itoa(proxyList[i].Port) + proxyList[i].Type + proxyList[i].UUID + proxyList[i].Password
if proxyList[i].Network == "ws" {
key += proxyList[i].WSOpts.Path + proxyList[i].WSOpts.Headers["Host"]
yamlBytes, err := yaml.Marshal(proxyList[i])
if err != nil {
logger.Logger.Debug("marshal proxy failed", zap.Error(err))
return nil, fmt.Errorf("marshal proxy failed: %w", err)
}
key := string(yamlBytes)
if _, exist := proxies[key]; !exist {
proxies[key] = &proxyList[i]
newProxies = append(newProxies, proxyList[i])
@@ -201,13 +218,14 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
}
proxyList = newProxies
// 移除
if strings.TrimSpace(query.Remove) != "" {
newProxyList := make([]P.Proxy, 0, len(proxyList))
for i := range proxyList {
removeReg, err := regexp.Compile(query.Remove)
if err != nil {
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
return nil, errors.New("remove 参数非法: " + err.Error())
return nil, NewRegexInvalidError("remove", err)
}
if removeReg.MatchString(proxyList[i].Name) {
@@ -218,29 +236,25 @@ func BuildSub(clashType model.ClashType, query model.SubConfig, template string,
proxyList = newProxyList
}
if len(query.ReplaceKeys) != 0 {
replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys))
for _, v := range query.ReplaceKeys {
replaceReg, err := regexp.Compile(v)
// 替换
if 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, errors.New("replace 参数非法: " + err.Error())
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,
)
}
}
}
}
// 重命名有相同名称的节点
names := make(map[string]int)
for i := range proxyList {
if _, exist := names[proxyList[i].Name]; exist {
@@ -258,17 +272,18 @@ 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.ProxyGroups))
sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroup))
case "sizedesc":
sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroups)))
sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroup)))
case "nameasc":
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroup))
case "namedesc":
sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroups)))
sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroup)))
default:
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroup))
}
MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup)
@@ -310,20 +325,20 @@ func FetchSubscriptionUserInfo(url string, userAgent string, retryTimes int) (st
resp, err := client.R().SetHeader("User-Agent", userAgent).Head(url)
if err != nil {
logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err))
return "", err
return "", NewNetworkRequestError(url, err)
}
defer resp.Body.Close()
if userInfo := resp.Header().Get("subscription-userinfo"); userInfo != "" {
return userInfo, nil
}
logger.Logger.Debug("目标 URL 未返回 subscription-userinfo 头", zap.Error(err))
return "", err
logger.Logger.Debug("subscription-userinfo header not found in response")
return "", NewNetworkResponseError("subscription-userinfo header not found", nil)
}
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {
var countryGroupNames []string
for _, proxyGroup := range sub.ProxyGroups {
for _, proxyGroup := range sub.ProxyGroup {
if proxyGroup.IsCountryGrop {
countryGroupNames = append(
countryGroupNames, proxyGroup.Name,
@@ -331,27 +346,27 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg
}
}
var proxyNames []string
for _, proxy := range sub.Proxies {
for _, proxy := range sub.Proxy {
proxyNames = append(proxyNames, proxy.Name)
}
temp.Proxies = append(temp.Proxies, sub.Proxies...)
temp.Proxy = append(temp.Proxy, sub.Proxy...)
for i := range temp.ProxyGroups {
if temp.ProxyGroups[i].IsCountryGrop {
for i := range temp.ProxyGroup {
if temp.ProxyGroup[i].IsCountryGrop {
continue
}
newProxies := make([]string, 0)
countryGroupMap := make(map[string]model.ProxyGroup)
for _, v := range sub.ProxyGroups {
for _, v := range sub.ProxyGroup {
if v.IsCountryGrop {
countryGroupMap[v.Name] = v
}
}
for j := range temp.ProxyGroups[i].Proxies {
for j := range temp.ProxyGroup[i].Proxies {
reg := regexp.MustCompile("<(.*?)>")
if reg.Match([]byte(temp.ProxyGroups[i].Proxies[j])) {
key := reg.FindStringSubmatch(temp.ProxyGroups[i].Proxies[j])[1]
if reg.Match([]byte(temp.ProxyGroup[i].Proxies[j])) {
key := reg.FindStringSubmatch(temp.ProxyGroup[i].Proxies[j])[1]
switch key {
case "all":
newProxies = append(newProxies, proxyNames...)
@@ -369,12 +384,12 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg
}
}
} else {
newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j])
newProxies = append(newProxies, temp.ProxyGroup[i].Proxies[j])
}
}
temp.ProxyGroups[i].Proxies = newProxies
temp.ProxyGroup[i].Proxies = newProxies
}
if !igcg {
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
temp.ProxyGroup = append(temp.ProxyGroup, sub.ProxyGroup...)
}
}

View File

@@ -1,7 +1,6 @@
package common
import (
"errors"
"io"
"os"
)
@@ -23,5 +22,5 @@ func LoadTemplate(templatePath string) ([]byte, error) {
}
return result, nil
}
return nil, errors.New("模板文件不存在")
return nil, NewFileNotFoundError(templatePath)
}

105
go.mod
View File

@@ -6,47 +6,140 @@ toolchain go1.24.3
require (
github.com/gin-gonic/gin v1.10.1
github.com/metacubex/mihomo v1.19.10
github.com/spf13/viper v1.20.1
go.etcd.io/bbolt v1.3.9
go.uber.org/zap v1.27.0
golang.org/x/text v0.21.0
golang.org/x/text v0.22.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
resty.dev/v3 v3.0.0-beta.3
)
require (
github.com/3andne/restls-client-go v0.1.6 // indirect
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.3 // indirect
github.com/enfein/mieru/v3 v3.13.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/uuid/v5 v5.3.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bart v0.20.5 // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.2 // indirect
github.com/metacubex/fswatch v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing v0.5.3 // indirect
github.com/metacubex/sing-mux v0.3.2 // indirect
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f // indirect
github.com/metacubex/sing-shadowsocks v0.2.10 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.4 // indirect
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c // indirect
github.com/metacubex/sing-vmess v0.2.2 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee // indirect
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 // indirect
github.com/metacubex/utls v1.7.3 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/openacid/low v0.1.21 // indirect
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/samber/lo v1.50.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
)

284
go.sum
View File

@@ -1,24 +1,75 @@
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
github.com/RyuaNerin/testingutil v0.1.0/go.mod h1:yTqj6Ta/ycHMPJHRyO12Mz3VrvTloWOsy23WOZH19AA=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -27,18 +78,49 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -46,21 +128,122 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/mihomo v1.19.10 h1:GXCOA1rJNfU5qYSvo+UBUFksh61M0tjPfvlZ1OsYtfs=
github.com/metacubex/mihomo v1.19.10/go.mod h1:ih7BKy1pfqSvPRqaCcuFFK4oNRIFyBotoHX0PbhF7SQ=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 h1:L+1brQNzBhCCxWlicwfK1TlceemCRmrDE4HmcVHc29w=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6h++Q/zf3AxcUCevJhJwgrskJumv+pZdR8g/E/10k=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.3 h1:QWdN16WFKMk06x4nzkc8SvZ7y2x+TLQrpkPoHs+WSVM=
github.com/metacubex/sing v0.5.3/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks v0.2.10 h1:Pr7LDbjMANIQHl07zWgl1vDuhpsfDQUpZ8cX6DPabfg=
github.com/metacubex/sing-shadowsocks v0.2.10/go.mod h1:MtRM0ZZjR0kaDOzy9zWSt6/4/UlrnsNBq+1FNAF4vBk=
github.com/metacubex/sing-shadowsocks2 v0.2.4 h1:Ec0x3hHR7xkld5Z09IGh16wtUUpBb2HgqZ9DExd8Q7s=
github.com/metacubex/sing-shadowsocks2 v0.2.4/go.mod h1:WP8+S0kqtnSbX1vlIpo5i8Irm/ijZITEPBcZ26B5unY=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c h1:Y6jk7AH5BEg9Dsvczrf/KokYsvxeKSZZlCLHg+hC4ro=
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U=
github.com/metacubex/sing-vmess v0.2.2 h1:nG6GIKF1UOGmlzs+BIetdGHkFZ20YqFVIYp5Htqzp+4=
github.com/metacubex/sing-vmess v0.2.2/go.mod h1:CVDNcdSLVYFgTHQlubr88d8CdqupAUDqLjROos+H9xk=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.3 h1:yDcMEWojFh+t8rU9X0HPcZDPAoFze/rIIyssqivzj8A=
github.com/metacubex/utls v1.7.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I=
github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do=
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU=
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo=
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
@@ -74,52 +257,119 @@ github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqj
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4=
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=

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,51 +1,18 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/anytls.go
type Anytls struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Server string `yaml:"server"`
Port int `yaml:"port"`
Password string `yaml:"password,omitempty"`
Alpn []string `yaml:"alpn,omitempty"`
SNI string `yaml:"sni,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
UDP bool `yaml:"udp,omitempty"`
IdleSessionCheckInterval int `yaml:"idle-session-check-interval,omitempty"`
IdleSessionTimeout int `yaml:"idle-session-timeout,omitempty"`
MinIdleSession int `yaml:"min-idle-session,omitempty"`
}
func ProxyToAnytls(p Proxy) Anytls {
return Anytls{
Type: "anytls",
Name: p.Name,
Server: p.Server,
Port: p.Port,
Password: p.Password,
Alpn: p.Alpn,
SNI: p.Sni,
ClientFingerprint: p.ClientFingerprint,
SkipCertVerify: p.SkipCertVerify,
Fingerprint: p.Fingerprint,
UDP: p.UDP,
IdleSessionCheckInterval: p.IdleSessionCheckInterval,
IdleSessionTimeout: p.IdleSessionTimeout,
MinIdleSession: p.MinIdleSession,
}
}
type AnytlsMarshaler struct{}
func (m *AnytlsMarshaler) GetType() string {
return "anytls"
}
func (m *AnytlsMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
return ProxyToAnytls(p), nil
}
func init() {
RegisterMarshaler(&AnytlsMarshaler{})
Server string `yaml:"server"`
Port int `yaml:"port"`
Password string `yaml:"password"`
ALPN []string `yaml:"alpn,omitempty"`
SNI string `yaml:"sni,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
UDP bool `yaml:"udp,omitempty"`
IdleSessionCheckInterval int `yaml:"idle-session-check-interval,omitempty"`
IdleSessionTimeout int `yaml:"idle-session-timeout,omitempty"`
MinIdleSession int `yaml:"min-idle-session,omitempty"`
}

View File

@@ -1,68 +1,29 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/hysteria.go
type Hysteria struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Server string `yaml:"server"`
Port int `yaml:"port,omitempty"`
Ports string `yaml:"ports,omitempty"`
Protocol string `yaml:"protocol,omitempty"`
ObfsProtocol string `yaml:"obfs-protocol,omitempty"`
Up string `yaml:"up"`
UpSpeed int `yaml:"up-speed,omitempty"`
Down string `yaml:"down"`
DownSpeed int `yaml:"down-speed,omitempty"`
Auth string `yaml:"auth,omitempty"`
AuthStringOLD string `yaml:"auth_str,omitempty"`
AuthString string `yaml:"auth-str,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
SNI string `yaml:"sni,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
ReceiveWindow int `yaml:"recv-window,omitempty"`
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
}
func ProxyToHysteria(p Proxy) Hysteria {
return Hysteria{
Type: "hysteria",
Name: p.Name,
Server: p.Server,
Port: p.Port,
Ports: p.Ports,
Protocol: p.Protocol,
Up: p.Up,
Down: p.Down,
Auth: p.Auth,
AuthStringOLD: p.AuthStringOLD,
AuthString: p.AuthString,
Obfs: p.Obfs,
SNI: p.Sni,
SkipCertVerify: p.SkipCertVerify,
Fingerprint: p.Fingerprint,
ALPN: p.Alpn,
CustomCA: p.CustomCA,
CustomCAString: p.CustomCAString,
ReceiveWindowConn: p.ReceiveWindowConn,
ReceiveWindow: p.ReceiveWindow,
DisableMTUDiscovery: p.DisableMTUDiscovery,
FastOpen: p.FastOpen,
HopInterval: p.HopInterval,
}
}
type HysteriaMarshaler struct{}
func (m *HysteriaMarshaler) GetType() string {
return "hysteria"
}
func (m *HysteriaMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
return ProxyToHysteria(p), nil
Server string `yaml:"server"`
Port int `yaml:"port,omitempty"`
Ports string `yaml:"ports,omitempty"`
Protocol string `yaml:"protocol,omitempty"`
ObfsProtocol string `yaml:"obfs-protocol,omitempty"` // compatible with Stash
Up string `yaml:"up"`
UpSpeed int `yaml:"up-speed,omitempty"` // compatible with Stash
Down string `yaml:"down"`
DownSpeed int `yaml:"down-speed,omitempty"` // compatible with Stash
Auth string `yaml:"auth,omitempty"`
AuthString string `yaml:"auth-str,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
SNI string `yaml:"sni,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
ReceiveWindow int `yaml:"recv-window,omitempty"`
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
}

View File

@@ -1,51 +1,29 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/hysteria2.go
type Hysteria2 struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Server string `yaml:"server"`
Port int `yaml:"port"`
Up string `yaml:"up,omitempty"`
Down string `yaml:"down,omitempty"`
Password string `yaml:"password,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ObfsPassword string `yaml:"obfs-password,omitempty"`
SNI string `yaml:"sni,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
CWND int `yaml:"cwnd,omitempty"`
}
Server string `yaml:"server"`
Port int `yaml:"port,omitempty"`
Ports string `yaml:"ports,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
Up string `yaml:"up,omitempty"`
Down string `yaml:"down,omitempty"`
Password string `yaml:"password,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ObfsPassword string `yaml:"obfs-password,omitempty"`
SNI string `yaml:"sni,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
CWND int `yaml:"cwnd,omitempty"`
UdpMTU int `yaml:"udp-mtu,omitempty"`
func ProxyToHysteria2(p Proxy) Hysteria2 {
return Hysteria2{
Type: "hysteria2",
Name: p.Name,
Server: p.Server,
Port: p.Port,
Up: p.Up,
Down: p.Down,
Password: p.Password,
Obfs: p.Obfs,
ObfsPassword: p.ObfsParam,
SNI: p.Sni,
SkipCertVerify: p.SkipCertVerify,
Fingerprint: p.Fingerprint,
ALPN: p.Alpn,
CustomCA: p.CustomCA,
CustomCAString: p.CustomCAString,
CWND: p.CWND,
}
}
type Hysteria2Marshaler struct{}
func (m *Hysteria2Marshaler) GetType() string {
return "hysteria2"
}
func (m *Hysteria2Marshaler) MarshalProxy(p Proxy) (interface{}, error) {
return ProxyToHysteria2(p), nil
// quic-go special config
InitialStreamReceiveWindow uint64 `yaml:"initial-stream-receive-window,omitempty"`
MaxStreamReceiveWindow uint64 `yaml:"max-stream-receive-window,omitempty"`
InitialConnectionReceiveWindow uint64 `yaml:"initial-connection-receive-window,omitempty"`
MaxConnectionReceiveWindow uint64 `yaml:"max-connection-receive-window,omitempty"`
}

View File

@@ -1,79 +1,42 @@
package proxy
import (
"fmt"
"gopkg.in/yaml.v3"
)
type HTTPOptions struct {
Method string `yaml:"method,omitempty"`
Path []string `yaml:"path,omitempty"`
Headers map[string][]string `yaml:"headers,omitempty"`
}
type HTTP2Options struct {
Host []string `yaml:"host,omitempty"`
Path string `yaml:"path,omitempty"`
}
type GrpcOptions struct {
GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
}
type RealityOptions struct {
PublicKey string `yaml:"public-key"`
ShortID string `yaml:"short-id,omitempty"`
}
type WSOptions struct {
Path string `yaml:"path,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
MaxEarlyData int `yaml:"max-early-data,omitempty"`
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
}
type SmuxStruct struct {
Enabled bool `yaml:"enable"`
}
type Proxy struct {
Name string `yaml:"name,omitempty"`
Server string `yaml:"server,omitempty"`
Port int `yaml:"port,omitempty"`
Type string `yaml:"type,omitempty"`
Cipher string `yaml:"cipher,omitempty"`
Username string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
UDP bool `yaml:"udp,omitempty"`
UUID string `yaml:"uuid,omitempty"`
Network string `yaml:"network,omitempty"`
Flow string `yaml:"flow,omitempty"`
TLS bool `yaml:"tls,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
Plugin string `yaml:"plugin,omitempty"`
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
Smux SmuxStruct `yaml:"smux,omitempty"`
Sni string `yaml:"sni,omitempty"`
AllowInsecure bool `yaml:"allow-insecure,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Alpn []string `yaml:"alpn,omitempty"`
XUDP bool `yaml:"xudp,omitempty"`
Servername string `yaml:"servername,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
AlterID int `yaml:"alterId,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
Protocol string `yaml:"protocol,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ObfsParam string `yaml:"obfs-param,omitempty"`
ProtocolParam string `yaml:"protocol-param,omitempty"`
Remarks []string `yaml:"remarks,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
PacketAddr bool `yaml:"packet-addr,omitempty"`
PacketEncoding string `yaml:"packet-encoding,omitempty"`
GlobalPadding bool `yaml:"global-padding,omitempty"`
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
SubName string `yaml:"-"`
Up string `yaml:"up,omitempty"`
Down string `yaml:"down,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
CWND int `yaml:"cwnd,omitempty"`
Auth string `yaml:"auth,omitempty"`
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
ReceiveWindow int `yaml:"recv-window,omitempty"`
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
Ports string `yaml:"ports,omitempty"`
AuthStringOLD string `yaml:"auth_str,omitempty"`
AuthString string `yaml:"auth-str,omitempty"`
Ip string `yaml:"ip,omitempty"`
Ipv6 string `yaml:"ipv6,omitempty"`
PrivateKey string `yaml:"private-key,omitempty"`
Workers int `yaml:"workers,omitempty"`
MTU int `yaml:"mtu,omitempty"`
PersistentKeepalive int `yaml:"persistent-keepalive,omitempty"`
Peers []WireGuardPeerOption `yaml:"peers,omitempty"`
RemoteDnsResolve bool `yaml:"remote-dns-resolve,omitempty"`
Dns []string `yaml:"dns,omitempty"`
IdleSessionCheckInterval int `yaml:"idle-session-check-interval,omitempty"`
IdleSessionTimeout int `yaml:"idle-session-timeout,omitempty"`
MinIdleSession int `yaml:"min-idle-session,omitempty"`
}
type WireGuardPeerOption struct {
Server string `yaml:"server"`
Port int `yaml:"port"`
@@ -83,33 +46,239 @@ type WireGuardPeerOption struct {
AllowedIPs []string `yaml:"allowed-ips,omitempty"`
}
type _Proxy Proxy
type ECHOptions struct {
Enable bool `yaml:"enable,omitempty" obfs:"enable,omitempty"`
Config string `yaml:"config,omitempty" obfs:"config,omitempty"`
}
func (p Proxy) MarshalYAML() (interface{}, error) {
// 尝试使用注册的序列化器
if marshaler, exists := GetMarshaler(p.Type); exists {
return marshaler.MarshalProxy(p)
}
type Proxy struct {
Type string
Name string
SubName string `yaml:"-"`
Anytls
Hysteria
Hysteria2
ShadowSocks
ShadowSocksR
Trojan
Vless
Vmess
Socks
}
// 保持向后兼容,对于未注册的类型使用原有逻辑
func (p Proxy) MarshalYAML() (any, error) {
switch p.Type {
case "vmess":
return ProxyToVmess(p), nil
case "ss":
return ProxyToShadowSocks(p), nil
case "ssr":
return ProxyToShadowSocksR(p), nil
case "vless":
return ProxyToVless(p), nil
case "trojan":
return ProxyToTrojan(p), nil
case "hysteria":
return ProxyToHysteria(p), nil
case "hysteria2":
return ProxyToHysteria2(p), nil
case "anytls":
return ProxyToAnytls(p), nil
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Anytls `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Anytls: p.Anytls,
}, nil
case "hysteria":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Hysteria `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Hysteria: p.Hysteria,
}, nil
case "hysteria2":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Hysteria2 `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Hysteria2: p.Hysteria2,
}, nil
case "ss":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
ShadowSocks `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
ShadowSocks: p.ShadowSocks,
}, nil
case "ssr":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
ShadowSocksR `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
ShadowSocksR: p.ShadowSocksR,
}, nil
case "trojan":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Trojan `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Trojan: p.Trojan,
}, nil
case "vless":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Vless `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Vless: p.Vless,
}, nil
case "vmess":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Vmess `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Vmess: p.Vmess,
}, nil
case "socks5":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Socks `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Socks: p.Socks,
}, nil
default:
return _Proxy(p), nil
return nil, fmt.Errorf("unsupported proxy type: %s", p.Type)
}
}
func (p *Proxy) UnmarshalYAML(node *yaml.Node) error {
var temp struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
}
if err := node.Decode(&temp); err != nil {
return err
}
p.Type = temp.Type
p.Name = temp.Name
switch temp.Type {
case "anytls":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Anytls `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Anytls = data.Anytls
case "hysteria":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Hysteria `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Hysteria = data.Hysteria
case "hysteria2":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Hysteria2 `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Hysteria2 = data.Hysteria2
case "ss":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
ShadowSocks `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.ShadowSocks = data.ShadowSocks
case "ssr":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
ShadowSocksR `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.ShadowSocksR = data.ShadowSocksR
case "trojan":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Trojan `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Trojan = data.Trojan
case "vless":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Vless `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Vless = data.Vless
case "vmess":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Vmess `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Vmess = data.Vmess
case "socks5":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Socks `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Socks = data.Socks
default:
return fmt.Errorf("unsupported proxy type: %s", temp.Type)
}
return nil
}

View File

@@ -1,64 +0,0 @@
package proxy
import (
"sync"
)
// ProxyMarshaler 代理YAML序列化接口
type ProxyMarshaler interface {
// MarshalProxy 将通用Proxy对象序列化为特定协议的YAML结构
MarshalProxy(p Proxy) (interface{}, error)
// GetType 返回支持的协议类型
GetType() string
}
// marshalerRegistry YAML序列化器注册中心
type marshalerRegistry struct {
mu sync.RWMutex
marshalers map[string]ProxyMarshaler // type -> marshaler
}
var yamlRegistry = &marshalerRegistry{
marshalers: make(map[string]ProxyMarshaler),
}
// RegisterMarshaler 注册YAML序列化器
func RegisterMarshaler(marshaler ProxyMarshaler) {
yamlRegistry.mu.Lock()
defer yamlRegistry.mu.Unlock()
yamlRegistry.marshalers[marshaler.GetType()] = marshaler
}
// GetMarshaler 根据协议类型获取序列化器
func GetMarshaler(proxyType string) (ProxyMarshaler, bool) {
yamlRegistry.mu.RLock()
defer yamlRegistry.mu.RUnlock()
marshaler, exists := yamlRegistry.marshalers[proxyType]
return marshaler, exists
}
// GetAllMarshalers 获取所有注册的序列化器
func GetAllMarshalers() map[string]ProxyMarshaler {
yamlRegistry.mu.RLock()
defer yamlRegistry.mu.RUnlock()
result := make(map[string]ProxyMarshaler)
for k, v := range yamlRegistry.marshalers {
result[k] = v
}
return result
}
// GetSupportedTypes 获取所有支持的协议类型
func GetSupportedTypes() []string {
yamlRegistry.mu.RLock()
defer yamlRegistry.mu.RUnlock()
types := make([]string, 0, len(yamlRegistry.marshalers))
for proxyType := range yamlRegistry.marshalers {
types = append(types, proxyType)
}
return types
}

View File

@@ -1,8 +1,7 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/shadowsocks.go
type ShadowSocks struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Server string `yaml:"server"`
Port int `yaml:"port"`
Password string `yaml:"password"`
@@ -14,38 +13,3 @@ type ShadowSocks struct {
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
}
func ProxyToShadowSocks(p Proxy) ShadowSocks {
return ShadowSocks{
Type: "ss",
Name: p.Name,
Server: p.Server,
Port: p.Port,
Password: p.Password,
Cipher: p.Cipher,
UDP: p.UDP,
Plugin: p.Plugin,
PluginOpts: p.PluginOpts,
UDPOverTCP: p.UDPOverTCP,
UDPOverTCPVersion: p.UDPOverTCPVersion,
ClientFingerprint: p.ClientFingerprint,
}
}
// ShadowsocksMarshaler Shadowsocks协议的YAML序列化器
type ShadowsocksMarshaler struct{}
// GetType 返回协议类型
func (m *ShadowsocksMarshaler) GetType() string {
return "ss"
}
// MarshalProxy 序列化Shadowsocks代理
func (m *ShadowsocksMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
return ProxyToShadowSocks(p), nil
}
// 注册序列化器
func init() {
RegisterMarshaler(&ShadowsocksMarshaler{})
}

View File

@@ -1,8 +1,7 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/shadowsocksr.go
type ShadowSocksR struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Server string `yaml:"server"`
Port int `yaml:"port"`
Password string `yaml:"password"`
@@ -13,29 +12,3 @@ type ShadowSocksR struct {
ProtocolParam string `yaml:"protocol-param,omitempty"`
UDP bool `yaml:"udp,omitempty"`
}
func ProxyToShadowSocksR(p Proxy) ShadowSocksR {
return ShadowSocksR{
Type: "ssr",
Name: p.Name,
Server: p.Server,
Port: p.Port,
Password: p.Password,
Cipher: p.Cipher,
Obfs: p.Obfs,
ObfsParam: p.ObfsParam,
Protocol: p.Protocol,
ProtocolParam: p.ProtocolParam,
UDP: p.UDP,
}
}
type ShadowsocksRMarshaler struct{}
func (m *ShadowsocksRMarshaler) GetType() string {
return "ssr"
}
func (m *ShadowsocksRMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
return ProxyToShadowSocksR(p), nil
}

13
model/proxy/socks.go Normal file
View File

@@ -0,0 +1,13 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/socks5.go
type Socks struct {
Server string `yaml:"server"`
Port int `yaml:"port"`
UserName string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
TLS bool `yaml:"tls,omitempty"`
UDP bool `yaml:"udp,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
}

View File

@@ -1,8 +1,7 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/trojan.go
type Trojan struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Server string `yaml:"server"`
Port int `yaml:"port"`
Password string `yaml:"password"`
@@ -12,38 +11,16 @@ type Trojan struct {
Fingerprint string `yaml:"fingerprint,omitempty"`
UDP bool `yaml:"udp,omitempty"`
Network string `yaml:"network,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
SSOpts TrojanSSOption `yaml:"ss-opts,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
}
func ProxyToTrojan(p Proxy) Trojan {
return Trojan{
Type: "trojan",
Name: p.Name,
Server: p.Server,
Port: p.Port,
Password: p.Password,
ALPN: p.Alpn,
SNI: p.Sni,
SkipCertVerify: p.SkipCertVerify,
Fingerprint: p.Fingerprint,
UDP: p.UDP,
Network: p.Network,
RealityOpts: p.RealityOpts,
GrpcOpts: p.GrpcOpts,
WSOpts: p.WSOpts,
ClientFingerprint: p.ClientFingerprint,
}
}
type TrojanMarshaler struct{}
func (m *TrojanMarshaler) GetType() string {
return "trojan"
}
func (m *TrojanMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
return ProxyToTrojan(p), nil
type TrojanSSOption struct {
Enabled bool `yaml:"enabled,omitempty"`
Method string `yaml:"method,omitempty"`
Password string `yaml:"password,omitempty"`
}

View File

@@ -1,8 +1,7 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/vless.go
type Vless struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Server string `yaml:"server"`
Port int `yaml:"port"`
UUID string `yaml:"uuid"`
@@ -14,6 +13,7 @@ type Vless struct {
XUDP bool `yaml:"xudp,omitempty"`
PacketEncoding string `yaml:"packet-encoding,omitempty"`
Network string `yaml:"network,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
@@ -26,42 +26,3 @@ type Vless struct {
ServerName string `yaml:"servername,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
}
func ProxyToVless(p Proxy) Vless {
return Vless{
Type: "vless",
Name: p.Name,
Server: p.Server,
Port: p.Port,
UUID: p.UUID,
Flow: p.Flow,
TLS: p.TLS,
ALPN: p.Alpn,
UDP: p.UDP,
PacketAddr: p.PacketAddr,
XUDP: p.XUDP,
PacketEncoding: p.PacketEncoding,
Network: p.Network,
RealityOpts: p.RealityOpts,
HTTPOpts: p.HTTPOpts,
HTTP2Opts: p.HTTP2Opts,
GrpcOpts: p.GrpcOpts,
WSOpts: p.WSOpts,
WSPath: p.WSOpts.Path,
WSHeaders: p.WSOpts.Headers,
SkipCertVerify: p.SkipCertVerify,
Fingerprint: p.Fingerprint,
ServerName: p.Servername,
ClientFingerprint: p.ClientFingerprint,
}
}
type VlessMarshaler struct{}
func (m *VlessMarshaler) GetType() string {
return "vless"
}
func (m *VlessMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
return ProxyToVless(p), nil
}

View File

@@ -1,53 +1,7 @@
package proxy
type HTTPOptions struct {
Method string `yaml:"method,omitempty"`
Path []string `yaml:"path,omitempty"`
Headers map[string][]string `yaml:"headers,omitempty"`
}
type HTTP2Options struct {
Host []string `yaml:"host,omitempty"`
Path string `yaml:"path,omitempty"`
}
type GrpcOptions struct {
GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
}
type RealityOptions struct {
PublicKey string `yaml:"public-key"`
ShortID string `yaml:"short-id,omitempty"`
}
type WSOptions struct {
Path string `yaml:"path,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
MaxEarlyData int `yaml:"max-early-data,omitempty"`
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
}
type VmessJson struct {
V string `json:"v"`
Ps string `json:"ps"`
Add string `json:"add"`
Port interface{} `json:"port"`
Id string `json:"id"`
Aid interface{} `json:"aid"`
Scy string `json:"scy"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
Path string `json:"path"`
Tls string `json:"tls"`
Sni string `json:"sni"`
Alpn string `json:"alpn"`
Fp string `json:"fp"`
}
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/vmess.go
type Vmess struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Server string `yaml:"server"`
Port int `yaml:"port"`
UUID string `yaml:"uuid"`
@@ -60,6 +14,7 @@ type Vmess struct {
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ServerName string `yaml:"servername,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
@@ -72,43 +27,3 @@ type Vmess struct {
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
}
func ProxyToVmess(p Proxy) Vmess {
return Vmess{
Type: "vmess",
Name: p.Name,
Server: p.Server,
Port: p.Port,
UUID: p.UUID,
AlterID: p.AlterID,
Cipher: p.Cipher,
UDP: p.UDP,
Network: p.Network,
TLS: p.TLS,
ALPN: p.Alpn,
SkipCertVerify: p.SkipCertVerify,
Fingerprint: p.Fingerprint,
ServerName: p.Servername,
RealityOpts: p.RealityOpts,
HTTPOpts: p.HTTPOpts,
HTTP2Opts: p.HTTP2Opts,
GrpcOpts: p.GrpcOpts,
WSOpts: p.WSOpts,
PacketAddr: p.PacketAddr,
XUDP: p.XUDP,
PacketEncoding: p.PacketEncoding,
GlobalPadding: p.GlobalPadding,
AuthenticatedLength: p.AuthenticatedLength,
ClientFingerprint: p.ClientFingerprint,
}
}
type VmessMarshaler struct{}
func (m *VmessMarshaler) GetType() string {
return "vmess"
}
func (m *VmessMarshaler) MarshalProxy(p Proxy) (interface{}, error) {
return ProxyToVmess(p), nil
}

View File

@@ -1,158 +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:""`
}
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,212 +1,83 @@
package model
import "github.com/bestnite/sub2clash/model/proxy"
import (
"net/netip"
"github.com/bestnite/sub2clash/model/proxy"
C "github.com/metacubex/mihomo/config"
LC "github.com/metacubex/mihomo/listener/config"
)
type NodeList struct {
Proxies []proxy.Proxy `yaml:"proxies,omitempty"`
Proxy []proxy.Proxy `yaml:"proxies,omitempty" json:"proxies"`
}
// https://github.com/MetaCubeX/mihomo/blob/Meta/config/config.go RawConfig
type Subscription struct {
Port int `yaml:"port,omitempty" json:"port"`
SocksPort int `yaml:"socks-port,omitempty" json:"socks-port"`
RedirPort int `yaml:"redir-port,omitempty" json:"redir-port"`
TProxyPort int `yaml:"tproxy-port,omitempty" json:"tproxy-port"`
MixedPort int `yaml:"mixed-port,omitempty" json:"mixed-port"`
ShadowSocksConfig string `yaml:"ss-config,omitempty"`
VmessConfig string `yaml:"vmess-config,omitempty"`
InboundTfo bool `yaml:"inbound-tfo,omitempty"`
InboundMPTCP bool `yaml:"inbound-mptcp,omitempty"`
Authentication []string `yaml:"authentication,omitempty" json:"authentication"`
SkipAuthPrefixes []string `yaml:"skip-auth-prefixes,omitempty"`
LanAllowedIPs []string `yaml:"lan-allowed-ips,omitempty"`
LanDisAllowedIPs []string `yaml:"lan-disallowed-ips,omitempty"`
AllowLan bool `yaml:"allow-lan,omitempty" json:"allow-lan"`
BindAddress string `yaml:"bind-address,omitempty" json:"bind-address"`
Mode string `yaml:"mode,omitempty" json:"mode"`
UnifiedDelay bool `yaml:"unified-delay,omitempty" json:"unified-delay"`
LogLevel string `yaml:"log-level,omitempty" json:"log-level"`
IPv6 bool `yaml:"ipv6,omitempty" json:"ipv6"`
ExternalController string `yaml:"external-controller,omitempty"`
ExternalControllerTLS string `yaml:"external-controller-tls,omitempty"`
ExternalUI string `yaml:"external-ui,omitempty"`
ExternalUIURL string `yaml:"external-ui-url,omitempty" json:"external-ui-url"`
ExternalUIName string `yaml:"external-ui-name,omitempty" json:"external-ui-name"`
Secret string `yaml:"secret,omitempty"`
Interface string `yaml:"interface-name,omitempty"`
RoutingMark int `yaml:"routing-mark,omitempty"`
//Tunnels []LC.Tunnel `yaml:"tunnels,omitempty"`
GeoAutoUpdate bool `yaml:"geo-auto-update,omitempty" json:"geo-auto-update"`
GeoUpdateInterval int `yaml:"geo-update-interval,omitempty" json:"geo-update-interval"`
GeodataMode bool `yaml:"geodata-mode,omitempty" json:"geodata-mode"`
GeodataLoader string `yaml:"geodata-loader,omitempty" json:"geodata-loader"`
GeositeMatcher string `yaml:"geosite-matcher,omitempty" json:"geosite-matcher"`
TCPConcurrent bool `yaml:"tcp-concurrent,omitempty" json:"tcp-concurrent"`
FindProcessMode string `yaml:"find-process-mode,omitempty" json:"find-process-mode"`
GlobalClientFingerprint string `yaml:"global-client-fingerprint,omitempty"`
GlobalUA string `yaml:"global-ua,omitempty"`
KeepAliveInterval int `yaml:"keep-alive-interval,omitempty"`
Port int `yaml:"port,omitempty" json:"port"`
SocksPort int `yaml:"socks-port,omitempty" json:"socks-port"`
RedirPort int `yaml:"redir-port,omitempty" json:"redir-port"`
TProxyPort int `yaml:"tproxy-port,omitempty" json:"tproxy-port"`
MixedPort int `yaml:"mixed-port,omitempty" json:"mixed-port"`
ShadowSocksConfig string `yaml:"ss-config,omitempty" json:"ss-config"`
VmessConfig string `yaml:"vmess-config,omitempty" json:"vmess-config"`
InboundTfo bool `yaml:"inbound-tfo,omitempty" json:"inbound-tfo"`
InboundMPTCP bool `yaml:"inbound-mptcp,omitempty" json:"inbound-mptcp"`
Authentication []string `yaml:"authentication,omitempty" json:"authentication"`
SkipAuthPrefixes []netip.Prefix `yaml:"skip-auth-prefixes,omitempty" json:"skip-auth-prefixes"`
LanAllowedIPs []netip.Prefix `yaml:"lan-allowed-ips,omitempty" json:"lan-allowed-ips"`
LanDisAllowedIPs []netip.Prefix `yaml:"lan-disallowed-ips,omitempty" json:"lan-disallowed-ips"`
AllowLan bool `yaml:"allow-lan,omitempty" json:"allow-lan"`
BindAddress string `yaml:"bind-address,omitempty" json:"bind-address"`
Mode string `yaml:"mode,omitempty" json:"mode"`
UnifiedDelay bool `yaml:"unified-delay,omitempty" json:"unified-delay"`
LogLevel string `yaml:"log-level,omitempty" json:"log-level"`
IPv6 bool `yaml:"ipv6,omitempty" json:"ipv6"`
ExternalController string `yaml:"external-controller,omitempty" json:"external-controller"`
ExternalControllerPipe string `yaml:"external-controller-pipe,omitempty" json:"external-controller-pipe"`
ExternalControllerUnix string `yaml:"external-controller-unix,omitempty" json:"external-controller-unix"`
ExternalControllerTLS string `yaml:"external-controller-tls,omitempty" json:"external-controller-tls"`
ExternalControllerCors C.RawCors `yaml:"external-controller-cors,omitempty" json:"external-controller-cors"`
ExternalUI string `yaml:"external-ui,omitempty" json:"external-ui"`
ExternalUIURL string `yaml:"external-ui-url,omitempty" json:"external-ui-url"`
ExternalUIName string `yaml:"external-ui-name,omitempty" json:"external-ui-name"`
ExternalDohServer string `yaml:"external-doh-server,omitempty" json:"external-doh-server"`
Secret string `yaml:"secret,omitempty" json:"secret"`
Interface string `yaml:"interface-name,omitempty" json:"interface-name"`
RoutingMark int `yaml:"routing-mark,omitempty" json:"routing-mark"`
Tunnels []LC.Tunnel `yaml:"tunnels,omitempty" json:"tunnels"`
GeoAutoUpdate bool `yaml:"geo-auto-update,omitempty" json:"geo-auto-update"`
GeoUpdateInterval int `yaml:"geo-update-interval,omitempty" json:"geo-update-interval"`
GeodataMode bool `yaml:"geodata-mode,omitempty" json:"geodata-mode"`
GeodataLoader string `yaml:"geodata-loader,omitempty" json:"geodata-loader"`
GeositeMatcher string `yaml:"geosite-matcher,omitempty" json:"geosite-matcher"`
TCPConcurrent bool `yaml:"tcp-concurrent,omitempty" json:"tcp-concurrent"`
FindProcessMode string `yaml:"find-process-mode,omitempty" json:"find-process-mode"`
GlobalClientFingerprint string `yaml:"global-client-fingerprint,omitempty" json:"global-client-fingerprint"`
GlobalUA string `yaml:"global-ua,omitempty" json:"global-ua"`
ETagSupport bool `yaml:"etag-support,omitempty" json:"etag-support"`
KeepAliveIdle int `yaml:"keep-alive-idle,omitempty" json:"keep-alive-idle"`
KeepAliveInterval int `yaml:"keep-alive-interval,omitempty" json:"keep-alive-interval"`
DisableKeepAlive bool `yaml:"disable-keep-alive,omitempty" json:"disable-keep-alive"`
Sniffer RawSniffer `yaml:"sniffer,omitempty" json:"sniffer"`
ProxyProvider map[string]map[string]any `yaml:"proxy-providers,omitempty"`
RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty"`
ProxyProvider map[string]map[string]any `yaml:"proxy-providers,omitempty" json:"proxy-providers"`
RuleProvider map[string]RuleProvider `yaml:"rule-providers,omitempty" json:"rule-providers"`
Proxy []proxy.Proxy `yaml:"proxies,omitempty" json:"proxies"`
ProxyGroup []ProxyGroup `yaml:"proxy-groups,omitempty" json:"proxy-groups"`
Rule []string `yaml:"rules,omitempty" json:"rule"`
SubRules map[string][]string `yaml:"sub-rules,omitempty" json:"sub-rules"`
Listeners []map[string]any `yaml:"listeners,omitempty" json:"listeners"`
Hosts map[string]any `yaml:"hosts,omitempty" json:"hosts"`
NTP RawNTP `yaml:"ntp,omitempty" json:"ntp"`
DNS RawDNS `yaml:"dns,omitempty" json:"dns"`
Tun RawTun `yaml:"tun,omitempty"`
TuicServer RawTuicServer `yaml:"tuic-server,omitempty"`
EBpf EBpf `yaml:"ebpf,omitempty"`
IPTables IPTables `yaml:"iptables,omitempty"`
Experimental Experimental `yaml:"experimental,omitempty"`
Profile Profile `yaml:"profile,omitempty"`
GeoXUrl GeoXUrl `yaml:"geox-url,omitempty"`
Proxies []proxy.Proxy `yaml:"proxies,omitempty"`
ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"`
Rules []string `yaml:"rules,omitempty"`
SubRules map[string][]string `yaml:"sub-rules,omitempty"`
RawTLS TLS `yaml:"tls,omitempty"`
Listeners []map[string]any `yaml:"listeners,omitempty"`
DNS C.RawDNS `yaml:"dns,omitempty" json:"dns"`
NTP C.RawNTP `yaml:"ntp,omitempty" json:"ntp"`
Tun C.RawTun `yaml:"tun,omitempty" json:"tun"`
TuicServer C.RawTuicServer `yaml:"tuic-server,omitempty" json:"tuic-server"`
IPTables C.RawIPTables `yaml:"iptables,omitempty" json:"iptables"`
Experimental C.RawExperimental `yaml:"experimental,omitempty" json:"experimental"`
Profile C.RawProfile `yaml:"profile,omitempty" json:"profile"`
GeoXUrl C.RawGeoXUrl `yaml:"geox-url,omitempty" json:"geox-url"`
Sniffer C.RawSniffer `yaml:"sniffer,omitempty" json:"sniffer"`
TLS C.RawTLS `yaml:"tls,omitempty" json:"tls"`
ClashForAndroid RawClashForAndroid `yaml:"clash-for-android,omitempty" json:"clash-for-android"`
}
type RawClashForAndroid struct {
AppendSystemDNS bool `yaml:"append-system-dns,omitempty" json:"append-system-dns"`
UiSubtitlePattern string `yaml:"ui-subtitle-pattern,omitempty" json:"ui-subtitle-pattern"`
}
type TLS struct {
Certificate string `yaml:"certificate,omitempty"`
PrivateKey string `yaml:"private-key,omitempty"`
CustomTrustCert []string `yaml:"custom-certifactes,omitempty"`
}
type GeoXUrl struct {
GeoIp string `yaml:"geoip,omitempty" json:"geoip"`
Mmdb string `yaml:"mmdb,omitempty" json:"mmdb"`
GeoSite string `yaml:"geosite,omitempty" json:"geosite"`
}
type Experimental struct {
Fingerprints []string `yaml:"fingerprints,omitempty"`
QUICGoDisableGSO bool `yaml:"quic-go-disable-gso,omitempty"`
QUICGoDisableECN bool `yaml:"quic-go-disable-ecn,omitempty"`
IP4PEnable bool `yaml:"dialer-ip4p-convert,omitempty"`
}
type Profile struct {
StoreSelected bool `yaml:"store-selected,omitempty"`
StoreFakeIP bool `yaml:"store-fake-ip,omitempty"`
}
type IPTables struct {
Enable bool `yaml:"enable,omitempty" json:"enable"`
InboundInterface string `yaml:"inbound-interface,omitempty" json:"inbound-interface"`
Bypass []string `yaml:"bypass,omitempty" json:"bypass"`
DnsRedirect bool `yaml:"dns-redirect,omitempty" json:"dns-redirect"`
}
type EBpf struct {
RedirectToTun []string `yaml:"redirect-to-tun,omitempty" json:"redirect-to-tun"`
AutoRedir []string `yaml:"auto-redir,omitempty" json:"auto-redir"`
}
type RawSniffer struct {
Enable bool `yaml:"enable,omitempty" json:"enable"`
OverrideDest bool `yaml:"override-destination,omitempty" json:"override-destination"`
Sniffing []string `yaml:"sniffing,omitempty" json:"sniffing"`
ForceDomain []string `yaml:"force-domain,omitempty" json:"force-domain"`
SkipDomain []string `yaml:"skip-domain,omitempty" json:"skip-domain"`
Ports []string `yaml:"port-whitelist,omitempty" json:"port-whitelist"`
ForceDnsMapping bool `yaml:"force-dns-mapping,omitempty" json:"force-dns-mapping"`
ParsePureIp bool `yaml:"parse-pure-ip,omitempty" json:"parse-pure-ip"`
Sniff map[string]RawSniffingConfig `yaml:"sniff,omitempty" json:"sniff"`
}
type RawSniffingConfig struct {
Ports []string `yaml:"ports,omitempty" json:"ports"`
OverrideDest *bool `yaml:"override-destination,omitempty" json:"override-destination"`
}
type RawNTP struct {
Enable bool `yaml:"enable,omitempty"`
Server string `yaml:"server,omitempty"`
ServerPort int `yaml:"server-port,omitempty"`
Interval int `yaml:"interval,omitempty"`
DialerProxy string `yaml:"dialer-proxy,omitempty"`
WriteToSystem bool `yaml:"write-to-system,omitempty"`
}
type RawDNS struct {
Enable bool `yaml:"enable,omitempty" json:"enable"`
PreferH3 bool `yaml:"prefer-h3,omitempty" json:"prefer-h3"`
IPv6 bool `yaml:"ipv6,omitempty" json:"ipv6"`
IPv6Timeout uint `yaml:"ipv6-timeout,omitempty" json:"ipv6-timeout"`
UseHosts bool `yaml:"use-hosts,omitempty" json:"use-hosts"`
NameServer []string `yaml:"nameserver,omitempty" json:"nameserver"`
Fallback []string `yaml:"fallback,omitempty" json:"fallback"`
FallbackFilter RawFallbackFilter `yaml:"fallback-filter,omitempty" json:"fallback-filter"`
Listen string `yaml:"listen,omitempty" json:"listen"`
EnhancedMode string `yaml:"enhanced-mode,omitempty" json:"enhanced-mode"`
FakeIPRange string `yaml:"fake-ip-range,omitempty" json:"fake-ip-range"`
FakeIPFilter []string `yaml:"fake-ip-filter,omitempty" json:"fake-ip-filter"`
DefaultNameserver []string `yaml:"default-nameserver,omitempty" json:"default-nameserver"`
CacheAlgorithm string `yaml:"cache-algorithm,omitempty" json:"cache-algorithm"`
//NameServerPolicy *orderedmap.OrderedMap[string, any] `yaml:"nameserver-policy,omitempty" json:"nameserver-policy"`
ProxyServerNameserver []string `yaml:"proxy-server-nameserver,omitempty" json:"proxy-server-nameserver"`
}
type RawFallbackFilter struct {
GeoIP bool `yaml:"geoip,omitempty" json:"geoip"`
GeoIPCode string `yaml:"geoip-code,omitempty" json:"geoip-code"`
IPCIDR []string `yaml:"ipcidr,omitempty" json:"ipcidr"`
Domain []string `yaml:"domain,omitempty" json:"domain"`
GeoSite []string `yaml:"geosite,omitempty" json:"geosite"`
}
type RawTun struct {
Enable bool `yaml:"enable,omitempty" json:"enable"`
Device string `yaml:"device,omitempty" json:"device"`
Stack string `yaml:"stack,omitempty" json:"stack"`
DNSHijack []string `yaml:"dns-hijack,omitempty" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route,omitempty" json:"auto-route"`
AutoDetectInterface bool `yaml:"auto-detect-interface,omitempty"`
RedirectToTun []string `yaml:"-,omitempty" json:"-"`
MTU uint32 `yaml:"mtu,omitempty" json:"mtu,omitempty"`
GSO bool `yaml:"gso,omitempty" json:"gso,omitempty"`
GSOMaxSize uint32 `yaml:"gso-max-size,omitempty" json:"gso-max-size,omitempty"`
//Inet4Address []netip.Prefix `yaml:"inet4-address,omitempty" json:"inet4_address,omitempty"`
Inet6Address []uint32 `yaml:"inet6-address,omitempty" json:"inet6_address,omitempty"`
StrictRoute bool `yaml:"strict-route,omitempty" json:"strict_route,omitempty"`
Inet4RouteAddress []uint32 `yaml:"inet4-route-address,omitempty" json:"inet4_route_address,omitempty"`
Inet6RouteAddress []uint32 `yaml:"inet6-route-address,omitempty" json:"inet6_route_address,omitempty"`
Inet4RouteExcludeAddress []uint32 `yaml:"inet4-route-exclude-address,omitempty" json:"inet4_route_exclude_address,omitempty"`
Inet6RouteExcludeAddress []uint32 `yaml:"inet6-route-exclude-address,omitempty" json:"inet6_route_exclude_address,omitempty"`
IncludeInterface []string `yaml:"include-interface,omitempty" json:"include-interface,omitempty"`
ExcludeInterface []string `yaml:"exclude-interface,omitempty" json:"exclude-interface,omitempty"`
IncludeUID []uint32 `yaml:"include-uid,omitempty" json:"include_uid,omitempty"`
IncludeUIDRange []string `yaml:"include-uid-range,omitempty" json:"include_uid_range,omitempty"`
ExcludeUID []uint32 `yaml:"exclude-uid,omitempty" json:"exclude_uid,omitempty"`
ExcludeUIDRange []string `yaml:"exclude-uid-range,omitempty" json:"exclude_uid_range,omitempty"`
IncludeAndroidUser []int `yaml:"include-android-user,omitempty" json:"include_android_user,omitempty"`
IncludePackage []string `yaml:"include-package,omitempty" json:"include_package,omitempty"`
ExcludePackage []string `yaml:"exclude-package,omitempty" json:"exclude_package,omitempty"`
EndpointIndependentNat bool `yaml:"endpoint-independent-nat,omitempty" json:"endpoint_independent_nat,omitempty"`
UDPTimeout int64 `yaml:"udp-timeout,omitempty" json:"udp_timeout,omitempty"`
FileDescriptor int `yaml:"file-descriptor,omitempty" json:"file-descriptor"`
}
type RawTuicServer struct {
Enable bool `yaml:"enable,omitempty" json:"enable"`
Listen string `yaml:"listen,omitempty" json:"listen"`
Token []string `yaml:"token,omitempty" json:"token"`
Users map[string]string `yaml:"users,omitempty" json:"users,omitempty"`
Certificate string `yaml:"certificate,omitempty" json:"certificate"`
PrivateKey string `yaml:"private-key,omitempty" json:"private-key"`
CongestionController string `yaml:"congestion-controller,omitempty" json:"congestion-controller,omitempty"`
MaxIdleTime int `yaml:"max-idle-time,omitempty" json:"max-idle-time,omitempty"`
AuthenticationTimeout int `yaml:"authentication-timeout,omitempty" json:"authentication-timeout,omitempty"`
ALPN []string `yaml:"alpn,omitempty" json:"alpn,omitempty"`
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size,omitempty" json:"max-udp-relay-packet-size,omitempty"`
CWND int `yaml:"cwnd,omitempty" json:"cwnd,omitempty"`
ClashForAndroid C.RawClashForAndroid `yaml:"clash-for-android,omitempty" json:"clash-for-android"`
}

View File

@@ -5,7 +5,6 @@ import (
"net/url"
"strings"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
@@ -27,18 +26,14 @@ func (p *AnytlsParser) GetType() string {
return "anytls"
}
func (p *AnytlsParser) Parse(proxy string) (P.Proxy, error) {
func (p *AnytlsParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
username := link.User.Username()
@@ -50,26 +45,15 @@ func (p *AnytlsParser) Parse(proxy string) (P.Proxy, error) {
query := link.Query()
server := link.Hostname()
if server == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Raw: portStr,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
insecure, sni := query.Get("insecure"), query.Get("sni")
insecureBool := insecure == "1"
@@ -80,13 +64,16 @@ func (p *AnytlsParser) Parse(proxy string) (P.Proxy, error) {
remarks = strings.TrimSpace(remarks)
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
Server: server,
Port: port,
Password: password,
Sni: sni,
SkipCertVerify: insecureBool,
Type: p.GetType(),
Name: remarks,
Anytls: P.Anytls{
Server: server,
Port: port,
Password: password,
SNI: sni,
SkipCertVerify: insecureBool,
UDP: config.UseUDP,
},
}
return result, nil
}

View File

@@ -1,14 +1,13 @@
package parser
import (
"encoding/base64"
"errors"
"strconv"
"strings"
"unicode/utf8"
"github.com/bestnite/sub2clash/logger"
P "github.com/bestnite/sub2clash/model/proxy"
"go.uber.org/zap"
"github.com/bestnite/sub2clash/utils"
)
func hasPrefix(proxy string, prefixes []string) bool {
@@ -34,8 +33,13 @@ func ParsePort(portStr string) (int, error) {
return port, nil
}
// isLikelyBase64 不严格判断是否是合法的 Base64, 很多分享链接不符合 Base64 规范
func isLikelyBase64(s string) bool {
if len(s)%4 == 0 && strings.HasSuffix(s, "=") && !strings.Contains(strings.TrimSuffix(s, "="), "=") {
if strings.TrimSpace(s) == "" {
return false
}
if !strings.Contains(strings.TrimSuffix(s, "="), "=") {
s = strings.TrimSuffix(s, "=")
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for _, c := range s {
@@ -43,45 +47,36 @@ func isLikelyBase64(s string) bool {
return false
}
}
return true
}
return false
}
func DecodeBase64(s string) (string, error) {
s = strings.TrimSpace(s)
if strings.Contains(s, "-") || strings.Contains(s, "_") {
s = strings.Replace(s, "-", "+", -1)
s = strings.Replace(s, "_", "/", -1)
}
if len(s)%4 != 0 {
s += strings.Repeat("=", 4-len(s)%4)
}
decodeStr, err := base64.StdEncoding.DecodeString(s)
decoded, err := utils.DecodeBase64(s, true)
if err != nil {
return "", err
return false
}
return string(decodeStr), nil
if !utf8.ValidString(decoded) {
return false
}
return true
}
func ParseProxies(proxies ...string) []P.Proxy {
type ParseConfig struct {
UseUDP bool
}
func ParseProxies(config ParseConfig, proxies ...string) ([]P.Proxy, error) {
var result []P.Proxy
for _, proxy := range proxies {
if proxy != "" {
var proxyItem P.Proxy
var err error
proxyItem, err = ParseProxyWithRegistry(proxy)
if err == nil {
result = append(result, proxyItem)
} else {
logger.Logger.Debug(
"parse proxy failed", zap.String("proxy", proxy), zap.Error(err),
)
proxyItem, err = ParseProxyWithRegistry(config, proxy)
if err != nil {
return nil, err
}
result = append(result, proxyItem)
}
}
return result
return result, nil
}

View File

@@ -1,10 +1,4 @@
package error
type ParseError struct {
Type ParseErrorType
Message string
Raw string
}
package parser
type ParseErrorType string
@@ -16,9 +10,6 @@ const (
ErrInvalidBase64 ParseErrorType = "invalid base64"
)
func (e *ParseError) Error() string {
if e.Message != "" {
return string(e.Type) + ": " + e.Message + " \"" + e.Raw + "\""
}
return string(e.Type)
func (e ParseErrorType) Error() string {
return string(e)
}

View File

@@ -6,7 +6,6 @@ import (
"strconv"
"strings"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
@@ -28,49 +27,33 @@ func (p *HysteriaParser) GetType() string {
return "hysteria"
}
func (p *HysteriaParser) Parse(proxy string) (P.Proxy, error) {
func (p *HysteriaParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
server := link.Hostname()
if server == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
query := link.Query()
protocol, auth, insecure, upmbps, downmbps, obfs, alpnStr := query.Get("protocol"), query.Get("auth"), query.Get("insecure"), query.Get("upmbps"), query.Get("downmbps"), query.Get("obfs"), query.Get("alpn")
protocol, auth, auth_str, insecure, upmbps, downmbps, obfs, alpnStr := query.Get("protocol"), query.Get("auth"), query.Get("auth-str"), query.Get("insecure"), query.Get("upmbps"), query.Get("downmbps"), query.Get("obfs"), query.Get("alpn")
insecureBool, err := strconv.ParseBool(insecure)
if err != nil {
insecureBool = false
@@ -89,18 +72,20 @@ func (p *HysteriaParser) Parse(proxy string) (P.Proxy, error) {
remarks = strings.TrimSpace(remarks)
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
Server: server,
Port: port,
Up: upmbps,
Down: downmbps,
Auth: auth,
Obfs: obfs,
SkipCertVerify: insecureBool,
Alpn: alpn,
Protocol: protocol,
AllowInsecure: insecureBool,
Type: p.GetType(),
Name: remarks,
Hysteria: P.Hysteria{
Server: server,
Port: port,
Up: upmbps,
Down: downmbps,
Auth: auth,
AuthString: auth_str,
Obfs: obfs,
SkipCertVerify: insecureBool,
ALPN: alpn,
Protocol: protocol,
},
}
return result, nil
}

View File

@@ -5,7 +5,6 @@ import (
"net/url"
"strings"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
@@ -27,18 +26,14 @@ func (p *Hysteria2Parser) GetType() string {
return "hysteria2"
}
func (p *Hysteria2Parser) Parse(proxy string) (P.Proxy, error) {
func (p *Hysteria2Parser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
username := link.User.Username()
@@ -50,29 +45,17 @@ func (p *Hysteria2Parser) Parse(proxy string) (P.Proxy, error) {
query := link.Query()
server := link.Hostname()
if server == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Raw: portStr,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
network, obfs, obfsPassword, pinSHA256, insecure, sni := query.Get("network"), query.Get("obfs"), query.Get("obfs-password"), query.Get("pinSHA256"), query.Get("insecure"), query.Get("sni")
enableTLS := pinSHA256 != "" || sni != ""
obfs, obfsPassword, insecure, sni := query.Get("obfs"), query.Get("obfs-password"), query.Get("insecure"), query.Get("sni")
insecureBool := insecure == "1"
remarks := link.Fragment
if remarks == "" {
@@ -81,17 +64,17 @@ func (p *Hysteria2Parser) Parse(proxy string) (P.Proxy, error) {
remarks = strings.TrimSpace(remarks)
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
Server: server,
Port: port,
Password: password,
Obfs: obfs,
ObfsParam: obfsPassword,
Sni: sni,
SkipCertVerify: insecureBool,
TLS: enableTLS,
Network: network,
Type: p.GetType(),
Name: remarks,
Hysteria2: P.Hysteria2{
Server: server,
Port: port,
Password: password,
Obfs: obfs,
ObfsPassword: obfsPassword,
SNI: sni,
SkipCertVerify: insecureBool,
},
}
return result, nil
}

View File

@@ -1,36 +1,30 @@
package parser
import (
"fmt"
"strings"
"sync"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
// ProxyParser 定义代理解析器接口
type ProxyParser interface {
// Parse 解析代理字符串并返回Proxy对象
Parse(proxy string) (P.Proxy, error)
// GetPrefix 返回支持的协议前缀(可以返回多个)
Parse(config ParseConfig, proxy string) (P.Proxy, error)
GetPrefixes() []string
// GetType 返回协议类型名称
GetType() string
SupportClash() bool
SupportMeta() bool
}
// parserRegistry 解析器注册中心
type parserRegistry struct {
mu sync.RWMutex
parsers map[string]ProxyParser // prefix -> parser
parsers map[string]ProxyParser
}
var registry = &parserRegistry{
parsers: make(map[string]ProxyParser),
}
// RegisterParser 注册解析器
func RegisterParser(parser ProxyParser) {
registry.mu.Lock()
defer registry.mu.Unlock()
@@ -40,7 +34,6 @@ func RegisterParser(parser ProxyParser) {
}
}
// GetParser 根据前缀获取解析器
func GetParser(prefix string) (ProxyParser, bool) {
registry.mu.RLock()
defer registry.mu.RUnlock()
@@ -49,7 +42,6 @@ func GetParser(prefix string) (ProxyParser, bool) {
return parser, exists
}
// GetAllParsers 获取所有注册的解析器
func GetAllParsers() map[string]ProxyParser {
registry.mu.RLock()
defer registry.mu.RUnlock()
@@ -61,19 +53,28 @@ func GetAllParsers() map[string]ProxyParser {
return result
}
// ParseProxyWithRegistry 使用注册机制解析代理
func ParseProxyWithRegistry(proxy string) (P.Proxy, error) {
func GetAllPrefixes() []string {
registry.mu.RLock()
defer registry.mu.RUnlock()
prefixes := make([]string, 0, len(registry.parsers))
for prefix := range registry.parsers {
prefixes = append(prefixes, prefix)
}
return prefixes
}
func ParseProxyWithRegistry(config ParseConfig, proxy string) (P.Proxy, error) {
proxy = strings.TrimSpace(proxy)
if proxy == "" {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: "empty proxy string"}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "empty proxy string")
}
// 查找匹配的解析器
for prefix, parser := range registry.parsers {
if strings.HasPrefix(proxy, prefix) {
return parser.Parse(proxy)
return parser.Parse(config, proxy)
}
}
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy, Message: "unsupported protocol"}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, "unsupported protocol")
}

View File

@@ -5,8 +5,8 @@ import (
"net/url"
"strings"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/utils"
)
// ShadowsocksParser Shadowsocks协议解析器
@@ -31,9 +31,9 @@ func (p *ShadowsocksParser) GetType() string {
}
// Parse 解析Shadowsocks代理
func (p *ShadowsocksParser) Parse(proxy string) (P.Proxy, error) {
func (p *ShadowsocksParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
if !strings.Contains(proxy, "@") {
@@ -44,13 +44,9 @@ func (p *ShadowsocksParser) Parse(proxy string) (P.Proxy, error) {
break
}
}
d, err := DecodeBase64(s[0])
d, err := utils.DecodeBase64(s[0], true)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
if len(s) == 2 {
proxy = "ss://" + d + "#" + s[1]
@@ -60,59 +56,42 @@ func (p *ShadowsocksParser) Parse(proxy string) (P.Proxy, error) {
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
server := link.Hostname()
if server == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
method := link.User.Username()
password, _ := link.User.Password()
password, hasPassword := link.User.Password()
if password == "" {
user, err := DecodeBase64(method)
if !hasPassword && isLikelyBase64(method) {
decodedStr, err := utils.DecodeBase64(method, true)
if err == nil {
methodAndPass := strings.SplitN(user, ":", 2)
methodAndPass := strings.SplitN(decodedStr, ":", 2)
if len(methodAndPass) == 2 {
method = methodAndPass[0]
password = methodAndPass[1]
} else {
method = decodedStr
}
}
}
if isLikelyBase64(password) {
password, err = DecodeBase64(password)
if password != "" && isLikelyBase64(password) {
password, err = utils.DecodeBase64(password, true)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "password decode error",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
}
@@ -123,14 +102,16 @@ func (p *ShadowsocksParser) Parse(proxy string) (P.Proxy, error) {
remarks = strings.TrimSpace(remarks)
result := P.Proxy{
Type: p.GetType(),
Cipher: method,
Password: password,
Server: server,
Port: port,
Name: remarks,
Type: p.GetType(),
Name: remarks,
ShadowSocks: P.ShadowSocks{
Cipher: method,
Password: password,
Server: server,
Port: port,
UDP: config.UseUDP,
},
}
return result, nil
}

View File

@@ -1,12 +1,13 @@
package parser
import (
"fmt"
"net/url"
"strconv"
"strings"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/utils"
)
type ShadowsocksRParser struct{}
@@ -27,9 +28,9 @@ func (p *ShadowsocksRParser) GetType() string {
return "ssr"
}
func (p *ShadowsocksRParser) Parse(proxy string) (P.Proxy, error) {
func (p *ShadowsocksRParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
for _, prefix := range p.GetPrefixes() {
@@ -39,34 +40,29 @@ func (p *ShadowsocksRParser) Parse(proxy string) (P.Proxy, error) {
}
}
proxy, err := DecodeBase64(proxy)
proxy, err := utils.DecodeBase64(proxy, true)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidBase64,
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidBase64, err.Error())
}
serverInfoAndParams := strings.SplitN(proxy, "/?", 2)
parts := strings.Split(serverInfoAndParams[0], ":")
if len(serverInfoAndParams) != 2 {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, proxy)
}
parts := SplitNRight(serverInfoAndParams[0], ":", 6)
if len(parts) < 6 {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, proxy)
}
server := parts[0]
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{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Raw: proxy,
Message: err.Error(),
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
port, err := ParsePort(parts[1])
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
var obfsParam string
@@ -75,48 +71,65 @@ func (p *ShadowsocksRParser) Parse(proxy string) (P.Proxy, error) {
if len(serverInfoAndParams) == 2 {
params, err := url.ParseQuery(serverInfoAndParams[1])
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
}
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)
}
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Raw: proxy,
Message: err.Error(),
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
}
result := P.Proxy{
Name: remarks,
Type: p.GetType(),
Server: server,
Port: port,
Protocol: protocol,
Cipher: method,
Obfs: obfs,
Password: password,
ObfsParam: obfsParam,
ProtocolParam: protoParam,
Type: p.GetType(),
Name: remarks,
ShadowSocksR: P.ShadowSocksR{
Server: server,
Port: port,
Protocol: protocol,
Cipher: method,
Obfs: obfs,
Password: password,
ObfsParam: obfsParam,
ProtocolParam: protoParam,
UDP: config.UseUDP,
},
}
return result, nil
}
func init() {
RegisterParser(&ShadowsocksRParser{})
}
func SplitNRight(s, sep string, n int) []string {
if n <= 0 {
return nil
}
if n == 1 {
return []string{s}
}
parts := strings.Split(s, sep)
if len(parts) <= n {
return parts
}
result := make([]string, n)
for i, j := len(parts)-1, 0; i >= 0; i, j = i-1, j+1 {
if j < n-1 {
result[n-j-1] = parts[len(parts)-j-1]
} else {
result[0] = strings.Join(parts[:i+1], sep)
break
}
}
return result
}

View File

@@ -5,8 +5,8 @@ import (
"net/url"
"strings"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/utils"
)
type SocksParser struct{}
@@ -19,48 +19,33 @@ func (p *SocksParser) SupportMeta() bool {
}
func (p *SocksParser) GetPrefixes() []string {
return []string{"socks://"}
return []string{"socks://", "socks5://"}
}
func (p *SocksParser) GetType() string {
return "socks5"
}
func (p *SocksParser) Parse(proxy string) (P.Proxy, error) {
func (p *SocksParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
server := link.Hostname()
if server == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Raw: portStr,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
remarks := link.Fragment
@@ -69,32 +54,38 @@ func (p *SocksParser) Parse(proxy string) (P.Proxy, error) {
}
remarks = strings.TrimSpace(remarks)
encodeStr := link.User.Username()
var username, password string
if encodeStr != "" {
decodeStr, err := DecodeBase64(encodeStr)
splitStr := strings.Split(decodeStr, ":")
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
username = link.User.Username()
password, hasPassword := link.User.Password()
if !hasPassword && isLikelyBase64(username) {
decodedStr, err := utils.DecodeBase64(username, true)
if err == nil {
usernameAndPassword := strings.SplitN(decodedStr, ":", 2)
if len(usernameAndPassword) == 2 {
username = usernameAndPassword[0]
password = usernameAndPassword[1]
} else {
username = decodedStr
}
}
username = splitStr[0]
if len(splitStr) == 2 {
password = splitStr[1]
}
}
return P.Proxy{
Type: p.GetType(),
Name: remarks,
Server: server,
Port: port,
Username: username,
Password: password,
}, nil
tls, udp := link.Query().Get("tls"), link.Query().Get("udp")
return P.Proxy{
Type: p.GetType(),
Name: remarks,
Socks: P.Socks{
Server: server,
Port: port,
UserName: username,
Password: password,
TLS: tls == "true",
UDP: udp == "true",
},
}, nil
}
func init() {

View File

@@ -5,7 +5,6 @@ import (
"net/url"
"strings"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
@@ -27,45 +26,29 @@ func (p *TrojanParser) GetType() string {
return "trojan"
}
func (p *TrojanParser) Parse(proxy string) (P.Proxy, error) {
func (p *TrojanParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
password := link.User.Username()
server := link.Hostname()
if server == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
remarks := link.Fragment
@@ -75,7 +58,17 @@ func (p *TrojanParser) Parse(proxy string) (P.Proxy, error) {
remarks = strings.TrimSpace(remarks)
query := link.Query()
network, security, alpnStr, sni, pbk, sid, fp, path, host, serviceName := query.Get("type"), query.Get("security"), query.Get("alpn"), query.Get("sni"), query.Get("pbk"), query.Get("sid"), query.Get("fp"), query.Get("path"), query.Get("host"), query.Get("serviceName")
network, security, alpnStr, sni, pbk, sid, fp, path, host, serviceName, udp, insecure := query.Get("type"), query.Get("security"), query.Get("alpn"), query.Get("sni"), query.Get("pbk"), query.Get("sid"), query.Get("fp"), query.Get("path"), query.Get("host"), query.Get("serviceName"), query.Get("udp"), query.Get("allowInsecure")
insecureBool := insecure == "1"
result := P.Trojan{
Server: server,
Port: port,
Password: password,
Network: network,
UDP: udp == "true",
SkipCertVerify: insecureBool,
}
var alpn []string
if strings.Contains(alpnStr, ",") {
@@ -83,30 +76,23 @@ func (p *TrojanParser) Parse(proxy string) (P.Proxy, error) {
} else {
alpn = nil
}
result := P.Proxy{
Type: p.GetType(),
Server: server,
Port: port,
Password: password,
Name: remarks,
Network: network,
if len(alpn) > 0 {
result.ALPN = alpn
}
if security == "xtls" || security == "tls" {
result.Alpn = alpn
result.Sni = sni
result.TLS = true
if fp != "" {
result.ClientFingerprint = fp
}
if sni != "" {
result.SNI = sni
}
if security == "reality" {
result.TLS = true
result.Sni = sni
result.RealityOpts = P.RealityOptions{
PublicKey: pbk,
ShortID: sid,
}
result.Fingerprint = fp
}
if network == "ws" {
@@ -125,7 +111,11 @@ func (p *TrojanParser) Parse(proxy string) (P.Proxy, error) {
}
}
return result, nil
return P.Proxy{
Type: p.GetType(),
Name: remarks,
Trojan: result,
}, nil
}
func init() {

View File

@@ -5,7 +5,6 @@ import (
"net/url"
"strings"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
@@ -27,41 +26,29 @@ func (p *VlessParser) GetType() string {
return "vless"
}
func (p *VlessParser) Parse(proxy string) (P.Proxy, error) {
func (p *VlessParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
server := link.Hostname()
if server == "" {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
query := link.Query()
uuid := link.User.Username()
flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName, _type := query.Get("flow"), query.Get("security"), query.Get("alpn"), query.Get("sni"), query.Get("allowInsecure"), query.Get("fp"), query.Get("pbk"), query.Get("sid"), query.Get("path"), query.Get("host"), query.Get("serviceName"), query.Get("type")
flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName, _type, udp := query.Get("flow"), query.Get("security"), query.Get("alpn"), query.Get("sni"), query.Get("allowInsecure"), query.Get("fp"), query.Get("pbk"), query.Get("sid"), query.Get("path"), query.Get("host"), query.Get("serviceName"), query.Get("type"), query.Get("udp")
insecureBool := insecure == "1"
var alpn []string
@@ -70,37 +57,44 @@ func (p *VlessParser) Parse(proxy string) (P.Proxy, error) {
} else {
alpn = nil
}
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
remarks = strings.TrimSpace(remarks)
result := P.Proxy{
Type: p.GetType(),
Server: server,
Name: remarks,
Port: port,
UUID: uuid,
Flow: flow,
result := P.Vless{
Server: server,
Port: port,
UUID: uuid,
Flow: flow,
UDP: udp == "true",
SkipCertVerify: insecureBool,
}
if len(alpn) > 0 {
result.ALPN = alpn
}
if fp != "" {
result.ClientFingerprint = fp
}
if sni != "" {
result.ServerName = sni
}
if security == "tls" {
result.TLS = true
result.Alpn = alpn
result.Sni = sni
result.AllowInsecure = insecureBool
result.ClientFingerprint = fp
}
if security == "reality" {
result.TLS = true
result.Servername = sni
result.RealityOpts = P.RealityOptions{
PublicKey: pbk,
ShortID: sid,
}
result.ClientFingerprint = fp
}
if _type == "ws" {
@@ -129,11 +123,7 @@ func (p *VlessParser) Parse(proxy string) (P.Proxy, error) {
hosts, err := url.QueryUnescape(host)
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrCannotParseParams, err.Error())
}
result.Network = "http"
if hosts != "" {
@@ -141,7 +131,11 @@ func (p *VlessParser) Parse(proxy string) (P.Proxy, error) {
}
}
return result, nil
return P.Proxy{
Type: p.GetType(),
Name: remarks,
Vless: result,
}, nil
}
func init() {

View File

@@ -2,14 +2,33 @@ package parser
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/utils"
)
type VmessJson struct {
V any `json:"v"`
Ps string `json:"ps"`
Add string `json:"add"`
Port any `json:"port"`
Id string `json:"id"`
Aid any `json:"aid"`
Scy string `json:"scy"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
Path string `json:"path"`
Tls string `json:"tls"`
Sni string `json:"sni"`
Alpn string `json:"alpn"`
Fp string `json:"fp"`
}
type VmessParser struct{}
func (p *VmessParser) SupportClash() bool {
@@ -28,9 +47,9 @@ func (p *VmessParser) GetType() string {
return "vmess"
}
func (p *VmessParser) Parse(proxy string) (P.Proxy, error) {
func (p *VmessParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
for _, prefix := range p.GetPrefixes() {
@@ -39,15 +58,15 @@ func (p *VmessParser) Parse(proxy string) (P.Proxy, error) {
break
}
}
base64, err := DecodeBase64(proxy)
base64, err := utils.DecodeBase64(proxy, true)
if err != nil {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidBase64, Raw: proxy, Message: err.Error()}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidBase64, err.Error())
}
var vmess P.VmessJson
var vmess VmessJson
err = json.Unmarshal([]byte(base64), &vmess)
if err != nil {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: err.Error()}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
var port int
@@ -55,11 +74,7 @@ func (p *VmessParser) Parse(proxy string) (P.Proxy, error) {
case string:
port, err = ParsePort(vmess.Port.(string))
if err != nil {
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
case float64:
port = int(vmess.Port.(float64))
@@ -70,7 +85,7 @@ func (p *VmessParser) Parse(proxy string) (P.Proxy, error) {
case string:
aid, err = strconv.Atoi(vmess.Aid.(string))
if err != nil {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: err.Error()}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
case float64:
aid = int(vmess.Aid.(float64))
@@ -85,27 +100,36 @@ func (p *VmessParser) Parse(proxy string) (P.Proxy, error) {
name = vmess.Ps
}
result := P.Proxy{
Name: name,
Type: p.GetType(),
var alpn []string
if strings.Contains(vmess.Alpn, ",") {
alpn = strings.Split(vmess.Alpn, ",")
} else {
alpn = nil
}
result := P.Vmess{
Server: vmess.Add,
Port: port,
UUID: vmess.Id,
AlterID: aid,
Cipher: vmess.Scy,
UDP: config.UseUDP,
}
if len(alpn) > 0 {
result.ALPN = alpn
}
if vmess.Fp != "" {
result.ClientFingerprint = vmess.Fp
}
if vmess.Sni != "" {
result.ServerName = vmess.Sni
}
if vmess.Tls == "tls" {
var alpn []string
if strings.Contains(vmess.Alpn, ",") {
alpn = strings.Split(vmess.Alpn, ",")
} else {
alpn = nil
}
result.TLS = true
result.Fingerprint = vmess.Fp
result.Alpn = alpn
result.Servername = vmess.Sni
}
if vmess.Net == "ws" {
@@ -139,7 +163,11 @@ func (p *VmessParser) Parse(proxy string) (P.Proxy, error) {
result.Network = "h2"
}
return result, nil
return P.Proxy{
Type: p.GetType(),
Name: name,
Vmess: result,
}, nil
}
func init() {

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
@@ -27,15 +27,14 @@ func SubHandler(model M.ClashType, template string) func(c *gin.Context) {
if len(query.Subs) == 1 {
userInfoHeader, err := common.FetchSubscriptionUserInfo(query.Subs[0], "clash", config.GlobalConfig.RequestRetryTimes)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
if err == nil {
c.Header("subscription-userinfo", userInfoHeader)
}
c.Header("subscription-userinfo", userInfoHeader)
}
if query.NodeListMode {
nodelist := M.NodeList{}
nodelist.Proxies = sub.Proxies
nodelist.Proxy = sub.Proxy
marshal, err := yaml.Marshal(nodelist)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())

View File

@@ -1,6 +1,7 @@
package handler
import (
"fmt"
"io"
"net/http"
"strings"
@@ -141,7 +142,6 @@ func UpdateLinkHandler(c *gin.Context) {
}
func GetRawConfHandler(c *gin.Context) {
hash := c.Param("hash")
password := c.Query("password")
@@ -168,7 +168,19 @@ func GetRawConfHandler(c *gin.Context) {
return
}
response, err := http.Get(strings.TrimSuffix(config.GlobalConfig.Address, "/") + "/" + shortLink.Url)
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
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())
return
@@ -185,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"
@@ -19,8 +18,6 @@ import (
var staticFiles embed.FS
func SetRoute(r *gin.Engine) {
r.Use(middleware.ZapLogger())
tpl, err := template.ParseFS(staticFiles, "static/*")
if err != nil {
log.Fatalf("Error parsing templates: %v", err)
@@ -42,10 +39,9 @@ func SetRoute(r *gin.Engine) {
)
},
)
r.GET("/clash", handler.SubHandler(model.Clash, config.GlobalConfig.ClashTemplate))
r.GET("/meta", handler.SubHandler(model.ClashMeta, config.GlobalConfig.MetaTemplate))
r.GET("/s/:hash", handler.GetRawConfHandler)
r.POST("/short", handler.GenerateLinkHandler)
r.PUT("/short", handler.UpdateLinkHandler)
r.GET("/short", handler.GetRawConfUriHandler)
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)
r.GET("/short", middleware.ZapLogger(), handler.GetRawConfUriHandler)
}

3
server/static/axios.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
server/static/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6
server/static/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +1,16 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-bs-theme="light">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>sub2clash</title>
<!-- Bootstrap CSS -->
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" rel="stylesheet" />
<link href="./static/bootstrap.min.css" rel="stylesheet" />
<!-- Bootstrap JS -->
<script crossorigin="anonymous" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="./static/bootstrap.bundle.min.js"></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios@latest/dist/axios.min.js"></script>
<script src="./static/axios.min.js"></script>
<style>
.container {
max-width: 800px;
@@ -30,10 +28,38 @@
height: 25px;
width: 25px;
}
/* 主题切换按钮样式 */
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.theme-toggle:hover {
transform: scale(1.1);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body class="bg-light">
<body>
<!-- 主题切换按钮 -->
<button class="theme-toggle btn btn-outline-secondary" onclick="toggleTheme()" title="切换深色/浅色模式">
<span id="theme-icon">🌙</span>
</button>
<div class="container mt-5">
<div class="mb-4">
<h2>sub2clash</h2>
@@ -56,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">Clash.Meta</option>
<option value="1">Clash</option>
<option value="2" selected>Clash.Meta</option>
</select>
</div>
<!-- Template -->
@@ -77,9 +103,9 @@
</div>
<!-- User Agent -->
<div class="form-group mb-3">
<label for="user-agent">ua标识:</label>
<label for="user-agent">UA 标识:</label>
<textarea class="form-control" id="user-agent" name="user-agent"
placeholder="用于获取订阅的http请求中的user-agent标识(可选)" rows="3"></textarea>
placeholder="用于获取订阅的 http 请求中的 User-Agent 标识可选" rows="3"></textarea>
</div>
<!-- Refresh -->
<div class="form-check mb-3">
@@ -106,6 +132,11 @@
<input class="form-check-input" id="igcg" name="igcg" type="checkbox" />
<label class="form-check-label" for="igcg">不输出国家策略组</label>
</div>
<!-- Use UDP -->
<div class="form-check mb-3">
<input class="form-check-input" id="useUDP" name="useUDP" type="checkbox" />
<label class="form-check-label" for="useUDP">使用 UDP</label>
</div>
<!-- Rule Provider -->
<div class="form-group mb-3" id="ruleProviderGroup">
<label>Rule Provider:</label>
@@ -147,7 +178,9 @@
<div class="form-group mb-5">
<label for="apiLink">配置链接:</label>
<div class="input-group mb-2">
<input class="form-control bg-light" id="apiLink" type="text" placeholder="链接" readonly style="cursor: not-allowed;" />
<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>
@@ -163,7 +196,8 @@
</button>
</div>
<div class="input-group">
<input class="form-control bg-light" id="apiShortLink" type="text" placeholder="短链接" readonly style="cursor: not-allowed;" />
<input class="form-control" id="apiShortLink" type="text" placeholder="短链接" readonly
style="cursor: not-allowed;" />
<button class="btn btn-primary" onclick="updateShortLink()" type="button">
更新短链
</button>

View File

@@ -1,11 +1,9 @@
function setInputReadOnly(input, readonly) {
if (readonly) {
input.readOnly = true;
input.classList.add('bg-light');
input.style.cursor = 'not-allowed';
} else {
input.readOnly = false;
input.classList.remove('bg-light');
input.style.cursor = 'auto';
}
}
@@ -19,28 +17,29 @@ function clearExistingValues() {
document.getElementById("autoTest").checked = false;
document.getElementById("lazy").checked = false;
document.getElementById("igcg").checked = false;
document.getElementById("useUDP").checked = false;
document.getElementById("template").value = "";
document.getElementById("sort").value = "nameasc";
document.getElementById("remove").value = "";
document.getElementById("apiLink").value = "";
document.getElementById("apiShortLink").value = "";
// 恢复短链ID和密码输入框状态
const customIdInput = document.getElementById("customId");
const passwordInput = document.getElementById("password");
const generateButton = document.querySelector('button[onclick="generateShortLink()"]');
customIdInput.value = "";
setInputReadOnly(customIdInput, false);
passwordInput.value = "";
setInputReadOnly(passwordInput, false);
// 恢复生成短链按钮状态
generateButton.disabled = false;
generateButton.classList.remove('btn-secondary');
generateButton.classList.add('btn-primary');
document.getElementById("nodeList").checked = false;
// 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组
@@ -50,139 +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"}`);
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 解析为参数
@@ -215,29 +213,29 @@ 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);
// 回显短链相关信息
const apiShortLinkInput = document.querySelector("#apiShortLink");
apiShortLinkInput.value = inputURL;
setInputReadOnly(apiShortLinkInput, true);
// 设置短链ID和密码并设置为只读
const customIdInput = document.querySelector("#customId");
const passwordInput = document.querySelector("#password");
const generateButton = document.querySelector('button[onclick="generateShortLink()"]');
customIdInput.value = hash;
setInputReadOnly(customIdInput, true);
passwordInput.value = password;
setInputReadOnly(passwordInput, true);
// 禁用生成短链按钮
generateButton.disabled = true;
generateButton.classList.add('btn-secondary');
@@ -247,94 +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 (config.useUDP) {
document.getElementById("useUDP").checked = config.useUDP;
}
}
@@ -346,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 {
@@ -580,4 +545,53 @@ function updateShortLink() {
});
}
// 主题切换功能
function initTheme() {
const html = document.querySelector('html');
const themeIcon = document.getElementById('theme-icon');
let theme;
// 从localStorage获取用户偏好的主题
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
// 如果用户之前设置过主题,使用保存的主题
theme = savedTheme;
} else {
// 如果没有设置过,检测系统主题偏好
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
theme = prefersDark ? 'dark' : 'light';
}
// 设置主题
html.setAttribute('data-bs-theme', theme);
// 更新图标
if (theme === 'dark') {
themeIcon.textContent = '☀️';
} else {
themeIcon.textContent = '🌙';
}
}
function toggleTheme() {
const html = document.querySelector('html');
const currentTheme = html.getAttribute('data-bs-theme');
// 切换主题
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-bs-theme', newTheme);
// 更新图标
if (newTheme === 'dark') {
themeIcon.textContent = '☀️';
} else {
themeIcon.textContent = '🌙';
}
// 保存用户偏好到localStorage
localStorage.setItem('theme', newTheme);
}
listenInput();
initTheme();

219
test/parser/anytls_test.go Normal file
View File

@@ -0,0 +1,219 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestAnytls_Basic_SimpleLink(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@127.0.0.1:8080#Anytls%20Proxy"
expected := proxy.Proxy{
Type: "anytls",
Name: "Anytls Proxy",
Anytls: proxy.Anytls{
Server: "127.0.0.1",
Port: 8080,
Password: "password123",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_WithSNI(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@proxy.example.com:443?sni=proxy.example.com#Anytls%20SNI"
expected := proxy.Proxy{
Type: "anytls",
Name: "Anytls SNI",
Anytls: proxy.Anytls{
Server: "proxy.example.com",
Port: 443,
Password: "password123",
SNI: "proxy.example.com",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_WithInsecure(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@proxy.example.com:443?insecure=1&sni=proxy.example.com#Anytls%20Insecure"
expected := proxy.Proxy{
Type: "anytls",
Name: "Anytls Insecure",
Anytls: proxy.Anytls{
Server: "proxy.example.com",
Port: 443,
Password: "password123",
SNI: "proxy.example.com",
SkipCertVerify: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_IPv6Address(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@[2001:db8::1]:8080#Anytls%20IPv6"
expected := proxy.Proxy{
Type: "anytls",
Name: "Anytls IPv6",
Anytls: proxy.Anytls{
Server: "2001:db8::1",
Port: 8080,
Password: "password123",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_ComplexPassword(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://ComplexPassword!%40%23%24@proxy.example.com:8443?sni=example.com&insecure=1#Anytls%20Full"
expected := proxy.Proxy{
Type: "anytls",
Name: "Anytls Full",
Anytls: proxy.Anytls{
Server: "proxy.example.com",
Port: 8443,
Password: "ComplexPassword!@#$",
SNI: "example.com",
SkipCertVerify: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_NoPassword(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://@127.0.0.1:8080#No%20Password"
expected := proxy.Proxy{
Type: "anytls",
Name: "No Password",
Anytls: proxy.Anytls{
Server: "127.0.0.1",
Port: 8080,
Password: "",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_UsernameOnly(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://username@127.0.0.1:8080#Username%20Only"
expected := proxy.Proxy{
Type: "anytls",
Name: "Username Only",
Anytls: proxy.Anytls{
Server: "127.0.0.1",
Port: 8080,
Password: "username",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Error_MissingServer(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestAnytls_Error_MissingPort(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestAnytls_Error_InvalidPort(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@127.0.0.1:99999"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestAnytls_Error_InvalidProtocol(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anyssl://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}

View File

@@ -0,0 +1,198 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestHysteria2_Basic_SimpleLink(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@127.0.0.1:8080#Hysteria2%20Proxy"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "Hysteria2 Proxy",
Hysteria2: proxy.Hysteria2{
Server: "127.0.0.1",
Port: 8080,
Password: "password123",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Basic_AltPrefix(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hy2://password123@proxy.example.com:443?insecure=1&sni=proxy.example.com#Hysteria2%20Alt"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "Hysteria2 Alt",
Hysteria2: proxy.Hysteria2{
Server: "proxy.example.com",
Port: 443,
Password: "password123",
SNI: "proxy.example.com",
SkipCertVerify: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Basic_WithObfs(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@127.0.0.1:8080?obfs=salamander&obfs-password=obfs123#Hysteria2%20Obfs"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "Hysteria2 Obfs",
Hysteria2: proxy.Hysteria2{
Server: "127.0.0.1",
Port: 8080,
Password: "password123",
Obfs: "salamander",
ObfsPassword: "obfs123",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Basic_IPv6Address(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@[2001:db8::1]:8080#Hysteria2%20IPv6"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "Hysteria2 IPv6",
Hysteria2: proxy.Hysteria2{
Server: "2001:db8::1",
Port: 8080,
Password: "password123",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Basic_FullConfig(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@proxy.example.com:443?insecure=1&sni=proxy.example.com&obfs=salamander&obfs-password=obfs123#Hysteria2%20Full"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "Hysteria2 Full",
Hysteria2: proxy.Hysteria2{
Server: "proxy.example.com",
Port: 443,
Password: "password123",
SNI: "proxy.example.com",
Obfs: "salamander",
ObfsPassword: "obfs123",
SkipCertVerify: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Basic_NoPassword(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://@127.0.0.1:8080#No%20Password"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "No Password",
Hysteria2: proxy.Hysteria2{
Server: "127.0.0.1",
Port: 8080,
Password: "",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Error_MissingServer(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria2_Error_MissingPort(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria2_Error_InvalidPort(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@127.0.0.1:99999"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria2_Error_InvalidProtocol(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}

View File

@@ -0,0 +1,183 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestHysteria_Basic_SimpleLink(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://127.0.0.1:8080?protocol=udp&auth=password123&upmbps=100&downmbps=100#Hysteria%20Proxy"
expected := proxy.Proxy{
Type: "hysteria",
Name: "Hysteria Proxy",
Hysteria: proxy.Hysteria{
Server: "127.0.0.1",
Port: 8080,
Protocol: "udp",
Auth: "password123",
Up: "100",
Down: "100",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria_Basic_WithAuthString(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://proxy.example.com:443?protocol=wechat-video&auth-str=myauth&upmbps=50&downmbps=200&insecure=true#Hysteria%20Auth"
expected := proxy.Proxy{
Type: "hysteria",
Name: "Hysteria Auth",
Hysteria: proxy.Hysteria{
Server: "proxy.example.com",
Port: 443,
Protocol: "wechat-video",
AuthString: "myauth",
Up: "50",
Down: "200",
SkipCertVerify: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria_Basic_WithObfs(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://127.0.0.1:8080?auth=password123&upmbps=100&downmbps=100&obfs=xplus&alpn=h3#Hysteria%20Obfs"
expected := proxy.Proxy{
Type: "hysteria",
Name: "Hysteria Obfs",
Hysteria: proxy.Hysteria{
Server: "127.0.0.1",
Port: 8080,
Auth: "password123",
Up: "100",
Down: "100",
Obfs: "xplus",
ALPN: []string{"h3"},
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria_Basic_IPv6Address(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://[2001:db8::1]:8080?auth=password123&upmbps=100&downmbps=100#Hysteria%20IPv6"
expected := proxy.Proxy{
Type: "hysteria",
Name: "Hysteria IPv6",
Hysteria: proxy.Hysteria{
Server: "2001:db8::1",
Port: 8080,
Auth: "password123",
Up: "100",
Down: "100",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria_Basic_MultiALPN(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://proxy.example.com:443?auth=password123&upmbps=100&downmbps=100&alpn=h3,h2,http/1.1#Hysteria%20Multi%20ALPN"
expected := proxy.Proxy{
Type: "hysteria",
Name: "Hysteria Multi ALPN",
Hysteria: proxy.Hysteria{
Server: "proxy.example.com",
Port: 443,
Auth: "password123",
Up: "100",
Down: "100",
ALPN: []string{"h3", "h2", "http/1.1"},
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria_Error_MissingServer(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://:8080?auth=password123"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria_Error_MissingPort(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://127.0.0.1?auth=password123"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria_Error_InvalidPort(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://127.0.0.1:99999?auth=password123"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria_Error_InvalidProtocol(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria2://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}

View File

@@ -0,0 +1,184 @@
package test
import (
"errors"
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestShadowsocks_Basic_SimpleLink(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1:8080"
expected := proxy.Proxy{
Type: "ss",
Name: "127.0.0.1:8080",
ShadowSocks: proxy.ShadowSocks{
Server: "127.0.0.1",
Port: 8080,
Cipher: "aes-256-gcm",
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocks_Basic_IPv6Address(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@[2001:db8::1]:8080"
expected := proxy.Proxy{
Type: "ss",
Name: "2001:db8::1:8080",
ShadowSocks: proxy.ShadowSocks{
Server: "2001:db8::1",
Port: 8080,
Cipher: "aes-256-gcm",
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocks_Basic_WithRemark(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@proxy.example.com:8080#My%20SS%20Proxy"
expected := proxy.Proxy{
Type: "ss",
Name: "My SS Proxy",
ShadowSocks: proxy.ShadowSocks{
Server: "proxy.example.com",
Port: 8080,
Cipher: "aes-256-gcm",
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocks_Advanced_Base64FullEncoded(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmRAbG9jYWxob3N0OjgwODA=#Local%20SS"
expected := proxy.Proxy{
Type: "ss",
Name: "Local SS",
ShadowSocks: proxy.ShadowSocks{
Server: "localhost",
Port: 8080,
Cipher: "aes-256-gcm",
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocks_Advanced_PlainUserPassword(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://aes-256-gcm:password@192.168.1.1:8080"
expected := proxy.Proxy{
Type: "ss",
Name: "192.168.1.1:8080",
ShadowSocks: proxy.ShadowSocks{
Server: "192.168.1.1",
Port: 8080,
Cipher: "aes-256-gcm",
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocks_Advanced_ChaCha20Cipher(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://chacha20-poly1305:mypassword@server.com:443#ChaCha20"
expected := proxy.Proxy{
Type: "ss",
Name: "ChaCha20",
ShadowSocks: proxy.ShadowSocks{
Server: "server.com",
Port: 443,
Cipher: "chacha20-poly1305",
Password: "mypassword",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
// 错误处理测试
func TestShadowsocks_Error_MissingServer(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if !errors.Is(err, parser.ErrInvalidStruct) {
t.Errorf("Error is not expected: %v", err)
}
}
func TestShadowsocks_Error_MissingPort(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if !errors.Is(err, parser.ErrInvalidStruct) {
t.Errorf("Error is not expected: %v", err)
}
}
func TestShadowsocks_Error_InvalidProtocol(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "http://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if !errors.Is(err, parser.ErrInvalidPrefix) {
t.Errorf("Error is not expected: %v", err)
}
}

View File

@@ -0,0 +1,112 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestShadowsocksR_Basic_SimpleLink(t *testing.T) {
p := &parser.ShadowsocksRParser{}
input := "ssr://MTI3LjAuMC4xOjQ0MzpvcmlnaW46YWVzLTE5Mi1jZmI6cGxhaW46TVRJek1USXovP2dyb3VwPVpHVm1ZWFZzZEEmcmVtYXJrcz1TRUZJUVE"
expected := proxy.Proxy{
Type: "ssr",
Name: "HAHA",
ShadowSocksR: proxy.ShadowSocksR{
Server: "127.0.0.1",
Port: 443,
Cipher: "aes-192-cfb",
Password: "123123",
ObfsParam: "",
Obfs: "plain",
Protocol: "origin",
ProtocolParam: "",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocksR_Basic_WithParams(t *testing.T) {
p := &parser.ShadowsocksRParser{}
input := "ssr://MTI3LjAuMC4xOjQ0MzpvcmlnaW46YWVzLTE5Mi1jZmI6dGxzMS4wX3Nlc3Npb25fYXV0aDpNVEl6TVRJei8/b2Jmc3BhcmFtPWIySm1jeTF3WVhKaGJXVjBaWEkmcHJvdG9wYXJhbT1jSEp2ZEc5allXd3RjR0Z5WVcxbGRHVnkmZ3JvdXA9WkdWbVlYVnNkQSZyZW1hcmtzPVNFRklRUQ"
expected := proxy.Proxy{
Type: "ssr",
Name: "HAHA",
ShadowSocksR: proxy.ShadowSocksR{
Server: "127.0.0.1",
Port: 443,
Cipher: "aes-192-cfb",
Password: "123123",
ObfsParam: "obfs-parameter",
Obfs: "tls1.0_session_auth",
Protocol: "origin",
ProtocolParam: "protocal-parameter",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocksR_Basic_IPv6Address(t *testing.T) {
p := &parser.ShadowsocksRParser{}
input := "ssr://WzIwMDE6MGRiODo4NWEzOjAwMDA6MDAwMDo4YTJlOjAzNzA6NzMzNF06NDQzOm9yaWdpbjphZXMtMTkyLWNmYjpwbGFpbjpNVEl6TVRJei8/Z3JvdXA9WkdWbVlYVnNkQSZyZW1hcmtzPVNFRklRUQ"
expected := proxy.Proxy{
Type: "ssr",
Name: "HAHA",
ShadowSocksR: proxy.ShadowSocksR{
Server: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
Port: 443,
Cipher: "aes-192-cfb",
Password: "123123",
ObfsParam: "",
Obfs: "plain",
Protocol: "origin",
ProtocolParam: "",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocksR_Error_InvalidBase64(t *testing.T) {
p := &parser.ShadowsocksRParser{}
input := "ssr://invalid_base64"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestShadowsocksR_Error_InvalidProtocol(t *testing.T) {
p := &parser.ShadowsocksRParser{}
input := "ss://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}

168
test/parser/socks_test.go Normal file
View File

@@ -0,0 +1,168 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestSocks_Basic_SimpleLink(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@127.0.0.1:1080#SOCKS%20Proxy"
expected := proxy.Proxy{
Type: "socks5",
Name: "SOCKS Proxy",
Socks: proxy.Socks{
Server: "127.0.0.1",
Port: 1080,
UserName: "user",
Password: "pass",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestSocks_Basic_NoAuth(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://127.0.0.1:1080#SOCKS%20No%20Auth"
expected := proxy.Proxy{
Type: "socks5",
Name: "SOCKS No Auth",
Socks: proxy.Socks{
Server: "127.0.0.1",
Port: 1080,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestSocks_Basic_IPv6Address(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@[2001:db8::1]:1080#SOCKS%20IPv6"
expected := proxy.Proxy{
Type: "socks5",
Name: "SOCKS IPv6",
Socks: proxy.Socks{
Server: "2001:db8::1",
Port: 1080,
UserName: "user",
Password: "pass",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestSocks_Basic_WithTLS(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@127.0.0.1:1080?tls=true&sni=example.com#SOCKS%20TLS"
expected := proxy.Proxy{
Type: "socks5",
Name: "SOCKS TLS",
Socks: proxy.Socks{
Server: "127.0.0.1",
Port: 1080,
UserName: "user",
Password: "pass",
TLS: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestSocks_Basic_WithUDP(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@127.0.0.1:1080?udp=true#SOCKS%20UDP"
expected := proxy.Proxy{
Type: "socks5",
Name: "SOCKS UDP",
Socks: proxy.Socks{
Server: "127.0.0.1",
Port: 1080,
UserName: "user",
Password: "pass",
UDP: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestSocks_Error_MissingServer(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@:1080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestSocks_Error_MissingPort(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestSocks_Error_InvalidPort(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@127.0.0.1:99999"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestSocks_Error_InvalidProtocol(t *testing.T) {
p := &parser.SocksParser{}
input := "ss://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}

182
test/parser/trojan_test.go Normal file
View File

@@ -0,0 +1,182 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestTrojan_Basic_SimpleLink(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:443#Trojan%20Proxy"
expected := proxy.Proxy{
Type: "trojan",
Name: "Trojan Proxy",
Trojan: proxy.Trojan{
Server: "127.0.0.1",
Port: 443,
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestTrojan_Basic_WithTLS(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:443?security=tls&sni=example.com&alpn=h2,http/1.1#Trojan%20TLS"
expected := proxy.Proxy{
Type: "trojan",
Name: "Trojan TLS",
Trojan: proxy.Trojan{
Server: "127.0.0.1",
Port: 443,
Password: "password",
ALPN: []string{"h2", "http/1.1"},
SNI: "example.com",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestTrojan_Basic_WithReality(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:443?security=reality&sni=example.com&pbk=publickey123&sid=shortid123&fp=chrome#Trojan%20Reality"
expected := proxy.Proxy{
Type: "trojan",
Name: "Trojan Reality",
Trojan: proxy.Trojan{
Server: "127.0.0.1",
Port: 443,
Password: "password",
SNI: "example.com",
RealityOpts: proxy.RealityOptions{
PublicKey: "publickey123",
ShortID: "shortid123",
},
Fingerprint: "chrome",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestTrojan_Basic_WithWebSocket(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:443?type=ws&path=/ws&host=example.com#Trojan%20WS"
expected := proxy.Proxy{
Type: "trojan",
Name: "Trojan WS",
Trojan: proxy.Trojan{
Server: "127.0.0.1",
Port: 443,
Password: "password",
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/ws",
Headers: map[string]string{
"Host": "example.com",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestTrojan_Basic_WithGrpc(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:443?type=grpc&serviceName=grpc_service#Trojan%20gRPC"
expected := proxy.Proxy{
Type: "trojan",
Name: "Trojan gRPC",
Trojan: proxy.Trojan{
Server: "127.0.0.1",
Port: 443,
Password: "password",
Network: "grpc",
GrpcOpts: proxy.GrpcOptions{
GrpcServiceName: "grpc_service",
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestTrojan_Error_MissingServer(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@:443"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestTrojan_Error_MissingPort(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestTrojan_Error_InvalidPort(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:99999"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestTrojan_Error_InvalidProtocol(t *testing.T) {
p := &parser.TrojanParser{}
input := "ss://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}

24
test/parser/utils.go Normal file
View File

@@ -0,0 +1,24 @@
package test
import (
"reflect"
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"gopkg.in/yaml.v3"
)
func validateResult(t *testing.T, expected proxy.Proxy, result proxy.Proxy) {
t.Helper()
if result.Type != expected.Type {
t.Errorf("Type mismatch: expected %s, got %s", expected.Type, result.Type)
}
if !reflect.DeepEqual(expected, result) {
expectedYaml, _ := yaml.Marshal(expected)
resultYaml, _ := yaml.Marshal(result)
t.Errorf("Structure mismatch: \nexpected:\n %s\ngot:\n %s", string(expectedYaml), string(resultYaml))
}
}

214
test/parser/vless_test.go Normal file
View File

@@ -0,0 +1,214 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestVless_Basic_SimpleLink(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:8080#VLESS%20Proxy"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS Proxy",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 8080,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Basic_WithTLS(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?security=tls&sni=example.com&alpn=h2,http/1.1#VLESS%20TLS"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS TLS",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 443,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
TLS: true,
ALPN: []string{"h2", "http/1.1"},
ServerName: "example.com",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Basic_WithReality(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?security=reality&sni=example.com&pbk=publickey123&sid=shortid123&fp=chrome#VLESS%20Reality"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS Reality",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 443,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
TLS: true,
ServerName: "example.com",
RealityOpts: proxy.RealityOptions{
PublicKey: "publickey123",
ShortID: "shortid123",
},
Fingerprint: "chrome",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Basic_WithWebSocket(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?type=ws&path=/ws&host=example.com#VLESS%20WS"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS WS",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 443,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/ws",
Headers: map[string]string{
"Host": "example.com",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Basic_WithGrpc(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?type=grpc&serviceName=grpc_service#VLESS%20gRPC"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS gRPC",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 443,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
Network: "grpc",
GrpcOpts: proxy.GrpcOptions{
GrpcServiceName: "grpc_service",
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Basic_WithHTTP(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?type=http&path=/path1,/path2&host=host1.com,host2.com#VLESS%20HTTP"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS HTTP",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 443,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
Network: "http",
HTTPOpts: proxy.HTTPOptions{
Path: []string{"/path1", "/path2"},
Headers: map[string][]string{
"host": {"host1.com", "host2.com"},
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Error_MissingServer(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestVless_Error_MissingPort(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestVless_Error_InvalidPort(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:99999"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestVless_Error_InvalidProtocol(t *testing.T) {
p := &parser.VlessParser{}
input := "ss://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}

232
test/parser/vmess_test.go Normal file
View File

@@ -0,0 +1,232 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestVmess_Basic_SimpleLink(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJ0bHMiOiJ0bHMiLCJ0eXBlIjoibm9uZSIsInYiOiIyIn0="
expected := proxy.Proxy{
Type: "vmess",
Name: "HAHA",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 0,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/",
Headers: map[string]string{
"Host": "127.0.0.1",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Basic_WithPath(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBhdGgiOiIvd3MiLCJwb3J0IjoiNDQzIiwicHMiOiJIQUNLIiwidGxzIjoidGxzIiwidHlwZSI6Im5vbmUiLCJ2IjoiMiJ9"
expected := proxy.Proxy{
Type: "vmess",
Name: "HACK",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 0,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/ws",
Headers: map[string]string{
"Host": "127.0.0.1",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Basic_WithHost(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaG9zdCI6ImV4YW1wbGUuY29tIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJ0bHMiOiJ0bHMiLCJ0eXBlIjoibm9uZSIsInYiOiIyIn0="
expected := proxy.Proxy{
Type: "vmess",
Name: "HAHA",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 0,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/",
Headers: map[string]string{
"Host": "example.com",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Basic_WithSNI(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJzbmkiOiJleGFtcGxlLmNvbSIsInRscyI6InRscyIsInR5cGUiOiJub25lIiwidiI6IjIifQ=="
expected := proxy.Proxy{
Type: "vmess",
Name: "HAHA",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 0,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "ws",
ServerName: "example.com",
WSOpts: proxy.WSOptions{
Path: "/",
Headers: map[string]string{
"Host": "127.0.0.1",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Basic_WithAlterID(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIxIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJ0bHMiOiJ0bHMiLCJ0eXBlIjoibm9uZSIsInYiOiIyIn0="
expected := proxy.Proxy{
Type: "vmess",
Name: "HAHA",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 1,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/",
Headers: map[string]string{
"Host": "127.0.0.1",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Basic_GRPC(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJncnBjIiwicG9ydCI6IjQ0MyIsInBzIjoiSEFIQSIsInRscyI6InRscyIsInR5cGUiOiJub25lIiwidiI6IjIifQ=="
expected := proxy.Proxy{
Type: "vmess",
Name: "HAHA",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 0,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "grpc",
GrpcOpts: proxy.GrpcOptions{},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Error_InvalidBase64(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://invalid_base64"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestVmess_Error_InvalidJSON(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJpbnZhbGlkIjoianNvbn0="
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestVmess_Error_InvalidProtocol(t *testing.T) {
p := &parser.VmessParser{}
input := "ss://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}

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