From 11cdb07e89cd506908ee4cb743c096dcc411fe6e Mon Sep 17 00:00:00 2001 From: root Date: Fri, 24 Apr 2026 11:25:10 +0800 Subject: [PATCH] feat: add Clash YAML subscription endpoint with template injection Add /clash/:subid endpoint that returns complete Clash YAML config. User provides full template (DNS, routing, proxy-groups, rules) in settings, panel generates proxies from inbound/client data and injects via proxies: [] placeholder replacement. - New SubClashService reads template, generates vmess/vless/trojan/ss proxy entries with transport (ws/grpc/h2/tcp/httpupgrade), TLS, and Reality support - Settings: subClashEnable, subClashPath, subClashURI, subClashTemplate - UI: Clash settings tab, QR code on subpage, Desktop dropdown with clash-verge:// deep link preferring Clash URL - Version bump to v1.5.2-beta --- config/version | 2 +- .../2026-04-24-clash-yaml-subscription.md | 30 ++ sub/sub.go | 18 +- sub/subClashService.go | 350 ++++++++++++++++++ sub/subController.go | 48 ++- web/assets/js/model/setting.js | 4 + web/assets/js/subscription.js | 13 + web/entity/entity.go | 6 + web/html/inbounds.html | 4 + web/html/modals/inbound_info_modal.html | 17 + web/html/modals/qrcode_modal.html | 14 + web/html/settings.html | 11 + .../settings/panel/subscription/clash.html | 51 +++ .../settings/panel/subscription/general.html | 7 + .../settings/panel/subscription/subpage.html | 42 ++- web/service/setting.go | 44 ++- web/translation/translate.en_US.toml | 1 + web/translation/translate.zh_CN.toml | 1 + 18 files changed, 646 insertions(+), 17 deletions(-) create mode 100644 docs/Tasktracking/2026-04-24-clash-yaml-subscription.md create mode 100644 sub/subClashService.go create mode 100644 web/html/settings/panel/subscription/clash.html diff --git a/config/version b/config/version index 53b5bbb1..89ab8a23 100644 --- a/config/version +++ b/config/version @@ -1 +1 @@ -v1.5.1 +v1.5.2-beta \ No newline at end of file diff --git a/docs/Tasktracking/2026-04-24-clash-yaml-subscription.md b/docs/Tasktracking/2026-04-24-clash-yaml-subscription.md new file mode 100644 index 00000000..3b3db3ff --- /dev/null +++ b/docs/Tasktracking/2026-04-24-clash-yaml-subscription.md @@ -0,0 +1,30 @@ +# Clash YAML Subscription Endpoint + +## Date: 2026-04-24 + +## Changes + +### New Files +- `sub/subClashService.go` — Clash YAML subscription service: reads user template, generates proxies from inbound/client data, injects via `proxies: []` placeholder replacement +- `web/html/settings/panel/subscription/clash.html` — Clash subscription settings panel (path, URI, template textarea) + +### Backend +- `web/entity/entity.go` — Added `SubClashEnable`, `SubClashPath`, `SubClashURI`, `SubClashTemplate` to `AllSetting` +- `web/service/setting.go` — Added defaults, getter functions, `GetSubSettings()` auto-build URI for Clash +- `sub/sub.go` — Read Clash settings, pass to controller +- `sub/subController.go` — Added Clash fields, route `GET /clash/:subid`, `clashSubs()` handler returning `text/yaml` + +### Frontend +- `web/html/settings.html` — Added Clash settings tab (key 6) +- `web/html/settings/panel/subscription/general.html` — Added Clash enable toggle +- `web/html/settings/panel/subscription/subpage.html` — Added Clash QR code, Desktop dropdown with Clash Verge deep link +- `web/assets/js/subscription.js` — Added `subClashUrl`, `clashvergeUrl` prefers Clash URL +- `web/assets/js/model/setting.js` — Added Clash defaults +- `web/html/inbounds.html` — Added `subClashEnable`, `subClashURI` to subscription settings +- `web/html/modals/qrcode_modal.html` — Added Clash QR code + `genSubClashLink()` +- `web/html/modals/inbound_info_modal.html` — Added Clash subscription link +- `web/translation/translate.en_US.toml` — Added `subClashEnable` i18n +- `web/translation/translate.zh_CN.toml` — Added `subClashEnable` i18n + +### Version +- `config/version` — Bumped to v1.5.2-beta diff --git a/sub/sub.go b/sub/sub.go index 7d1fde9f..c6d87c97 100644 --- a/sub/sub.go +++ b/sub/sub.go @@ -227,6 +227,21 @@ func (s *Server) initRouter() (*gin.Engine, error) { SubRoutingRules = "" } + subClashEnable, err := s.settingService.GetSubClashEnable() + if err != nil { + subClashEnable = false + } + + SubClashPath, err := s.settingService.GetSubClashPath() + if err != nil { + SubClashPath = "/clash/" + } + + SubClashTemplate, err := s.settingService.GetSubClashTemplate() + if err != nil { + SubClashTemplate = "" + } + // set per-request localizer from headers/cookies engine.Use(locale.LocalizerMiddleware()) @@ -307,7 +322,8 @@ func (s *Server) initRouter() (*gin.Engine, error) { s.sub = NewSUBController( g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates, SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl, - SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules) + SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules, + subClashEnable, SubClashPath, SubClashTemplate) return engine, nil } diff --git a/sub/subClashService.go b/sub/subClashService.go new file mode 100644 index 00000000..bb58836a --- /dev/null +++ b/sub/subClashService.go @@ -0,0 +1,350 @@ +package sub + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/mhsanaei/3x-ui/v2/database/model" + "github.com/mhsanaei/3x-ui/v2/logger" + "github.com/mhsanaei/3x-ui/v2/web/service" + "github.com/mhsanaei/3x-ui/v2/xray" +) + +// SubClashService handles Clash YAML subscription generation. +type SubClashService struct { + template string + inboundService service.InboundService + SubService *SubService +} + +// NewSubClashService creates a new Clash subscription service with the given template. +func NewSubClashService(template string, subService *SubService) *SubClashService { + return &SubClashService{ + template: template, + SubService: subService, + } +} + +// GetClash generates a Clash YAML configuration for the given subscription ID and host. +func (s *SubClashService) GetClash(subId string, host string) (string, string, error) { + if s.template == "" { + return "", "", fmt.Errorf("clash template is empty") + } + + inbounds, err := s.SubService.getInboundsBySubId(subId) + if err != nil || len(inbounds) == 0 { + return "", "", err + } + + var header string + var traffic xray.ClientTraffic + var clientTraffics []xray.ClientTraffic + var proxies []string + + for _, inbound := range inbounds { + clients, err := s.inboundService.GetClients(inbound) + if err != nil { + logger.Error("SubClashService - GetClients: Unable to get clients from inbound") + } + if clients == nil { + continue + } + if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' { + listen, port, streamSettings, err := s.SubService.getFallbackMaster(inbound.Listen, inbound.StreamSettings) + if err == nil { + inbound.Listen = listen + inbound.Port = port + inbound.StreamSettings = streamSettings + } + } + + for _, client := range clients { + if client.Enable && client.SubID == subId { + clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email)) + newProxies := s.getProxy(inbound, client) + proxies = append(proxies, newProxies...) + } + } + } + + if len(proxies) == 0 { + return "", "", nil + } + + // Aggregate traffic stats + for index, clientTraffic := range clientTraffics { + if index == 0 { + traffic.Up = clientTraffic.Up + traffic.Down = clientTraffic.Down + traffic.Total = clientTraffic.Total + if clientTraffic.ExpiryTime > 0 { + traffic.ExpiryTime = clientTraffic.ExpiryTime + } + } else { + traffic.Up += clientTraffic.Up + traffic.Down += clientTraffic.Down + if traffic.Total == 0 || clientTraffic.Total == 0 { + traffic.Total = 0 + } else { + traffic.Total += clientTraffic.Total + } + if clientTraffic.ExpiryTime != traffic.ExpiryTime { + traffic.ExpiryTime = 0 + } + } + } + + // Build proxies YAML block + proxiesYaml := "" + for _, p := range proxies { + proxiesYaml += " - " + p + "\n" + } + + // Inject proxies into template by replacing "proxies: []" + result := strings.Replace(s.template, "proxies: []", "proxies:\n"+proxiesYaml, 1) + + header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) + return result, header, nil +} + +// getProxy generates Clash proxy entries for a client. +func (s *SubClashService) getProxy(inbound *model.Inbound, client model.Client) []string { + var proxies []string + var stream map[string]any + json.Unmarshal([]byte(inbound.StreamSettings), &stream) + + // Resolve address + var address string + if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { + address = s.SubService.address + } else { + address = inbound.Listen + } + + // Get remark + remark := s.SubService.genRemark(inbound, client.Email, "") + + // Parse stream settings + network, _ := stream["network"].(string) + security, _ := stream["security"].(string) + + // Handle external proxies + externalProxies, ok := stream["externalProxy"].([]any) + if !ok || len(externalProxies) == 0 { + externalProxies = []any{ + map[string]any{ + "forceTls": "same", + }, + } + } + + for _, ep := range externalProxies { + externalProxy, _ := ep.(map[string]any) + destAddress := address + destPort := inbound.Port + + if dest, ok := externalProxy["dest"].(string); ok && dest != "" { + destAddress = dest + } + if port, ok := externalProxy["port"].(float64); ok && port > 0 { + destPort = int(port) + } + + forceTls, _ := externalProxy["forceTls"].(string) + tlsEnabled := false + switch forceTls { + case "tls": + tlsEnabled = true + case "none": + tlsEnabled = false + default: // "same" + tlsEnabled = security == "tls" || security == "reality" + } + + remarkExtra := remark + if customRemark, ok := externalProxy["remark"].(string); ok && customRemark != "" { + remarkExtra = customRemark + } + + proxy := s.buildProxyEntry(inbound, client, destAddress, destPort, network, security, tlsEnabled, remarkExtra, stream) + proxies = append(proxies, proxy) + } + + return proxies +} + +// buildProxyEntry builds a single Clash proxy entry as an inline YAML map string. +func (s *SubClashService) buildProxyEntry(inbound *model.Inbound, client model.Client, address string, port int, network, security string, tlsEnabled bool, remark string, stream map[string]any) string { + var parts []string + + parts = append(parts, fmt.Sprintf("name: %q", remark)) + + // Protocol-specific fields + switch inbound.Protocol { + case model.VMESS: + parts = append(parts, "type: vmess") + parts = append(parts, fmt.Sprintf("server: %s", address)) + parts = append(parts, fmt.Sprintf("port: %d", port)) + parts = append(parts, fmt.Sprintf("uuid: %s", client.ID)) + parts = append(parts, "alterId: 0") + parts = append(parts, "cipher: auto") + + case model.VLESS: + parts = append(parts, "type: vless") + parts = append(parts, fmt.Sprintf("server: %s", address)) + parts = append(parts, fmt.Sprintf("port: %d", port)) + parts = append(parts, fmt.Sprintf("uuid: %s", client.ID)) + if client.Flow != "" { + parts = append(parts, fmt.Sprintf("flow: %s", client.Flow)) + } + + case model.Trojan: + parts = append(parts, "type: trojan") + parts = append(parts, fmt.Sprintf("server: %s", address)) + parts = append(parts, fmt.Sprintf("port: %d", port)) + parts = append(parts, fmt.Sprintf("password: %s", client.Password)) + + case model.Shadowsocks: + parts = append(parts, "type: ss") + parts = append(parts, fmt.Sprintf("server: %s", address)) + parts = append(parts, fmt.Sprintf("port: %d", port)) + cipher, password := s.parseShadowsocksSettings(client) + parts = append(parts, fmt.Sprintf("cipher: %s", cipher)) + parts = append(parts, fmt.Sprintf("password: %s", password)) + parts = append(parts, "udp: true") + return strings.Join(parts, "\n ") + } + + // TLS settings + if tlsEnabled { + parts = append(parts, "tls: true") + if security == "reality" { + realitySetting, _ := stream["realitySettings"].(map[string]any) + if publicKey, ok := realitySetting["publicKey"].(string); ok && publicKey != "" { + realityOpts := fmt.Sprintf("reality-opts:\n public-key: %s", publicKey) + if shortId, ok := realitySetting["shortId"].(string); ok && shortId != "" { + realityOpts += fmt.Sprintf("\n short-id: %s", shortId) + } + parts = append(parts, realityOpts) + } + // Reality server names + serverNames, _ := realitySetting["serverNames"].([]any) + if len(serverNames) > 0 { + sni := fmt.Sprintf("%v", serverNames[0]) + parts = append(parts, fmt.Sprintf("sni: %s", sni)) + } + } else { + // TLS settings + tlsSetting, _ := stream["tlsSettings"].(map[string]any) + if serverName, ok := tlsSetting["serverName"].(string); ok && serverName != "" { + parts = append(parts, fmt.Sprintf("sni: %s", serverName)) + } + if alpn, ok := tlsSetting["alpn"].([]any); ok && len(alpn) > 0 { + alpnStrs := make([]string, len(alpn)) + for i, a := range alpn { + alpnStrs[i] = fmt.Sprintf("%v", a) + } + parts = append(parts, fmt.Sprintf("alpn: [%s]", strings.Join(alpnStrs, ", "))) + } + } + // Fingerprint + if fp, ok := stream["fingerprint"].(string); ok && fp != "" { + parts = append(parts, fmt.Sprintf("client-fingerprint: %s", fp)) + } + } else { + parts = append(parts, "tls: false") + } + + parts = append(parts, "udp: true") + + // Network-specific settings + switch network { + case "ws": + ws, _ := stream["wsSettings"].(map[string]any) + if path, ok := ws["path"].(string); ok && path != "" { + wsOpts := fmt.Sprintf("ws-opts:\n path: %s", path) + if host, ok := ws["host"].(string); ok && host != "" { + wsOpts += fmt.Sprintf("\n headers:\n Host: %s", host) + } else { + headers, _ := ws["headers"].(map[string]any) + if h, ok := headers["Host"].(string); ok && h != "" { + wsOpts += fmt.Sprintf("\n headers:\n Host: %s", h) + } + } + parts = append(parts, wsOpts) + } + + case "grpc": + grpc, _ := stream["grpcSettings"].(map[string]any) + if serviceName, ok := grpc["serviceName"].(string); ok && serviceName != "" { + parts = append(parts, fmt.Sprintf("grpc-opts:\n grpc-service-name: %s", serviceName)) + } + + case "h2": + h2, _ := stream["h2Settings"].(map[string]any) + if path, ok := h2["path"].(string); ok && path != "" { + h2Opts := fmt.Sprintf("h2-opts:\n path: %s", path) + if host, ok := h2["host"].([]any); ok && len(host) > 0 { + hostStrs := make([]string, len(host)) + for i, h := range host { + hostStrs[i] = fmt.Sprintf("%v", h) + } + h2Opts += fmt.Sprintf("\n host: [%s]", strings.Join(hostStrs, ", ")) + } + parts = append(parts, h2Opts) + } + + case "tcp": + tcp, _ := stream["tcpSettings"].(map[string]any) + header, _ := tcp["header"].(map[string]any) + if typeStr, ok := header["type"].(string); ok && typeStr == "http" { + request, _ := header["request"].(map[string]any) + httpOpts := "http-opts:" + if path, ok := request["path"].([]any); ok && len(path) > 0 { + httpOpts += fmt.Sprintf("\n path:\n - %v", path[0]) + } + if headers, ok := request["headers"].(map[string]any); ok && len(headers) > 0 { + httpOpts += "\n headers:" + for k, v := range headers { + if vals, ok := v.([]any); ok && len(vals) > 0 { + httpOpts += fmt.Sprintf("\n %s:\n - %v", k, vals[0]) + } + } + } + parts = append(parts, httpOpts) + } + + case "httpupgrade": + hu, _ := stream["httpupgradeSettings"].(map[string]any) + if path, ok := hu["path"].(string); ok && path != "" { + huOpts := fmt.Sprintf("httpupgrade-opts:\n path: %s", path) + if host, ok := hu["host"].(string); ok && host != "" { + huOpts += fmt.Sprintf("\n host: %s", host) + } else { + headers, _ := hu["headers"].(map[string]any) + if h, ok := headers["Host"].(string); ok && h != "" { + huOpts += fmt.Sprintf("\n host: %s", h) + } + } + parts = append(parts, huOpts) + } + } + + return strings.Join(parts, "\n ") +} + +// parseShadowsocksSettings extracts cipher and password from shadowsocks client settings. +func (s *SubClashService) parseShadowsocksSettings(client model.Client) (string, string) { + // Default cipher + cipher := "aes-128-gcm" + password := client.Password + + // Try to parse the method from the client's settings + // Shadowsocks protocol stores method in client.ID for some configurations + if client.Security != "" { + cipher = client.Security + } + + return cipher, password +} diff --git a/sub/subController.go b/sub/subController.go index 79ea755d..38648af8 100644 --- a/sub/subController.go +++ b/sub/subController.go @@ -25,8 +25,12 @@ type SUBController struct { subEncrypt bool updateInterval string - subService *SubService - subJsonService *SubJsonService + clashEnabled bool + subClashPath string + + subService *SubService + subJsonService *SubJsonService + subClashService *SubClashService } // NewSUBController creates a new subscription controller with the given configuration. @@ -49,6 +53,9 @@ func NewSUBController( subAnnounce string, subEnableRouting bool, subRoutingRules string, + clashEnabled bool, + subClashPath string, + subClashTemplate string, ) *SUBController { sub := NewSubService(showInfo, rModel) a := &SUBController{ @@ -64,8 +71,12 @@ func NewSUBController( subEncrypt: encrypt, updateInterval: update, - subService: sub, - subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub), + clashEnabled: clashEnabled, + subClashPath: subClashPath, + + subService: sub, + subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub), + subClashService: NewSubClashService(subClashTemplate, sub), } a.initRouter(g) return a @@ -80,6 +91,10 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) { gJson := g.Group(a.subJsonPath) gJson.GET(":subid", a.subJsons) } + if a.clashEnabled { + gClash := g.Group(a.subClashPath) + gClash.GET(":subid", a.clashSubs) + } } // subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data. @@ -103,6 +118,10 @@ func (a *SUBController) subs(c *gin.Context) { if !a.jsonEnabled { subJsonURL = "" } + subClashURL := "" + if a.clashEnabled { + subClashURL = a.subService.buildSingleURL("", scheme, hostWithPort, a.subClashPath, subId) + } // Get base_path from context (set by middleware) basePath, exists := c.Get("base_path") if !exists { @@ -136,6 +155,7 @@ func (a *SUBController) subs(c *gin.Context) { "totalByte": page.TotalByte, "subUrl": page.SubUrl, "subJsonUrl": page.SubJsonUrl, + "subClashUrl": subClashURL, "result": page.Result, }) return @@ -176,6 +196,26 @@ func (a *SUBController) subJsons(c *gin.Context) { } } +// clashSubs handles HTTP requests for Clash YAML subscription configurations. +func (a *SUBController) clashSubs(c *gin.Context) { + subId := c.Param("subid") + scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c) + clashYaml, header, err := a.subClashService.GetClash(subId, host) + if err != nil || len(clashYaml) == 0 { + c.String(400, "Error!") + } else { + // Add headers + profileUrl := a.subProfileUrl + if profileUrl == "" { + profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI) + } + a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules) + + c.Header("Content-Type", "text/yaml; charset=utf-8") + c.String(200, clashYaml) + } +} + // ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title. func (a *SUBController) ApplyCommonHeaders( c *gin.Context, diff --git a/web/assets/js/model/setting.js b/web/assets/js/model/setting.js index 8814bf43..a3c15dca 100644 --- a/web/assets/js/model/setting.js +++ b/web/assets/js/model/setting.js @@ -52,6 +52,10 @@ class AllSetting { this.subJsonNoises = ""; this.subJsonMux = ""; this.subJsonRules = ""; + this.subClashEnable = false; + this.subClashPath = "/clash/"; + this.subClashURI = ""; + this.subClashTemplate = ""; this.timeLocation = "Local"; diff --git a/web/assets/js/subscription.js b/web/assets/js/subscription.js index 228dcfa0..9cc496df 100644 --- a/web/assets/js/subscription.js +++ b/web/assets/js/subscription.js @@ -9,6 +9,7 @@ sId: el.getAttribute('data-sid') || '', subUrl: el.getAttribute('data-sub-url') || '', subJsonUrl: el.getAttribute('data-subjson-url') || '', + subClashUrl: el.getAttribute('data-subclash-url') || '', download: el.getAttribute('data-download') || '', upload: el.getAttribute('data-upload') || '', used: el.getAttribute('data-used') || '', @@ -99,6 +100,8 @@ const tpl = document.getElementById('subscription-data'); const sj = tpl ? tpl.getAttribute('data-subjson-url') : ''; if (sj) this.app.subJsonUrl = sj; + const sc = tpl ? tpl.getAttribute('data-subclash-url') : ''; + if (sc) this.app.subClashUrl = sc; drawQR(this.app.subUrl); try { const elJson = document.getElementById('qrcode-subjson'); @@ -106,6 +109,12 @@ new QRious({ element: elJson, value: this.app.subJsonUrl, size: 220 }); } } catch (e) { /* ignore */ } + try { + const elClash = document.getElementById('qrcode-subclash'); + if (elClash && this.app.subClashUrl) { + new QRious({ element: elClash, value: this.app.subClashUrl, size: 220 }); + } + } catch (e) { /* ignore */ } this._onResize = () => { this.viewportWidth = window.innerWidth; }; window.addEventListener('resize', this._onResize); }, @@ -145,6 +154,10 @@ }, happUrl() { return `happ://add/${this.app.subUrl}`; + }, + clashvergeUrl() { + const url = this.app.subClashUrl || this.app.subUrl; + return `clash-verge://install-config?url=${encodeURIComponent(url)}&name=${encodeURIComponent(this.app.sId)}`; } }, methods: { diff --git a/web/entity/entity.go b/web/entity/entity.go index c163591a..f0214e92 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -81,6 +81,12 @@ type AllSetting struct { SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` // JSON subscription mux configuration SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` + // Clash subscription settings + SubClashEnable bool `json:"subClashEnable" form:"subClashEnable"` // Enable Clash subscription endpoint + SubClashPath string `json:"subClashPath" form:"subClashPath"` // Path for Clash subscription endpoint + SubClashURI string `json:"subClashURI" form:"subClashURI"` // Clash subscription server URI + SubClashTemplate string `json:"subClashTemplate" form:"subClashTemplate"` // Clash YAML template content + // LDAP settings LdapEnable bool `json:"ldapEnable" form:"ldapEnable"` LdapHost string `json:"ldapHost" form:"ldapHost"` diff --git a/web/html/inbounds.html b/web/html/inbounds.html index f0c7fff2..c8f7363f 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -748,6 +748,8 @@ subURI: '', subJsonURI: '', subJsonEnable: false, + subClashEnable: false, + subClashURI: '', }, remarkModel: '-ieo', datepicker: 'gregorian', @@ -812,6 +814,8 @@ subURI: subURI, subJsonURI: subJsonURI, subJsonEnable: subJsonEnable, + subClashEnable: subClashEnable, + subClashURI: subClashURI, }; this.pageSize = pageSize; this.remarkModel = remarkModel; diff --git a/web/html/modals/inbound_info_modal.html b/web/html/modals/inbound_info_modal.html index d67dfc53..b17a2706 100644 --- a/web/html/modals/inbound_info_modal.html +++ b/web/html/modals/inbound_info_modal.html @@ -365,6 +365,18 @@ [[ infoModal.subJsonLink ]] + + + Clash Link + + + + + [[ + infoModal.subClashLink ]] + {{ template "settings/panel/subscription/json" . }} + + + {{ template "settings/panel/subscription/clash" . }} + @@ -623,6 +630,10 @@ const subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath; if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}'); } + if (this.allSetting.subClashEnable) { + const subClashPath = this.allSetting.subClashURI.length > 0 ? new URL(this.allSetting.subClashURI).pathname : this.allSetting.subClashPath; + if (subClashPath == '/clash/') alerts.push('Consider changing the default Clash subscription path for security.'); + } return alerts } } diff --git a/web/html/settings/panel/subscription/clash.html b/web/html/settings/panel/subscription/clash.html new file mode 100644 index 00000000..12a260af --- /dev/null +++ b/web/html/settings/panel/subscription/clash.html @@ -0,0 +1,51 @@ +{{define "settings/panel/subscription/clash"}} + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/web/html/settings/panel/subscription/general.html b/web/html/settings/panel/subscription/general.html index 5d83aa37..ea00bdd3 100644 --- a/web/html/settings/panel/subscription/general.html +++ b/web/html/settings/panel/subscription/general.html @@ -15,6 +15,13 @@ + + + + + diff --git a/web/html/settings/panel/subscription/subpage.html b/web/html/settings/panel/subscription/subpage.html index 4b729b8e..42f71893 100644 --- a/web/html/settings/panel/subscription/subpage.html +++ b/web/html/settings/panel/subscription/subpage.html @@ -83,7 +83,7 @@ - + {{ i18n @@ -97,7 +97,7 @@ - + {{ i18n @@ -112,6 +112,21 @@ + + + + {{ i18n + "pages.settings.subSettings"}} + Clash + + + + + + + + @@ -187,7 +202,7 @@ - + Happ + Clash Verge - + Happ + Clash Verge + + + + + + + + Desktop + + + Clash Verge @@ -242,7 +274,7 @@ -