# oxideav-http
HTTP/HTTPS source driver for oxideav (pure-Rust via ureq + rustls + webpki-roots).
Registers as a `BytesSource` on the new typed `SourceRegistry`, so
`reg.open(uri)` yields `SourceOutput::Bytes(_)` ready for any container
demuxer.
Part of the [oxideav](https://github.com/OxideAV/oxideav-workspace) framework — a pure-Rust media transcoding and streaming stack. Codec, container, and filter crates are implemented from the spec (no C codec libraries linked or wrapped, no `*-sys` crates). Optional hardware-engine crates (`oxideav-videotoolbox` / `-audiotoolbox` / `-vaapi` / `-vdpau` / `-nvidia` / `-vulkan-video`) bridge to OS APIs via runtime `libloading`; pass `--no-hwaccel` (or omit the `hwaccel` feature) to opt out.
## Usage
```toml
[dependencies]
oxideav-http = "0.0"
```
```rust
let mut ctx = oxideav_core::RuntimeContext::new();
ctx.sources = oxideav_source::with_defaults();
oxideav_http::register(&mut ctx); // installs http:// + https://
let _r = ctx.sources.open("https://example.com/clip.mp4")?;
```
## Configuring the agent
The default agent uses `ureq` defaults. To tighten policy (cap redirects,
strip `Authorization` on cross-host redirects, require https, set a
custom `User-Agent`, bound connect/global timeouts) build an
`HttpConfig` and either install it process-wide or scope it to one
source:
```rust
use std::time::Duration;
use oxideav_http::{HttpConfig, RedirectAuthPolicy, HttpSource, install_default_config};
let cfg = HttpConfig::builder()
.max_redirects(5)
.redirect_auth_policy(RedirectAuthPolicy::SameHost)
.user_agent("my-app/1.0")
.https_only(true)
.timeout_connect(Some(Duration::from_secs(5)))
.timeout_global(Some(Duration::from_secs(60)))
.build();
// (A) install once at startup so every registry-dispatched open()
// uses these settings:
install_default_config(cfg.clone()).ok();
// (B) or scope per-call without touching the global agent:
let _src = HttpSource::open_with_config("https://example.com/clip.mp4", &cfg)?;
```
`install_default_config` is one-shot — it returns `ConfigAlreadyInstalled`
once the process-wide agent has materialised. Call it before the first
`ctx.sources.open(...)` if you need it to take effect on
registry-dispatched opens.
## Range-response validation
Every 206 (Partial Content) response is validated against RFC 7233
§4.2 before any byte is exposed to the reader:
- `Content-Range` header MUST be present.
- Range unit MUST be `bytes` (case-insensitive).
- `first-byte-pos` MUST equal the byte position we asked for —
a cache / CDN that slides the start would otherwise silently
misalign every subsequent demuxer read.
- `last-byte-pos >= first-byte-pos`.
- `complete-length`, when concrete, MUST equal the `Content-Length`
observed at HEAD construction — a mid-stream resource resize is a
fatal origin/cache disagreement.
- `last-byte-pos < complete-length`.
- `*` complete-length is accepted (§4.2 explicitly permits it when
the server doesn't know the total).
- `bytes */N` unsatisfied-range payloads are rejected on a 206 (they
are a 416 payload, never a 206 payload).
If a server ignores the `Range` header and responds with `200 OK`
plus the full body (§3.1 permits this), the prefix `[0, self.pos)`
is drained in 8 KiB chunks before bytes reach the reader, so the
demuxer's file-offset view stays consistent.
## 416 Range Not Satisfiable
A 416 response is treated as a distinct error path per RFC 9110
§15.5.17. When the server includes the `Content-Range: bytes
*/<complete-length>` body that §14.4 SHOULDs for 416 responses,
the parser extracts the server's authoritative resource length and
the resulting `io::Error` surfaces BOTH the server's reported length
AND the length observed at HEAD construction. That lets a caller
tell "I asked past EOF" apart from "the resource shrank between the
HEAD and the GET" — the latter is a cache/origin disagreement worth
reporting upstream.
If the 416 omits the SHOULD'd Content-Range, the error still names
the status cleanly. If the 416 carries a malformed Content-Range,
the parse error surfaces rather than a fabricated length.
## RFC 9110 §8.6 Content-Length sanity
Beyond the §13.1.5 strong-validator path (below), the driver
cross-checks the GET-time `Content-Length` against §8.6's invariants:
- On a 200-fallback (server ignored `Range` and shipped the full
body per RFC 7233 §3.1), the GET's `Content-Length` — when present
— MUST equal the `Content-Length` observed at HEAD. §8.6 says
"a server MUST NOT send Content-Length in [a HEAD] response
unless its field value equals the decimal number of octets that
would have been sent in the content of a response if the same
request had used the GET method." A different value is a
mid-stream resource resize disguised as a soft-fallback; surfacing
it stops the demuxer from draining a now-wrong-sized prefix and
reading short.
- On a 206, the GET's `Content-Length` (when present) MUST equal
the byte span implied by `Content-Range: bytes <first>-<last>/N`
(i.e. `last - first + 1`). A mismatch is either a server bug or a
multipart/byteranges body (which we never request); either way
it would let the reader drift past the satisfied range silently.
- Both checks are skipped silently when the GET reply omits
`Content-Length` (§8.6 makes it a SHOULD, not a MUST, outside
specific cases).
## Mid-stream mutation detection
The driver implements RFC 9110 §13.1.5 `If-Range` to catch the case
where a CDN, cache, or origin replaces the resource between the
opening `HEAD` and a later `Range` GET. At `HEAD` we capture a
*strong* validator:
- An `ETag` is taken as-is when it lacks the `W/` weakness prefix
(§8.8.3 — weak entity-tags are MUST-NOT for `If-Range` per
§13.1.5).
- Failing that, `Last-Modified` is taken only when the companion
`Date` header is at least one second after it (§8.8.2.2's
promotion rule from "implicitly weak" to "strong").
- Otherwise no validator is captured and the read path issues
plain `Range` GETs (matching pre-r186 behaviour).
Every range GET that has a captured validator carries
`If-Range: <wire-form>`. Per §13.1.5 the server then either
satisfies the range normally (`206 Partial Content`) or responds
with a full `200 OK` for the *new* representation. The latter is
treated as a fatal `io::Error` naming "If-Range validator did not
match — representation changed since HEAD" so a downstream demuxer
never silently re-anchors against a different resource. When no
`If-Range` was sent (no strong validator at HEAD), the §3.1
prefix-drain fallback still applies unchanged.
## Fuzzing
`fuzz/` carries a cargo-fuzz harness (`parse_headers`) that drives
every internal response-header parser used by the source driver —
`parse_byte_content_range` (RFC 7233 §4.2 / RFC 9110 §14.4),
`parse_byte_unsatisfied_range` (§14.4), `parse_entity_tag` (§8.8.3),
`parse_imf_fixdate` (§5.6.7), and the composite
`derive_strong_validator` (§13.1.5 + §8.8.2.2 + §8.8.3). The harness
reaches the parsers through a `#[doc(hidden)] pub mod __fuzz`
re-export gated on the `fuzz` cargo feature, so the stable public
surface is unchanged when the crate is consumed normally.
```sh
cargo +nightly fuzz run --fuzz-dir fuzz parse_headers
```
A small seed corpus lives under `fuzz/corpus/parse_headers/`.
## License
MIT — see [LICENSE](LICENSE).