The https:// source type: audio fetched from an origin over TLS.
https://media.example/track.wav names a URL, and the host is the whole of
the policy: it is matched against AP_SOURCE_ALLOWLIST
(AudioProxy.Source.Allowlist), which is deny-by-default — unset means no
HTTPS source works at all. A proxy that fetches arbitrary URLs is a
server-side request forgery primitive pointed at whatever the container can
reach, so an operator has to name the origins they trust before any of this
runs. An allowlisted host is trusted by definition; that trust is theirs to
grant.
Refused at the grammar
http:// never reaches this module: the resolver dispatches on scheme, no
type claims http, and the source is {:error, :unknown_scheme}. Userinfo is
refused here. Neither is merely left unallowlisted, because both are wrong
independent of which host they name — a cleartext fetch has no place in a
source, and credentials in a URL end up in logs, in argv, and in a cache key.
Keeping them out also keeps the allowlist a single-axis policy: host, and
nothing else.
Canonical identity
Everything that is a second spelling of one resource folds away, because each surviving spelling would otherwise buy one object a second cache key:
- scheme and host lowercased, and a trailing root dot stripped
(
https://Media.Example./a→https://media.example/a); - an IP literal rendered in its canonical form
(
https://[0:0:0:0:0:0:0:1]/a→https://[::1]/a); - an explicit
:443dropped — it is the scheme's default; - an absent path rendered
/; - an empty query dropped, and any fragment dropped outright (a fragment is never sent to an origin, so it cannot name a different object).
Two things are deliberately not normalized, because folding them would be a guess about a server this proxy does not run:
- The URL's own percent-encoding.
https://h/a%2Fbandhttps://h/a/bare different objects on many origins, so collapsing that layer would hand two objects one cache key. The visible cost is that an already-escaped URL needs double escaping in theplain/form (%2520) — which is exactly whatenc/exists for. - Dot segments.
https://h/a/../bis left as written: only the origin knows whether it resolves them.
IP literals normalize through :inet.parse_strict_address/1 rather than the
lenient parser, and that choice is a security boundary. The lenient parser
accepts inet_aton shorthand, where 1.2 means 1.0.0.2 and 01.2.3.4 means
1.2.3.4. Folding those would let an allowlist entry for 1.2.3.4 silently
admit 01.2.3.4; left as text they stay distinct subjects, and are refused
unless an operator names them in that exact spelling.
Limits
A URL may be 2048 bytes and a host 253 — the de-facto interoperable URL
maximum and DNS's own name limit. Both were chosen after measuring rather
than assumed: URI.new/1 is linear in input length here (0.026 ms for a
4 KB URL, 0.51 ms for 80 KB, ~6 ns/byte), and so is
:inet.parse_strict_address/1, so unlike AudioProxy.Source.Local's caps —
where Path.safe_relative/2 is superlinear and the cap is a denial-of-service
control — these are protocol bounds, refusing at the door what no origin
would answer anyway.
Status: no backend yet
stat/1 and ffmpeg_input/1 answer {:error, :no_backend}. The origin
client they need — a HEAD for size and ETag, and the rules for what ffmpeg may
be handed — arrives with add-https-source-backend; until then an https://
source parses, canonicalizes and authorizes but cannot be rendered. The gap is
pinned by a test, so it is a failing assertion away from being forgotten
rather than a crash in production.
Summary
Functions
A short human-readable sentence for one of this type's own reasons.
Every rejection reason this type can produce.
Types
@type t() :: {:http, String.t()}
An HTTPS source: its canonical URL, which is also its identity.
Functions
A short human-readable sentence for one of this type's own reasons.
@spec reasons() :: [atom()]
Every rejection reason this type can produce.
Declared rather than inferred, so a test can hold the two ends together — see
AudioProxy.Source.S3.reasons/0 for why.