mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-13 17:35:02 +03:00
fix(sub): isolate Incy subscription routing from Happ headers (#6810)
* sub: isolate Incy subscription routing from Happ headers The resolved Incy line is written to the body only when IsIncyClient matches the subscription User-Agent, Incy routing is enabled, and the resolved text is non-blank. ApplyCommonHeaders computes that same Incy check once and, for a matching client, skips the Happ routing-header block, leaving Routing-Enable, the Happ Routing value, and the JSON routing header unset. ApplyIncyHeaders is called with that incyClient flag. The new test requests the subscription as INCY/1.0.0/ios and as Happ/1.2.0 and expects the Incy response to end with the incy:// line and to have empty Routing headers, and the Happ response to keep the happ:// Routing header with Routing-Enable true and no incy:// line in the body. Fixes #6062 * fix(sub): keep the shared JSON routing profile for INCY clients The INCY isolation wrapped the whole Routing block, JSON fallback included, in !incyClient. INCY therefore lost the subJsonRoutingRules profile that #6402 sends as the Routing header so Happ and INCY fetch the geo files the baked rules reference, on the raw, JSON and Clash endpoints alike, while every other client kept it, incy:// deeplinks included. INCY accepts a routing link of any scheme in that header (docs.incy.cc/en/routing), so only the Happ tab is Happ-only: an INCY request now reads it as blank and falls back to the shared profile, as the setting text promises. Where the raw body carries INCY's own profile the header is dropped, because INCY ranks a Routing header above a base64 body line (docs.incy.cc/en/app-management) and the shared profile would override it, which is #6062 again. --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -504,11 +504,15 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
metadata := a.metadataForSubRequest(func() *SubService { return subReq }, subId, builtinProfileURL(c, scheme, hostWithPort))
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, metadata.Title, metadata.SupportURL, metadata.ProfileURL, metadata.Announce, a.subEnableRouting, a.subRoutingRules, a.subHideSettings)
|
||||
|
||||
if a.subIncyEnableRouting && a.subIncyRoutingRules != "" {
|
||||
// Incy's profile rides the raw body; other clients must not see that line (#6062).
|
||||
if IsIncyClient(userAgent) && a.subIncyEnableRouting && a.subIncyRoutingRules != "" {
|
||||
incyRules, _, err := resolveIncyRoutingSource(a.subIncyRoutingRules)
|
||||
if err == nil && strings.TrimSpace(incyRules) != "" {
|
||||
result.WriteString(incyRules)
|
||||
result.WriteString("\n")
|
||||
// INCY ranks a Routing header above a body line, so the shared
|
||||
// profile must not override INCY's own one here.
|
||||
c.Writer.Header().Del("Routing")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -957,6 +961,12 @@ func (a *SUBController) ApplyCommonHeaders(
|
||||
c.Writer.Header().Set("Announce", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileAnnounce)))
|
||||
}
|
||||
|
||||
// The Happ tab is Happ-only, so INCY reads it as blank and gets the shared
|
||||
// JSON profile instead; INCY takes a routing link of any scheme (#6062).
|
||||
incyClient := c.Request != nil && IsIncyClient(c.GetHeader("User-Agent"))
|
||||
if incyClient {
|
||||
profileEnableRouting, profileRoutingRules = false, ""
|
||||
}
|
||||
rules, remote, routingErr := resolveRoutingSource(remoteRoutingHapp, profileRoutingRules)
|
||||
if strings.TrimSpace(profileRoutingRules) == "" {
|
||||
// Happ/INCY fetch the geo files the baked rules reference through this
|
||||
@@ -981,5 +991,5 @@ func (a *SUBController) ApplyCommonHeaders(
|
||||
}
|
||||
|
||||
ApplyHappHeaders(c, a.happConfig, happManaged)
|
||||
ApplyIncyHeaders(c, a.incyConfig, a.incyConfig.AutoDetect && c.Request != nil && IsIncyClient(c.GetHeader("User-Agent")))
|
||||
ApplyIncyHeaders(c, a.incyConfig, a.incyConfig.AutoDetect && incyClient)
|
||||
}
|
||||
|
||||
@@ -802,3 +802,58 @@ func TestLegacyClashEndpointNormalizesShadowsocksCipher(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncyRoutingIsolatedFromHappHeaders(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
seedSubInbound(t, "s1", "vl", 4492, 1, `{"network":"tcp","security":"none"}`)
|
||||
gin.SetMode(gin.TestMode)
|
||||
const happRules = "happ://routing/onadd/existing-rules"
|
||||
const incyRules = "incy://routing/onadd/AbC+/="
|
||||
sharedRules := "incy://routing/onadd/" + base64.StdEncoding.EncodeToString([]byte(`{"Name":"shared","DirectSites":["geosite:category-ru"]}`))
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
path string
|
||||
userAgent string
|
||||
incyRouting bool
|
||||
wantRouting string
|
||||
wantRoutingEnable string
|
||||
wantIncyLine bool
|
||||
}{
|
||||
{name: "happ keeps its own tab and never sees the incy line", path: "/sub/s1", userAgent: "Happ/1.2.0", incyRouting: true, wantRouting: happRules, wantRoutingEnable: "true"},
|
||||
{name: "incy without its own tab gets the shared json profile", path: "/sub/s1", userAgent: "INCY/1.0.0/ios", wantRouting: sharedRules},
|
||||
{name: "incy body profile is not outranked by a routing header", path: "/sub/s1", userAgent: "INCY/1.0.0/ios", incyRouting: true, wantIncyLine: true},
|
||||
{name: "incy json documents keep the shared profile header", path: "/json/s1", userAgent: "INCY/1.0.0/ios", incyRouting: true, wantRouting: sharedRules},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
router := gin.New()
|
||||
NewSUBController(router.Group("/"),
|
||||
WithSUBEncryption(false),
|
||||
WithSUBJsonEnabled(true),
|
||||
WithSUBEnableRouting(true),
|
||||
WithSUBRoutingRules(happRules),
|
||||
WithSUBJsonRoutingRules(sharedRules),
|
||||
WithSUBIncyEnableRouting(tc.incyRouting),
|
||||
WithSUBIncyRoutingRules(incyRules),
|
||||
)
|
||||
req := httptest.NewRequest(http.MethodGet, "http://sub.example.com"+tc.path, nil)
|
||||
req.Header.Set("User-Agent", tc.userAgent)
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
body := resp.Body.String()
|
||||
if resp.Code != http.StatusOK || !strings.Contains(body, "vless") {
|
||||
t.Fatalf("status=%d, body=%s", resp.Code, body)
|
||||
}
|
||||
if got := resp.Header().Get("Routing"); got != tc.wantRouting {
|
||||
t.Fatalf("Routing = %q, want %q", got, tc.wantRouting)
|
||||
}
|
||||
if got := resp.Header().Get("Routing-Enable"); got != tc.wantRoutingEnable {
|
||||
t.Fatalf("Routing-Enable = %q, want %q", got, tc.wantRoutingEnable)
|
||||
}
|
||||
if got := strings.HasSuffix(body, incyRules+"\n"); got != tc.wantIncyLine || (!got && strings.Contains(body, incyRules)) {
|
||||
t.Fatalf("incy line at body end = %v, want %v; body=%s", got, tc.wantIncyLine, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user