Skip to main content

src/plume.erl

-module(plume).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/plume.gleam").
-export([new/0, default/0, set_headers/2, middleware/2]).
-export_type([config/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Sensible HTTP security headers for Gleam web servers, inspired by\n"
    " [helmet](https://helmetjs.github.io/). Built on\n"
    " [gleam_http](https://hexdocs.pm/gleam_http/), so it works with\n"
    " [wisp](https://hexdocs.pm/wisp/), [mist](https://hexdocs.pm/mist/), or\n"
    " any other compatible server.\n"
    "\n"
    " Build a `Config` describing which headers to set on outgoing responses,\n"
    " then apply it. `default` ships a reasonable starter policy; `new`\n"
    " starts with no headers set.\n"
    "\n"
    " As `use` middleware:\n"
    "\n"
    " ```gleam\n"
    " use <- plume.middleware(plume.default())\n"
    " response.new(200)\n"
    " ```\n"
    "\n"
    " Or directly on a response:\n"
    "\n"
    " ```gleam\n"
    " response.new(200)\n"
    " |> plume.set_headers(plume.default())\n"
    " ```\n"
).

-type config() :: {config,
        gleam@option:option(plume@content_security_policy:content_security_policy()),
        gleam@option:option(plume@content_type_options:content_type_options()),
        gleam@option:option(plume@cross_origin_embedder_policy:cross_origin_embedder_policy()),
        gleam@option:option(plume@cross_origin_opener_policy:cross_origin_opener_policy()),
        gleam@option:option(plume@cross_origin_resource_policy:cross_origin_resource_policy()),
        gleam@option:option(plume@dns_prefetch_control:dns_prefetch_control()),
        gleam@option:option(plume@download_options:download_options()),
        gleam@option:option(plume@frame_options:frame_options()),
        gleam@option:option(plume@origin_agent_cluster:origin_agent_cluster()),
        gleam@option:option(plume@permissions_policy:permissions_policy()),
        gleam@option:option(plume@permitted_cross_domain_policies:permitted_cross_domain_policies()),
        gleam@option:option(plume@referrer_policy:referrer_policy()),
        gleam@option:option(plume@strict_transport_security:strict_transport_security()),
        gleam@option:option(plume@xss_protection:xss_protection())}.

-file("src/plume.gleam", 67).
?DOC(
    " A `Config` with no headers configured. Use this when you want to opt in\n"
    " to each header individually rather than starting from `default`.\n"
).
-spec new() -> config().
new() ->
    {config,
        none,
        none,
        none,
        none,
        none,
        none,
        none,
        none,
        none,
        none,
        none,
        none,
        none,
        none}.

-file("src/plume.gleam", 98).
?DOC(
    " A `Config` with sensible defaults: a starter CSP, `nosniff`,\n"
    " `SameOrigin` frame options, HSTS for one year on the host and its\n"
    " subdomains, and other widely-recommended values.\n"
    "\n"
    " ## Examples\n"
    "\n"
    " Override individual fields with record update syntax:\n"
    "\n"
    " ```gleam\n"
    " Config(..default(), frame_options: Some(frame_options.Deny))\n"
    " ```\n"
).
-spec default() -> config().
default() ->
    {config,
        {some,
            {policy,
                [{default_src, [self]},
                    {base_uri, [self]},
                    {font_src,
                        [self,
                            {scheme, <<"https"/utf8>>},
                            {scheme, <<"data"/utf8>>}]},
                    {form_action, [self]},
                    {frame_ancestors, [self]},
                    {img_src, [self, {scheme, <<"data"/utf8>>}]},
                    {object_src, [none]},
                    {script_src, [self]},
                    {script_src_attr, [none]},
                    {style_src,
                        [self, {scheme, <<"https"/utf8>>}, unsafe_inline]},
                    upgrade_insecure_requests]}},
        {some, no_sniff},
        none,
        {some, same_origin},
        {some, same_origin},
        {some, off},
        {some, no_open},
        {some, same_origin},
        {some, enabled},
        none,
        {some, none},
        {some, no_referrer},
        {some, {include_sub_domains, 31536000}},
        {some, disabled}}.

-file("src/plume.gleam", 204).
-spec set_header_if_some(
    gleam@http@response:response(ERK),
    gleam@option:option(ERM),
    binary(),
    fun((ERM) -> binary())
) -> gleam@http@response:response(ERK).
set_header_if_some(Resp, Value, Name, Render) ->
    case Value of
        {some, Value@1} ->
            gleam@http@response:set_header(Resp, Name, Render(Value@1));

        none ->
            Resp
    end.

-file("src/plume.gleam", 142).
?DOC(" Set the headers from `config` on an existing response.\n").
-spec set_headers(gleam@http@response:response(ERH), config()) -> gleam@http@response:response(ERH).
set_headers(Resp, Config) ->
    _pipe = Resp,
    _pipe@1 = set_header_if_some(
        _pipe,
        erlang:element(2, Config),
        <<"content-security-policy"/utf8>>,
        fun plume@content_security_policy:to_string/1
    ),
    _pipe@2 = set_header_if_some(
        _pipe@1,
        erlang:element(3, Config),
        <<"x-content-type-options"/utf8>>,
        fun plume@content_type_options:to_string/1
    ),
    _pipe@3 = set_header_if_some(
        _pipe@2,
        erlang:element(4, Config),
        <<"cross-origin-embedder-policy"/utf8>>,
        fun plume@cross_origin_embedder_policy:to_string/1
    ),
    _pipe@4 = set_header_if_some(
        _pipe@3,
        erlang:element(5, Config),
        <<"cross-origin-opener-policy"/utf8>>,
        fun plume@cross_origin_opener_policy:to_string/1
    ),
    _pipe@5 = set_header_if_some(
        _pipe@4,
        erlang:element(6, Config),
        <<"cross-origin-resource-policy"/utf8>>,
        fun plume@cross_origin_resource_policy:to_string/1
    ),
    _pipe@6 = set_header_if_some(
        _pipe@5,
        erlang:element(7, Config),
        <<"x-dns-prefetch-control"/utf8>>,
        fun plume@dns_prefetch_control:to_string/1
    ),
    _pipe@7 = set_header_if_some(
        _pipe@6,
        erlang:element(8, Config),
        <<"x-download-options"/utf8>>,
        fun plume@download_options:to_string/1
    ),
    _pipe@8 = set_header_if_some(
        _pipe@7,
        erlang:element(9, Config),
        <<"x-frame-options"/utf8>>,
        fun plume@frame_options:to_string/1
    ),
    _pipe@9 = set_header_if_some(
        _pipe@8,
        erlang:element(10, Config),
        <<"origin-agent-cluster"/utf8>>,
        fun plume@origin_agent_cluster:to_string/1
    ),
    _pipe@10 = set_header_if_some(
        _pipe@9,
        erlang:element(11, Config),
        <<"permissions-policy"/utf8>>,
        fun plume@permissions_policy:to_string/1
    ),
    _pipe@11 = set_header_if_some(
        _pipe@10,
        erlang:element(12, Config),
        <<"x-permitted-cross-domain-policies"/utf8>>,
        fun plume@permitted_cross_domain_policies:to_string/1
    ),
    _pipe@12 = set_header_if_some(
        _pipe@11,
        erlang:element(13, Config),
        <<"referrer-policy"/utf8>>,
        fun plume@referrer_policy:to_string/1
    ),
    _pipe@13 = set_header_if_some(
        _pipe@12,
        erlang:element(14, Config),
        <<"strict-transport-security"/utf8>>,
        fun plume@strict_transport_security:to_string/1
    ),
    set_header_if_some(
        _pipe@13,
        erlang:element(15, Config),
        <<"x-xss-protection"/utf8>>,
        fun plume@xss_protection:to_string/1
    ).

-file("src/plume.gleam", 133).
?DOC(" Run `handler` and set the headers from `config` on the resulting response.\n").
-spec middleware(config(), fun(() -> gleam@http@response:response(ERE))) -> gleam@http@response:response(ERE).
middleware(Config, Handler) ->
    _pipe = Handler(),
    set_headers(_pipe, Config).