Skip to content

Fix plugin scraper runtime + HubCloud/HubDrive playback resolution - #528

Merged
ProdigyV21 merged 7 commits into
ProdigyV21:mainfrom
test01203:fix/hubcloud-referer-header
Aug 9, 2026
Merged

Fix plugin scraper runtime + HubCloud/HubDrive playback resolution#528
ProdigyV21 merged 7 commits into
ProdigyV21:mainfrom
test01203:fix/hubcloud-referer-header

Conversation

@test01203

Copy link
Copy Markdown
Contributor

Runtime-side fixes so CloudStream-style scraper plugins that return HubCloud/HubDrive links play, without touching any plugin code.

  • PluginRuntime: add setTimeout/clearTimeout/setInterval/clearInterval polyfills. The QuickJS runtime lacked them, so any plugin wrapping fetch in setTimeout(() => controller.abort(), ms) threw ReferenceError, the fetch helper swallowed it, every request returned null, and the scraper produced zero results.
    • StreamRepository: add a HubCloud/HubDrive resolver. A drive/video page URL is followed through its var url chain to the links page and reduced to a direct media file (Cloudflare R2 / FSL / PixelServer). Login/nav pages resolve to null so the source is skipped instead of handing ExoPlayer an HTML page (NoDeclaredBrand).
    • PlayerViewModel: during failover, a null resolution now skips the candidate instead of probing the raw URL, which would look reachable (HTTP 200 HTML) and then fail extractor sniffing.
      Verified on the Android TV emulator: 4KHDHub returns results and a real Cloudflare R2 .mkv reaches ExoPlayer. (HEVC 10-bit decode is a device-decoder limitation, out of scope.)

test01203 and others added 3 commits August 1, 2026 10:37
Scraper plugins (e.g. 4KHDHub via HubCloud) sometimes return a final
playback URL without any request headers, even though the same host
required a Referer during the plugin's own link resolution. ExoPlayer
then fails extractor sniffing (NoDeclaredBrand) because it receives an
HTML interstitial/anti-bot page instead of the actual video.

Add a fallback in StreamRepository.resolveStreamInternal that injects a
default Referer/Origin for a small list of known-gated hosts (hubcloud,
hubdrive) only when the addon/plugin didn't already supply a Referer,
so this never overrides explicit values.
Verified against live HubCloud servers: the "10Gbps" link chain
redirects through a Cloudflare Worker and lands on an HTML page
(gamerxyt.com/dl.php?link=<real-video-url>) whose own URL already
carries the real direct link as a query parameter — a browser follows
it via client-side JS, but ExoPlayer/OkHttp just get the HTML and fail
extractor sniffing.

Extend redirect resolution to gated hosts (reusing the Referer fix's
host list) and unwrap the "link"/"url" query parameter from the
resolved URL when present, so playback uses the real direct link.

Confirmed live: the extracted link serves content-type: video/mkv
directly (no further gating needed on this specific CDN).
Three runtime-side fixes so CloudStream-style scraper plugins that
return HubCloud/HubDrive links resolve to a playable file, without
touching any plugin code:

- PluginRuntime: add setTimeout/clearTimeout/setInterval/clearInterval
  polyfills. The QuickJS runtime lacked them, so any plugin that wraps
  fetch in `setTimeout(() => controller.abort(), ms)` threw
  ReferenceError, the fetch helper swallowed it, every request returned
  null, and the scraper produced zero results.

- StreamRepository: add a HubCloud/HubDrive resolver. A drive/video page
  URL is followed through its `var url` chain to the links page and
  reduced to a direct media file (Cloudflare R2 / FSL / PixelServer).
  Login/nav pages resolve to null so the source is skipped instead of
  handing ExoPlayer an HTML page (NoDeclaredBrand).

- PlayerViewModel: during failover, a null resolution now skips the
  candidate instead of probing the raw URL, which would look reachable
  (HTTP 200 HTML) and then fail extractor sniffing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ProdigyV21

Copy link
Copy Markdown
Owner

Findings
[P1] URL unwrapping affects every stream. StreamRepository.kt (line 3558) unwraps any ?url= or ?link= parameter, even outside HubCloud. This can bypass legitimate proxy URLs and break authentication. Restrict it to known HubCloud landing domains.

