# `SuperPi.Web.Policy`
[🔗](https://github.com/oleksandr/super-pi-beam/blob/v0.1.0/lib/super_pi/web/policy.ex#L1)

Request-forgery policy for `web_fetch`: which URLs an agent may make the node request.

The model controls the URL, so the node must not become a proxy into its own network. The
rule is deny by default, allow global unicast only.

`check_url/2` refuses a URL when:

- the scheme is not `http` or `https`, or the URL has userinfo, a backslash or whitespace;
- the port is not 80 or 443;
- the host is `localhost`, `*.localhost`, `*.local`, `*.internal` or `metadata.google.internal`,
  whatever it resolves to;
- the host is a numeric form that does not parse as one clear address (`0x7f.1`, `2130706433`
  are checked as the address they mean);
- ANY address the host resolves to is reserved (`reserved?/1`).

`reserved?/1` covers loopback, private, link-local (including `169.254.0.0/16`, the cloud
metadata endpoint), CGNAT, multicast, unspecified, documentation and benchmarking ranges, and
for IPv6 everything outside `2000::/3` (ULA `fc00::/7`, link-local `fe80::/10` and so on).
IPv4-mapped (`::ffff:a.b.c.d`), NAT64 (`64:ff9b::/96`) and 6to4 (`2002::/16`) forms are
unwrapped and the embedded IPv4 address is checked, so a form is refused exactly when its
unwrapped address is.

Redirects are followed by the caller, never by the HTTP client: at most `max_redirects/0`
hops, and `check_url/2` runs on every hop (`redirect_url/2` resolves a `Location` value).
A public host that redirects to `127.0.0.1` is refused at the second hop.

The module is pure except for the default resolver. Tests and callers inject `:resolver`
(`fn host -> {:ok, [ip]} | {:error, reason} end`) so no check touches DNS.

## Residual risk: DNS rebinding

The check resolves the name, the HTTP client resolves it again when it connects. A hostile
DNS server can answer with a public address to the first lookup and `127.0.0.1` to the
second. `pin/2` closes the window by building a request against the checked address, and the
fetcher uses it when `pin_ip: true`. That path is OFF by default and UNVERIFIED against the
installed Req, Finch and Mint (TLS verification and the `Host` header need an end-to-end
check). A deployment that cannot tolerate the window should use `network: "allowlist"` or an
egress proxy that filters by destination address.

## Test-only option

`unsafe_allow_local: true` skips the port, name and address checks so a test can reach a
server on `127.0.0.1`. It is an option of the call and cannot come from a tool argument. It
must never be set in production config.

# `ip`

```elixir
@type ip() :: :inet.ip_address()
```

# `target`

```elixir
@type target() :: %{uri: URI.t(), host: String.t(), ips: [ip()]}
```

# `check_url`

```elixir
@spec check_url(term(), keyword()) ::
  {:ok, target()} | {:error, {:refused, String.t()} | {:dns, term()}}
```

Checks a URL and resolves its host.

Returns the parsed URI, the normalized host and the checked addresses, or
`{:error, {:refused, message}}` or `{:error, {:dns, reason}}`.

Options: `:resolver` (default `system_resolver/1`) and `:unsafe_allow_local` (test only).

# `ip_to_string`

```elixir
@spec ip_to_string(ip()) :: String.t()
```

Text form of an address.

# `max_redirects`

```elixir
@spec max_redirects() :: pos_integer()
```

Maximum number of redirects a fetch follows.

# `pin`

```elixir
@spec pin(target(), keyword()) :: {URI.t(), String.t()}
```

Builds the request target for a pinned-IP connect: the URI with the checked address as host
and the original host name for the `Host` header, SNI and certificate check.

Unverified, see the moduledoc.

# `redirect_url`

```elixir
@spec redirect_url(URI.t(), term()) ::
  {:ok, String.t()} | {:error, {:refused, String.t()}}
```

Resolves a `Location` value against the URL it came from. Never raises.

# `reserved?`

```elixir
@spec reserved?(term()) :: boolean()
```

True when an address is outside global unicast space and so must never be fetched.

A malformed tuple counts as reserved.

# `system_resolver`

```elixir
@spec system_resolver(String.t()) :: {:ok, [ip()]} | {:error, term()}
```

Default resolver: the system resolver, IPv4 and IPv6, every address returned.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
