diff --git a/lib/hexdocs/bucket.ex b/lib/hexdocs/bucket.ex index 61f0388..46f747b 100644 --- a/lib/hexdocs/bucket.ex +++ b/lib/hexdocs/bucket.ex @@ -117,7 +117,7 @@ defmodule Hexdocs.Bucket do for version <- versions do map = %{ version: "v#{version}", - url: Hexdocs.Utils.hexdocs_url(repository, "/#{package}/#{version}") + url: Hexdocs.Utils.hexdocs_url(repository, package, "/#{version}") } map = if latest_version == version, do: Map.put(map, :latest, true), else: map diff --git a/lib/hexdocs/package_sitemap.ex b/lib/hexdocs/package_sitemap.ex index ea2eac6..337a2a7 100644 --- a/lib/hexdocs/package_sitemap.ex +++ b/lib/hexdocs/package_sitemap.ex @@ -9,7 +9,7 @@ defmodule Hexdocs.PackageSitemap do xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> <%= for page <- pages do %> - <%= Hexdocs.Utils.hexdocs_url("hexpm", "/#{package_name}/#{page}") %> + <%= Hexdocs.Utils.hexdocs_apex_url("/#{package_name}/#{page}") %> <%= format_datetime updated_at %> daily 0.8 diff --git a/lib/hexdocs/plug.ex b/lib/hexdocs/plug.ex index 501ea1c..1f4954a 100644 --- a/lib/hexdocs/plug.ex +++ b/lib/hexdocs/plug.ex @@ -71,9 +71,6 @@ defmodule Hexdocs.Plug do :error -> send_resp(conn, 400, "") - {:redirect, subdomain} -> - redirect_to_private_host(conn, subdomain) - {:ok, subdomain} -> cond do # OAuth callback - exchange code for tokens @@ -90,20 +87,6 @@ defmodule Hexdocs.Plug do end end - defp redirect_to_private_host(conn, subdomain) do - scheme = Application.get_env(:hexdocs, :scheme) - host = Application.get_env(:hexdocs, :private_host) - url = "#{scheme}://#{subdomain}.#{host}#{conn.request_path}" - - html = Plug.HTML.html_escape(url) - body = "You are being redirected." - - conn - |> put_resp_header("location", url) - |> put_resp_header("content-type", "text/html") - |> send_resp(301, body) - end - defp redirect_oauth(conn, organization) do code_verifier = Hexdocs.OAuth.generate_code_verifier() code_challenge = Hexdocs.OAuth.generate_code_challenge(code_verifier) @@ -285,12 +268,10 @@ defmodule Hexdocs.Plug do end defp subdomain(host) do - public_host = Application.get_env(:hexdocs, :host) private_host = Application.get_env(:hexdocs, :private_host) case String.split(host, ".", parts: 2) do [subdomain, ^private_host] -> {:ok, subdomain} - [subdomain, ^public_host] -> {:redirect, subdomain} _ -> :error end end diff --git a/lib/hexdocs/utils.ex b/lib/hexdocs/utils.ex index ff9fb5b..c4e33dd 100644 --- a/lib/hexdocs/utils.ex +++ b/lib/hexdocs/utils.ex @@ -3,20 +3,34 @@ defmodule Hexdocs.Utils do @special_package_names Map.keys(Application.compile_env!(:hexdocs, :special_packages)) - def hexdocs_url(repository, path) do + def hexdocs_url(repository, package, path) do "/" <> _ = path if repository == "hexpm" do host = Application.get_env(:hexdocs, :host) scheme = if host == "hexdocs.pm", do: "https", else: "http" - URI.encode("#{scheme}://#{host}#{path}") + URI.encode("#{scheme}://#{package_to_subdomain(package)}.#{host}#{path}") else host = Application.get_env(:hexdocs, :private_host) scheme = if host in ["hexdocs.pm", "hexorgs.pm"], do: "https", else: "http" - URI.encode("#{scheme}://#{repository}.#{host}#{path}") + URI.encode("#{scheme}://#{repository}.#{host}/#{package}#{path}") end end + # Hex package names allow underscores (`^[a-z][a-z0-9_]*$`), but RFC 1123 + # hostname labels and RFC 6125 wildcard SAN matching don't, and Fastly + # enforces strict SAN matching at the HTTP edge. Map `_` -> `-` for the + # public hexdocs.pm subdomain. The mapping is reversed in the Fastly + # Compute subdomain handler before the GCS bucket key is built. + def package_to_subdomain(name), do: String.replace(name, "_", "-") + + def hexdocs_apex_url(path) do + "/" <> _ = path + host = Application.get_env(:hexdocs, :host) + scheme = if host == "hexdocs.pm", do: "https", else: "http" + URI.encode("#{scheme}://#{host}#{path}") + end + def latest_version(versions) do Enum.find(versions, &(&1.pre == [])) || List.first(versions) end diff --git a/test/hexdocs/plug_test.exs b/test/hexdocs/plug_test.exs index b3717ca..c8206cd 100644 --- a/test/hexdocs/plug_test.exs +++ b/test/hexdocs/plug_test.exs @@ -309,26 +309,16 @@ defmodule Hexdocs.PlugTest do end end - describe "redirect from public host to private host" do + describe "host handling" do setup do - original_host = Application.get_env(:hexdocs, :host) original_private_host = Application.get_env(:hexdocs, :private_host) - Application.put_env(:hexdocs, :host, "hexdocs.test") Application.put_env(:hexdocs, :private_host, "hexorgs.test") on_exit(fn -> - Application.put_env(:hexdocs, :host, original_host) Application.put_env(:hexdocs, :private_host, original_private_host) end) end - test "301 redirects from *.hexdocs.test to *.hexorgs.test" do - conn = conn(:get, "http://myorg.hexdocs.test:5002/my_package/index.html") |> call() - assert conn.status == 301 - [location] = get_resp_header(conn, "location") - assert location == "http://myorg.hexorgs.test/my_package/index.html" - end - test "serves docs on private host" do conn = conn(:get, "http://myorg.hexorgs.test:5002/foo") |> call() assert conn.status == 302 @@ -341,6 +331,15 @@ defmodule Hexdocs.PlugTest do conn = conn(:get, "http://other.example.com:5002/foo") |> call() assert conn.status == 400 end + + test "returns 400 for *.hexdocs.pm hosts (handled by Fastly)" do + original_host = Application.get_env(:hexdocs, :host) + Application.put_env(:hexdocs, :host, "hexdocs.test") + on_exit(fn -> Application.put_env(:hexdocs, :host, original_host) end) + + conn = conn(:get, "http://phoenix.hexdocs.test:5002/index.html") |> call() + assert conn.status == 400 + end end test "sets security headers" do diff --git a/test/hexdocs/queue_test.exs b/test/hexdocs/queue_test.exs index 9e0969f..9f9b168 100644 --- a/test/hexdocs/queue_test.exs +++ b/test/hexdocs/queue_test.exs @@ -239,14 +239,16 @@ defmodule Hexdocs.QueueTest do ["var versionNodes = " <> versions_json, "var searchNodes = " <> search_json] = String.split(docs_config, [";", "\n"], trim: true) + subdomain = URI.encode(String.replace(Atom.to_string(test), "_", "-")) + assert JSON.decode!(versions_json) == [ %{ - "url" => "http://localhost/#{URI.encode(Atom.to_string(test))}/3.0.0", + "url" => "http://#{subdomain}.localhost/3.0.0", "version" => "v3.0.0", "latest" => true }, %{ - "url" => "http://localhost/#{URI.encode(Atom.to_string(test))}/1.0.0", + "url" => "http://#{subdomain}.localhost/1.0.0", "version" => "v1.0.0", "retired" => true }