AudioProxy.Source.Https (audio_proxy v0.4.0)

Copy Markdown View Source

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./ahttps://media.example/a);
  • an IP literal rendered in its canonical form (https://[0:0:0:0:0:0:0:1]/ahttps://[::1]/a);
  • an explicit :443 dropped — 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%2Fb and https://h/a/b are 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 the plain/ form (%2520) — which is exactly what enc/ exists for.
  • Dot segments. https://h/a/../b is 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

Types

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.

Every rejection reason this type can produce.

Types

t()

@type t() :: {:http, String.t()}

An HTTPS source: its canonical URL, which is also its identity.

Functions

message(atom)

@spec message(atom()) :: String.t()

A short human-readable sentence for one of this type's own reasons.

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.