Skip to content

6.71.0 - A host decides which origins it trusts, and AVPlayer hears the answer

Choose a tag to compare

@superuser404notfound superuser404notfound released this 06 Sep 19:54
· 15 commits to main since this release

A self-hosted media server behind its own certificate has never played through this engine, because URLSession enforces the system trust that the in-demuxer network stacks it replaces never did. Two contributed PRs close that, and the second one closes the half nobody usually gets to.

Added

EngineTLS.serverTrustEvaluator (#495, #506). A host decides which origins it accepts a server certificate from that failed system trust evaluation.

EngineTLS.serverTrustEvaluator = { $0.host == "media.lan" }

(@Sendable (URLProtectionSpace) -> Bool)?, asked per challenge about the origin the challenge came from. nil, the default, keeps the system's default handling everywhere, and so does every non-server-trust challenge, so client certificates and HTTP auth are untouched. Read per challenge rather than at session construction, so replacing it applies from the next connection without rebuilding anything.

Per origin rather than a flag, because a host commonly holds a LAN address behind a private certificate and a WAN address with a real one, and accepting the first must not quietly relax the second. A host that pins an SPKI hash reads the protection space and decides for itself. Returning true for everything is one line.

The coverage was re-counted against current main rather than the fork's base: the AVIOReader probe, chunk, persistent and streaming paths, the disc reader, both HLS ingest readers, the audio tap fetcher, the carriage probe and the remote HLS subtitle proxy. The live subtitle rendition fetch was the one engine fetch borrowing URLSession.shared, which cannot carry a delegate and which no host decision could ever have reached; it owns a session now.

The trust decision reaches AVPlayer, through the remote HLS stand-in (#507). EngineTLS governs the sessions the engine opens, and AVPlayer's own networking is not one of them. On the native remote HLS route the origin URL goes to AVURLAsset, which asks no delegate about the certificate and which an ATS exception does not reach, so an origin could direct play, since that reads through AVIOReader, and then fail the moment it transcoded.

An HLSOriginRelay mounts on the HLSLocalServer that #316 already stands in front of a remote master, so the https request is made by the engine and the handshake happens where the evaluator is asked. Mounted on rather than beside: the accept loop, the session token, the 0.0.0.0 bind and the AirPlay LAN swap are the server's and come for free.

  • Playlists are rewritten so every variant, key, map and segment follows, resolved against the playlist they came from. The allow list is seeded by the load and grows as playlists reveal where their own sub-resources live, so a stream split across hosts keeps working while a request naming somewhere nobody advertised is refused.
  • Everything else is relayed as it stands. Range forwarded verbatim, never parsed or rebuilt; Content-Range and status mirrored.
  • Media is written to the player as it arrives, not read whole first. A held segment puts its entire download in front of the player's first byte, which is the one budget AVPlayer does not extend (about 3.5 s, -12889), and AVPlayer sizes its next rendition off what it measured, which behind a buffer is a loopback burst rather than the link. Playlists, refusals, and bodies whose length the origin did not state are still read whole, because they have to be rewritten, or framed by measuring.
  • Relayed requests are charged to OriginRequestBudget like every other fetch the engine makes, so a metered origin arms the same pacer the reader and the subtitle prefetcher read, and a redirect folds into one origin (#388).
  • The blocking-reload parameters ride along. AVPlayer appends _HLS_msn / _HLS_part to a playlist URL that advertises CAN-BLOCK-RELOAD (#441); they reach the origin rather than being read past, so a reload that should block still blocks.
  • The relay is mounted only where the system actually refuses the origin, decided by one handshake made the way AVPlayer makes its own. The evaluator cannot answer this at load time, since there is no protection space and no serverTrust to read, but the system can be asked and its answer is the question: an origin the system trusts is one the native route reaches unaided. Without that test, opting one LAN server in would move every byte of every remote HLS session through the process.

Sessions with no evaluator set reach AVPlayer exactly as before.

The two reasons to stand in front of a master compose. With sidecars and a self-signed origin, the rewritten master carries the injected subtitle renditions AND its variants come back through the relay, rather than the session choosing one. The renditions the engine serves itself are named relatively and stay with the server that owns them.

Changed

A refused certificate stays classified on the relayed route. 6.69.0 reads the refusal off the failed item's NSUnderlyingErrorKey chain, and behind a relay the player's request went to loopback and came back a plain 502, so nothing in that chain says certificate any more. The relay remembers the handshake it lost and the item classification asks it, so the session error names the certificate rather than a bad gateway.

Acknowledgements

Both PRs from @RadicalMuffinMan, who had carried this as a fork since 6.5.3, rebased it across sixty releases, and closed the AVURLAsset gap himself rather than filing it. The relay follow-ups above are on top of that work.

Full changelog: CHANGELOG.md | 6.70.0...6.71.0