fix(sub): apply the device limit to ?view=raw

subJsons and subClashs served the raw body and returned before enforceHwid ran,
so appending ?view=raw to a JSON or Clash subscription URL handed out a complete,
client-consumable config however many devices were already registered. The branch
exists to stop a browser's Accept: text/html from being answered with the info
page, not to skip the gate.

Gate the raw branch and leave the other gate where it was, below
maybeServeSubPage, so the HTML info page stays ungated as before.
This commit is contained in:
Sanaei
2026-09-03 18:06:35 +02:00
parent f9de0226fe
commit f17e4684e0
2 changed files with 28 additions and 9 deletions
+8 -1
View File
@@ -705,9 +705,13 @@ func (a *SUBController) loadSubTemplate(themeDir string) (*template.Template, er
return tmpl, nil return tmpl, nil
} }
// subJsons handles HTTP requests for JSON subscription configurations. // subJsons handles HTTP requests for JSON subscription configurations. The
// device limit is enforced on every body route, ?view=raw included (#GHSA-7ww3).
func (a *SUBController) subJsons(c *gin.Context) { func (a *SUBController) subJsons(c *gin.Context) {
if strings.EqualFold(c.Query("view"), "raw") { if strings.EqualFold(c.Query("view"), "raw") {
if !a.enforceHwid(c) {
return
}
if !a.serveJsonBody(c, a.jsonAlwaysArray, "application/json; charset=utf-8", true) { if !a.serveJsonBody(c, a.jsonAlwaysArray, "application/json; charset=utf-8", true) {
writeSubError(c, nil) writeSubError(c, nil)
} }
@@ -760,6 +764,9 @@ func (a *SUBController) serveJsonBody(c *gin.Context, alwaysReturnArray bool, co
func (a *SUBController) subClashs(c *gin.Context) { func (a *SUBController) subClashs(c *gin.Context) {
if strings.EqualFold(c.Query("view"), "raw") { if strings.EqualFold(c.Query("view"), "raw") {
if !a.enforceHwid(c) {
return
}
if !a.serveClashBody(c, true) { if !a.serveClashBody(c, true) {
writeSubError(c, nil) writeSubError(c, nil)
} }
+17 -5
View File
@@ -87,7 +87,17 @@ func requestSub(t *testing.T, router *gin.Engine, method string, path string, hw
func TestSubscriptionHwidGateAcrossBodyRoutes(t *testing.T) { func TestSubscriptionHwidGateAcrossBodyRoutes(t *testing.T) {
router, subID := initHwidSubRouter(t, 1) router, subID := initHwidSubRouter(t, 1)
for _, path := range []string{"/sub/" + subID, "/json/" + subID, "/clash/" + subID} { // ?view=raw only tells /json/ and /clash/ to serve the body instead of the
// HTML page, so it stays gated like the plain route (#GHSA-7ww3).
bodyRoutes := []string{
"/sub/" + subID,
"/json/" + subID,
"/clash/" + subID,
"/json/" + subID + "?view=raw",
"/clash/" + subID + "?view=RaW",
}
for _, path := range bodyRoutes {
rec := requestSub(t, router, http.MethodGet, path, "", "") rec := requestSub(t, router, http.MethodGet, path, "", "")
if rec.Code != http.StatusNotFound { if rec.Code != http.StatusNotFound {
t.Fatalf("%s missing HWID status = %d, want 404", path, rec.Code) t.Fatalf("%s missing HWID status = %d, want 404", path, rec.Code)
@@ -102,7 +112,7 @@ func TestSubscriptionHwidGateAcrossBodyRoutes(t *testing.T) {
t.Fatalf("HEAD missing HWID = %d %#v", rec.Code, rec.Header()) t.Fatalf("HEAD missing HWID = %d %#v", rec.Code, rec.Header())
} }
for _, path := range []string{"/sub/" + subID, "/json/" + subID, "/clash/" + subID} { for _, path := range bodyRoutes {
rec = requestSub(t, router, http.MethodGet, path, "device-one", "") rec = requestSub(t, router, http.MethodGet, path, "device-one", "")
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("%s registered HWID status = %d, body=%q", path, rec.Code, rec.Body.String()) t.Fatalf("%s registered HWID status = %d, body=%q", path, rec.Code, rec.Body.String())
@@ -112,12 +122,14 @@ func TestSubscriptionHwidGateAcrossBodyRoutes(t *testing.T) {
} }
} }
rec = requestSub(t, router, http.MethodGet, "/json/"+subID, "device-two", "") for _, path := range bodyRoutes {
rec = requestSub(t, router, http.MethodGet, path, "device-two", "")
if rec.Code != http.StatusNotFound { if rec.Code != http.StatusNotFound {
t.Fatalf("new HWID after limit status = %d, want 404", rec.Code) t.Fatalf("%s new HWID after limit status = %d, want 404", path, rec.Code)
} }
if rec.Header().Get("X-Hwid-Max-Devices-Reached") != "true" || rec.Header().Get("X-Hwid-Limit") != "true" { if rec.Header().Get("X-Hwid-Max-Devices-Reached") != "true" || rec.Header().Get("X-Hwid-Limit") != "true" {
t.Fatalf("limit headers missing: %#v", rec.Header()) t.Fatalf("%s limit headers missing: %#v", path, rec.Header())
}
} }
} }