module

Hwaro::Core::Build::Csp

Constants

CLOUDFLARE_HEADER_LIMIT = 2000

Header lines longer than this are dropped by Cloudflare Pages.

CLOUDFLARE_RULE_LIMIT = 100

Rules beyond which Cloudflare Pages stops reading a _headers file.

EXECUTED_SCRIPT_TYPES = ["module", "importmap", "speculationrules", "application/ecmascript", "application/javascript", "application/x-ecmascript", "application/x-javascript", "text/ecmascript", "text/javascript", "text/javascript1.0", "text/javascript1.1", "text/javascript1.2", "text/javascript1.3", "text/javascript1.4", "text/javascript1.5", "text/jscript", "text/livescript", "text/x-ecmascript", "text/x-javascript"] of ::String

<script type> values the browser executes (and so checks against script-src): the JavaScript MIME types, module, and the two inline-only JSON types the HTML spec runs through CSP. Every other type (application/ld+json, text/template, …) is a data block.

FEATURE_SOURCES = [{markers: ["https://cdnjs.cloudflare.com/ajax/libs/highlight.js/"], sources: {"script-src" => "https://cdnjs.cloudflare.com", "style-src" => "https://cdnjs.cloudflare.com"}}, {markers: ["https://cdn.jsdelivr.net/npm/katex@"], sources: {"script-src" => "https://cdn.jsdelivr.net", "style-src" => "https://cdn.jsdelivr.net", "font-src" => "https://cdn.jsdelivr.net"}}, {markers: ["https://cdn.jsdelivr.net/npm/mathjax@"], sources: {"script-src" => "https://cdn.jsdelivr.net", "font-src" => "https://cdn.jsdelivr.net"}}, {markers: ["https://cdn.jsdelivr.net/npm/mermaid@"], sources: {"script-src" => "https://cdn.jsdelivr.net"}}, {markers: ["https://www.youtube.com/embed/"], sources: {"frame-src" => "https://www.youtube.com"}}, {markers: ["https://player.vimeo.com/video/"], sources: {"frame-src" => "https://player.vimeo.com"}}, {markers: ["https://gist.github.com/", "class=\"sc-gist\""], sources: {"script-src" => "https://gist.github.com", "style-src" => "https://github.githubassets.com", "img-src" => "https://gist.github.com https://gist.githubusercontent.com"}}, {markers: ["https://platform.twitter.com/widgets.js", "class=\"twitter-tweet\""], sources: {"script-src" => "https://platform.twitter.com", "frame-src" => "https://platform.twitter.com"}}, {markers: ["https://codepen.io/"], sources: {"frame-src" => "https://codepen.io"}}]

Sources a feature Hwaro itself emits needs, keyed by markers the feature leaves in the page: its URL, and for an embed whose script [privacy] can localize while it still loads more from its host, the shortcode's own markup. A page gets a row's sources only when its final HTML contains a marker, so a CDN that [privacy] fully localized, or a shortcode the page does not use, adds nothing.

META_IGNORED = ["frame-ancestors", "report-uri", "sandbox"] of ::String

Directives a <meta> policy cannot carry (CSP3 §6.1).

META_PREFIX = "<meta http-equiv=\"Content-Security-Policy\" content=\""
OPAQUE_ELEMENTS = ["textarea", "title", "xmp", "iframe", "noembed", "noframes"] of ::String

Elements whose content the parser does not read as markup, skipped without hashing. Not <noscript>: with scripting off its content is markup, and a <style> in it is checked against the policy.

TEXT_ELEMENTS = (["script", "style"] of ::String) + OPAQUE_ELEMENTS

Class methods

apply(config : Models::Config, output_dir : String, excluded : Set(String), parallel : Bool = true) : Result

Hash every HTML page under output_dir (except the excluded absolute paths: verbatim copies of user files) and emit the policies. Returns what each page got, for the post-hook check.

Source
changed_pages(config : Models::CspConfig, result : Result) : Array(String)

Pages (output-relative) whose inline bytes a [build] hooks.post command changed after their policy was emitted.

Source
decode_attribute(raw : String) : String

An attribute value as the HTML parser decodes it: a named reference without ; stays literal when = or an alphanumeric follows it (url(a?x=1&copy=2) keeps its &copy), which HTML.unescape alone would decode.

Source
hash(body : String) : String

'sha256-…' of body as the browser hashes it: after the input stream's newline normalization (CRLF and lone CR become LF).

Source
headers_file(rules : Array(Tuple(String, String)), header : String, user : String | Nil) : String

The headers file: the user's own file verbatim, then one block per page. A user block that already sets header for a path wins.

Source
inject_meta(html : String, policy : String) : String

html with its policy <meta> as the first child of <head> — after a leading <meta charset>, which must stay within the first 1024 bytes. A meta a previous build injected is replaced. Unchanged when the page has no <head>.

Source
policy(config : Models::CspConfig, html : String, scan : Scan = scan(html), meta : Bool = config.meta?) : String

The policy for one page. meta drops what a <meta> cannot carry.

Source
rule_path(relative : String) : String | Nil

The _headers path line for a page file: its URL encoded exactly as the sitemap <loc> is (# → %23 and ? → %3F as Page#url= does, then encode_url_path), so the rule matches what the browser requests. Nil when no path line can match the page: hosts read : and * as a placeholder and a splat, a control character breaks the file, and a % in the file name cannot be traced back to one URL (a%b is published for /a%b/ and /a%25b/ alike).

Source
scan(html : String) : Scan
Source
strip_meta(html : String) : String

html without the policy <meta> a previous build injected. Hwaro reads a --cache hit's HTML back from disk before Finalize (the AMP converter, the PWA cache name); stripping it there keeps those outputs identical to a cold build's.

Source
url_path(relative : String) : String

The URL path a page file is served at (blog/x/index.html → /blog/x/), before base_path.

Source

Nested types