diff --git a/API_README.md b/API_README.md index 0462aa8..583d1b8 100644 --- a/API_README.md +++ b/API_README.md @@ -4,7 +4,7 @@ | Query 参数 | 类型 | 是否必须 | 默认值 | 说明 | |--------------|--------|-------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| sub | string | sub/proxy 至少有一项存在 | - | 订阅链接(可以输入多个,用 `,` 分隔) | +| sub | string | sub/proxy 至少有一项存在 | - | 订阅链接,可以在链接结尾加上`#名称`,来给订阅中的节点加上统一前缀(可以输入多个,用 `,` 分隔) | | proxy | string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个,用 `,` 分隔) | | refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) | | template | string | 否 | - | 外部模板链接或内部模板名称 | @@ -15,11 +15,12 @@ | sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc` | | replace | string | 否 | - | 通过正则表达式重命名节点,格式 `[,],[,]...` | | remove | string | 否 | - | 通过正则表达式删除节点 | +| nodeList | bool | 否 | `false` | 只输出节点 | | # `/short` 获取短链,Content-Type 为 `application/json` -具体参考使用可以参考 [api\templates\index.html](./api/templates/index.html) +具体参考使用可以参考 [api\templates\index.html](api/static/index.html) | Body 参数 | 类型 | 是否必须 | 默认值 | 说明 | |----------|--------|------|-----|------------------| diff --git a/README.md b/README.md index e178a88..a343498 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ | 变量名 | 说明 | 默认值 | |-----------------------|-----------------------------------------------------------|-----------------------| -| BASE_PATH | 程序运行子路径,例如将服务反代在 `https://example.com/sub` 则此变量值应为 `/sub` | `/` | | PORT | 端口 | `8011` | | META_TEMPLATE | meta 模板文件名 | `template_meta.yaml` | | CLASH_TEMPLATE | clash 模板文件名 | `template_clash.yaml` | @@ -37,17 +36,18 @@ | LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` | | SHORT_LINK_LENGTH | 短链长度 | `6` | -### API +### API [API文档](./API_README.md) ### 模板 -可以通过变量自定义模板中的策略组代理节点 +可以通过变量自定义模板中的策略组代理节点 解释的不太清楚,可以参考下方默认模板 - `` 为添加所有节点 - `` 为添加所有国家策略组 +- `<地区二位字母代码>` 为添加指定地区所有节点,例如 `` 将添加所有香港节点 #### 默认模板 diff --git a/api/controller/clash.go b/api/controller/clash.go index 4b873ef..ee24966 100644 --- a/api/controller/clash.go +++ b/api/controller/clash.go @@ -22,6 +22,17 @@ func SubmodHandler(c *gin.Context) { 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()) diff --git a/api/controller/default.go b/api/controller/default.go index e0eed50..0f2676f 100644 --- a/api/controller/default.go +++ b/api/controller/default.go @@ -58,6 +58,10 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template // 加载订阅 for i := range query.Subs { data, err := utils.LoadSubscription(query.Subs[i], query.Refresh) + 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), @@ -66,11 +70,12 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template } // 解析订阅 err = yaml.Unmarshal(data, &sub) + newProxies := make([]model.Proxy, 0) if err != nil { reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless)://") if reg.Match(data) { p := utils.ParseProxy(strings.Split(string(data), "\n")...) - proxyList = append(proxyList, p...) + newProxies = p } else { // 如果无法直接解析,尝试Base64解码 base64, err := parser.DecodeBase64(string(data)) @@ -83,16 +88,28 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template return nil, errors.New("加载订阅失败: " + err.Error()) } p := utils.ParseProxy(strings.Split(base64, "\n")...) - proxyList = append(proxyList, p...) + newProxies = p } } else { - proxyList = append(proxyList, sub.Proxies...) + 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, utils.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)) @@ -233,11 +250,28 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) { continue } newProxies := make([]string, 0, len(temp.ProxyGroups[i].Proxies)) + 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 { - if temp.ProxyGroups[i].Proxies[j] == "" { - newProxies = append(newProxies, proxyNames...) - } else if temp.ProxyGroups[i].Proxies[j] == "" { - newProxies = append(newProxies, countryGroupNames...) + 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": + newProxies = append(newProxies, countryGroupNames...) + default: + if len(key) == 2 { + newProxies = append( + newProxies, countryGroupMap[utils.GetContryName(key)].Proxies..., + ) + } + } } else { newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j]) } diff --git a/api/controller/meta.go b/api/controller/meta.go index f6a2fe9..57a1f2c 100644 --- a/api/controller/meta.go +++ b/api/controller/meta.go @@ -23,6 +23,17 @@ func SubHandler(c *gin.Context) { 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()) diff --git a/api/route.go b/api/route.go index 17d54b4..b518f6d 100644 --- a/api/route.go +++ b/api/route.go @@ -4,23 +4,40 @@ import ( "embed" "github.com/gin-gonic/gin" "html/template" + "log" + "net/http" "sub2clash/api/controller" "sub2clash/config" "sub2clash/middleware" ) -//go:embed templates/* -var templates embed.FS +//go:embed static +var staticFiles embed.FS func SetRoute(r *gin.Engine) { r.Use(middleware.ZapLogger()) + // 使用内嵌的模板文件 - r.SetHTMLTemplate(template.Must(template.New("").ParseFS(templates, "templates/*"))) + tpl, err := template.ParseFS(staticFiles, "static/*") + if err != nil { + log.Fatalf("Error parsing templates: %v", err) + } + r.SetHTMLTemplate(tpl) + + r.GET( + "/static/*filepath", func(c *gin.Context) { + c.FileFromFS("static/"+c.Param("filepath"), http.FS(staticFiles)) + }, + ) r.GET( "/", func(c *gin.Context) { + version := config.Version + if len(config.Version) > 7 { + version = config.Version[:7] + } c.HTML( 200, "index.html", gin.H{ - "Version": config.Version, + "Version": version, }, ) }, diff --git a/api/static/index.html b/api/static/index.html new file mode 100644 index 0000000..15d2490 --- /dev/null +++ b/api/static/index.html @@ -0,0 +1,258 @@ + + + + + + sub2clash + + + + + + + + + + +
+
+

