This commit is contained in:
2025-06-12 02:17:31 +10:00
parent b5fcbab1a5
commit da9a17201b
61 changed files with 1362 additions and 1106 deletions

View File

@ -1,7 +0,0 @@
PORT=8011
META_TEMPLATE=meta_template.json
CLASH_TEMPLATE=clash_template.json
REQUEST_RETRY_TIMES=3
REQUEST_MAX_FILE_SIZE=1048576
CACHE_EXPIRE=300
LOG_LEVEL=info

View File

@ -18,7 +18,7 @@ jobs:
with:
images: |
nite07/sub2clash
ghcr.io/nitezs/sub2clash
ghcr.io/bestnite/sub2clash
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}

View File

@ -25,22 +25,83 @@
### 部署
- [docker compose](./docker-compose.yml)
- 运行[二进制文件](https://github.com/nitezs/sub2clash/releases/latest)
- 运行[二进制文件](https://github.com/bestnite/sub2clash/releases/latest)
### 配置
可以通过编辑 .env 文件来修改默认配置docker 直接添加环境变量
支持多种配置方式,按优先级排序:
| 变量名 | 说明 | 默认值 |
| --------------------- | ---------------------------------------------- | --------------------- |
| PORT | 端口 | `8011` |
| META_TEMPLATE | 默认 meta 模板文件名 | `template_meta.yaml` |
| CLASH_TEMPLATE | 默认 clash 模板文件名 | `template_clash.yaml` |
| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` |
| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小byte | `1048576` |
| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
| LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` |
| SHORT_LINK_LENGTH | 短链长度 | `6` |
1. **配置文件**支持多种格式YAML、JSON、TOML、INI按以下优先级搜索
- `config.yaml` / `config.yml`
- `config.json`
- `config.toml`
- `config.ini`
- `sub2clash.yaml` / `sub2clash.yml`
- `sub2clash.json`
- `sub2clash.toml`
- `sub2clash.ini`
2. **环境变量**:使用 `SUB2CLASH_` 前缀,例如 `SUB2CLASH_PORT=8080`
3. **默认值**:内置默认配置
| 配置项 | 环境变量 | 说明 | 默认值 |
| --------------------- | ------------------------------- | --------------------------------------- | -------------- |
| port | SUB2CLASH_PORT | 服务端口 | `8011` |
| meta_template | SUB2CLASH_META_TEMPLATE | 默认 meta 模板 URL | [默认模板链接] |
| clash_template | SUB2CLASH_CLASH_TEMPLATE | 默认 clash 模板 URL | [默认模板链接] |
| request_retry_times | SUB2CLASH_REQUEST_RETRY_TIMES | 请求重试次数 | `3` |
| request_max_file_size | SUB2CLASH_REQUEST_MAX_FILE_SIZE | 请求文件最大大小byte | `1048576` |
| cache_expire | SUB2CLASH_CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
| log_level | SUB2CLASH_LOG_LEVEL | 日志等级:`debug`,`info`,`warn`,`error` | `info` |
| short_link_length | SUB2CLASH_SHORT_LINK_LENGTH | 短链长度 | `6` |
#### 配置文件示例
参考示例文件:
- [config.example.yaml](./config.example.yaml) - YAML 格式
- [config.example.json](./config.example.json) - JSON 格式
- [config.example.toml](./config.example.toml) - TOML 格式
**YAML 格式** (`config.yaml`):
```yaml
port: 8011
meta_template: "https://example.com/meta.yaml"
clash_template: "https://example.com/clash.yaml"
request_retry_times: 5
request_max_file_size: 2097152 # 2MB
cache_expire: 600 # 10 minutes
log_level: "debug"
short_link_length: 8
```
**JSON 格式** (`config.json`):
```json
{
"port": 8011,
"meta_template": "https://example.com/meta.yaml",
"clash_template": "https://example.com/clash.yaml",
"request_retry_times": 5,
"request_max_file_size": 2097152,
"cache_expire": 600,
"log_level": "debug",
"short_link_length": 8
}
```
**TOML 格式** (`config.toml`):
```toml
port = 8011
meta_template = "https://example.com/meta.yaml"
clash_template = "https://example.com/clash.yaml"
request_retry_times = 5
request_max_file_size = 2097152
cache_expire = 600
log_level = "debug"
short_link_length = 8
```
### API

View File

@ -1,44 +0,0 @@
package handler
import (
"net/http"
"github.com/nitezs/sub2clash/config"
"github.com/nitezs/sub2clash/model"
"github.com/nitezs/sub2clash/validator"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v3"
)
func SubmodHandler(c *gin.Context) {
query, err := validator.ParseQuery(c)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
sub, err := BuildSub(model.Clash, query, config.Default.ClashTemplate)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
if query.NodeListMode {
nodelist := model.NodeList{}
nodelist.Proxies = sub.Proxies
marshal, err := yaml.Marshal(nodelist)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
return
}
marshal, err := yaml.Marshal(sub)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
}

View File

@ -1,308 +0,0 @@
package handler
import (
"crypto/sha256"
"encoding/hex"
"errors"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"github.com/nitezs/sub2clash/common"
"github.com/nitezs/sub2clash/logger"
"github.com/nitezs/sub2clash/model"
"github.com/nitezs/sub2clash/parser"
"github.com/nitezs/sub2clash/validator"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
)
func BuildSub(clashType model.ClashType, query validator.SubValidator, template string) (
*model.Subscription, error,
) {
var temp = &model.Subscription{}
var sub = &model.Subscription{}
var err error
var templateBytes []byte
if query.Template != "" {
template = query.Template
}
if strings.HasPrefix(template, "http") {
templateBytes, err = common.LoadSubscription(template, query.Refresh, query.UserAgent)
if err != nil {
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, errors.New("加载模板失败: " + err.Error())
}
} else {
unescape, err := url.QueryUnescape(template)
if err != nil {
return nil, errors.New("加载模板失败: " + err.Error())
}
templateBytes, err = common.LoadTemplate(unescape)
if err != nil {
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, errors.New("加载模板失败: " + err.Error())
}
}
err = yaml.Unmarshal(templateBytes, &temp)
if err != nil {
logger.Logger.Debug("parse template failed", zap.Error(err))
return nil, errors.New("解析模板失败: " + err.Error())
}
var proxyList []model.Proxy
for i := range query.Subs {
data, err := common.LoadSubscription(query.Subs[i], query.Refresh, query.UserAgent)
subName := ""
if strings.Contains(query.Subs[i], "#") {
subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:]
}
if err != nil {
logger.Logger.Debug(
"load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
)
return nil, errors.New("加载订阅失败: " + err.Error())
}
err = yaml.Unmarshal(data, &sub)
var newProxies []model.Proxy
if err != nil {
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria|hy2|hysteria2)://")
if reg.Match(data) {
p := common.ParseProxy(strings.Split(string(data), "\n")...)
newProxies = p
} else {
base64, err := parser.DecodeBase64(string(data))
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())
}
p := common.ParseProxy(strings.Split(base64, "\n")...)
newProxies = p
}
} else {
newProxies = sub.Proxies
}
if subName != "" {
for i := range newProxies {
newProxies[i].SubName = subName
}
}
proxyList = append(proxyList, newProxies...)
}
if len(query.Proxies) != 0 {
proxyList = append(proxyList, common.ParseProxy(query.Proxies...)...)
}
for i := range proxyList {
if proxyList[i].SubName != "" {
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
}
}
proxies := make(map[string]*model.Proxy)
newProxies := make([]model.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"]
}
if _, exist := proxies[key]; !exist {
proxies[key] = &proxyList[i]
newProxies = append(newProxies, proxyList[i])
}
}
proxyList = newProxies
if strings.TrimSpace(query.Remove) != "" {
newProxyList := make([]model.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())
}
if removeReg.MatchString(proxyList[i].Name) {
continue
}
newProxyList = append(newProxyList, proxyList[i])
}
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 err != nil {
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
return nil, errors.New("replace 参数非法: " + err.Error())
}
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],
)
}
}
}
}
names := make(map[string]int)
for i := range proxyList {
if _, exist := names[proxyList[i].Name]; exist {
names[proxyList[i].Name] = names[proxyList[i].Name] + 1
proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
} else {
names[proxyList[i].Name] = 0
}
}
for i := range proxyList {
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
}
var t = &model.Subscription{}
common.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
switch query.Sort {
case "sizeasc":
sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups))
case "sizedesc":
sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroups)))
case "nameasc":
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
case "namedesc":
sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroups)))
default:
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
}
MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup)
for _, v := range query.Rules {
if v.Prepend {
common.PrependRules(temp, v.Rule)
} else {
common.AppendRules(temp, v.Rule)
}
}
for _, v := range query.RuleProviders {
hash := sha256.Sum224([]byte(v.Url))
name := hex.EncodeToString(hash[:])
provider := model.RuleProvider{
Type: "http",
Behavior: v.Behavior,
Url: v.Url,
Path: "./" + name + ".yaml",
Interval: 3600,
}
if v.Prepend {
common.PrependRuleProvider(
temp, v.Name, v.Group, provider,
)
} else {
common.AppenddRuleProvider(
temp, v.Name, v.Group, provider,
)
}
}
return temp, nil
}
func fetchSubscriptionUserInfo(url string, userAgent string) (string, error) {
resp, err := common.Head(url, common.WithUserAgent(userAgent))
if err != nil {
logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err))
return "", 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
}
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {
var countryGroupNames []string
for _, proxyGroup := range sub.ProxyGroups {
if proxyGroup.IsCountryGrop {
countryGroupNames = append(
countryGroupNames, proxyGroup.Name,
)
}
}
var proxyNames []string
for _, proxy := range sub.Proxies {
proxyNames = append(proxyNames, proxy.Name)
}
temp.Proxies = append(temp.Proxies, sub.Proxies...)
for i := range temp.ProxyGroups {
if temp.ProxyGroups[i].IsCountryGrop {
continue
}
newProxies := make([]string, 0)
countryGroupMap := make(map[string]model.ProxyGroup)
for _, v := range sub.ProxyGroups {
if v.IsCountryGrop {
countryGroupMap[v.Name] = v
}
}
for j := range temp.ProxyGroups[i].Proxies {
reg := regexp.MustCompile("<(.*?)>")
if reg.Match([]byte(temp.ProxyGroups[i].Proxies[j])) {
key := reg.FindStringSubmatch(temp.ProxyGroups[i].Proxies[j])[1]
switch key {
case "all":
newProxies = append(newProxies, proxyNames...)
case "countries":
if !igcg {
newProxies = append(newProxies, countryGroupNames...)
}
default:
if !igcg {
if len(key) == 2 {
newProxies = append(
newProxies, countryGroupMap[common.GetContryName(key)].Proxies...,
)
}
}
}
} else {
newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j])
}
}
temp.ProxyGroups[i].Proxies = newProxies
}
if !igcg {
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
}
}

