defmodule PlugCanonicalHost do @moduledoc """ A Plug for ensuring that all requests are served by a single canonical host """ # Imports import Plug.Conn # Aliases alias Plug.Conn # Behaviours @behaviour Plug # Constants @location_header "location" @forwarded_port_header "x-forwarded-port" @forwarded_proto_header "x-forwarded-proto" @status_code 301 @html_template """ 301 Moved Permanently

Moved Permanently

The document has moved here.

""" # Types @type opts :: binary | tuple | atom | integer | float | [opts] | %{opts => opts} @doc """ Initialize this plug with a canonical host option. """ @spec init(opts) :: opts def init(opts) do [ canonical_host: Keyword.fetch!(opts, :canonical_host) ] end @doc """ Call the plug. """ @spec call(%Conn{}, opts) :: Conn.t() def call(conn = %Conn{host: host}, canonical_host: canonical_host) when is_nil(canonical_host) == false and canonical_host !== "" and host !== canonical_host do location = conn |> redirect_location(canonical_host) conn |> put_resp_header(@location_header, location) |> send_resp(@status_code, String.replace(@html_template, "%s", location)) |> halt end def call(conn, _), do: conn @spec redirect_location(%Conn{}, String.t()) :: String.t() defp redirect_location(conn, canonical_host) do conn |> request_uri |> URI.parse() |> sanitize_empty_query() |> Map.put(:host, canonical_host) |> URI.to_string() end @spec request_uri(%Conn{}) :: String.t() defp request_uri(conn = %Conn{host: host, request_path: request_path, query_string: query_string}) do "#{canonical_scheme(conn)}://#{host}:#{canonical_port(conn)}#{request_path}?#{query_string}" end @spec canonical_port(%Conn{}) :: binary | integer defp canonical_port(conn = %Conn{port: port}) do case {get_req_header(conn, @forwarded_port_header), get_req_header(conn, @forwarded_proto_header)} do {[forwarded_port], _} -> forwarded_port {[], ["http"]} -> 80 {[], ["https"]} -> 443 {[], []} -> port end end @spec canonical_scheme(%Conn{}) :: binary defp canonical_scheme(conn = %Conn{scheme: scheme}) do case get_req_header(conn, @forwarded_proto_header) do [forwarded_proto] -> forwarded_proto [] -> scheme end end @spec sanitize_empty_query(%URI{}) :: %URI{} defp sanitize_empty_query(uri = %URI{query: ""}), do: Map.put(uri, :query, nil) defp sanitize_empty_query(uri), do: uri end