MishkaCmsCore.Runtime.OriginValidator (Mishka CMS Core v0.0.5-alpha.4)

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.