View File

@ -1,53 +0,0 @@
package handler
import (
_ "embed"
"net/http"
"github.com/nitezs/sub2clash/config"
"github.com/nitezs/sub2clash/model"
"github.com/nitezs/sub2clash/validator"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v3"
)
func SubHandler(c *gin.Context) {
query, err := validator.ParseQuery(c)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
sub, err := BuildSub(model.ClashMeta, query, config.Default.MetaTemplate)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
if len(query.Subs) == 1 {
userInfoHeader, err := fetchSubscriptionUserInfo(query.Subs[0], "clash")
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
}
c.Header("subscription-userinfo", userInfoHeader)
}
if query.NodeListMode {
nodelist := model.NodeList{}
nodelist.Proxies = sub.Proxies
marshal, err := yaml.Marshal(nodelist)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
return
}
marshal, err := yaml.Marshal(sub)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
}

View File

@ -5,7 +5,7 @@ import (
"errors"
"path/filepath"
"github.com/nitezs/sub2clash/model"
"github.com/bestnite/sub2clash/model"
"go.etcd.io/bbolt"
)

View File

@ -1,102 +0,0 @@
package common
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/nitezs/sub2clash/config"
)
type GetConfig struct {
userAgent string
}
type GetOption func(*GetConfig)
func WithUserAgent(userAgent string) GetOption {
return func(config *GetConfig) {
config.userAgent = userAgent
}
}
func Get(url string, options ...GetOption) (resp *http.Response, err error) {
retryTimes := config.Default.RequestRetryTimes
haveTried := 0
retryDelay := time.Second
getConfig := GetConfig{}
for _, option := range options {
option(&getConfig)
}
var req *http.Request
var get *http.Response
for haveTried < retryTimes {
client := &http.Client{}
//client.Timeout = time.Second * 10
req, err = http.NewRequest("GET", url, nil)
if err != nil {
haveTried++
time.Sleep(retryDelay)
continue
}
if getConfig.userAgent != "" {
req.Header.Set("User-Agent", getConfig.userAgent)
}
get, err = client.Do(req)
if err != nil {
haveTried++
time.Sleep(retryDelay)
continue
} else {
if get != nil && get.ContentLength > config.Default.RequestMaxFileSize {
return nil, errors.New("文件过大")
}
return get, nil
}
}
return nil, fmt.Errorf("请求失败:%v", err)
}
func Head(url string, options ...GetOption) (resp *http.Response, err error) {
retryTimes := config.Default.RequestRetryTimes
haveTried := 0
retryDelay := time.Second
// 解析可选参数(如 User-Agent
getConfig := GetConfig{}
for _, option := range options {
option(&getConfig)
}
var req *http.Request
var headResp *http.Response
for haveTried < retryTimes {
client := &http.Client{}
req, err = http.NewRequest("HEAD", url, nil)
if err != nil {
haveTried++
time.Sleep(retryDelay)
continue
}
// 设置 User-Agent如果提供
if getConfig.userAgent != "" {
req.Header.Set("User-Agent", getConfig.userAgent)
}
headResp, err = client.Do(req)
if err != nil {
haveTried++
time.Sleep(retryDelay)
continue
}
// HEAD 请求不检查 ContentLength因为没有响应体
return headResp, nil
}
return nil, fmt.Errorf("HEAD 请求失败:%v", err)
}

View File

@ -20,9 +20,6 @@ func MkEssentialDir() error {
if err := MKDir("subs"); err != nil {
return errors.New("create subs dir failed" + err.Error())
}
if err := MKDir("templates"); err != nil {
return errors.New("create templates dir failed" + err.Error())
}
if err := MKDir("logs"); err != nil {
return errors.New("create logs dir failed" + err.Error())
}

View File

@ -3,12 +3,8 @@ package common
import (
"strings"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/logger"
"github.com/nitezs/sub2clash/model"
"github.com/nitezs/sub2clash/parser"
"go.uber.org/zap"
"github.com/bestnite/sub2clash/model"
"github.com/bestnite/sub2clash/model/proxy"
)
func GetContryName(countryKey string) string {
@ -52,7 +48,7 @@ func GetContryName(countryKey string) string {
func AddProxy(
sub *model.Subscription, autotest bool,
lazy bool, clashType model.ClashType, proxies ...model.Proxy,
lazy bool, clashType model.ClashType, proxies ...proxy.Proxy,
) {
proxyTypes := model.GetSupportProxyTypes(clashType)
@ -98,49 +94,3 @@ func AddProxy(
}
}
}
func ParseProxy(proxies ...string) []model.Proxy {
var result []model.Proxy
for _, proxy := range proxies {
if proxy != "" {
var proxyItem model.Proxy
var err error
if strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
proxyItem, err = parser.ParseShadowsocks(proxy)
}
if strings.HasPrefix(proxy, constant.TrojanPrefix) {
proxyItem, err = parser.ParseTrojan(proxy)
}
if strings.HasPrefix(proxy, constant.VMessPrefix) {
proxyItem, err = parser.ParseVmess(proxy)
}
if strings.HasPrefix(proxy, constant.VLESSPrefix) {
proxyItem, err = parser.ParseVless(proxy)
}
if strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) {
proxyItem, err = parser.ParseShadowsocksR(proxy)
}
if strings.HasPrefix(proxy, constant.Hysteria2Prefix1) || strings.HasPrefix(proxy, constant.Hysteria2Prefix2) {
proxyItem, err = parser.ParseHysteria2(proxy)
}
if strings.HasPrefix(proxy, constant.HysteriaPrefix) {
proxyItem, err = parser.ParseHysteria(proxy)
}
if strings.HasPrefix(proxy, constant.SocksPrefix) {
proxyItem, err = parser.ParseSocks(proxy)
}
if strings.HasPrefix(proxy, constant.AnytlsPrefix) {
proxyItem, err = parser.ParseAnytls(proxy)
}
if err == nil {
result = append(result, proxyItem)
} else {
logger.Logger.Debug(
"parse proxy failed", zap.String("proxy", proxy), zap.Error(err),
)
}
}
}
return result
}

12
common/request.go Normal file
View File

