Skip to content

Commit a71b23b

Browse files
authored
Switch to per-package subdomains and drop public-host redirect (#117)
* Switch to per-package subdomains and drop public-host redirect `Hexdocs.Utils.hexdocs_url/3` now emits `https://PACKAGE.hexdocs.pm/path` for the hexpm repo. The sitemap keeps apex URLs via a new `hexdocs_apex_url/1` helper so Googlebot still discovers packages via the apex 301 chain. The Plug-level `*.hexdocs.pm -> *.hexorgs.pm` redirect arm is removed. The Fastly Compute service will handle that redirect once the DNS flip lands; this app now only serves the `*.hexorgs.pm` (private/org) path. * Map underscore to hyphen in hexpm-repo subdomain URLs Hex package names allow underscores (^[a-z][a-z0-9_]*$). RFC 1123 hostname labels and RFC 6125 wildcard SAN matching don't, and Fastly enforces strict SAN matching at the HTTP edge — phoenix_live_view.hexdocs.pm returns 421 'Misdirected Request' even though the wildcard cert technically covers *.hexdocs.pm. Hexdocs.Utils.hexdocs_url/3 for the hexpm-repo branch now maps _ -> - when building the subdomain. The mapping is reversed in the Fastly Compute subdomain handler before building the GCS bucket key, so canonical hex names continue to be the storage key. The org-repo branch is unchanged: org subdomains live under hexorgs.pm, which still A-records to GKE and doesn't enforce strict SAN matching. Queue test updated for the new URL shape.
1 parent 1d70636 commit a71b23b

6 files changed

Lines changed: 33 additions & 37 deletions

File tree

lib/hexdocs/bucket.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ defmodule Hexdocs.Bucket do
117117
for version <- versions do
118118
map = %{
119119
version: "v#{version}",
120-
url: Hexdocs.Utils.hexdocs_url(repository, "/#{package}/#{version}")
120+
url: Hexdocs.Utils.hexdocs_url(repository, package, "/#{version}")
121121
}
122122

123123
map = if latest_version == version, do: Map.put(map, :latest, true), else: map

lib/hexdocs/package_sitemap.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule Hexdocs.PackageSitemap do
99
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
1010
<%= for page <- pages do %>
1111
<url>
12-
<loc><%= Hexdocs.Utils.hexdocs_url("hexpm", "/#{package_name}/#{page}") %></loc>
12+
<loc><%= Hexdocs.Utils.hexdocs_apex_url("/#{package_name}/#{page}") %></loc>
1313
<lastmod><%= format_datetime updated_at %></lastmod>
1414
<changefreq>daily</changefreq>
1515
<priority>0.8</priority>

lib/hexdocs/plug.ex

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,6 @@ defmodule Hexdocs.Plug do
7171
:error ->
7272
send_resp(conn, 400, "")
7373

74-
{:redirect, subdomain} ->
75-
redirect_to_private_host(conn, subdomain)
76-
7774
{:ok, subdomain} ->
7875
cond do
7976
# OAuth callback - exchange code for tokens
@@ -90,20 +87,6 @@ defmodule Hexdocs.Plug do
9087
end
9188
end
9289

93-
defp redirect_to_private_host(conn, subdomain) do
94-
scheme = Application.get_env(:hexdocs, :scheme)
95-
host = Application.get_env(:hexdocs, :private_host)
96-
url = "#{scheme}://#{subdomain}.#{host}#{conn.request_path}"
97-
98-
html = Plug.HTML.html_escape(url)
99-
body = "<html><body>You are being <a href=\"#{html}\">redirected</a>.</body></html>"
100-
101-
conn
102-
|> put_resp_header("location", url)
103-
|> put_resp_header("content-type", "text/html")
104-
|> send_resp(301, body)
105-
end
106-
10790
defp redirect_oauth(conn, organization) do
10891
code_verifier = Hexdocs.OAuth.generate_code_verifier()
10992
code_challenge = Hexdocs.OAuth.generate_code_challenge(code_verifier)
@@ -285,12 +268,10 @@ defmodule Hexdocs.Plug do
285268
end
286269

287270
defp subdomain(host) do
288-
public_host = Application.get_env(:hexdocs, :host)
289271
private_host = Application.get_env(:hexdocs, :private_host)
290272

291273
case String.split(host, ".", parts: 2) do
292274
[subdomain, ^private_host] -> {:ok, subdomain}
293-
[subdomain, ^public_host] -> {:redirect, subdomain}
294275
_ -> :error
295276
end
296277
end

lib/hexdocs/utils.ex

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,34 @@ defmodule Hexdocs.Utils do
33

44
@special_package_names Map.keys(Application.compile_env!(:hexdocs, :special_packages))
55

6-
def hexdocs_url(repository, path) do
6+
def hexdocs_url(repository, package, path) do
77
"/" <> _ = path
88

99
if repository == "hexpm" do
1010
host = Application.get_env(:hexdocs, :host)
1111
scheme = if host == "hexdocs.pm", do: "https", else: "http"
12-
URI.encode("#{scheme}://#{host}#{path}")
12+
URI.encode("#{scheme}://#{package_to_subdomain(package)}.#{host}#{path}")
1313
else
1414
host = Application.get_env(:hexdocs, :private_host)
1515
scheme = if host in ["hexdocs.pm", "hexorgs.pm"], do: "https", else: "http"
16-
URI.encode("#{scheme}://#{repository}.#{host}#{path}")
16+
URI.encode("#{scheme}://#{repository}.#{host}/#{package}#{path}")
1717
end
1818
end
1919

20+
# Hex package names allow underscores (`^[a-z][a-z0-9_]*$`), but RFC 1123
21+
# hostname labels and RFC 6125 wildcard SAN matching don't, and Fastly
22+
# enforces strict SAN matching at the HTTP edge. Map `_` -> `-` for the
23+
# public hexdocs.pm subdomain. The mapping is reversed in the Fastly
24+
# Compute subdomain handler before the GCS bucket key is built.
25+
def package_to_subdomain(name), do: String.replace(name, "_", "-")
26+
27+
def hexdocs_apex_url(path) do
28+
"/" <> _ = path
29+
host = Application.get_env(:hexdocs, :host)
30+
scheme = if host == "hexdocs.pm", do: "https", else: "http"
31+
URI.encode("#{scheme}://#{host}#{path}")
32+
end
33+
2034
def latest_version(versions) do
2135
Enum.find(versions, &(&1.pre == [])) || List.first(versions)
2236
end

test/hexdocs/plug_test.exs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -309,26 +309,16 @@ defmodule Hexdocs.PlugTest do
309309
end
310310
end
311311

312-
describe "redirect from public host to private host" do
312+
describe "host handling" do
313313
setup do
314-
original_host = Application.get_env(:hexdocs, :host)
315314
original_private_host = Application.get_env(:hexdocs, :private_host)
316-
Application.put_env(:hexdocs, :host, "hexdocs.test")
317315
Application.put_env(:hexdocs, :private_host, "hexorgs.test")
318316

319317
on_exit(fn ->
320-
Application.put_env(:hexdocs, :host, original_host)
321318
Application.put_env(:hexdocs, :private_host, original_private_host)
322319
end)
323320
end
324321

325-
test "301 redirects from *.hexdocs.test to *.hexorgs.test" do
326-
conn = conn(:get, "http://myorg.hexdocs.test:5002/my_package/index.html") |> call()
327-
assert conn.status == 301
328-
[location] = get_resp_header(conn, "location")
329-
assert location == "http://myorg.hexorgs.test/my_package/index.html"
330-
end
331-
332322
test "serves docs on private host" do
333323
conn = conn(:get, "http://myorg.hexorgs.test:5002/foo") |> call()
334324
assert conn.status == 302
@@ -341,6 +331,15 @@ defmodule Hexdocs.PlugTest do
341331
conn = conn(:get, "http://other.example.com:5002/foo") |> call()
342332
assert conn.status == 400
343333
end
334+
335+
test "returns 400 for *.hexdocs.pm hosts (handled by Fastly)" do
336+
original_host = Application.get_env(:hexdocs, :host)
337+
Application.put_env(:hexdocs, :host, "hexdocs.test")
338+
on_exit(fn -> Application.put_env(:hexdocs, :host, original_host) end)
339+
340+
conn = conn(:get, "http://phoenix.hexdocs.test:5002/index.html") |> call()
341+
assert conn.status == 400
342+
end
344343
end
345344

346345
test "sets security headers" do

test/hexdocs/queue_test.exs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,16 @@ defmodule Hexdocs.QueueTest do
239239
["var versionNodes = " <> versions_json, "var searchNodes = " <> search_json] =
240240
String.split(docs_config, [";", "\n"], trim: true)
241241

242+
subdomain = URI.encode(String.replace(Atom.to_string(test), "_", "-"))
243+
242244
assert JSON.decode!(versions_json) == [
243245
%{
244-
"url" => "http://localhost/#{URI.encode(Atom.to_string(test))}/3.0.0",
246+
"url" => "http://#{subdomain}.localhost/3.0.0",
245247
"version" => "v3.0.0",
246248
"latest" => true
247249
},
248250
%{
249-
"url" => "http://localhost/#{URI.encode(Atom.to_string(test))}/1.0.0",
251+
"url" => "http://#{subdomain}.localhost/1.0.0",
250252
"version" => "v1.0.0",
251253
"retired" => true
252254
}

0 commit comments

Comments
 (0)