lib/phoenix_asset_pipeline.ex

defmodule PhoenixAssetPipeline do
  @moduledoc false
  import Plug.Conn,
    only: [
      halt: 1,
      put_private: 3,
      put_resp_header: 3,
      register_before_send: 2,
      send_resp: 3
    ]

  alias PhoenixAssetPipeline.Storage

  defmacro __using__(opts) do
    quote do
      import PhoenixAssetPipeline.Plug, only: [assets: 2, static: 2]
      import unquote(__MODULE__)

      alias PhoenixAssetPipeline.Utils

      paths = Utils.static_paths()

      @env Mix.env()
      @on_load :on_load
      @paths_hash :erlang.md5(paths)
      @static_files Utils.static_files()

      for path <- paths, do: @external_resource(path)

      plug :heath_check
      plug :put_router_url
      plug :put_static_url
      plug :assets
      plug :static, unquote(opts)
      plug :content_security_policy, @env
      plug :minify_html_body, @env

      def __mix_recompile__? do
        Utils.static_paths() |> :erlang.md5() != @paths_hash
      end

      def static_files, do: @static_files

      def on_load, do: Storage.put(:endpoint, __MODULE__)
    end
  end

  def content_security_policy(conn, :prod) do
    register_before_send(conn, &put_content_security_policy/1)
  end

  def content_security_policy(conn, _), do: conn

  def heath_check(%{path_info: ["health"]} = conn, _) do
    conn
    |> send_resp(200, "ok")
    |> halt()
  end

  def heath_check(conn, _), do: conn

  def minify_html_body(conn, :prod), do: register_before_send(conn, &minify/1)
  def minify_html_body(conn, _), do: conn

  def put_router_url(%{host: host, private: %{phoenix_endpoint: phoenix_endpoint}} = conn, _) do
    router_url = phoenix_endpoint.url()
    router_url = phoenix_endpoint.struct_url() |> base_url(host, router_url)

    put_private(conn, :phoenix_router_url, router_url)
  end

  def put_static_url(%{host: host, private: %{phoenix_endpoint: phoenix_endpoint}} = conn, _) do
    static_url = phoenix_endpoint.static_url()
    static_url = static_url |> URI.parse() |> base_url(host, static_url)

    put_private(conn, :phoenix_static_url, static_url)
  end

  defp base_url(uri, host), do: URI.to_string(%{uri | host: host})
  defp base_url(%{host: "localhost"} = uri, host, _), do: base_url(uri, host)
  defp base_url(_, _, url), do: url

  defp minify(%{resp_body: body, resp_headers: [{"content-type", "text/html" <> _} | _]} = conn) do
    body =
      body
      |> Floki.parse_document!()
      |> Floki.raw_html()

    %{conn | resp_body: "<!DOCTYPE html>" <> body}
  end

  defp minify(conn), do: conn

  defp put_content_security_policy(conn) do
    integrities = Storage.get(:modules) |> Enum.flat_map(& &1.integrities())
    fun = &"'sha512-#{&1}'"

    script_src = for {".js", integrity} <- integrities, do: fun.(integrity)
    style_src = for {".css", integrity} <- integrities, do: fun.(integrity)

    directives =
      [
        "base-uri 'self'",
        "default-src 'self'",
        "script-src 'unsafe-inline' #{conn.private.phoenix_static_url} #{Enum.join(script_src, " ")} 'strict-dynamic'",
        "style-src 'self' #{Enum.join(style_src, " ")}"
      ]
      |> Enum.join("; ")

    put_resp_header(conn, "content-security-policy", directives)
  end
end