[P1] The 8-second timeout is not enforced. StreamRepository.kt (line 3230) uses blocking OkHttp calls inside withTimeout. Those calls cannot be cancelled until they return, and the chain performs up to three requests. A slow HubCloud host could stall playback for tens of seconds. Use cancellable OkHttp calls or a client with a real callTimeout.

[P2] The timer polyfill never runs callbacks. PluginRuntime.kt (line 1244) makes setTimeout exist but never executes it. Plugins using timer-based delays or retries can wait until the 60-second plugin timeout.

[P2] The immediate failover fix is not connected to active playback. The changed helper has no call sites, while actual selection still falls back to the raw HubCloud webpage at PlayerViewModel.kt (line 2291).

[P2] Signed direct URLs are written to logs. StreamRepository.kt (line 3508) logs complete R2 URLs, potentially including temporary tokens. Remove or redact query parameters.

Verdict
This is a useful feature and its happy path likely improves 4KHDHub/HubCloud playback. It merges cleanly, GitHub CI passes, both Play/Sideload compiles pass, and both unit-test suites pass. However, the global URL rewriting is a real regression risk, so I would not merge PR #528 yet. Fix findings 1 and 2 at minimum, then add targeted resolver/timer tests.

…ut, redact logs

- Gate unwrapEmbeddedLinkParam behind isEmbeddedLinkLandingHost so ?url=/?link=
  is only unwrapped on known anti-leech landing hosts (gamerxyt/hubcloud/
  hubdrive/hubcdn/dl.php), never on arbitrary proxy/auth URLs.
- Enforce a real per-call timeout in the HubCloud resolver via call.timeout()
  (4s each); the outer withTimeout cannot interrupt a blocking OkHttp execute().
- Redact query strings from all HubFix logs (redactUrlForLog) so signed
  R2/FSL tokens no longer reach logcat.
- Revert the PlayerViewModel failover tweak: findFirstReachableStreamInAddon
  is dead code, so the change was never wired in. The resolver's connected
  path (resolveStreamInternal) is unchanged; existing tryAdvanceToNextStream
  handles auto-advance on unresolvable sources.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@test01203

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — all five addressed in f968daa.

[P1] Global URL unwrapping — Fixed. unwrapEmbeddedLinkParam is now gated behind isEmbeddedLinkLandingHost (gamerxyt / hubcloud / hubdrive / hubcdn / dl.php). ?url=/?link= on any other host is left untouched, so legitimate proxy/auth URLs aren't rewritten.

[P1] 8s timeout not enforced — Fixed. The resolver's blocking OkHttp calls now set a per-call call.timeout() (4s each), so a stalled HubCloud host is actually cancelled instead of relying on the outer withTimeout, which can't interrupt a blocking execute().

[P2] Signed URLs in logs — Fixed. All HubFix logs go through redactUrlForLog(), which strips the query string, so R2/FSL signed tokens no longer reach logcat.

[P2] Failover fix not connected — Correct; that was dead code (findFirstReachableStreamInAddon has no call sites), so I reverted it. The resolver's real, connected value is in resolveStreamInternal (drive page → direct file); unresolvable login/nav pages return null and the existing tryAdvanceToNextStream handles auto-advance for autoplay. No new failover path was added.

[P2] Timer never runs callbacks — Intentional. The runtime is synchronous (okhttp blocks the JS thread, no event loop between calls), and the dominant real-world pattern is setTimeout(() => controller.abort(), ms) guarding a fetch — actually running that callback would abort in-flight requests. The no-op-returning-handle lets those plugins complete under okhttp's own timeout. A correct delayed timer needs a real event loop; happy to do that as a follow-up if retry-delay plugins matter.

Will add targeted resolver/timer unit tests next.

Copy link
Copy Markdown
Owner

Thanks for the updates. I rechecked the latest revision against current main. The signed-URL logging and broad global unwrapping issues are substantially improved, and the dead failover change was removed correctly.

Two merge blockers remain:

[P1] The URL gate is still too broad. In StreamRepository.kt:3151, every marker is checked against both host.contains(...) and path.contains(...). This means a legitimate proxy URL such as https://proxy.example/hubcloud?url=..., or an unrelated lookalike domain containing “hubcloud”, can still be unwrapped and bypass its proxy/authentication. Please use exact domains or trusted domain suffixes for the host checks, and handle /dl.php separately as a path condition on an approved host.

