Skip to content

Commit bc20364

Browse files
committed
fix: properly handle canonical host redirection for docs
1 parent 50da4ad commit bc20364

File tree

2 files changed

+107
-8
lines changed

2 files changed

+107
-8
lines changed

lib/algora_web/endpoint.ex

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ defmodule AlgoraWeb.Endpoint do
44
# The session will be stored in the cookie and signed,
55
# this means its contents can be read but not tampered with.
66
# Set :encryption_salt if you would also like to encrypt it.
7+
alias AlgoraWeb.Plugs.CanonicalHostPlug
8+
79
@session_options [
810
store: :cookie,
911
key: "_algora_key",
@@ -60,20 +62,18 @@ defmodule AlgoraWeb.Endpoint do
6062
# Legacy tRPC endpoint
6163
defp canonical_host(%{path_info: ["api", "trpc" | _]} = conn, _opts), do: conn
6264

63-
defp canonical_host(%{host: "docs.algora.io"} = conn, _opts) do
64-
conn = %{conn | request_path: "/docs" <> conn.request_path}
65-
redirect_to_canonical_host(conn)
66-
end
65+
defp canonical_host(%{host: "docs.algora.io"} = conn, _opts),
66+
do: redirect_to_canonical_host(conn, Path.join(["/docs", conn.request_path]))
6767

68-
defp canonical_host(conn, _opts), do: redirect_to_canonical_host(conn)
68+
defp canonical_host(conn, _opts), do: redirect_to_canonical_host(conn, conn.request_path)
6969

70-
defp redirect_to_canonical_host(conn) do
70+
defp redirect_to_canonical_host(conn, path) do
7171
:algora
7272
|> Application.get_env(:canonical_host)
7373
|> case do
7474
host when is_binary(host) ->
75-
opts = PlugCanonicalHost.init(canonical_host: host)
76-
PlugCanonicalHost.call(conn, opts)
75+
opts = CanonicalHostPlug.init(canonical_host: host, path: path)
76+
CanonicalHostPlug.call(conn, opts)
7777

7878
_ ->
7979
conn
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
defmodule AlgoraWeb.Plugs.CanonicalHostPlug do
2+
@moduledoc """
3+
A Plug for ensuring that all requests are served by a single canonical host
4+
Adapted from https://github.com/remi/plug_canonical_host
5+
"""
6+
@behaviour Plug
7+
8+
# Imports
9+
import Plug.Conn
10+
11+
# Aliases
12+
alias Plug.Conn
13+
14+
# Behaviours
15+
16+
# Constants
17+
@location_header "location"
18+
@forwarded_port_header "x-forwarded-port"
19+
@forwarded_proto_header "x-forwarded-proto"
20+
@status_code 301
21+
@html_template """
22+
<!DOCTYPE html>
23+
<html lang="en-US">
24+
<head><title>301 Moved Permanently</title></head>
25+
<body>
26+
<h1>Moved Permanently</h1>
27+
<p>The document has moved <a href="%s">here</a>.</p>
28+
</body>
29+
</html>
30+
"""
31+
32+
# Types
33+
@type opts :: binary | tuple | atom | integer | float | [opts] | %{opts => opts}
34+
35+
@doc """
36+
Initialize this plug with a canonical host option.
37+
"""
38+
@spec init(opts) :: opts
39+
def init(opts) do
40+
[
41+
canonical_host: Keyword.fetch!(opts, :canonical_host),
42+
path: Keyword.fetch!(opts, :path)
43+
]
44+
end
45+
46+
@doc """
47+
Call the plug.
48+
"""
49+
@spec call(%Conn{}, opts) :: Conn.t()
50+
def call(%Conn{host: host} = conn, canonical_host: canonical_host, path: path)
51+
when is_nil(canonical_host) == false and canonical_host !== "" and host !== canonical_host do
52+
location = redirect_location(conn, canonical_host, path)
53+
54+
conn
55+
|> put_resp_header(@location_header, location)
56+
|> send_resp(@status_code, String.replace(@html_template, "%s", location))
57+
|> halt()
58+
end
59+
60+
def call(conn, _), do: conn
61+
62+
@spec redirect_location(%Conn{}, String.t(), String.t()) :: String.t()
63+
defp redirect_location(conn, canonical_host, path) do
64+
conn
65+
|> request_uri(path)
66+
|> URI.parse()
67+
|> sanitize_empty_query()
68+
|> Map.put(:host, canonical_host)
69+
|> Map.put(:path, path)
70+
|> URI.to_string()
71+
end
72+
73+
@spec request_uri(%Conn{}, String.t()) :: String.t()
74+
defp request_uri(%Conn{host: host, query_string: query_string} = conn, path) do
75+
"#{canonical_scheme(conn)}://#{host}:#{canonical_port(conn)}#{path}?#{query_string}"
76+
end
77+
78+
@spec canonical_port(%Conn{}) :: binary | integer
79+
defp canonical_port(%Conn{port: port} = conn) do
80+
case {get_req_header(conn, @forwarded_port_header), get_req_header(conn, @forwarded_proto_header)} do
81+
{[forwarded_port], _} -> forwarded_port
82+
{[], ["http"]} -> 80
83+
{[], ["https"]} -> 443
84+
{[], []} -> port
85+
end
86+
end
87+
88+
@spec canonical_scheme(%Conn{}) :: binary
89+
defp canonical_scheme(%Conn{scheme: scheme} = conn) do
90+
case get_req_header(conn, @forwarded_proto_header) do
91+
[forwarded_proto] -> forwarded_proto
92+
[] -> scheme
93+
end
94+
end
95+
96+
@spec sanitize_empty_query(%URI{}) :: %URI{}
97+
defp sanitize_empty_query(%URI{query: ""} = uri), do: Map.put(uri, :query, nil)
98+
defp sanitize_empty_query(uri), do: uri
99+
end

0 commit comments

Comments
 (0)