lib/audio_proxy/plugs/verify_signature.ex
Rama main · réplica de lectura del repositorio original.
defmodule AudioProxy.Plugs.VerifySignature do
@moduledoc """
Gate for the signed URL space (API doc §1/§2).
Splits `conn.request_path` into the signature segment and everything after
it, then verifies with `AudioProxy.Signature.verify/2` — the signed string
is the raw request path, so no re-encoding ambiguity can creep in.
On success the rest-of-path (leading `/` included) is stashed in
`conn.assigns[:rest_of_path]` for the downstream options/source parsers. On
failure the plug halts with the `401` row of `AudioProxy.ErrorJSON`; a
missing signature segment is just another invalid signature.
Invariants downstream code must respect:
- **Parse `assigns.rest_of_path`, never `path_info`.** The signature covers
the raw request path, and `conn.path_info` cannot reproduce those bytes:
`Plug.Conn.Adapter` builds it by splitting on `/` and *dropping empty
segments* (`/{sig}//a` and `/{sig}/a` collide), and `Plug.Router`
additionally percent-decodes its own copy for route matching. Neither
form is the verified byte sequence.
- **The signature covers the path only.** The query string and the HTTP
method are not signed — never let an unsigned query param influence
processing, and don't assume a signed URL is GET-only.
- **`rest_of_path` can be `"/"`** (a request for `/{sig}/` with a valid
signature). That carries no options/source; reject it downstream.
Mount this plug on the signed routes only — `/health` (and later `/metrics`)
live outside the signed URL space and must not pass through it.
"""
@behaviour Plug
import Plug.Conn
alias AudioProxy.{ErrorJSON, Signature}
@impl true
def init(opts), do: opts
@impl true
def call(conn, _opts) do
with {:ok, sig_segment, rest_of_path} <- split(conn.request_path),
:ok <- Signature.verify(sig_segment, rest_of_path) do
assign(conn, :rest_of_path, rest_of_path)
else
_ -> ErrorJSON.halt_with(conn, :invalid_signature)
end
end
# "/sig/rest/of/path" → {"sig", "/rest/of/path"}; anything without a second
# segment ("/sig", "/", "") carries no signed content and is rejected.
defp split(path) do
case String.split(path, "/", parts: 3) do
["", sig_segment, rest] -> {:ok, sig_segment, "/" <> rest}
_ -> {:error, :invalid_signature}
end
end
end