@ -0,0 +1,12 @@
package common
import (
"resty.dev/v3"
)
func Request(retryTimes int) *resty.Client {
client := resty.New()
client.
SetRetryCount(retryTimes)
return client
}

View File

@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/nitezs/sub2clash/model"
"github.com/bestnite/sub2clash/model"
)
func PrependRuleProvider(

View File

@ -3,23 +3,33 @@ package common
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/nitezs/sub2clash/config"
"github.com/bestnite/sub2clash/logger"
"github.com/bestnite/sub2clash/model"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
)
var subsDir = "subs"
var fileLock sync.RWMutex
func LoadSubscription(url string, refresh bool, userAgent string) ([]byte, error) {
func LoadSubscription(url string, refresh bool, userAgent string, cacheExpire int64, retryTimes int) ([]byte, error) {
if refresh {
return FetchSubscriptionFromAPI(url, userAgent)
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
}
hash := sha256.Sum224([]byte(url))
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
@ -28,10 +38,10 @@ func LoadSubscription(url string, refresh bool, userAgent string) ([]byte, error
if !os.IsNotExist(err) {
return nil, err
}
return FetchSubscriptionFromAPI(url, userAgent)
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
}
lastGetTime := stat.ModTime().Unix()
if lastGetTime+config.Default.CacheExpire > time.Now().Unix() {
if lastGetTime+cacheExpire > time.Now().Unix() {
file, err := os.Open(fileName)
if err != nil {
return nil, err
@ -49,21 +59,18 @@ func LoadSubscription(url string, refresh bool, userAgent string) ([]byte, error
}
return subContent, nil
}
return FetchSubscriptionFromAPI(url, userAgent)
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
}
func FetchSubscriptionFromAPI(url string, userAgent string) ([]byte, error) {
func FetchSubscriptionFromAPI(url string, userAgent string, retryTimes int) ([]byte, error) {
hash := sha256.Sum224([]byte(url))
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
resp, err := Get(url, WithUserAgent(userAgent))
client := Request(retryTimes)
defer client.Close()
resp, err := client.R().SetHeader("User-Agent", userAgent).Get(url)
if err != nil {
return nil, err
}
defer func(resp *http.Response) {
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
}(resp)
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
@ -85,3 +92,289 @@ func FetchSubscriptionFromAPI(url string, userAgent string) ([]byte, error) {
}
return data, nil
}
func BuildSub(clashType model.ClashType, query model.SubConfig, template string, cacheExpire int64, retryTimes int) (
*model.Subscription, error,
) {
var temp = &model.Subscription{}
var sub = &model.Subscription{}
var err error
var templateBytes []byte
if query.Template != "" {
template = query.Template
}
if strings.HasPrefix(template, "http") {
templateBytes, err = LoadSubscription(template, query.Refresh, query.UserAgent, cacheExpire, retryTimes)
if err != nil {
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, errors.New("加载模板失败: " + err.Error())
}
} else {
unescape, err := url.QueryUnescape(template)
if err != nil {
return nil, errors.New("加载模板失败: " + err.Error())
}
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())
}
}
err = yaml.Unmarshal(templateBytes, &temp)
if err != nil {
logger.Logger.Debug("parse template failed", zap.Error(err))
return nil, errors.New("解析模板失败: " + err.Error())
}
var proxyList []P.Proxy
for i := range query.Subs {
data, err := LoadSubscription(query.Subs[i], query.Refresh, query.UserAgent, cacheExpire, retryTimes)
subName := ""
if strings.Contains(query.Subs[i], "#") {
subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:]
}
if err != nil {
logger.Logger.Debug(
"load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
)
return nil, errors.New("加载订阅失败: " + err.Error())
}
err = yaml.Unmarshal(data, &sub)
var newProxies []P.Proxy
if err != nil {
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria|hy2|hysteria2)://")
if reg.Match(data) {
p := parser.ParseProxies(strings.Split(string(data), "\n")...)
newProxies = p
} else {
base64, err := parser.DecodeBase64(string(data))
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())
}
p := parser.ParseProxies(strings.Split(base64, "\n")...)
newProxies = p
}
} else {
newProxies = sub.Proxies
}
if subName != "" {
for i := range newProxies {
newProxies[i].SubName = subName
}
}
proxyList = append(proxyList, newProxies...)
}
if len(query.Proxies) != 0 {
proxyList = append(proxyList, parser.ParseProxies(query.Proxies...)...)
}
for i := range proxyList {
if proxyList[i].SubName != "" {
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
}
}
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"]
}
if _, exist := proxies[key]; !exist {
proxies[key] = &proxyList[i]
newProxies = append(newProxies, proxyList[i])
}
}
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())
}
if removeReg.MatchString(proxyList[i].Name) {
continue
}
newProxyList = append(newProxyList, proxyList[i])
}
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 err != nil {
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
return nil, errors.New("replace 参数非法: " + err.Error())
}
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],
)
}
}
}
}
names := make(map[string]int)
for i := range proxyList {
if _, exist := names[proxyList[i].Name]; exist {
names[proxyList[i].Name] = names[proxyList[i].Name] + 1
proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
} else {
names[proxyList[i].Name] = 0
}
}
for i := range proxyList {
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
}
var t = &model.Subscription{}
AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
switch query.Sort {
case "sizeasc":
sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups))
case "sizedesc":
sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroups)))
case "nameasc":
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
case "namedesc":
sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroups)))
default:
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
}
MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup)
for _, v := range query.Rules {
if v.Prepend {
PrependRules(temp, v.Rule)
} else {
AppendRules(temp, v.Rule)
}
}
for _, v := range query.RuleProviders {
hash := sha256.Sum224([]byte(v.Url))
name := hex.EncodeToString(hash[:])
provider := model.RuleProvider{
Type: "http",
Behavior: v.Behavior,
Url: v.Url,
Path: "./" + name + ".yaml",
Interval: 3600,
}
if v.Prepend {
PrependRuleProvider(
temp, v.Name, v.Group, provider,
)
} else {
AppenddRuleProvider(
temp, v.Name, v.Group, provider,
)
}
}
return temp, nil
}
func FetchSubscriptionUserInfo(url string, userAgent string, retryTimes int) (string, error) {
client := Request(retryTimes)
defer client.Close()
resp, err := client.R().SetHeader("User-Agent", userAgent).Head(url)
if err != nil {
logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err))
return "", 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
}
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {
var countryGroupNames []string
for _, proxyGroup := range sub.ProxyGroups {
if proxyGroup.IsCountryGrop {
countryGroupNames = append(
countryGroupNames, proxyGroup.Name,
)
}
}
var proxyNames []string
for _, proxy := range sub.Proxies {
proxyNames = append(proxyNames, proxy.Name)
}
temp.Proxies = append(temp.Proxies, sub.Proxies...)
for i := range temp.ProxyGroups {
if temp.ProxyGroups[i].IsCountryGrop {
continue
}
newProxies := make([]string, 0)
countryGroupMap := make(map[string]model.ProxyGroup)
for _, v := range sub.ProxyGroups {
if v.IsCountryGrop {
countryGroupMap[v.Name] = v
}
}
for j := range temp.ProxyGroups[i].Proxies {
reg := regexp.MustCompile("<(.*?)>")
if reg.Match([]byte(temp.ProxyGroups[i].Proxies[j])) {
key := reg.FindStringSubmatch(temp.ProxyGroups[i].Proxies[j])[1]
switch key {
case "all":
newProxies = append(newProxies, proxyNames...)
case "countries":
if !igcg {
newProxies = append(newProxies, countryGroupNames...)
}
default:
if !igcg {
if len(key) == 2 {
newProxies = append(
newProxies, countryGroupMap[GetContryName(key)].Proxies...,
)
}
}
}
} else {
newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j])
}
}
temp.ProxyGroups[i].Proxies = newProxies
}
if !igcg {
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
}
}

View File