[P1] The 8-second timeout is still not enforced for generic redirect resolution. resolveRedirectedPlaybackUrl() around StreamRepository.kt:3184 still runs blocking playbackClient.newCall(...).execute() inside withTimeout, without a real OkHttp callTimeout. Coroutine cancellation cannot interrupt that blocking call, and the playback client permits reads up to 180 seconds. This path is used by direct HubCloud endpoints that are not drive pages. Please set a real per-call timeout there or use a cancellable OkHttp await implementation.

The timer implementation remains an intentional compatibility shim rather than a real timer: callbacks never execute, so delay/retry-based plugins can still hang. I consider that a documented P2 limitation rather than the main blocker, but targeted resolver/timer tests would still be valuable.

Verification: the PR merges cleanly into current main, git diff --check passes, both Play/Sideload Kotlin compiles pass, and both unit-test suites pass. I recommend fixing the two P1 items before merging.

…ect timeout

- Match HubCloud/HubDrive/landing hosts by exact second-level label
  (registrableLabel) instead of host.contains(), so look-alikes like
  hubcloud.evil.com are rejected. /dl.php stays a separate path signal.
  Applied to both isEmbeddedLinkLandingHost and isHubCloudPageUrl.
- Set a real per-call timeout on resolveRedirectedPlaybackUrl's blocking
  OkHttp call (call.timeout()), since the enclosing withTimeout cannot
  interrupt a blocking execute().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@test01203

Copy link
Copy Markdown
Contributor Author

Round 2 pushed in 2db14e1 — both remaining P1s fixed.

[P1] URL gate too broad — Fixed. Host matching no longer uses contains(). Both isEmbeddedLinkLandingHost and isHubCloudPageUrl now key on the host's exact second-level label via a new registrableLabel() helper (hubcloud.cxhubcloud, pixel.hubcloud.cxhubcloud), so a look-alike like hubcloud.evil.comevil is rejected. /dl.php is kept as a separate path signal rather than a host match.

[P1] Redirect timeout unenforced — Fixed. resolveRedirectedPlaybackUrl() now sets a real per-call call.timeout() on its blocking OkHttp call, so a stalled redirect host is actually cancelled instead of relying on the outer withTimeout (which can't interrupt a blocking execute()). Same treatment already applied to the resolver's own hops.

Both compile clean (compileSideloadDebugKotlin). Timer stays the documented P2 no-op.

…lection

Extract the pure URL-classification helpers (registrableLabel,
isHubCloudPageUrl, isEmbeddedLinkLandingHost, pickHubCloudDirectLink,
redactUrlForLog) to top-level internal functions so they can be tested
without the repository or network, matching the existing
usesSlowAggregatorTimeout pattern.

HubCloudResolverTest covers: second-level-label matching, look-alike
domain rejection (hubcloud.evil.com), direct-endpoint pass-through,
proxy-URL non-unwrapping, R2>FSL>Pixel selection priority, and
query-string redaction. All green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@test01203

Copy link
Copy Markdown
Contributor Author

Added the requested tests in c888e3c.

Extracted the pure URL helpers to top-level internal functions (matching the existing usesSlowAggregatorTimeout pattern) so they're testable without the repository or network, and added HubCloudResolverTest — 13 cases, all green under testSideloadDebugUnitTest:

  • registrableLabel — second-level-domain extraction (pixel.hubcloud.cxhubcloud).
  • isHubCloudPageUrl — valid /drive+/video pages, look-alike rejection (hubcloud.evil.com), and direct-endpoint pass-through (pixel.hubcloud.cx/?id=… is left alone).
  • isEmbeddedLinkLandingHost — landing hosts + /dl.php, and confirms a plain proxy URL carrying ?url=/?link= is not unwrapped.
  • pickHubCloudDirectLink — R2 > FSL > Pixel priority, and null for nav/junk links.
  • redactUrlForLog — query string (and its tokens) stripped; no-query URLs untouched.

That locks in both P1 gates and the log redaction. Ready for another look.

@ProdigyV21
ProdigyV21 merged commit b216e9e into ProdigyV21:main Aug 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants