mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
fix(web-backend): validate and pin every provider redirect hop
This commit is contained in:
1 parent
9de480826c
commit
2da2884ba2
21 files changed
+1505
-290
No files matched your search
@@ -47,11 +47,15 @@ so a silent host hung on OS-level TCP timeouts. Both runtimes now use the same
|
||||
budgets: Xtream 30 s, Stalker 15 s (30 s for `create_link`, which mints a stream
|
||||
URL before answering), playlist and XMLTV downloads 30 s.
|
||||
|
||||
Those numbers are safe for large downloads. On axios' default
|
||||
(follow-redirects) transport `timeout` is **not** a wall-clock deadline for the
|
||||
Those numbers are safe for large downloads. On axios 1.20's native HTTP
|
||||
transport (`maxRedirects: 0`) each hop's `timeout` is **not** a wall-clock deadline for the
|
||||
whole response: it bounds the time to response headers and then continues as the
|
||||
socket's inactivity timeout for the body. A multi-megabyte XMLTV file that keeps
|
||||
delivering bytes is never cut off mid-transfer — only a stalled one is.
|
||||
delivering bytes is never cut off mid-transfer — only a stalled one is. The web
|
||||
backend explicitly restores socket inactivity handling while reading the final
|
||||
stream response, because axios stream mode stops its timeout handler at headers.
|
||||
Truncated, malformed and timed-out final bodies retain their received response
|
||||
status for reachability classification; cancellation retains its own semantics.
|
||||
|
||||
### Scope: portal calls only
|
||||
|
||||
@@ -124,33 +128,25 @@ Merely declining to count it would leave an earlier direct failure standing, and
|
||||
a single later timeout would then fast-fail an endpoint that answered in between.
|
||||
Every caller passes the URL it asked for as the baseline.
|
||||
|
||||
**Where the failed hop is found depends on the transport, and both are in play.**
|
||||
**Web backend records redirect evidence explicitly.** Its `ValidatedHttpClient`
|
||||
disables automatic redirects and holds one admission for the entire chain.
|
||||
`ProviderRequestError.initialResponded` becomes true only after receiving an
|
||||
actual redirect response. A later transport or DNS failure, URL policy refusal,
|
||||
invalid location, cycle or exhausted budget therefore clears the initial
|
||||
endpoint's failure streak when the chain settles. This also handles redirects
|
||||
that change only the query. It does not release the half-open slot early: the
|
||||
route retains ownership through validation, all hops and the final body, and
|
||||
releases in `finally` even when reporting throws. Before any redirect, initial
|
||||
DNS failures carry their internal resolver cause into normal classification;
|
||||
policy refusals remain inconclusive. Error bodies never serialize that cause.
|
||||
|
||||
| | Electron | Web backend |
|
||||
| --- | --- | --- |
|
||||
| Redirects | followed hop by hop (`maxRedirects: 0`), each its own request | followed inside one request by follow-redirects |
|
||||
| Failed hop is in | `error.config.url` | `error.request._currentUrl` |
|
||||
|
||||
`failedRequestUrlOf` reads `_currentUrl` first and falls back to `config.url`,
|
||||
which is correct for both: a per-hop request exposes no `_currentUrl`, and on the
|
||||
following transport `config` is built once and keeps the URL we asked for — so
|
||||
reading `config.url` there would compare a URL with itself, find no redirect, and
|
||||
charge a dead destination to the provider that answered. Anything added here must
|
||||
work on both, because the same helper serves both.
|
||||
|
||||
**The comparison is origin + path, not the whole URL.** A same-origin redirect
|
||||
(`/player_api.php` → `/slow/player_api.php`) proves the endpoint answered just as
|
||||
much as a cross-origin one, and charging it would fast-fail every OTHER call to a
|
||||
portal that answers — so the path has to be part of it. The query must NOT be:
|
||||
the web backend passes Xtream credentials through axios' `params`, so the sent
|
||||
URL always carries a query the baseline does not, and comparing whole URLs made
|
||||
every ordinary failure look like a redirect and stopped the breaker from ever
|
||||
opening. What that gives up is a redirect that changes nothing but the query,
|
||||
which is then counted as an ordinary failure — the safe direction.
|
||||
|
||||
It requires positive evidence — anything unparseable or unknown counts the
|
||||
failure as usual, because guessing "redirect" here would stop the guard from ever
|
||||
tripping — and a failure that names no URL at all is still counted.
|
||||
**Electron retains URL-based attribution.** `failedRequestUrlOf` reads
|
||||
`error.request._currentUrl` first (for a following transport) and falls back to
|
||||
`error.config.url` (for a native per-hop transport). The comparison is origin +
|
||||
path, not the query: axios `params` can append credentials absent from the
|
||||
caller's baseline. Unknown/unparseable URLs conservatively count as ordinary
|
||||
failures, and query-only redirects cannot be distinguished by this fallback.
|
||||
The shared helper and Electron behavior are unchanged by the web-backend fix.
|
||||
|
||||
Known gap: the failing hop is not guarded either (it has no token of its own),
|
||||
so a permanently broken redirect chain keeps costing a full timeout.
|
||||
|
||||
@@ -139,6 +139,57 @@ before any outbound request:
|
||||
- `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS=1` explicitly enables trusted
|
||||
local/LAN targets for development, mock servers, or private deployments
|
||||
|
||||
### Provider redirects and connection policy
|
||||
|
||||
`ValidatedHttpClient` applies the registration URL policy again before every
|
||||
outbound request on `/xtream`, `/stalker`, `/parse`, and `/parse-xml`, including
|
||||
the initial request. Axios automatic redirects are disabled (`maxRedirects: 0`);
|
||||
301, 302, 303, 307 and 308 are followed manually, with at most five redirects.
|
||||
Relative `Location` values resolve against the URL actually sent, including its
|
||||
query. Missing/malformed locations, repeated URLs (ignoring fragments), and an
|
||||
exhausted redirect budget fail without dispatching another request. Intermediate
|
||||
bodies are destroyed at headers; malformed or endless redirect bodies do not
|
||||
block following an otherwise valid location. Final bodies retain a native socket
|
||||
inactivity timer: axios stream mode stops its own timeout handling at headers,
|
||||
so `provider-response.ts` keeps that timer active until consumption finishes.
|
||||
A body failure preserves received HTTP status evidence; cancellation remains
|
||||
inconclusive before any redirect.
|
||||
|
||||
Every DNS answer must be a valid permitted IP. Mixed public/private answers
|
||||
fail closed. The strict policy rejects private/reserved IPv4, IPv4-mapped private
|
||||
IPv6, and IPv6 outside global unicast `2000::/3`, plus protocol-assignment,
|
||||
documentation and 6to4 ranges within it. These deliberately conservative ranges
|
||||
follow the [IANA IPv6 address space](https://www.iana.org/assignments/ipv6-address-space/)
|
||||
and [special-purpose registry](https://www.iana.org/assignments/iana-ipv6-special-registry/).
|
||||
The LAN opt-in applies to the whole
|
||||
chain; it still requires HTTP(S), no URL userinfo and concrete DNS addresses.
|
||||
|
||||
Fresh HTTP/HTTPS agents pin socket lookup to the exact validated IPv4/IPv6
|
||||
answer set for that hop. No second DNS query or pooled connection may substitute
|
||||
an unvalidated address. The original hostname remains in the URL for Host, TLS
|
||||
SNI and certificate hostname checks. Axios proxies and Node environment proxies
|
||||
are disabled for these requests so a proxy cannot independently resolve the
|
||||
origin. Deployments that require an outbound HTTP proxy must use a different
|
||||
network arrangement; setting `HTTP_PROXY`/`HTTPS_PROXY` does not route provider
|
||||
requests through it. This guarantee concerns address selection in the Node
|
||||
transport, not routing/NAT performed outside the process. Private-network
|
||||
opt-in intentionally relaxes address restrictions.
|
||||
|
||||
Original axios `params` are applied only to the initial request. Subsequent
|
||||
queries come from `Location` resolution, without appending the original
|
||||
credentials again. Authorization, Cookie, Proxy-Authorization and Stalker SN
|
||||
headers are retained only on the same origin; changes of host, port or scheme
|
||||
(including HTTPS downgrades) strip them for the rest of the chain. User-Agent
|
||||
and other non-secret protocol headers survive. Providers can explicitly put
|
||||
query values in `Location`; the backend does not rewrite provider-issued URLs.
|
||||
|
||||
Policy errors contain fixed messages/statuses without URLs, DNS exceptions or
|
||||
transport objects. Portal routes keep HTTP 200 with a `{ message, status }`
|
||||
error envelope; playlist/XMLTV routes use the actual error status. Registration
|
||||
continues to use real HTTP error statuses. See
|
||||
[host connectivity guard](host-connectivity-guard.md) for chain ownership and
|
||||
failure attribution.
|
||||
|
||||
Do not disable TLS certificate validation in the backend proxy. For private
|
||||
certificate authorities, configure Node with `NODE_EXTRA_CA_CERTS`.
|
||||
|
||||
|
||||
Reference in new issue
Block a user