@ -4,13 +4,11 @@ import (
"errors"
"io"
"os"
"path/filepath"
)
func LoadTemplate(template string) ([]byte, error) {
tPath := filepath.Join("templates", template)
if _, err := os.Stat(tPath); err == nil {
file, err := os.Open(tPath)
func LoadTemplate(templatePath string) ([]byte, error) {
if _, err := os.Stat(templatePath); err == nil {
file, err := os.Open(templatePath)
if err != nil {
return nil, err
}

View File

@ -1,40 +0,0 @@
package common
import (
"os"
"path/filepath"
"github.com/nitezs/sub2clash/config"
)
func writeTemplate(path string, template string) error {
tPath := filepath.Join(
"templates", path,
)
if _, err := os.Stat(tPath); os.IsNotExist(err) {
file, err := os.Create(tPath)
if err != nil {
return err
}
defer func(file *os.File) {
if file != nil {
_ = file.Close()
}
}(file)
_, err = file.WriteString(template)
if err != nil {
return err
}
}
return nil
}
func WriteDefalutTemplate(templateMeta string, templateClash string) error {
if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil {
return err
}
if err := writeTemplate(config.Default.ClashTemplate, templateClash); err != nil {
return err
}
return nil
}

12
compose.yml Normal file
View File

@ -0,0 +1,12 @@
name: sub2clash
services:
sub2clash:
container_name: sub2clash
restart: unless-stopped
image: nite07/sub2clash:latest
ports:
- "8011:8011"
volumes:
- ./logs:/app/logs
- ./templates:/app/templates
- ./data:/app/data

10
config.example.json Normal file
View File

@ -0,0 +1,10 @@
{
"port": 8011,
"meta_template": "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml",
"clash_template": "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml",
"request_retry_times": 3,
"request_max_file_size": 1048576,
"cache_expire": 300,
"log_level": "info",
"short_link_length": 6
}

22
config.example.toml Normal file
View File

@ -0,0 +1,22 @@
# Sub2Clash 配置文件示例 (TOML 格式)
# 复制此文件为 config.toml 并根据需要修改配置
# 服务端口
port = 8011
# 模板配置
meta_template = "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml"
clash_template = "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml"
# 请求配置
request_retry_times = 3
request_max_file_size = 1048576 # 1MB in bytes
# 缓存配置 (秒)
cache_expire = 300 # 5 minutes
# 日志级别 (debug, info, warn, error)
log_level = "info"
# 短链接长度
short_link_length = 6

22
config.example.yaml Normal file
View File

@ -0,0 +1,22 @@
# Sub2Clash 配置文件示例
# 复制此文件为 config.yaml 并根据需要修改配置
# 服务端口
port: 8011
# 模板配置
meta_template: "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml"
clash_template: "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml"
# 请求配置
request_retry_times: 3
request_max_file_size: 1048576 # 1MB in bytes
# 缓存配置 (秒)
cache_expire: 300 # 5 minutes
# 日志级别 (debug, info, warn, error)
log_level: "info"
# 短链接长度
short_link_length: 6

View File

@ -1,90 +1,76 @@
package config
import (
"errors"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
"github.com/spf13/viper"
)
type Config struct {
Port int
MetaTemplate string
ClashTemplate string
RequestRetryTimes int
RequestMaxFileSize int64
CacheExpire int64
LogLevel string
//BasePath string
ShortLinkLength int
Address string `mapstructure:"address"`
MetaTemplate string `mapstructure:"meta_template"`
ClashTemplate string `mapstructure:"clash_template"`
RequestRetryTimes int `mapstructure:"request_retry_times"`
RequestMaxFileSize int64 `mapstructure:"request_max_file_size"`
CacheExpire int64 `mapstructure:"cache_expire"`
LogLevel string `mapstructure:"log_level"`
ShortLinkLength int `mapstructure:"short_link_length"`
}
var Default *Config
var GlobalConfig *Config
var Dev string
func LoadConfig() error {
Default = &Config{
MetaTemplate: "template_meta.yaml",
ClashTemplate: "template_clash.yaml",
RequestRetryTimes: 3,
RequestMaxFileSize: 1024 * 1024 * 1,
Port: 8011,
CacheExpire: 60 * 5,
LogLevel: "info",
//BasePath: "/",
ShortLinkLength: 6,
}
_ = godotenv.Load()
if os.Getenv("PORT") != "" {
atoi, err := strconv.Atoi(os.Getenv("PORT"))
if err != nil {
return errors.New("PORT invalid")
v := viper.New()
// 添加配置文件搜索路径
v.AddConfigPath(".")
v.AddConfigPath("./config")
v.AddConfigPath("/etc/sub2clash/")
// 设置默认值
setDefaults(v)
// 设置环境变量前缀和自动绑定
v.SetEnvPrefix("SUB2CLASH")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// 尝试按优先级加载不同格式的配置文件
configLoaded := false
configNames := []string{"config", "sub2clash"}
configExts := []string{"yaml", "yml", "json", "toml", "ini"}
for _, name := range configNames {
for _, ext := range configExts {
v.SetConfigName(name)
v.SetConfigType(ext)
if err := v.ReadInConfig(); err == nil {
configLoaded = true
break
}
}
Default.Port = atoi
}
if os.Getenv("META_TEMPLATE") != "" {
Default.MetaTemplate = os.Getenv("META_TEMPLATE")
}
if os.Getenv("CLASH_TEMPLATE") != "" {
Default.ClashTemplate = os.Getenv("CLASH_TEMPLATE")
}
if os.Getenv("REQUEST_RETRY_TIMES") != "" {
atoi, err := strconv.Atoi(os.Getenv("REQUEST_RETRY_TIMES"))
if err != nil {
return errors.New("REQUEST_RETRY_TIMES invalid")
if configLoaded {
break
}
Default.RequestRetryTimes = atoi
}
if os.Getenv("REQUEST_MAX_FILE_SIZE") != "" {
atoi, err := strconv.Atoi(os.Getenv("REQUEST_MAX_FILE_SIZE"))
if err != nil {
return errors.New("REQUEST_MAX_FILE_SIZE invalid")
}
Default.RequestMaxFileSize = int64(atoi)
}
if os.Getenv("CACHE_EXPIRE") != "" {
atoi, err := strconv.Atoi(os.Getenv("CACHE_EXPIRE"))
if err != nil {
return errors.New("CACHE_EXPIRE invalid")
}
Default.CacheExpire = int64(atoi)
}
if os.Getenv("LOG_LEVEL") != "" {
Default.LogLevel = os.Getenv("LOG_LEVEL")
}
//if os.Getenv("BASE_PATH") != "" {
// Default.BasePath = os.Getenv("BASE_PATH")
// if Default.BasePath[len(Default.BasePath)-1] != '/' {
// Default.BasePath += "/"
// }
//}
if os.Getenv("SHORT_LINK_LENGTH") != "" {
atoi, err := strconv.Atoi(os.Getenv("SHORT_LINK_LENGTH"))
if err != nil {
return errors.New("SHORT_LINK_LENGTH invalid")
}
Default.ShortLinkLength = atoi
// 将配置解析到结构体
GlobalConfig = &Config{}
if err := v.Unmarshal(GlobalConfig); err != nil {
return err
}
return nil
}
func setDefaults(v *viper.Viper) {
v.SetDefault("address", "0.0.0.0:8011")
v.SetDefault("meta_template", "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml")
v.SetDefault("clash_template", "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml")
v.SetDefault("request_retry_times", 3)
v.SetDefault("request_max_file_size", 1024*1024*1)
v.SetDefault("cache_expire", 60*5)
v.SetDefault("log_level", "info")
v.SetDefault("short_link_length", 6)
}

View File

@ -1,14 +0,0 @@
package constant
const (
HysteriaPrefix string = "hysteria://"
Hysteria2Prefix1 string = "hysteria2://"
Hysteria2Prefix2 string = "hy2://"
ShadowsocksPrefix string = "ss://"
ShadowsocksRPrefix string = "ssr://"
TrojanPrefix string = "trojan://"
VLESSPrefix string = "vless://"
VMessPrefix string = "vmess://"
SocksPrefix string = "socks"
AnytlsPrefix string = "anytls://"
)

View File

@ -1,21 +0,0 @@
version: "3"
services:
sub2clash:
container_name: sub2clash
restart: unless-stopped
image: nite07/sub2clash:latest
ports:
- "8011:8011"
volumes:
- ./logs:/app/logs
- ./templates:/app/templates
- ./data:/app/data
# environment:
# - PORT=8011
# - META_TEMPLATE=template_meta.yaml
# - PROXY_TEMPLATE=template_clash.yaml
# - REQUEST_RETRY_TIMES=3
# - REQUEST_MAX_FILE_SIZE=1048576
# - CACHE_EXPIRE=300
# - LOG_LEVEL=info

View File

@ -1,4 +1,4 @@
package parser
package error
type ParseError struct {
Type ParseErrorType

40
go.mod
View File

@ -1,42 +1,52 @@
module github.com/nitezs/sub2clash
module github.com/bestnite/sub2clash
go 1.21
go 1.21.0
toolchain go1.24.3
require (
github.com/gin-gonic/gin v1.9.1
github.com/joho/godotenv v1.5.1
github.com/gin-gonic/gin v1.10.1
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.14.0
golang.org/x/text v0.21.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/bytedance/sonic v1.11.5 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.3 // 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/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.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.19.0 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/sagikazarmark/locafero v0.7.0 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
google.golang.org/protobuf v1.33.0 // 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
google.golang.org/protobuf v1.36.1 // indirect
)

88
go.sum
View File

@ -1,42 +1,49 @@
github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k=
github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw=
github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY=
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/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg=
github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8=
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/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/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/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/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.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
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-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=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
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-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
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/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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
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/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/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=
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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -46,22 +53,36 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
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/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
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=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
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.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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
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/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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@ -75,28 +96,31 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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=
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/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=
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=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

31
main.go
View File

@ -3,24 +3,17 @@ package main
import (
_ "embed"
"io"
"strconv"
"github.com/nitezs/sub2clash/api"
"github.com/nitezs/sub2clash/common"
"github.com/nitezs/sub2clash/common/database"
"github.com/nitezs/sub2clash/config"
"github.com/nitezs/sub2clash/logger"
"github.com/bestnite/sub2clash/common"
"github.com/bestnite/sub2clash/common/database"
"github.com/bestnite/sub2clash/config"
"github.com/bestnite/sub2clash/logger"
"github.com/bestnite/sub2clash/server"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
//go:embed templates/template_meta.yaml
var templateMeta string
//go:embed templates/template_clash.yaml
var templateClash string
func init() {
var err error
@ -31,16 +24,11 @@ func init() {
err = config.LoadConfig()
logger.InitLogger(config.Default.LogLevel)
logger.InitLogger(config.GlobalConfig.LogLevel)
if err != nil {
logger.Logger.Panic("load config failed", zap.Error(err))
}
err = common.WriteDefalutTemplate(templateMeta, templateClash)
if err != nil {
logger.Logger.Panic("write default template failed", zap.Error(err))
}
err = database.ConnectDB()
if err != nil {
logger.Logger.Panic("database connect failed", zap.Error(err))
@ -49,16 +37,15 @@ func init() {
}
func main() {
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard
r := gin.Default()
api.SetRoute(r)
logger.Logger.Info("server is running at http://localhost:" + strconv.Itoa(config.Default.Port))
err := r.Run(":" + strconv.Itoa(config.Default.Port))
server.SetRoute(r)
logger.Logger.Info("server is running at " + config.GlobalConfig.Address)
err := r.Run(config.GlobalConfig.Address)
if err != nil {
logger.Logger.Error("server running failed", zap.Error(err))
return

View File

@ -17,6 +17,7 @@ func GetSupportProxyTypes(clashType ClashType) map[string]bool {
"socks5": true,
}
}
if clashType == ClashMeta {
return map[string]bool{
"ss": true,
@ -30,5 +31,6 @@ func GetSupportProxyTypes(clashType ClashType) map[string]bool {
"anytls": true,
}
}
return nil
}

View File

@ -1,12 +0,0 @@
package model
type Tags []struct {
Name string `json:"name"`
ZipballUrl string `json:"zipball_url"`
TarballUrl string `json:"tarball_url"`
Commit struct {
Sha string `json:"sha"`
Url string `json:"url"`
}
NodeId string `json:"node_id"`
}

View File

@ -1,4 +1,4 @@
package model
package proxy
type Anytls struct {
Type string `yaml:"type"`
@ -35,3 +35,17 @@ func ProxyToAnytls(p Proxy) Anytls {
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{})
}

View File

@ -1,4 +1,4 @@
package model
package proxy
type Hysteria struct {
Type string `yaml:"type"`
@ -56,3 +56,13 @@ func ProxyToHysteria(p Proxy) Hysteria {
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
}

View File

@ -1,4 +1,4 @@
package model
package proxy
type Hysteria2 struct {
Type string `yaml:"type"`
@ -39,3 +39,13 @@ func ProxyToHysteria2(p Proxy) Hysteria2 {
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
}

View File

@ -1,4 +1,4 @@
package model
package proxy
type SmuxStruct struct {
Enabled bool `yaml:"enable"`
@ -86,6 +86,12 @@ type WireGuardPeerOption struct {
type _Proxy Proxy
func (p Proxy) MarshalYAML() (interface{}, error) {
// 尝试使用注册的序列化器
if marshaler, exists := GetMarshaler(p.Type); exists {
return marshaler.MarshalProxy(p)
}
// 保持向后兼容,对于未注册的类型使用原有逻辑
switch p.Type {
case "vmess":
return ProxyToVmess(p), nil

64
model/proxy/registry.go Normal file
View File

@ -0,0 +1,64 @@
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,4 +1,4 @@
package model
package proxy
type ShadowSocks struct {
Type string `yaml:"type"`
@ -31,3 +31,21 @@ func ProxyToShadowSocks(p Proxy) ShadowSocks {
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,4 +1,4 @@
package model
package proxy
type ShadowSocksR struct {
Type string `yaml:"type"`
@ -29,3 +29,13 @@ func ProxyToShadowSocksR(p Proxy) ShadowSocksR {
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
}

View File

@ -1,4 +1,4 @@
package model
package proxy
type Trojan struct {
Type string `yaml:"type"`
@ -37,3 +37,13 @@ func ProxyToTrojan(p Proxy) Trojan {
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
}

View File

@ -1,4 +1,4 @@
package model
package proxy
type Vless struct {
Type string `yaml:"type"`
@ -55,3 +55,13 @@ func ProxyToVless(p Proxy) Vless {
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,4 +1,4 @@
package model
package proxy
type HTTPOptions struct {
Method string `yaml:"method,omitempty"`
@ -102,3 +102,13 @@ func ProxyToVmess(p Proxy) Vmess {
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,4 +1,4 @@
package validator
package model
import (
"crypto/sha256"
@ -11,7 +11,7 @@ import (
"github.com/gin-gonic/gin"
)
type SubValidator struct {
type SubConfig struct {
Sub string `form:"sub" binding:""`
Subs []string `form:"-" binding:""`
Proxy string `form:"proxy" binding:""`
@ -47,22 +47,22 @@ type RuleStruct struct {
Prepend bool
}
func ParseQuery(c *gin.Context) (SubValidator, error) {
var query SubValidator
func ParseSubQuery(c *gin.Context) (SubConfig, error) {
var query SubConfig
if err := c.ShouldBind(&query); err != nil {
return SubValidator{}, errors.New("参数错误: " + err.Error())
return SubConfig{}, errors.New("参数错误: " + err.Error())
}
if query.Sub == "" && query.Proxy == "" {
return SubValidator{}, errors.New("参数错误: sub 和 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 SubValidator{}, errors.New("参数错误: sub 格式错误")
return SubConfig{}, errors.New("参数错误: sub 格式错误")
}
if _, err := url.ParseRequestURI(query.Subs[i]); err != nil {
return SubValidator{}, errors.New("参数错误: " + err.Error())
return SubConfig{}, errors.New("参数错误: " + err.Error())
}
}
} else {
@ -77,7 +77,7 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
if strings.HasPrefix(query.Template, "http") {
uri, err := url.ParseRequestURI(query.Template)
if err != nil {
return SubValidator{}, err
return SubConfig{}, err
}
query.Template = uri.String()
}
@ -89,12 +89,12 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
length := len(ruleProviders)
parts := strings.Split(ruleProviders[length-i-1][1], ",")
if len(parts) < 4 {
return SubValidator{}, errors.New("参数错误: ruleProvider 格式错误")
return SubConfig{}, errors.New("参数错误: ruleProvider 格式错误")
}
u := parts[1]
uri, err := url.ParseRequestURI(u)
if err != nil {
return SubValidator{}, errors.New("参数错误: " + err.Error())
return SubConfig{}, errors.New("参数错误: " + err.Error())
}
u = uri.String()
if len(parts) == 4 {
@ -115,7 +115,7 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
names := make(map[string]bool)
for _, ruleProvider := range query.RuleProviders {
if _, ok := names[ruleProvider.Name]; ok {
return SubValidator{}, errors.New("参数错误: Rule-Provider 名称重复")
return SubConfig{}, errors.New("参数错误: Rule-Provider 名称重复")
}
names[ruleProvider.Name] = true
}
@ -148,7 +148,7 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
for i := range replaces {
length := len(replaces[i])
if length != 3 {
return SubValidator{}, errors.New("参数错误: replace 格式错误")
return SubConfig{}, errors.New("参数错误: replace 格式错误")
}
query.ReplaceKeys = append(query.ReplaceKeys, replaces[i][1])
query.ReplaceTo = append(query.ReplaceTo, replaces[i][2])

View File

@ -1,7 +1,9 @@
package model
import "github.com/bestnite/sub2clash/model/proxy"
type NodeList struct {
Proxies []Proxy `yaml:"proxies,omitempty"`
Proxies []proxy.Proxy `yaml:"proxies,omitempty"`
}
type Subscription struct {
@ -57,7 +59,7 @@ type Subscription struct {
Experimental Experimental `yaml:"experimental,omitempty"`
Profile Profile `yaml:"profile,omitempty"`
GeoXUrl GeoXUrl `yaml:"geox-url,omitempty"`
Proxies []Proxy `yaml:"proxies,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"`

View File

@ -5,19 +5,29 @@ import (
"net/url"
"strings"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/model"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
func ParseAnytls(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.AnytlsPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
type AnytlsParser struct{}
func (p *AnytlsParser) GetPrefixes() []string {
return []string{"anytls://"}
}
func (p *AnytlsParser) GetType() string {
return "anytls"
}
func (p *AnytlsParser) Parse(proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
}
link, err := url.Parse(proxy)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
@ -32,24 +42,24 @@ func ParseAnytls(proxy string) (model.Proxy, error) {
query := link.Query()
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
}
portStr := link.Port()
if portStr == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
}
port, err := ParsePort(portStr)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Raw: portStr,
}
}
@ -61,8 +71,8 @@ func ParseAnytls(proxy string) (model.Proxy, error) {
}
remarks = strings.TrimSpace(remarks)
result := model.Proxy{
Type: "anytls",
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
Server: server,
Port: port,
@ -72,3 +82,7 @@ func ParseAnytls(proxy string) (model.Proxy, error) {
}
return result, nil
}
func init() {
RegisterParser(&AnytlsParser{})
}

View File

@ -1,23 +0,0 @@
package parser
import (
"encoding/base64"
"strings"
)
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)
if err != nil {
return "", err
}
return string(decodeStr), nil
}

87
parser/common.go Normal file
View File

@ -0,0 +1,87 @@
package parser
import (
"encoding/base64"
"errors"
"strconv"
"strings"
"github.com/bestnite/sub2clash/logger"
P "github.com/bestnite/sub2clash/model/proxy"
"go.uber.org/zap"
)
func hasPrefix(proxy string, prefixes []string) bool {
hasPrefix := false
for _, prefix := range prefixes {
if strings.HasPrefix(proxy, prefix) {
hasPrefix = true
break
}
}
return hasPrefix
}
func ParsePort(portStr string) (int, error) {
port, err := strconv.Atoi(portStr)
if err != nil {
return 0, err
}
if port < 1 || port > 65535 {
return 0, errors.New("invaild port range")
}
return port, nil
}
func isLikelyBase64(s string) bool {
if len(s)%4 == 0 && strings.HasSuffix(s, "=") && !strings.Contains(strings.TrimSuffix(s, "="), "=") {
s = strings.TrimSuffix(s, "=")
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for _, c := range s {
if !strings.ContainsRune(chars, c) {
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)
if err != nil {
return "", err
}
return string(decodeStr), nil
}
func ParseProxies(proxies ...string) []P.Proxy {
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),
)
}
}
}
return result
}

View File

@ -6,27 +6,37 @@ import (
"strconv"
"strings"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/model"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
func ParseHysteria(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.HysteriaPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
type HysteriaParser struct{}
func (p *HysteriaParser) GetPrefixes() []string {
return []string{"hysteria://"}
}
func (p *HysteriaParser) GetType() string {
return "hysteria"
}
func (p *HysteriaParser) Parse(proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
}
link, err := url.Parse(proxy)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
}
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
@ -34,8 +44,8 @@ func ParseHysteria(proxy string) (model.Proxy, error) {
portStr := link.Port()
if portStr == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
@ -43,8 +53,8 @@ func ParseHysteria(proxy string) (model.Proxy, error) {
port, err := ParsePort(portStr)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
@ -70,8 +80,8 @@ func ParseHysteria(proxy string) (model.Proxy, error) {
}
remarks = strings.TrimSpace(remarks)
result := model.Proxy{
Type: "hysteria",
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
Server: server,
Port: port,
@ -86,3 +96,7 @@ func ParseHysteria(proxy string) (model.Proxy, error) {
}
return result, nil
}
func init() {
RegisterParser(&HysteriaParser{})
}

View File

@ -5,20 +5,29 @@ import (
"net/url"
"strings"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/model"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
func ParseHysteria2(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.Hysteria2Prefix1) &&
!strings.HasPrefix(proxy, constant.Hysteria2Prefix2) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
type Hysteria2Parser struct{}
func (p *Hysteria2Parser) GetPrefixes() []string {
return []string{"hysteria2://", "hy2://"}
}
func (p *Hysteria2Parser) GetType() string {
return "hysteria2"
}
func (p *Hysteria2Parser) Parse(proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
}
link, err := url.Parse(proxy)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
@ -33,24 +42,24 @@ func ParseHysteria2(proxy string) (model.Proxy, error) {
query := link.Query()
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
}
portStr := link.Port()
if portStr == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
}
port, err := ParsePort(portStr)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Raw: portStr,
}
}
@ -63,8 +72,8 @@ func ParseHysteria2(proxy string) (model.Proxy, error) {
}
remarks = strings.TrimSpace(remarks)
result := model.Proxy{
Type: "hysteria2",
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
Server: server,
Port: port,
@ -78,3 +87,7 @@ func ParseHysteria2(proxy string) (model.Proxy, error) {
}
return result, nil
}
func init() {
RegisterParser(&Hysteria2Parser{})
}

View File

@ -1,18 +0,0 @@
package parser
import (
"errors"
"strconv"
)
func ParsePort(portStr string) (int, error) {
port, err := strconv.Atoi(portStr)
if err != nil {
return 0, err
}
if port < 1 || port > 65535 {
return 0, errors.New("invaild port range")
}
return port, nil
}

77
parser/registry.go Normal file
View File

@ -0,0 +1,77 @@
package parser
import (
"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 返回支持的协议前缀(可以返回多个)
GetPrefixes() []string
// GetType 返回协议类型名称
GetType() string
}
// parserRegistry 解析器注册中心
type parserRegistry struct {
mu sync.RWMutex
parsers map[string]ProxyParser // prefix -> parser
}
var registry = &parserRegistry{
parsers: make(map[string]ProxyParser),
}
// RegisterParser 注册解析器
func RegisterParser(parser ProxyParser) {
registry.mu.Lock()
defer registry.mu.Unlock()
for _, prefix := range parser.GetPrefixes() {
registry.parsers[prefix] = parser
}
}
// GetParser 根据前缀获取解析器
func GetParser(prefix string) (ProxyParser, bool) {
registry.mu.RLock()
defer registry.mu.RUnlock()
parser, exists := registry.parsers[prefix]
return parser, exists
}
// GetAllParsers 获取所有注册的解析器
func GetAllParsers() map[string]ProxyParser {
registry.mu.RLock()
defer registry.mu.RUnlock()
result := make(map[string]ProxyParser)
for k, v := range registry.parsers {
result[k] = v
}
return result
}
// ParseProxyWithRegistry 使用注册机制解析代理
func ParseProxyWithRegistry(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"}
}
// 查找匹配的解析器
for prefix, parser := range registry.parsers {
if strings.HasPrefix(proxy, prefix) {
return parser.Parse(proxy)
}
}
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy, Message: "unsupported protocol"}
}

View File

@ -5,20 +5,41 @@ import (
"net/url"
"strings"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/model"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
func ParseShadowsocks(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
// ShadowsocksParser Shadowsocks协议解析器
type ShadowsocksParser struct{}
// GetPrefixes 返回支持的协议前缀
func (p *ShadowsocksParser) GetPrefixes() []string {
return []string{"ss://"}
}
// GetType 返回协议类型
func (p *ShadowsocksParser) GetType() string {
return "ss"
}
// Parse 解析Shadowsocks代理
func (p *ShadowsocksParser) Parse(proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
}
if !strings.Contains(proxy, "@") {
s := strings.SplitN(proxy, "#", 2)
d, err := DecodeBase64(strings.TrimPrefix(s[0], "ss://"))
for _, prefix := range p.GetPrefixes() {
if strings.HasPrefix(s[0], prefix) {
s[0] = strings.TrimPrefix(s[0], prefix)
break
}
}
d, err := DecodeBase64(s[0])
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
@ -31,8 +52,8 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
}
link, err := url.Parse(proxy)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
@ -40,8 +61,8 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
@ -49,16 +70,16 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
portStr := link.Port()
if portStr == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
}
port, err := ParsePort(portStr)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Raw: proxy,
}
}
@ -79,8 +100,8 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
if isLikelyBase64(password) {
password, err = DecodeBase64(password)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "password decode error",
Raw: proxy,
}
@ -93,8 +114,8 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
}
remarks = strings.TrimSpace(remarks)
result := model.Proxy{
Type: "ss",
result := P.Proxy{
Type: p.GetType(),
Cipher: method,
Password: password,
Server: server,
@ -105,16 +126,7 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
return result, nil
}
func isLikelyBase64(s string) bool {
if len(s)%4 == 0 && strings.HasSuffix(s, "=") && !strings.Contains(strings.TrimSuffix(s, "="), "=") {
s = strings.TrimSuffix(s, "=")
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for _, c := range s {
if !strings.ContainsRune(chars, c) {
return false
}
}
return true
}
return false
// 注册解析器
func init() {
RegisterParser(&ShadowsocksParser{})
}

View File

@ -5,20 +5,36 @@ import (
"strconv"
"strings"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/model"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
func ParseShadowsocksR(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
type ShadowsocksRParser struct{}
func (p *ShadowsocksRParser) GetPrefixes() []string {
return []string{"ssr://"}
}
func (p *ShadowsocksRParser) GetType() string {
return "ssr"
}
func (p *ShadowsocksRParser) Parse(proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
}
for _, prefix := range p.GetPrefixes() {
if strings.HasPrefix(proxy, prefix) {
proxy = strings.TrimPrefix(proxy, prefix)
break
}
}
proxy = strings.TrimPrefix(proxy, constant.ShadowsocksRPrefix)
proxy, err := DecodeBase64(proxy)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidBase64,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidBase64,
Raw: proxy,
}
}
@ -30,16 +46,16 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) {
obfs := parts[4]
password, err := DecodeBase64(parts[5])
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Raw: proxy,
Message: err.Error(),
}
}
port, err := ParsePort(parts[1])
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
@ -51,8 +67,8 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) {
if len(serverInfoAndParams) == 2 {
params, err := url.ParseQuery(serverInfoAndParams[1])
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrCannotParseParams,
return P.Proxy{}, &E.ParseError{
Type: E.ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
}
@ -69,17 +85,17 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) {
remarks = server + ":" + strconv.Itoa(port)
}
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Raw: proxy,
Message: err.Error(),
}
}
}
result := model.Proxy{
result := P.Proxy{
Name: remarks,
Type: "ssr",
Type: p.GetType(),
Server: server,
Port: port,
Protocol: protocol,
@ -92,3 +108,7 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) {
return result, nil
}
func init() {
RegisterParser(&ShadowsocksRParser{})
}

View File

@ -2,44 +2,56 @@ package parser
import (
"fmt"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/model"
"net/url"
"strings"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
func ParseSocks(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.SocksPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
type SocksParser struct{}
func (p *SocksParser) GetPrefixes() []string {
return []string{"socks://"}
}
func (p *SocksParser) GetType() string {
return "socks5"
}
func (p *SocksParser) Parse(proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
}
link, err := url.Parse(proxy)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
}
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
}
portStr := link.Port()
if portStr == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
}
port, err := ParsePort(portStr)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Raw: portStr,
}
}
@ -56,8 +68,8 @@ func ParseSocks(proxy string) (model.Proxy, error) {
decodeStr, err := DecodeBase64(encodeStr)
splitStr := strings.Split(decodeStr, ":")
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
@ -67,8 +79,8 @@ func ParseSocks(proxy string) (model.Proxy, error) {
password = splitStr[1]
}
}
return model.Proxy{
Type: "socks5",
return P.Proxy{
Type: p.GetType(),
Name: remarks,
Server: server,
Port: port,
@ -77,3 +89,7 @@ func ParseSocks(proxy string) (model.Proxy, error) {
}, nil
}
func init() {
RegisterParser(&SocksParser{})
}

View File

@ -5,19 +5,29 @@ import (
"net/url"
"strings"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/model"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
func ParseTrojan(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.TrojanPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
type TrojanParser struct{}
func (p *TrojanParser) GetPrefixes() []string {
return []string{"trojan://"}
}
func (p *TrojanParser) GetType() string {
return "trojan"
}
func (p *TrojanParser) Parse(proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
}
link, err := url.Parse(proxy)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
@ -26,16 +36,16 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
password := link.User.Username()
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
}
portStr := link.Port()
if portStr == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
@ -43,8 +53,8 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
port, err := ParsePort(portStr)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
@ -66,8 +76,8 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
alpn = nil
}
result := model.Proxy{
Type: "trojan",
result := P.Proxy{
Type: p.GetType(),
Server: server,
Port: port,
Password: password,
@ -84,7 +94,7 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
if security == "reality" {
result.TLS = true
result.Sni = sni
result.RealityOpts = model.RealityOptions{
result.RealityOpts = P.RealityOptions{
PublicKey: pbk,
ShortID: sid,
}
@ -93,7 +103,7 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
if network == "ws" {
result.Network = "ws"
result.WSOpts = model.WSOptions{
result.WSOpts = P.WSOptions{
Path: path,
Headers: map[string]string{
"Host": host,
@ -102,10 +112,14 @@ func ParseTrojan(proxy string) (model.Proxy, error) {
}
if network == "grpc" {
result.GrpcOpts = model.GrpcOptions{
result.GrpcOpts = P.GrpcOptions{
GrpcServiceName: serviceName,
}
}
return result, nil
}
func init() {
RegisterParser(&TrojanParser{})
}

View File

@ -5,19 +5,29 @@ import (
"net/url"
"strings"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/model"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
func ParseVless(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.VLESSPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
type VlessParser struct{}
func (p *VlessParser) GetPrefixes() []string {
return []string{"vless://"}
}
func (p *VlessParser) GetType() string {
return "vless"
}
func (p *VlessParser) Parse(proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
}
link, err := url.Parse(proxy)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
@ -25,8 +35,8 @@ func ParseVless(proxy string) (model.Proxy, error) {
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
@ -34,8 +44,8 @@ func ParseVless(proxy string) (model.Proxy, error) {
portStr := link.Port()
port, err := ParsePort(portStr)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
@ -58,8 +68,8 @@ func ParseVless(proxy string) (model.Proxy, error) {
}
remarks = strings.TrimSpace(remarks)
result := model.Proxy{
Type: "vless",
result := P.Proxy{
Type: p.GetType(),
Server: server,
Name: remarks,
Port: port,
@ -78,7 +88,7 @@ func ParseVless(proxy string) (model.Proxy, error) {
if security == "reality" {
result.TLS = true
result.Servername = sni
result.RealityOpts = model.RealityOptions{
result.RealityOpts = P.RealityOptions{
PublicKey: pbk,
ShortID: sid,
}
@ -87,7 +97,7 @@ func ParseVless(proxy string) (model.Proxy, error) {
if _type == "ws" {
result.Network = "ws"
result.WSOpts = model.WSOptions{
result.WSOpts = P.WSOptions{
Path: path,
}
if host != "" {
@ -98,21 +108,21 @@ func ParseVless(proxy string) (model.Proxy, error) {
if _type == "grpc" {
result.Network = "grpc"
result.GrpcOpts = model.GrpcOptions{
result.GrpcOpts = P.GrpcOptions{
GrpcServiceName: serviceName,
}
}
if _type == "http" {
result.HTTPOpts = model.HTTPOptions{}
result.HTTPOpts = P.HTTPOptions{}
result.HTTPOpts.Headers = map[string][]string{}
result.HTTPOpts.Path = strings.Split(path, ",")
hosts, err := url.QueryUnescape(host)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrCannotParseParams,
return P.Proxy{}, &E.ParseError{
Type: E.ErrCannotParseParams,
Raw: proxy,
Message: err.Error(),
}
@ -125,3 +135,7 @@ func ParseVless(proxy string) (model.Proxy, error) {
return result, nil
}
func init() {
RegisterParser(&VlessParser{})
}

View File

@ -6,25 +6,40 @@ import (
"strconv"
"strings"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/model"
E "github.com/bestnite/sub2clash/error"
P "github.com/bestnite/sub2clash/model/proxy"
)
func ParseVmess(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.VMessPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
type VmessParser struct{}
func (p *VmessParser) GetPrefixes() []string {
return []string{"vmess://"}
}
func (p *VmessParser) GetType() string {
return "vmess"
}
func (p *VmessParser) Parse(proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidPrefix, Raw: proxy}
}
proxy = strings.TrimPrefix(proxy, constant.VMessPrefix)
for _, prefix := range p.GetPrefixes() {
if strings.HasPrefix(proxy, prefix) {
proxy = strings.TrimPrefix(proxy, prefix)
break
}
}
base64, err := DecodeBase64(proxy)
if err != nil {
return model.Proxy{}, &ParseError{Type: ErrInvalidBase64, Raw: proxy, Message: err.Error()}
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidBase64, Raw: proxy, Message: err.Error()}
}
var vmess model.VmessJson
var vmess P.VmessJson
err = json.Unmarshal([]byte(base64), &vmess)
if err != nil {
return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()}
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: err.Error()}
}
var port int
@ -32,8 +47,8 @@ func ParseVmess(proxy string) (model.Proxy, error) {
case string:
port, err = ParsePort(vmess.Port.(string))
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
return P.Proxy{}, &E.ParseError{
Type: E.ErrInvalidPort,
Message: err.Error(),
Raw: proxy,
}
@ -47,7 +62,7 @@ func ParseVmess(proxy string) (model.Proxy, error) {
case string:
aid, err = strconv.Atoi(vmess.Aid.(string))
if err != nil {
return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()}
return P.Proxy{}, &E.ParseError{Type: E.ErrInvalidStruct, Raw: proxy, Message: err.Error()}
}
case float64:
aid = int(vmess.Aid.(float64))
@ -62,9 +77,9 @@ func ParseVmess(proxy string) (model.Proxy, error) {
name = vmess.Ps
}
result := model.Proxy{
result := P.Proxy{
Name: name,
Type: "vmess",
Type: p.GetType(),
Server: vmess.Add,
Port: port,
UUID: vmess.Id,
@ -93,7 +108,7 @@ func ParseVmess(proxy string) (model.Proxy, error) {
vmess.Host = vmess.Add
}
result.Network = "ws"
result.WSOpts = model.WSOptions{
result.WSOpts = P.WSOptions{
Path: vmess.Path,
Headers: map[string]string{
"Host": vmess.Host,
@ -102,14 +117,14 @@ func ParseVmess(proxy string) (model.Proxy, error) {
}
if vmess.Net == "grpc" {
result.GrpcOpts = model.GrpcOptions{
result.GrpcOpts = P.GrpcOptions{
GrpcServiceName: vmess.Path,
}
result.Network = "grpc"
}
if vmess.Net == "h2" {
result.HTTP2Opts = model.HTTP2Options{
result.HTTP2Opts = P.HTTP2Options{
Host: strings.Split(vmess.Host, ","),
Path: vmess.Path,
}
@ -118,3 +133,7 @@ func ParseVmess(proxy string) (model.Proxy, error) {
return result, nil
}
func init() {
RegisterParser(&VmessParser{})
}

View File

@ -3,26 +3,36 @@ package handler
import (
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/nitezs/sub2clash/common"
"github.com/nitezs/sub2clash/common/database"
"github.com/nitezs/sub2clash/config"
"github.com/nitezs/sub2clash/model"
"github.com/nitezs/sub2clash/validator"
"github.com/bestnite/sub2clash/common"
"github.com/bestnite/sub2clash/common/database"
"github.com/bestnite/sub2clash/config"
"github.com/bestnite/sub2clash/model"
"github.com/gin-gonic/gin"
)
type shortLinkGenRequset struct {
Url string `form:"url" binding:"required"`
Password string `form:"password"`
CustomID string `form:"customId"`
}
type shortLinkUpdateRequest struct {
Hash string `form:"hash" binding:"required"`
Url string `form:"url" binding:"required"`
Password string `form:"password" binding:"required"`
}
func respondWithError(c *gin.Context, code int, message string) {
c.String(code, message)
c.Abort()
}
func GenerateLinkHandler(c *gin.Context) {
var params validator.ShortLinkGenValidator
var params shortLinkGenRequset
if err := c.ShouldBind(&params); err != nil {
respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error())
return
@ -51,7 +61,7 @@ func GenerateLinkHandler(c *gin.Context) {
password = params.Password
} else {
// 自动生成短链ID和密码
hash, err = generateUniqueHash()
hash, err = generateUniqueHash(config.GlobalConfig.ShortLinkLength)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "生成短链接失败")
return
@ -76,15 +86,15 @@ func GenerateLinkHandler(c *gin.Context) {
// 返回生成的短链ID和密码
response := map[string]string{
"hash": hash,
"hash": hash,
"password": password,
}
c.JSON(http.StatusOK, response)
}
func generateUniqueHash() (string, error) {
func generateUniqueHash(length int) (string, error) {
for {
hash := common.RandomString(config.Default.ShortLinkLength)
hash := common.RandomString(length)
exists, err := database.CheckShortLinkHashExists(hash)
if err != nil {
return "", err
@ -96,7 +106,7 @@ func generateUniqueHash() (string, error) {
}
func UpdateLinkHandler(c *gin.Context) {
var params validator.ShortLinkUpdateValidator
var params shortLinkUpdateRequest
if err := c.ShouldBindJSON(&params); err != nil {
respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error())
return
@ -158,7 +168,7 @@ func GetRawConfHandler(c *gin.Context) {
return
}
response, err := http.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url)
response, err := http.Get(strings.TrimSuffix(config.GlobalConfig.Address, "/") + "/" + shortLink.Url)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "请求错误: "+err.Error())
return

54
server/handler/sub.go Normal file
View File

@ -0,0 +1,54 @@
package handler
import (
_ "embed"
"net/http"
"github.com/bestnite/sub2clash/common"
"github.com/bestnite/sub2clash/config"
M "github.com/bestnite/sub2clash/model"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v3"
)
func SubHandler(model M.ClashType, template string) func(c *gin.Context) {
return func(c *gin.Context) {
query, err := M.ParseSubQuery(c)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
sub, err := common.BuildSub(model, query, template, config.GlobalConfig.CacheExpire, config.GlobalConfig.RequestRetryTimes)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
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())
}
c.Header("subscription-userinfo", userInfoHeader)
}
if query.NodeListMode {
nodelist := M.NodeList{}
nodelist.Proxies = sub.Proxies
marshal, err := yaml.Marshal(nodelist)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
return
}
marshal, err := yaml.Marshal(sub)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
}
}

View File

@ -4,7 +4,7 @@ import (
"strconv"
"time"
"github.com/nitezs/sub2clash/logger"
"github.com/bestnite/sub2clash/logger"
"github.com/gin-gonic/gin"
"go.uber.org/zap"

View File

@ -1,4 +1,4 @@
package api
package server
import (
"embed"
@ -6,9 +6,11 @@ import (
"log"
"net/http"
"github.com/nitezs/sub2clash/api/handler"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/middleware"
"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"
"github.com/gin-gonic/gin"
)
@ -40,8 +42,8 @@ func SetRoute(r *gin.Engine) {
)
},
)
r.GET("/clash", handler.SubmodHandler)
r.GET("/meta", handler.SubHandler)
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)

View File

@ -38,7 +38,7 @@
<div class="mb-4">
<h2>sub2clash</h2>
<span class="text-muted fst-italic">通用订阅链接转 Clash(Meta) 配置工具
<a href="https://github.com/nitezs/sub2clash#clash-meta" target="_blank">使用文档</a></span><br /><span
<a href="https://github.com/bestnite/sub2clash#clash-meta" target="_blank">使用文档</a></span><br /><span
class="text-muted fst-italic">注意:本程序非纯前端程序,输入的订阅将被后端缓存,请确保您信任当前站点</span>
</div>
@ -174,7 +174,7 @@
<footer>
<p class="text-center">
Powered by
<a class="link-primary" href="https://github.com/nitezs/sub2clash">sub2clash</a>
<a class="link-primary" href="https://github.com/bestnite/sub2clash">sub2clash</a>
</p>
<p class="text-center">Version {{.Version}}</p>
</footer>

View File

@ -1,18 +0,0 @@
package validator
type ShortLinkGenValidator struct {
Url string `form:"url" binding:"required"`
Password string `form:"password"`
CustomID string `form:"customId"`
}
type GetUrlValidator struct {
Hash string `form:"hash" binding:"required"`
Password string `form:"password"`
}
type ShortLinkUpdateValidator struct {
Hash string `form:"hash" binding:"required"`
Url string `form:"url" binding:"required"`
Password string `form:"password" binding:"required"`
}