From ff3bd636565a6587fba78a3dcc3c89e69e2741f3 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 3 Jul 2026 09:31:00 +0200 Subject: [PATCH] feat(sub): serve the HTML info page for browser requests on JSON and Clash URLs Opening the /json or /clash subscription URL in a browser dumped raw JSON/YAML while the base64 URL rendered the info page. Extract the browser-detection and page-rendering branch from subs into maybeServeSubPage and run it first in all three handlers, so every subscription URL shows the same info page in a browser while client apps keep receiving the raw body. Closes #5348 --- internal/sub/controller.go | 79 ++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/internal/sub/controller.go b/internal/sub/controller.go index 04b97e315..8e68bcf5c 100644 --- a/internal/sub/controller.go +++ b/internal/sub/controller.go @@ -146,18 +146,54 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) { } } -// subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data. -func (a *SUBController) subs(c *gin.Context) { - subId := c.Param("subid") - scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c) - subReq := a.subService.ForRequest(host) - // The remark template's per-client info is for the content a client app - // imports — the raw subscription body. A browser viewing the HTML info page - // gets clean, name-only remarks (usage is shown in the page summary). +// maybeServeSubPage renders the HTML info page when the request comes from a +// browser (Accept: text/html) or explicitly asks for it (?html=1 or ?view=html). +// It reports whether the request was handled. The remark template's per-client +// info is for the content a client app imports — the raw subscription body. A +// browser viewing the HTML info page gets clean, name-only remarks (usage is +// shown in the page summary). +func (a *SUBController) maybeServeSubPage(c *gin.Context) bool { accept := c.GetHeader("Accept") wantsHTML := strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") - subReq.subscriptionBody = !wantsHTML + if !wantsHTML { + return false + } + subId := c.Param("subid") + _, host, _, hostHeader := a.subService.ResolveRequest(c) + subReq := a.subService.ForRequest(host) + subReq.subscriptionBody = false subs, emails, lastOnline, traffic, err := subReq.getSubs(subId) + if err != nil || len(subs) == 0 { + writeSubError(c, err) + return true + } + subURL, subJsonURL, subClashURL := subReq.BuildURLs(a.subPath, a.subJsonPath, a.subClashPath, subId) + if !a.jsonEnabled { + subJsonURL = "" + } + if !a.clashEnabled { + subClashURL = "" + } + basePath, exists := c.Get("base_path") + if !exists { + basePath = "/" + } + basePathStr := basePath.(string) + page := subReq.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, emails, subURL, subJsonURL, subClashURL, basePathStr, a.subTitle, a.subSupportUrl) + a.serveSubPage(c, basePathStr, page) + return true +} + +// subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data. +func (a *SUBController) subs(c *gin.Context) { + if a.maybeServeSubPage(c) { + return + } + subId := c.Param("subid") + scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c) + subReq := a.subService.ForRequest(host) + subReq.subscriptionBody = true + subs, _, _, traffic, err := subReq.getSubs(subId) if err != nil || len(subs) == 0 { writeSubError(c, err) } else { @@ -167,25 +203,6 @@ func (a *SUBController) subs(c *gin.Context) { result.WriteString("\n") } - // If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here - if wantsHTML { - subURL, subJsonURL, subClashURL := subReq.BuildURLs(a.subPath, a.subJsonPath, a.subClashPath, subId) - if !a.jsonEnabled { - subJsonURL = "" - } - if !a.clashEnabled { - subClashURL = "" - } - basePath, exists := c.Get("base_path") - if !exists { - basePath = "/" - } - basePathStr := basePath.(string) - page := subReq.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, emails, subURL, subJsonURL, subClashURL, basePathStr, a.subTitle, a.subSupportUrl) - a.serveSubPage(c, basePathStr, page) - return - } - // Add headers header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) profileUrl := a.subProfileUrl @@ -366,6 +383,9 @@ func (a *SUBController) loadSubTemplate(themeDir string) (*template.Template, er // subJsons handles HTTP requests for JSON subscription configurations. func (a *SUBController) subJsons(c *gin.Context) { + if a.maybeServeSubPage(c) { + return + } subId := c.Param("subid") scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c) jsonSub, header, err := a.subJsonService.GetJson(subId, host) @@ -383,6 +403,9 @@ func (a *SUBController) subJsons(c *gin.Context) { } func (a *SUBController) subClashs(c *gin.Context) { + if a.maybeServeSubPage(c) { + return + } subId := c.Param("subid") scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c) clashSub, header, err := a.subClashService.GetClash(subId, host)