MishkaCmsCore.Runtime.OriginValidator (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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 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.

Summary

Types

{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}.

Functions

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.

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.

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

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.

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

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.

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.

Types

origin()

@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}.

Functions

host_name(header)

@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(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(value)

@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(arg)

@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(site)

@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?(value)

@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?(origin)

web_url?(value)

@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.