sourceafSleepSafe::CspGuard.fan

using afIocConfig::Config
using afIocConfig::ConfigSource
using afBedSheet::HttpRequest
using afBedSheet::HttpResponse

** Guards against Cross Site Scripting (XSS) by setting an 'Content-Security-Policy' HTTP response header that tells browsers to restrict where content can be loaded from.
**
**   Content-Security-Policy: default-src 'self'; font-src 'self' https://fonts.googleapis.com/; object-src 'none'
**
** See `https://content-security-policy.com/` and [Content-Security-Policy on MDN]`https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy` for details.
**
** By default, Sleep Safe sets the following content directives:
**
**   Content-Security-Policy:
**       default-src 'self';
**       base-uri 'self';
**       form-action 'self';
**       frame-ancestors 'self';
**       object-src 'none';
**       report-uri /_sleepSafeCspViolation;
**
** Which essentially locks all content down to that served by the BedSheet server and disables object tags.
**
** SleepSafe also sets up a BedSheet Route ('report-uri') that browsers can report violations to.
** The default implementation logs a pretty printed version of the report JSON.
**
** The default strategy is a good base to start with. You can then upgrade the directives as and when you need to.
** Although beware of inline scripts and style tags, as these will also be disabled. See [Implementing Content Security Policy]`https://hacks.mozilla.org/2016/02/implementing-content-security-policy/` for details.
**
** The reporting mechanism is good for development, but you may want to turn it off for production as browser add-ons can
** cause violations, flooding your server.
**
**
**
** Ioc Configuration
** *****************
**
**   table:
**   afIocConfig Key              Value
**   ---------------------------  ------------
**   'afSleepSafe.csp.XXXX'       Any config starting with 'afSleepSafe.csp.' (note the trailing dot) is taken as a CSP directive and used as is. Set to 'null' to remove a directive.
**   'afSleepSafe.cspReportOnly'  If 'true' then the 'Content-Security-Policy-Report-Only' header is set, which doesn't block anything but still sends violation reports. Defaults to 'false'.
**   'afSleepSafe.cspReportFn'    The reporting function (immutable) that's invoked with the browsers violation JSON. Set to 'null' to disable report handling and the default BedSheet route.
**
** Example:
**
**   syntax: fantom
**   @Contribute { serviceType=ApplicationDefaults# }
**   Void contributeAppDefaults(Configuration config) {
**       // configure CSP
**       config["afSleepSafe.cspReportOnly"]   = true
**       config["afSleepSafe.cspReportFn"]     = |Str:Obj? reportJson| { echo(reportJson) }.toImmutable
**
**       // set CSP directives
**       config["afSleepSafe.csp.default-src"] = "'none'"
**       config["afSleepSafe.csp.font-src"]    = "'self' https://fonts.googleapis.com/"
**   }
**
** To prevent CSP violations from being logged on the server, override the FactoryDefaults by setting either (or both) of the following to 'null' in ApplicationDefaults:
**
**   syntax: fantom
**   config["afSleepSafe.csp.report-uri"] = null
**   config["afSleepSafe.cspReportFn"]    = null
**
** To disable CSP, remove this class from the 'SleepSafeMiddleware' configuration:
**
**   syntax: fantom
**   @Contribute { serviceType=SleepSafeMiddleware# }
**   Void contributeSleepSafeMiddleware(Configuration config) {
**       config.remove(CspGuard#)
**   }
**
const class CspGuard : Guard {

    private const Str? csp
    @Config { id="afSleepSafe.cspReportOnly" }
    private const Bool reportOnly

    ** The 'Content-Security-Protection' directives that get passed to the browser
    const Str:Str directives

    @NoDoc
    override const Str protectsAgainst  := "XSS"

    private new make(ConfigSource configSrc, |This| f) {
        f(this)

        directives := Str:Str[:]
        configSrc.config.each |val, key| {
            if (key.startsWith("csp.") || key.startsWith("afSleepSafe.csp.")) {
                if (key.startsWith("afSleepSafe."))
                    key = key["afSleepSafe.".size..-1]
                if (key.startsWith("csp."))
                    key = key["csp.".size..-1]
                if (val != null)
                    directives[key] = val.toStr
            }

        }
        directives2 := Str:Str[:] { it.ordered = true }
        directives.keys.sort.each { directives2[it] = directives[it] }
        this.directives = directives2
        this.csp = directives2.join("; ") |val, key| { "${key} ${val}" }.trimToNull
    }

    @NoDoc
    override Str? guard(HttpRequest httpReq, HttpResponse httpRes) {
        if (csp != null) {
            // set the headers at the start of the request so other code may add or manipulate it
            if (reportOnly)
                httpRes.headers["Content-Security-Policy-Report-Only"] = csp
            else
                httpRes.headers["Content-Security-Policy"] = csp

            // don't bother setting the CSP header for non-HTML files
            // https://stackoverflow.com/questions/48151455/for-which-content-types-should-i-set-security-related-http-response-headers
            httpRes.onCommit |->| {
                contentType := httpRes.headers.contentType?.noParams?.toStr?.lower
                if (contentType == "text/html" || contentType == "application/xhtml+xml")
                    return

                // if it's not a HTML page, then remove the headers
                if (reportOnly) {
                    httpRes.headers["Content-Security-Policy-Report-Only"] = null
                } else {
                    httpRes.headers["Content-Security-Policy"] = null
                }
            }
        }

        return null
    }
}