diff --git a/internal/sub/build_urls_test.go b/internal/sub/build_urls_test.go index 97a10d6ae..deb7afc9f 100644 --- a/internal/sub/build_urls_test.go +++ b/internal/sub/build_urls_test.go @@ -69,3 +69,51 @@ func TestBuildURLs_EmptySubId(t *testing.T) { t.Fatalf("empty subId must yield empty URLs, got %q %q %q", a, b, c) } } + +// A subscriber arriving via a reverse proxy (subURI configured with full +// HTTPS URL) must see the same scheme+host in the JSON and Clash Copy +// URLs as in the main subURL — not the raw sub-server port 2096. +func TestBuildURLs_DerivesJsonFromConfiguredSubURI(t *testing.T) { + initSubDB(t) + s := &SubService{} + s.PrepareForRequest("sub.example.com") + + // Simulate the admin having set subURI (reverse-proxy setup). + database.GetDB().Exec( + "INSERT INTO settings (key, value) VALUES (?, ?)", + "subURI", "https://example.com/sub-xxx/") + + subURL, jsonURL, clashURL := s.BuildURLs("/sub-xxx/", "/json/", "/clash/", "ABC") + + if subURL != "https://example.com/sub-xxx/ABC" { + t.Fatalf("subURL = %q", subURL) + } + if jsonURL != "https://example.com/json/ABC" { + t.Fatalf("jsonURL = %q (should derive scheme+host from subURI), want %q", jsonURL, "https://example.com/json/ABC") + } + if clashURL != "https://example.com/clash/ABC" { + t.Fatalf("clashURL = %q (should derive scheme+host from subURI), want %q", clashURL, "https://example.com/clash/ABC") + } +} + +// A malformed subURI (no scheme/host) must not leak a broken base into the +// JSON/Clash URLs; BuildURLs should fall back to the request-derived base. +func TestBuildURLs_MalformedSubURIFallsBackToRequestBase(t *testing.T) { + initSubDB(t) + s := &SubService{} + s.PrepareForRequest("sub.example.com") + + // A value with no scheme can't yield a usable scheme+host. + database.GetDB().Exec( + "INSERT INTO settings (key, value) VALUES (?, ?)", + "subURI", "example.com/sub-xxx/") + + _, jsonURL, clashURL := s.BuildURLs("/sub-xxx/", "/json/", "/clash/", "ABC") + + if jsonURL != "http://sub.example.com:2096/json/ABC" { + t.Fatalf("jsonURL = %q, want fallback to request base %q", jsonURL, "http://sub.example.com:2096/json/ABC") + } + if clashURL != "http://sub.example.com:2096/clash/ABC" { + t.Fatalf("clashURL = %q, want fallback to request base %q", clashURL, "http://sub.example.com:2096/clash/ABC") + } +} diff --git a/internal/sub/service.go b/internal/sub/service.go index a83a505f5..1634b5a41 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -2123,12 +2123,37 @@ func (s *SubService) BuildURLs(subPath, subJsonPath, subClashPath, subId string) base := s.settingService.BuildSubURIBase(s.address) subURL = s.buildSingleURL(configuredSubURI, base, subPath, subId) - subJsonURL = s.buildSingleURL(configuredSubJsonURI, base, subJsonPath, subId) - subClashURL = s.buildSingleURL(configuredSubClashURI, base, subClashPath, subId) + + // When subURI is explicitly configured (reverse-proxy setup), use its + // scheme+host as the base for JSON and Clash URLs so they match the + // reverse-proxy endpoint instead of the raw sub-server port. Fall back + // to the request-derived base if subURI is empty or can't be parsed + // into a scheme+host (e.g. a malformed value with no scheme). + jsonClashBase := base + if configuredSubURI != "" { + if derived := s.extractBaseFromURI(configuredSubURI); derived != "" { + jsonClashBase = derived + } + } + + subJsonURL = s.buildSingleURL(configuredSubJsonURI, jsonClashBase, subJsonPath, subId) + subClashURL = s.buildSingleURL(configuredSubClashURI, jsonClashBase, subClashPath, subId) return subURL, subJsonURL, subClashURL } +// extractBaseFromURI extracts scheme://host from a configured URI. +// e.g., "https://example.com/sub-xxx/" → "https://example.com". +// Returns "" when the URI is empty or lacks a scheme/host, so callers can +// fall back to the request-derived base instead of emitting a broken value. +func (s *SubService) extractBaseFromURI(uri string) string { + u, err := url.Parse(uri) + if err != nil || u.Scheme == "" || u.Host == "" { + return "" + } + return fmt.Sprintf("%s://%s", u.Scheme, u.Host) +} + // buildSingleURL constructs a single URL using configured URI or base components func (s *SubService) buildSingleURL(configuredURI, base, basePath, subId string) string { if configuredURI != "" {