# `MishkaCmsCore.Runtime.OriginValidator`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/origin_validator.ex#L5)

Validates origins against sites stored in ETS for WebSocket connections and CORS, and reads the
host or the address out of what a request or a setting carries (`host_name/1`, `web_url?/1`,
`site_path?/1`) with `URI.parse/1` rather than by cutting strings.

## Origins

A CORS origin is a scheme, a host and a port, held as `t:origin/0`. `request_origin/1` reads the
one a request's `Origin` header names and `site_origins/1` lists the ones a site allows; both go
through the same reading, so the two compare as equal or not at all. `serialize/1` writes one back
the way a browser writes it.

# `origin`

```elixir
@type origin() :: {String.t(), String.t(), 1..65535}
```

`{scheme, host, port}`: the scheme `"http"` or `"https"`, the host lowercased and without IPv6
brackets, and the port always written — `https://Site.com` is `{"https", "site.com", 443}`.

# `host_name`

```elixir
@spec host_name(String.t() | nil) :: String.t() | nil
```

The host a `Host` header names, without its port: `"example.com:4000"` is `"example.com"` and
`"[::1]:4000"` is `"::1"`. `nil` for a header that names none.

# `is_loopback_host`
*macro* 

Whether `host`, already read out of an address and lowercased, names this machine: `localhost`, a
name under `.localhost` (RFC 6761), `127.0.0.1` or `::1`. Usable in a guard.

    is_loopback_host("site-a.localhost")    #=> true
    is_loopback_host("evil-localhost.com")  #=> false
    is_loopback_host("localhost.evil.com")  #=> false

# `request_origin`

```elixir
@spec request_origin(term()) :: {:ok, origin()} | :error
```

The origin a request's `Origin` header names, or `:error`.

Only a bare origin is one: an `http` or `https` scheme, a host and an optional port from 1 to
65535, with no path, query, fragment or credentials. `null`, a malformed value and a port that is
not a number are `:error`.

    request_origin("HTTPS://Site.com")       #=> {:ok, {"https", "site.com", 443}}
    request_origin("http://[::1]:3000")      #=> {:ok, {"http", "::1", 3000}}
    request_origin("https://site.com/path")  #=> :error
    request_origin("null")                   #=> :error

# `serialize`

```elixir
@spec serialize(origin()) :: String.t()
```

`origin` written the way a browser writes it in an `Origin` header: no port when it is the
scheme's own, and an IPv6 host in brackets.

    serialize({"https", "site.com", 443})  #=> "https://site.com"
    serialize({"http", "::1", 3000})       #=> "http://[::1]:3000"

# `site_origins`

```elixir
@spec site_origins(map()) :: [origin()]
```

Every origin `site` allows, read from its `:host`, `:frontend_domain` and `:allowed_origins`:

  * `host` is a domain, allowed as `https://<host>` on port 443. A loopback host (see
    `is_loopback_host/1`) is also allowed as `http://<host>` on port 80.
  * `frontend_domain` written as an origin (`https://app.example.com:8443`) is that origin. Written
    as a bare domain (`app.example.com`, `app.example.com:3000`) it is read as `host` is, on the
    port it writes.
  * each entry of `allowed_origins` is the origin it writes, on the scheme's own port when it
    writes none. What follows the host and port is ignored, so `https://partner.com/` is
    `https://partner.com`.

A value that is none of these allows nothing.

# `site_path?`

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

Whether `value` is a path on this site (`"/uploads/a.png"`): one leading slash, so neither a
protocol-relative address (`"//host/a"`) nor a backslash form a browser reads as one.

# `verify_origin?`

# `web_url?`

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

Whether `value` is an absolute `http` or `https` address with a host. A bare domain such as
`"httpbin.example.com"` is not one, whatever its first letters are.

---

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