`ImageElementBase.LoadImage` fetched http(s) images with a throwaway `new HttpClient()` + `GetStreamAsync`: no timeout override (the 100s default), no size cap, unbounded redirects, no pooling — all inside stream startup, while ffmpeg waits on the pipe. #502 routed ordinary channel-logo watermarks onto that path, widening a pre-existing weakness. Introduce `IRemoteImageFetcher` / `HttpRemoteImageFetcher`, modelled on the neighbouring `IRemoteStreamProber`: - deadline covers headers AND body (linked CTS + `CancelAfter`, client `Timeout = InfiniteTimeSpan`) — under `ResponseHeadersRead` the body read falls outside `HttpClient.Timeout` (the #289 lesson) - 10 MiB cap enforced during the copy; `Content-Length` is only a cheap early reject, since it can be absent or a lie - permissive content-type check (rejects an HTML error page, allows a missing type and octet-stream) - pooled via `IHttpClientFactory`; redirects capped at 3, not 50 A byte cap does NOT bound decoding, so `DecodeRemoteImage` additionally reads declared dimensions + frame count from the header and rejects before `Image.LoadAsync` allocates (50 MP / 600 frames). A 4 KB PNG declaring 30000x30000 costs ~3.6 GB to decode and passes every wire-size check — caught by adversarial review of the first version of this change, which capped bytes and wrongly claimed that was decode-bomb protection. Not cached and SSRF not mitigated — both deliberate, with the reasoning recorded in docs/decisions.md. fixes #511
25 lines
1.1 KiB
C#
25 lines
1.1 KiB
C#
namespace ErsatzTV.Core.Interfaces.Streaming;
|
|
|
|
/// <summary>
|
|
/// Fetches a remote (http/https) image for the graphics engine, under a bounded timeout and a
|
|
/// bounded response size.
|
|
/// </summary>
|
|
public interface IRemoteImageFetcher
|
|
{
|
|
/// <summary>
|
|
/// Fetches <paramref name="uri" /> fully into memory.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A seekable, fully-buffered stream positioned at zero. The caller owns and must dispose it.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// Throws rather than returning a failure value: every call site already wraps element
|
|
/// initialization in a catch that disables the element, so a throw degrades to "no watermark"
|
|
/// rather than a killed stream. An implementation's own deadline should surface as
|
|
/// <see cref="TimeoutException" /> and caller cancellation as
|
|
/// <see cref="OperationCanceledException" /> — though note the current call sites catch both
|
|
/// alike, so today the distinction only sharpens the log message.
|
|
/// </remarks>
|
|
Task<Stream> Fetch(Uri uri, CancellationToken cancellationToken);
|
|
}
|