diff --git a/config/config.exs b/config/config.exs index ff1b64f..43a18ed 100644 --- a/config/config.exs +++ b/config/config.exs @@ -25,6 +25,7 @@ config :hexdocs, session_signing_salt: "QftsNdJO", session_encryption_salt: "QftsNdJO", host: "localhost", + private_host: "localhost", gcs_put_debounce: 0, special_packages: %{ "eex" => "elixir-lang/elixir", diff --git a/config/runtime.exs b/config/runtime.exs index 9eeac79..dfb7708 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -19,7 +19,8 @@ if config_env() == :prod do queue_concurrency: String.to_integer(System.fetch_env!("HEXDOCS_QUEUE_CONCURRENCY")), github_user: System.fetch_env!("HEXDOCS_GITHUB_USER"), github_token: System.fetch_env!("HEXDOCS_GITHUB_TOKEN"), - host: System.fetch_env!("HEXDOCS_HOST") + host: System.fetch_env!("HEXDOCS_HOST"), + private_host: System.fetch_env!("HEXDOCS_PRIVATE_HOST") config :hexdocs, :repo_bucket, name: System.fetch_env!("HEXDOCS_REPO_BUCKET") diff --git a/config/test.exs b/config/test.exs index 12dbd50..9345425 100644 --- a/config/test.exs +++ b/config/test.exs @@ -8,6 +8,7 @@ config :hexdocs, cdn_impl: Hexdocs.CDN.Local, search_impl: Hexdocs.Search.Local, source_repo_impl: Hexdocs.SourceRepo.Mock, - hex_repo_impl: Hexdocs.HexRepo.Mock + hex_repo_impl: Hexdocs.HexRepo.Mock, + private_host: "localhost" config :logger, level: :warning diff --git a/lib/hexdocs/file_rewriter.ex b/lib/hexdocs/file_rewriter.ex index f5b0ac3..5dd73e2 100644 --- a/lib/hexdocs/file_rewriter.ex +++ b/lib/hexdocs/file_rewriter.ex @@ -9,7 +9,7 @@ defmodule Hexdocs.FileRewriter do @noindex_hook ~s|| - @official_domains ~w(hex.pm hexdocs.pm elixir-lang.org erlang.org) + @official_domains ~w(hex.pm hexdocs.pm hexorgs.pm elixir-lang.org erlang.org) def run(path, content) do content diff --git a/lib/hexdocs/plug.ex b/lib/hexdocs/plug.ex index dd1986b..a0a278c 100644 --- a/lib/hexdocs/plug.ex +++ b/lib/hexdocs/plug.ex @@ -58,25 +58,43 @@ defmodule Hexdocs.Plug do end defp run(conn, _opts) do - subdomain = subdomain(conn.host) - - cond do - !subdomain -> + case subdomain(conn.host) do + :error -> send_resp(conn, 400, "") - # OAuth callback - exchange code for tokens - conn.request_path == "/oauth/callback" -> - handle_oauth_callback(conn, subdomain) + {:redirect, subdomain} -> + redirect_to_private_host(conn, subdomain) - # OAuth access token in session - access_token = get_session(conn, "access_token") -> - try_serve_page_oauth(conn, subdomain, access_token) + {:ok, subdomain} -> + cond do + # OAuth callback - exchange code for tokens + conn.request_path == "/oauth/callback" -> + handle_oauth_callback(conn, subdomain) - true -> - redirect_oauth(conn, subdomain) + # OAuth access token in session + access_token = get_session(conn, "access_token") -> + try_serve_page_oauth(conn, subdomain, access_token) + + true -> + redirect_oauth(conn, subdomain) + end 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) @@ -103,7 +121,7 @@ defmodule Hexdocs.Plug do defp build_oauth_redirect_uri(_conn, organization) do scheme = Application.get_env(:hexdocs, :scheme) - host = Application.get_env(:hexdocs, :host) + host = Application.get_env(:hexdocs, :private_host) "#{scheme}://#{organization}.#{host}/oauth/callback" end @@ -258,11 +276,13 @@ defmodule Hexdocs.Plug do end defp subdomain(host) do - app_host = Application.get_env(:hexdocs, :host) + public_host = Application.get_env(:hexdocs, :host) + private_host = Application.get_env(:hexdocs, :private_host) case String.split(host, ".", parts: 2) do - [subdomain, ^app_host] -> subdomain - _ -> nil + [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 eba3664..ff9fb5b 100644 --- a/lib/hexdocs/utils.ex +++ b/lib/hexdocs/utils.ex @@ -5,10 +5,16 @@ defmodule Hexdocs.Utils do def hexdocs_url(repository, path) do "/" <> _ = path - host = Application.get_env(:hexdocs, :host) - scheme = if host == "hexdocs.pm", do: "https", else: "http" - subdomain = if repository == "hexpm", do: "", else: "#{repository}." - URI.encode("#{scheme}://#{subdomain}#{host}#{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}") + 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}") + end end def latest_version(versions) do diff --git a/test/hexdocs/plug_test.exs b/test/hexdocs/plug_test.exs index 2a93652..106786c 100644 --- a/test/hexdocs/plug_test.exs +++ b/test/hexdocs/plug_test.exs @@ -309,6 +309,40 @@ defmodule Hexdocs.PlugTest do end end + describe "redirect from public host to private host" 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 + + [location] = get_resp_header(conn, "location") + assert String.starts_with?(location, "http://localhost:5000/oauth/authorize?") + end + + test "returns 400 for unrecognized host" do + conn = conn(:get, "http://other.example.com:5002/foo") |> call() + assert conn.status == 400 + end + end + defp call(conn) do Hexdocs.Plug.call(conn, []) end