sub2clash

+ 通用订阅链接转 Clash(Meta) 配置工具 + 使用文档 +
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ + + +
+
+ + + + +
+
+ +
+

+ Powered by + sub2clash +

+

Version {{.Version}}

+
+
+ + diff --git a/api/static/index.js b/api/static/index.js new file mode 100644 index 0000000..2e9d9ea --- /dev/null +++ b/api/static/index.js @@ -0,0 +1,415 @@ +function clearExistingValues() { + // 清除简单输入框和复选框的值 + document.getElementById("endpoint").value = "clash"; + document.getElementById("sub").value = ""; + document.getElementById("proxy").value = ""; + document.getElementById("refresh").checked = false; + document.getElementById("autoTest").checked = false; + document.getElementById("lazy").checked = false; + document.getElementById("template").value = ""; + document.getElementById("sort").value = "nameasc"; + document.getElementById("remove").value = ""; + document.getElementById("apiLink").value = ""; + document.getElementById("apiShortLink").value = ""; + document.getElementById("password").value = ""; + document.getElementById("nodeList").checked = false; + + // 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组 + clearInputGroup("ruleProviderGroup"); + clearInputGroup("replaceGroup"); + clearInputGroup("ruleGroup"); +} + +function generateURI() { + const queryParams = []; + + // 获取 API Endpoint + const endpoint = document.getElementById("endpoint").value; + + // 获取并组合订阅链接 + let subLines = document + .getElementById("sub") + .value.split("\n") + .filter((line) => line.trim() !== ""); + let noSub = false; + // 去除 subLines 中空元素 + subLines = subLines.map((item) => { + if (item !== "") { + return item; + } + }); + if (subLines.length > 0) { + queryParams.push(`sub=${encodeURIComponent(subLines.join(","))}`); + } else { + noSub = true; + } + + // 获取并组合节点分享链接 + let proxyLines = document + .getElementById("proxy") + .value.split("\n") + .filter((line) => line.trim() !== ""); + let noProxy = false; + // 去除 proxyLines 中空元素 + proxyLines = proxyLines.map((item) => { + if (item !== "") { + return item; + } + }); + if (proxyLines.length > 0) { + queryParams.push(`proxy=${encodeURIComponent(proxyLines.join(","))}`); + } else { + noProxy = true; + } + if (noSub && noProxy) { + alert("订阅链接和节点分享链接不能同时为空!"); + return ""; + } + // 获取复选框的值 + const refresh = document.getElementById("refresh").checked; + queryParams.push(`refresh=${refresh ? "true" : "false"}`); + const autoTest = document.getElementById("autoTest").checked; + queryParams.push(`autoTest=${autoTest ? "true" : "false"}`); + const lazy = document.getElementById("lazy").checked; + queryParams.push(`lazy=${lazy ? "true" : "false"}`); + const nodeList = document.getElementById("nodeList").checked; + queryParams.push(`nodeList=${nodeList ? "true" : "false"}`); + + // 获取模板链接或名称(如果存在) + const template = document.getElementById("template").value; + if (template.trim() !== "") { + queryParams.push(`template=${encodeURIComponent(template)}`); + } + + // 获取Rule Provider和规则 + const ruleProviders = document.getElementsByName("ruleProvider"); + const rules = document.getElementsByName("rule"); + let providers = []; + for (let i = 0; i < ruleProviders.length / 5; i++) { + let baseIndex = i * 5; + let behavior = ruleProviders[baseIndex].value; + let url = ruleProviders[baseIndex + 1].value; + let group = ruleProviders[baseIndex + 2].value; + let prepend = ruleProviders[baseIndex + 3].value; + let name = ruleProviders[baseIndex + 4].value; + // 是否存在空值 + if ( + behavior.trim() === "" || + url.trim() === "" || + group.trim() === "" || + prepend.trim() === "" || + name.trim() === "" + ) { + alert("Rule Provider 中存在空值,请检查后重试!"); + return ""; + } + providers.push(`[${behavior},${url},${group},${prepend},${name}]`); + } + queryParams.push(`ruleProvider=${encodeURIComponent(providers.join(","))}`); + + let ruleList = []; + for (let i = 0; i < rules.length / 3; i++) { + if (rules[i * 3].value.trim() !== "") { + let rule = rules[i * 3].value; + let prepend = rules[i * 3 + 1].value; + let group = rules[i * 3 + 2].value; + // 是否存在空值 + if (rule.trim() === "" || prepend.trim() === "" || group.trim() === "") { + alert("Rule 中存在空值,请检查后重试!"); + return ""; + } + ruleList.push(`[${rule},${prepend},${group}]`); + } + } + queryParams.push(`rule=${encodeURIComponent(ruleList.join(","))}`); + + // 获取排序策略 + const sort = document.getElementById("sort").value; + queryParams.push(`sort=${sort}`); + + // 获取删除节点的正则表达式 + const remove = document.getElementById("remove").value; + if (remove.trim() !== "") { + queryParams.push(`remove=${encodeURIComponent(remove)}`); + } + + // 获取替换节点名称的正则表达式 + let replaceList = []; + const replaces = document.getElementsByName("replace"); + for (let i = 0; i < replaces.length / 2; i++) { + let replaceStr = `<${replaces[i * 2].value}>`; + let replaceTo = `<${replaces[i * 2 + 1].value}>`; + if (replaceStr.trim() === "") { + alert("重命名设置中存在空值,请检查后重试!"); + return ""; + } + replaceList.push(`[${replaceStr},${replaceTo}]`); + } + queryParams.push(`replace=${encodeURIComponent(replaceList.join(","))}`); + + return `${endpoint}?${queryParams.join("&")}`; +} + +// 将输入框中的 URL 解析为参数 +function parseInputURL() { + // 获取输入框中的 URL + const inputURL = document.getElementById("urlInput").value; + + if (!inputURL) { + alert("请输入有效的链接!"); + return; + } + + let url; + try { + url = new URL(inputURL); + } catch (_) { + alert("无效的链接!"); + return; + } + + // 清除现有的输入框值 + clearExistingValues(); + + // 获取查询参数 + const params = new URLSearchParams(url.search); + + // 分配值到对应的输入框 + const pathSections = url.pathname.split("/"); + const lastSection = pathSections[pathSections.length - 1]; + const clientTypeSelect = document.getElementById("endpoint"); + switch (lastSection.toLowerCase()) { + case "meta": + clientTypeSelect.value = "meta"; + break; + case "clash": + default: + clientTypeSelect.value = "clash"; + break; + } + + if (params.has("sub")) { + document.getElementById("sub").value = decodeURIComponent(params.get("sub")) + .split(",") + .join("\n"); + } + + if (params.has("proxy")) { + document.getElementById("proxy").value = decodeURIComponent( + params.get("proxy"), + ) + .split(",") + .join("\n"); + } + + if (params.has("refresh")) { + document.getElementById("refresh").checked = + params.get("refresh") === "true"; + } + + if (params.has("autoTest")) { + document.getElementById("autoTest").checked = + params.get("autoTest") === "true"; + } + + if (params.has("lazy")) { + document.getElementById("lazy").checked = params.get("lazy") === "true"; + } + + if (params.has("template")) { + document.getElementById("template").value = decodeURIComponent( + params.get("template"), + ); + } + + if (params.has("sort")) { + document.getElementById("sort").value = params.get("sort"); + } + + if (params.has("remove")) { + document.getElementById("remove").value = decodeURIComponent( + params.get("remove"), + ); + } + + if (params.has("replace")) { + parseAndFillReplaceParams(decodeURIComponent(params.get("replace"))); + } + + if (params.has("ruleProvider")) { + parseAndFillRuleProviderParams( + decodeURIComponent(params.get("ruleProvider")), + ); + } + + if (params.has("rule")) { + parseAndFillRuleParams(decodeURIComponent(params.get("rule"))); + } + + if (params.has("nodeList")) { + document.getElementById("nodeList").checked = + params.get("nodeList") === "true"; + } +} + +function clearInputGroup(groupId) { + // 清空第二个之后的child + const group = document.getElementById(groupId); + while (group.children.length > 2) { + group.removeChild(group.lastChild); + } +} + +function parseAndFillReplaceParams(replaceParams) { + const replaceGroup = document.getElementById("replaceGroup"); + let matches; + const regex = /\[(<.*?>),(<.*?>)\]/g; + const str = decodeURIComponent(replaceParams); + while ((matches = regex.exec(str)) !== null) { + const div = createReplace(); + const original = matches[1].slice(1, -1); // Remove < and > + const replacement = matches[2].slice(1, -1); // Remove < and > + + div.children[0].value = original; + div.children[1].value = replacement; + replaceGroup.appendChild(div); + } +} + +function parseAndFillRuleProviderParams(ruleProviderParams) { + const ruleProviderGroup = document.getElementById("ruleProviderGroup"); + let matches; + const regex = /\[(.*?),(.*?),(.*?),(.*?),(.*?)\]/g; + const str = decodeURIComponent(ruleProviderParams); + while ((matches = regex.exec(str)) !== null) { + const div = createRuleProvider(); + div.children[0].value = matches[1]; + div.children[1].value = matches[2]; + div.children[2].value = matches[3]; + div.children[3].value = matches[4]; + div.children[4].value = matches[5]; + ruleProviderGroup.appendChild(div); + } +} + +function parseAndFillRuleParams(ruleParams) { + const ruleGroup = document.getElementById("ruleGroup"); + let matches; + const regex = /\[(.*?),(.*?),(.*?)\]/g; + const str = decodeURIComponent(ruleParams); + while ((matches = regex.exec(str)) !== null) { + const div = createRule(); + div.children[0].value = matches[1]; + div.children[1].value = matches[2]; + div.children[2].value = matches[3]; + ruleGroup.appendChild(div); + } +} + +async function copyToClipboard(elem, e) { + const apiLinkInput = document.querySelector(`#${elem}`).value; + try { + await navigator.clipboard.writeText(apiLinkInput); + let text = e.textContent; + e.addEventListener("mouseout", function () { + e.textContent = text; + }); + e.textContent = "复制成功"; + } catch (err) { + console.error("复制到剪贴板失败:", err); + } +} + +function createRuleProvider() { + const div = document.createElement("div"); + div.classList.add("input-group", "mb-2"); + div.innerHTML = ` + + + + + + + `; + return div; +} + +function createReplace() { + const div = document.createElement("div"); + div.classList.add("input-group", "mb-2"); + div.innerHTML = ` + + + + `; + return div; +} + +function createRule() { + const div = document.createElement("div"); + div.classList.add("input-group", "mb-2"); + div.innerHTML = ` + + + + + `; + return div; +} + +function addRuleProvider() { + const div = createRuleProvider(); + document.getElementById("ruleProviderGroup").appendChild(div); +} + +function addRule() { + const div = createRule(); + document.getElementById("ruleGroup").appendChild(div); +} + +function addReplace() { + const div = createReplace(); + document.getElementById("replaceGroup").appendChild(div); +} + +function removeElement(button) { + button.parentElement.remove(); +} + +function generateURL() { + const apiLink = document.getElementById("apiLink"); + let uri = generateURI(); + if (uri === "") { + return; + } + apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`; +} + +function generateShortLink() { + const apiShortLink = document.getElementById("apiShortLink"); + const password = document.getElementById("password"); + let uri = generateURI(); + if (uri === "") { + return; + } + axios + .post( + "./short", + { + url: uri, + password: password.value.trim(), + }, + { + headers: { + "Content-Type": "application/json", + }, + }, + ) + .then((response) => { + apiShortLink.value = `${window.location.origin}${window.location.pathname}s/${response.data}`; + }) + .catch((error) => { + console.log(error); + alert("生成短链失败,请重试!"); + }); +} diff --git a/api/templates/index.html b/api/templates/index.html deleted file mode 100644 index 3c6d4e7..0000000 --- a/api/templates/index.html +++ /dev/null @@ -1,496 +0,0 @@ - - - - - - sub2clash - - - - - - - - - - - - - - -
-
-

sub2clash

- 通用订阅链接转 Clash(Meta) 配置工具 - 使用文档 -
- -
- -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
-
- - -
- -
- - - -
-
- - - - -
-
- - -
-

- Powered by - sub2clash -

-

Version {{.Version}}

-
-
- - - - diff --git a/config/config.go b/config/config.go index 2371992..fe67c6d 100644 --- a/config/config.go +++ b/config/config.go @@ -15,8 +15,8 @@ type Config struct { RequestMaxFileSize int64 CacheExpire int64 LogLevel string - BasePath string - ShortLinkLength int + //BasePath string + ShortLinkLength int } var Default *Config @@ -32,8 +32,8 @@ func LoadConfig() error { Port: 8011, CacheExpire: 60 * 5, LogLevel: "info", - BasePath: "/", - ShortLinkLength: 6, + //BasePath: "/", + ShortLinkLength: 6, } _ = godotenv.Load() if os.Getenv("PORT") != "" { @@ -73,12 +73,12 @@ func LoadConfig() error { 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("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 { diff --git a/model/contry_map.go b/model/country_code_map.go similarity index 100% rename from model/contry_map.go rename to model/country_code_map.go diff --git a/model/proxy.go b/model/proxy.go index 17e5b20..a8faee1 100644 --- a/model/proxy.go +++ b/model/proxy.go @@ -1,83 +1,64 @@ package model -type PluginOptsStruct struct { - Mode string `yaml:"mode"` -} - type SmuxStruct struct { Enabled bool `yaml:"enable"` } -type HeaderStruct struct { - Host string `yaml:"Host"` -} - -type WSOptsStruct struct { - Path string `yaml:"path,omitempty"` - Headers HeaderStruct `yaml:"headers,omitempty"` - MaxEarlyData int `yaml:"max-early-data,omitempty"` - EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"` -} - -type Vmess struct { - V string `json:"v"` - Ps string `json:"ps"` - Add string `json:"add"` - Port string `json:"port"` - Id string `json:"id"` - Aid string `json:"aid"` - Scy string `json:"scy"` - Net string `json:"net"` - Type string `json:"type"` - Host string `json:"host"` - Path string `json:"path"` - Tls string `json:"tls"` - Sni string `json:"sni"` - Alpn string `json:"alpn"` - Fp string `json:"fp"` -} - -type GRPCOptsStruct struct { - GRPCServiceName string `yaml:"grpc-service-name,omitempty"` -} - -type RealityOptsStruct struct { - PublicKey string `yaml:"public-key,omitempty"` - ShortId string `yaml:"short-id,omitempty"` -} - type Proxy struct { - Name string `yaml:"name,omitempty"` - Server string `yaml:"server,omitempty"` - Port int `yaml:"port,omitempty"` - Type string `yaml:"type,omitempty"` - Cipher string `yaml:"cipher,omitempty"` - Password string `yaml:"password,omitempty"` - UDP bool `yaml:"udp,omitempty"` - UUID string `yaml:"uuid,omitempty"` - Network string `yaml:"network,omitempty"` - Flow string `yaml:"flow,omitempty"` - TLS bool `yaml:"tls,omitempty"` - ClientFingerprint string `yaml:"client-fingerprint,omitempty"` - UdpOverTcp bool `yaml:"udp-over-tcp,omitempty"` - UdpOverTcpVersion string `yaml:"udp-over-tcp-version,omitempty"` - Plugin string `yaml:"plugin,omitempty"` - PluginOpts PluginOptsStruct `yaml:"plugin-opts,omitempty"` - Smux SmuxStruct `yaml:"smux,omitempty"` - Sni string `yaml:"sni,omitempty"` - AllowInsecure bool `yaml:"allow-insecure,omitempty"` - Fingerprint string `yaml:"fingerprint,omitempty"` - SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` - Alpn []string `yaml:"alpn,omitempty"` - XUDP bool `yaml:"xudp,omitempty"` - Servername string `yaml:"servername,omitempty"` - WSOpts WSOptsStruct `yaml:"ws-opts,omitempty"` - AlterID string `yaml:"alterId,omitempty"` - GRPCOpts GRPCOptsStruct `yaml:"grpc-opts,omitempty"` - RealityOpts RealityOptsStruct `yaml:"reality-opts,omitempty"` - Protocol string `yaml:"protocol,omitempty"` - Obfs string `yaml:"obfs,omitempty"` - ObfsParam string `yaml:"obfs-param,omitempty"` - ProtocolParam string `yaml:"protocol-param,omitempty"` - Remarks []string `yaml:"remarks,omitempty"` + Name string `yaml:"name,omitempty"` + Server string `yaml:"server,omitempty"` + Port int `yaml:"port,omitempty"` + Type string `yaml:"type,omitempty"` + Cipher string `yaml:"cipher,omitempty"` + Password string `yaml:"password,omitempty"` + UDP bool `yaml:"udp,omitempty"` + UUID string `yaml:"uuid,omitempty"` + Network string `yaml:"network,omitempty"` + Flow string `yaml:"flow,omitempty"` + TLS bool `yaml:"tls,omitempty"` + ClientFingerprint string `yaml:"client-fingerprint,omitempty"` + Plugin string `yaml:"plugin,omitempty"` + PluginOpts map[string]any `yaml:"plugin-opts,omitempty"` + Smux SmuxStruct `yaml:"smux,omitempty"` + Sni string `yaml:"sni,omitempty"` + AllowInsecure bool `yaml:"allow-insecure,omitempty"` + Fingerprint string `yaml:"fingerprint,omitempty"` + SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` + Alpn []string `yaml:"alpn,omitempty"` + XUDP bool `yaml:"xudp,omitempty"` + Servername string `yaml:"servername,omitempty"` + WSOpts WSOptions `yaml:"ws-opts,omitempty"` + AlterID int `yaml:"alterId,omitempty"` + GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"` + RealityOpts RealityOptions `yaml:"reality-opts,omitempty"` + Protocol string `yaml:"protocol,omitempty"` + Obfs string `yaml:"obfs,omitempty"` + ObfsParam string `yaml:"obfs-param,omitempty"` + ProtocolParam string `yaml:"protocol-param,omitempty"` + Remarks []string `yaml:"remarks,omitempty"` + HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"` + HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"` + PacketAddr bool `yaml:"packet-addr,omitempty"` + PacketEncoding string `yaml:"packet-encoding,omitempty"` + GlobalPadding bool `yaml:"global-padding,omitempty"` + AuthenticatedLength bool `yaml:"authenticated-length,omitempty"` + UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"` + UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"` + SubName string `yaml:"-"` +} + +func (p Proxy) MarshalYAML() (interface{}, error) { + switch p.Type { + case "vmess": + return ProxyToVmess(p), nil + case "ss": + return ProxyToShadowSocks(p), nil + case "ssr": + return ProxyToShadowSocksR(p), nil + case "vless": + return ProxyToVless(p), nil + case "trojan": + return ProxyToTrojan(p), nil + } + return nil, nil } diff --git a/model/proxy_group.go b/model/proxy_group.go index 14ba7cf..2bbba1c 100644 --- a/model/proxy_group.go +++ b/model/proxy_group.go @@ -17,6 +17,44 @@ type ProxyGroup struct { Size int `yaml:"-"` } +type SelectProxyGroup struct { + Name string `yaml:"name,omitempty"` + Type string `yaml:"type,omitempty"` + Proxies []string `yaml:"proxies,omitempty"` +} + +type UrlTestProxyGroup struct { + Name string `yaml:"name,omitempty"` + Type string `yaml:"type,omitempty"` + Proxies []string `yaml:"proxies,omitempty"` + Url string `yaml:"url,omitempty"` + Interval int `yaml:"interval,omitempty"` + Tolerance int `yaml:"tolerance,omitempty"` + Lazy bool `yaml:"lazy"` +} + +func (p ProxyGroup) MarshalYAML() (interface{}, error) { + switch p.Type { + case "select": + return SelectProxyGroup{ + Name: p.Name, + Type: p.Type, + Proxies: p.Proxies, + }, nil + case "url-test": + return UrlTestProxyGroup{ + Name: p.Name, + Type: p.Type, + Proxies: p.Proxies, + Url: p.Url, + Interval: p.Interval, + Tolerance: p.Tolerance, + Lazy: p.Lazy, + }, nil + } + return nil, nil +} + type ProxyGroupsSortByName []ProxyGroup type ProxyGroupsSortBySize []ProxyGroup diff --git a/model/proxy_shadowsocks.go b/model/proxy_shadowsocks.go new file mode 100644 index 0000000..18602ab --- /dev/null +++ b/model/proxy_shadowsocks.go @@ -0,0 +1,33 @@ +package model + +type ShadowSocks struct { + Type string `yaml:"type"` + Name string `yaml:"name"` + Server string `yaml:"server"` + Port int `yaml:"port"` + Password string `yaml:"password"` + Cipher string `yaml:"cipher"` + UDP bool `yaml:"udp,omitempty"` + Plugin string `yaml:"plugin,omitempty"` + PluginOpts map[string]any `yaml:"plugin-opts,omitempty"` + UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"` + UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"` + ClientFingerprint string `yaml:"client-fingerprint,omitempty"` +} + +func ProxyToShadowSocks(p Proxy) ShadowSocks { + return ShadowSocks{ + Type: "ss", + Name: p.Name, + Server: p.Server, + Port: p.Port, + Password: p.Password, + Cipher: p.Cipher, + UDP: p.UDP, + Plugin: p.Plugin, + PluginOpts: p.PluginOpts, + UDPOverTCP: p.UDPOverTCP, + UDPOverTCPVersion: p.UDPOverTCPVersion, + ClientFingerprint: p.ClientFingerprint, + } +} diff --git a/model/proxy_shadowsocksr.go b/model/proxy_shadowsocksr.go new file mode 100644 index 0000000..6b33ce8 --- /dev/null +++ b/model/proxy_shadowsocksr.go @@ -0,0 +1,31 @@ +package model + +type ShadowSocksR struct { + Type string `yaml:"type"` + Name string `yaml:"name"` + Server string `yaml:"server"` + Port int `yaml:"port"` + Password string `yaml:"password"` + Cipher string `yaml:"cipher"` + Obfs string `yaml:"obfs"` + ObfsParam string `yaml:"obfs-param,omitempty"` + Protocol string `yaml:"protocol"` + ProtocolParam string `yaml:"protocol-param,omitempty"` + UDP bool `yaml:"udp,omitempty"` +} + +func ProxyToShadowSocksR(p Proxy) ShadowSocksR { + return ShadowSocksR{ + Type: "ssr", + Name: p.Name, + Server: p.Server, + Port: p.Port, + Password: p.Password, + Cipher: p.Cipher, + Obfs: p.Obfs, + ObfsParam: p.ObfsParam, + Protocol: p.Protocol, + ProtocolParam: p.ProtocolParam, + UDP: p.UDP, + } +} diff --git a/model/proxy_trojan.go b/model/proxy_trojan.go new file mode 100644 index 0000000..7d54831 --- /dev/null +++ b/model/proxy_trojan.go @@ -0,0 +1,39 @@ +package model + +type Trojan struct { + Type string `yaml:"type"` + Name string `yaml:"name"` + Server string `yaml:"server"` + Port int `yaml:"port"` + Password string `yaml:"password"` + ALPN []string `yaml:"alpn,omitempty"` + SNI string `yaml:"sni,omitempty"` + SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` + Fingerprint string `yaml:"fingerprint,omitempty"` + UDP bool `yaml:"udp,omitempty"` + Network string `yaml:"network,omitempty"` + RealityOpts RealityOptions `yaml:"reality-opts,omitempty"` + GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"` + WSOpts WSOptions `yaml:"ws-opts,omitempty"` + ClientFingerprint string `yaml:"client-fingerprint,omitempty"` +} + +func ProxyToTrojan(p Proxy) Trojan { + return Trojan{ + Type: "trojan", + Name: p.Name, + Server: p.Server, + Port: p.Port, + Password: p.Password, + ALPN: p.Alpn, + SNI: p.Sni, + SkipCertVerify: p.SkipCertVerify, + Fingerprint: p.Fingerprint, + UDP: p.UDP, + Network: p.Network, + RealityOpts: p.RealityOpts, + GrpcOpts: p.GrpcOpts, + WSOpts: p.WSOpts, + ClientFingerprint: p.ClientFingerprint, + } +} diff --git a/model/proxy_vless.go b/model/proxy_vless.go new file mode 100644 index 0000000..e143296 --- /dev/null +++ b/model/proxy_vless.go @@ -0,0 +1,57 @@ +package model + +type Vless struct { + Type string `yaml:"type"` + Name string `yaml:"name"` + Server string `yaml:"server"` + Port int `yaml:"port"` + UUID string `yaml:"uuid"` + Flow string `yaml:"flow,omitempty"` + TLS bool `yaml:"tls,omitempty"` + ALPN []string `yaml:"alpn,omitempty"` + UDP bool `yaml:"udp,omitempty"` + PacketAddr bool `yaml:"packet-addr,omitempty"` + XUDP bool `yaml:"xudp,omitempty"` + PacketEncoding string `yaml:"packet-encoding,omitempty"` + Network string `yaml:"network,omitempty"` + RealityOpts RealityOptions `yaml:"reality-opts,omitempty"` + HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"` + HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"` + GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"` + WSOpts WSOptions `yaml:"ws-opts,omitempty"` + WSPath string `yaml:"ws-path,omitempty"` + WSHeaders map[string]string `yaml:"ws-headers,omitempty"` + SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` + Fingerprint string `yaml:"fingerprint,omitempty"` + ServerName string `yaml:"servername,omitempty"` + ClientFingerprint string `yaml:"client-fingerprint,omitempty"` +} + +func ProxyToVless(p Proxy) Vless { + return Vless{ + Type: "vless", + Name: p.Name, + Server: p.Server, + Port: p.Port, + UUID: p.UUID, + Flow: p.Flow, + TLS: p.TLS, + ALPN: p.Alpn, + UDP: p.UDP, + PacketAddr: p.PacketAddr, + XUDP: p.XUDP, + PacketEncoding: p.PacketEncoding, + Network: p.Network, + RealityOpts: p.RealityOpts, + HTTPOpts: p.HTTPOpts, + HTTP2Opts: p.HTTP2Opts, + GrpcOpts: p.GrpcOpts, + WSOpts: p.WSOpts, + WSPath: p.WSOpts.Path, + WSHeaders: p.WSOpts.Headers, + SkipCertVerify: p.SkipCertVerify, + Fingerprint: p.Fingerprint, + ServerName: p.Servername, + ClientFingerprint: p.ClientFingerprint, + } +} diff --git a/model/proxy_vmess.go b/model/proxy_vmess.go new file mode 100644 index 0000000..f70d41f --- /dev/null +++ b/model/proxy_vmess.go @@ -0,0 +1,104 @@ +package model + +type HTTPOptions struct { + Method string `proxy:"method,omitempty"` + Path []string `proxy:"path,omitempty"` + Headers map[string][]string `proxy:"headers,omitempty"` +} + +type HTTP2Options struct { + Host []string `proxy:"host,omitempty"` + Path string `proxy:"path,omitempty"` +} + +type GrpcOptions struct { + GrpcServiceName string `proxy:"grpc-service-name,omitempty"` +} + +type RealityOptions struct { + PublicKey string `proxy:"public-key"` + ShortID string `proxy:"short-id"` +} + +type WSOptions struct { + Path string `proxy:"path,omitempty"` + Headers map[string]string `proxy:"headers,omitempty"` + MaxEarlyData int `proxy:"max-early-data,omitempty"` + EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"` +} + +type VmessJson struct { + V string `json:"v"` + Ps string `json:"ps"` + Add string `json:"add"` + Port interface{} `json:"port"` + Id string `json:"id"` + Aid interface{} `json:"aid"` + Scy string `json:"scy"` + Net string `json:"net"` + Type string `json:"type"` + Host string `json:"host"` + Path string `json:"path"` + Tls string `json:"tls"` + Sni string `json:"sni"` + Alpn string `json:"alpn"` + Fp string `json:"fp"` +} + +type Vmess struct { + Type string `yaml:"type"` + Name string `yaml:"name"` + Server string `yaml:"server"` + Port int `yaml:"port"` + UUID string `yaml:"uuid"` + AlterID int `yaml:"alterId"` + Cipher string `yaml:"cipher"` + UDP bool `yaml:"udp,omitempty"` + Network string `yaml:"network,omitempty"` + TLS bool `yaml:"tls,omitempty"` + ALPN []string `yaml:"alpn,omitempty"` + SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` + Fingerprint string `yaml:"fingerprint,omitempty"` + ServerName string `yaml:"servername,omitempty"` + RealityOpts RealityOptions `yaml:"reality-opts,omitempty"` + HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"` + HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"` + GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"` + WSOpts WSOptions `yaml:"ws-opts,omitempty"` + PacketAddr bool `yaml:"packet-addr,omitempty"` + XUDP bool `yaml:"xudp,omitempty"` + PacketEncoding string `yaml:"packet-encoding,omitempty"` + GlobalPadding bool `yaml:"global-padding,omitempty"` + AuthenticatedLength bool `yaml:"authenticated-length,omitempty"` + ClientFingerprint string `yaml:"client-fingerprint,omitempty"` +} + +func ProxyToVmess(p Proxy) Vmess { + return Vmess{ + Type: "vmess", + Name: p.Name, + Server: p.Server, + Port: p.Port, + UUID: p.UUID, + AlterID: p.AlterID, + Cipher: p.Cipher, + UDP: p.UDP, + Network: p.Network, + TLS: p.TLS, + ALPN: p.Alpn, + SkipCertVerify: p.SkipCertVerify, + Fingerprint: p.Fingerprint, + ServerName: p.Servername, + RealityOpts: p.RealityOpts, + HTTPOpts: p.HTTPOpts, + HTTP2Opts: p.HTTP2Opts, + GrpcOpts: p.GrpcOpts, + WSOpts: p.WSOpts, + PacketAddr: p.PacketAddr, + XUDP: p.XUDP, + PacketEncoding: p.PacketEncoding, + GlobalPadding: p.GlobalPadding, + AuthenticatedLength: p.AuthenticatedLength, + ClientFingerprint: p.ClientFingerprint, + } +} diff --git a/model/sub.go b/model/sub.go index 7d255b6..2bab7b7 100644 --- a/model/sub.go +++ b/model/sub.go @@ -12,3 +12,7 @@ type Subscription struct { Rules []string `yaml:"rules,omitempty"` RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"` } + +type NodeList struct { + Proxies []Proxy `yaml:"proxies,omitempty"` +} diff --git a/parser/base64.go b/parser/base64.go index 4909d04..b14d423 100644 --- a/parser/base64.go +++ b/parser/base64.go @@ -2,9 +2,14 @@ package parser import ( "encoding/base64" + "strings" ) func DecodeBase64(s string) (string, error) { + s = strings.TrimSpace(s) + if len(s)%4 != 0 { + s += strings.Repeat("=", 4-len(s)%4) + } decodeStr, err := base64.StdEncoding.DecodeString(s) if err != nil { return "", err diff --git a/parser/ss.go b/parser/ss.go index a654fdf..11eceb2 100644 --- a/parser/ss.go +++ b/parser/ss.go @@ -1,7 +1,7 @@ package parser import ( - "fmt" + "errors" "net/url" "strconv" "strings" @@ -12,35 +12,35 @@ import ( func ParseSS(proxy string) (model.Proxy, error) { // 判断是否以 ss:// 开头 if !strings.HasPrefix(proxy, "ss://") { - return model.Proxy{}, fmt.Errorf("invalid ss Url") + return model.Proxy{}, errors.New("invalid ss Url") } // 分割 parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2) if len(parts) != 2 { - return model.Proxy{}, fmt.Errorf("invalid ss Url") + return model.Proxy{}, errors.New("invalid ss Url") } if !strings.Contains(parts[0], ":") { // 解码 decoded, err := DecodeBase64(parts[0]) if err != nil { - return model.Proxy{}, err + return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) } parts[0] = decoded } credentials := strings.SplitN(parts[0], ":", 2) if len(credentials) != 2 { - return model.Proxy{}, fmt.Errorf("invalid ss Url") + return model.Proxy{}, errors.New("invalid ss Url") } // 分割 serverInfo := strings.SplitN(parts[1], "#", 2) serverAndPort := strings.SplitN(serverInfo[0], ":", 2) if len(serverAndPort) != 2 { - return model.Proxy{}, fmt.Errorf("invalid ss Url") + return model.Proxy{}, errors.New("invalid ss Url") } // 转换端口字符串为数字 port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) if err != nil { - return model.Proxy{}, err + return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) } // 返回结果 result := model.Proxy{ @@ -56,7 +56,7 @@ func ParseSS(proxy string) (model.Proxy, error) { if len(serverInfo) == 2 { unescape, err := url.QueryUnescape(serverInfo[1]) if err != nil { - return model.Proxy{}, err + return model.Proxy{}, errors.New("invalid ss Url" + err.Error()) } result.Name = strings.TrimSpace(unescape) } else { diff --git a/parser/vless.go b/parser/vless.go index 5e56467..1a0c326 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -47,7 +47,7 @@ func ParseVless(proxy string) (model.Proxy, error) { Flow: params.Get("flow"), Fingerprint: params.Get("fp"), Servername: params.Get("sni"), - RealityOpts: model.RealityOptsStruct{ + RealityOpts: model.RealityOptions{ PublicKey: params.Get("pbk"), }, } @@ -55,16 +55,16 @@ func ParseVless(proxy string) (model.Proxy, error) { result.Alpn = strings.Split(params.Get("alpn"), ",") } if params.Get("type") == "ws" { - result.WSOpts = model.WSOptsStruct{ + result.WSOpts = model.WSOptions{ Path: params.Get("path"), - Headers: model.HeaderStruct{ - Host: params.Get("host"), + Headers: map[string]string{ + "Host": params.Get("host"), }, } } if params.Get("type") == "grpc" { - result.GRPCOpts = model.GRPCOptsStruct{ - GRPCServiceName: params.Get("serviceName"), + result.GrpcOpts = model.GrpcOptions{ + GrpcServiceName: params.Get("serviceName"), } } // 如果有节点名称 diff --git a/parser/vmess.go b/parser/vmess.go index 60b846c..b7a1e9b 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -3,7 +3,6 @@ package parser import ( "encoding/json" "errors" - "fmt" "strconv" "strings" "sub2clash/model" @@ -12,24 +11,42 @@ import ( func ParseVmess(proxy string) (model.Proxy, error) { // 判断是否以 vmess:// 开头 if !strings.HasPrefix(proxy, "vmess://") { - return model.Proxy{}, fmt.Errorf("invalid vmess Url") + return model.Proxy{}, errors.New("invalid vmess url") } // 解码 base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) if err != nil { - return model.Proxy{}, errors.New("无效的 vmess Url") + return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) } // 解析 - var vmess model.Vmess + var vmess model.VmessJson err = json.Unmarshal([]byte(base64), &vmess) if err != nil { - return model.Proxy{}, errors.New("无效的 vmess Url") + return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) } - // 处理端口 - port, err := strconv.Atoi(strings.TrimSpace(vmess.Port)) - if err != nil { - return model.Proxy{}, errors.New("无效的 vmess Url") + // 解析端口 + port := 0 + switch vmess.Port.(type) { + case string: + port, err = strconv.Atoi(vmess.Port.(string)) + if err != nil { + return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) + } + case float64: + port = int(vmess.Port.(float64)) } + // 解析Aid + aid := 0 + switch vmess.Aid.(type) { + case string: + aid, err = strconv.Atoi(vmess.Aid.(string)) + if err != nil { + return model.Proxy{}, errors.New("invalid vmess url" + err.Error()) + } + case float64: + aid = int(vmess.Aid.(float64)) + } + // 设置默认值 if vmess.Scy == "" { vmess.Scy = "auto" } @@ -46,7 +63,7 @@ func ParseVmess(proxy string) (model.Proxy, error) { Server: vmess.Add, Port: port, UUID: vmess.Id, - AlterID: vmess.Aid, + AlterID: aid, Cipher: vmess.Scy, UDP: true, TLS: vmess.Tls == "tls", @@ -57,10 +74,10 @@ func ParseVmess(proxy string) (model.Proxy, error) { Network: vmess.Net, } if vmess.Net == "ws" { - result.WSOpts = model.WSOptsStruct{ + result.WSOpts = model.WSOptions{ Path: vmess.Path, - Headers: model.HeaderStruct{ - Host: vmess.Host, + Headers: map[string]string{ + "Host": vmess.Host, }, } } diff --git a/templates/template_meta.yaml b/templates/template_meta.yaml index 8885bf4..5d47b72 100644 --- a/templates/template_meta.yaml +++ b/templates/template_meta.yaml @@ -111,9 +111,11 @@ rules: - GEOSITE,microsoft,Microsoft - GEOSITE,apple,Apple - GEOSITE,netflix,Netflix + - GEOIP,netflix,Netflix - GEOSITE,onedrive,Onedrive - GEOSITE,youtube,Youtube - GEOSITE,telegram,Telegram + - GEOIP,telegram,Telegram - GEOSITE,openai,OpenAI - GEOSITE,bilibili,哔哩哔哩 - GEOSITE,bahamut,巴哈姆特 diff --git a/utils/proxy.go b/utils/proxy.go index 7868791..a6ba7bd 100644 --- a/utils/proxy.go +++ b/utils/proxy.go @@ -1,12 +1,14 @@ package utils import ( + "go.uber.org/zap" "strings" + "sub2clash/logger" "sub2clash/model" "sub2clash/parser" ) -func GetContryName(proxy model.Proxy) string { +func GetContryName(countryKey string) string { // 创建一个切片包含所有的国家映射 countryMaps := []map[string]string{ model.CountryFlag, @@ -16,14 +18,34 @@ func GetContryName(proxy model.Proxy) string { } // 对每一个映射进行检查 - for _, countryMap := range countryMaps { + for i, countryMap := range countryMaps { + if i == 2 { + // 对ISO匹配做特殊处理 + // 根据常用分割字符分割字符串 + splitChars := []string{"-", "_", " "} + key := make([]string, 0) + for _, splitChar := range splitChars { + slic := strings.Split(countryKey, splitChar) + for _, v := range slic { + if len(v) == 2 { + key = append(key, v) + } + } + } + // 对每一个分割后的字符串进行检查 + for _, v := range key { + // 如果匹配到了国家 + if country, ok := countryMap[strings.ToUpper(v)]; ok { + return country + } + } + } for k, v := range countryMap { - if strings.Contains(proxy.Name, k) { + if strings.Contains(countryKey, k) { return v } } } - return "其他地区" } @@ -39,7 +61,7 @@ func AddProxy( } sub.Proxies = append(sub.Proxies, proxy) haveProxyGroup := false - countryName := GetContryName(proxy) + countryName := GetContryName(proxy.Name) for i := range sub.ProxyGroups { group := &sub.ProxyGroups[i] @@ -106,6 +128,10 @@ func ParseProxy(proxies ...string) []model.Proxy { } if err == nil { result = append(result, proxyItem) + } else { + logger.Logger.Debug( + "parse proxy failed", zap.String("proxy", proxy), zap.Error(err), + ) } } } diff --git a/validator/sub.go b/validator/sub.go index aca9116..14e0fff 100644 --- a/validator/sub.go +++ b/validator/sub.go @@ -29,6 +29,7 @@ type SubValidator struct { Replace string `form:"replace" binding:""` ReplaceKeys []string `form:"-" binding:""` ReplaceTo []string `form:"-" binding:""` + NodeListMode bool `form:"nodeList,default=false" binding:""` } type RuleProviderStruct struct {