Current section
Files
Jump to
Current section
Files
lib/mpp/client/accept_policy.ex
defmodule MPP.Client.AcceptPolicy do
@moduledoc """
Gates `Accept-Payment` header injection on outgoing HTTP requests.
Without a gate, a global payment middleware advertises supported payment
methods on every cross-origin request, which can break CORS preflight and
leak wallet capabilities.
Defaults to `:always` for backwards compatibility (matches mpp-rs
`AcceptPaymentPolicy::Always`).
"""
use Descripex, namespace: "/client"
@type t ::
:always
| :never
| {:same_origin, String.t()}
| {:origins, [String.t()]}
api(:allows?, "Return true if `Accept-Payment` injection is permitted for `url`.",
params: [
policy: [kind: :value, description: "AcceptPolicy value"],
url: [kind: :value, description: "Request URL string or `%URI{}`"]
],
returns: %{type: :boolean, description: "true when the header may be sent"}
)
@doc """
Return `true` if `Accept-Payment` header injection is permitted for `url`.
"""
@spec allows?(t(), String.t() | URI.t()) :: boolean()
def allows?(:always, _url), do: true
def allows?(:never, _url), do: false
def allows?({:same_origin, same_origin}, url) do
with {:ok, origin} <- parse_origin(same_origin),
{:ok, request_origin} <- origin_from_url(url) do
origin == request_origin
else
_ -> false
end
end
def allows?({:origins, patterns}, url) when is_list(patterns) do
uri = normalize_uri(url)
Enum.any?(patterns, &matches_origin_pattern?(uri, &1))
end
api(:default, "Return the default policy (`:always`).", returns: %{type: :atom, description: "`:always`"})
@doc "Return the default policy (`:always`)."
@spec default() :: t()
def default, do: :always
defp normalize_uri(%URI{} = uri), do: uri
defp normalize_uri(url) when is_binary(url) do
URI.parse(url)
end
defp parse_origin(origin) when is_binary(origin) do
case URI.parse(origin) do
%URI{scheme: scheme, host: host} = uri when is_binary(scheme) and is_binary(host) ->
{:ok, origin_key(uri)}
_ ->
:error
end
end
defp origin_from_url(url) do
case normalize_uri(url) do
%URI{scheme: scheme, host: host} = uri when is_binary(scheme) and is_binary(host) ->
{:ok, origin_key(uri)}
_ ->
:error
end
end
defp origin_key(%URI{scheme: scheme, host: host, port: port}) do
default_port? =
(scheme == "https" and port == 443) or
(scheme == "http" and port == 80) or port == nil
host = String.downcase(host)
if default_port? do
"#{scheme}://#{host}"
else
"#{scheme}://#{host}:#{port}"
end
end
defp matches_origin_pattern?(%URI{} = uri, pattern) do
if String.starts_with?(pattern, "*.") do
match_wildcard_host?(uri, String.slice(pattern, 2..-1//1))
else
exact_origin_match?(uri, pattern)
end
end
defp exact_origin_match?(uri, pattern) do
with {:ok, pattern_origin} <- parse_origin(pattern),
{:ok, request_origin} <- origin_from_url(uri) do
pattern_origin == request_origin
else
_ -> false
end
end
defp match_wildcard_host?(%URI{host: host}, suffix) when is_binary(host) do
host = String.downcase(host)
suffix = String.downcase(suffix)
host == suffix or String.ends_with?(host, "." <> suffix)
end
defp match_wildcard_host?(_, _), do: false
end