diff --git a/lib/master_proxy.ex b/lib/master_proxy.ex new file mode 100644 index 0000000..d884c1f --- /dev/null +++ b/lib/master_proxy.ex @@ -0,0 +1,82 @@ +defmodule MasterProxy do + require Logger + + @type scheme :: :http | :https + + @callback choose_backend(Plug.Conn.t()) :: + {:phoenix_endpoint, module()} | {:plug, module()} | :fallback + + @callback merge_config(scheme(), keyword()) :: keyword() + + @optional_callbacks choose_backend: 1, merge_config: 2 + + defmacro __using__(opts) do + # TODO: Check that backends was passed at compile time + # Maybe use a similar pattern with nimble_options as site_encrypt? + + quote do + use Supervisor + + @behaviour MasterProxy + + def start_link(callback_module) do + Supervisor.start_link(__MODULE__, callback_module, name: __MODULE__) + end + + @impl Supervisor + def init(callback_module) do + backends = Keyword.fetch!(unquote(opts), :backends) + children = MasterProxy.spec([backends: backends, callback_module: __MODULE__], __MODULE__) + + Supervisor.init(children, strategy: :one_for_one) + end + end + end + + @doc false + def spec(handler_opts, callback_module) do + Enum.reduce([:http, :https], [], fn scheme, result -> + case Application.get_env(:master_proxy, scheme) do + nil -> + # no config for this scheme, that's ok, just skip + result + + scheme_opts -> + opts = build_opts(scheme, scheme_opts, handler_opts, callback_module) + + Logger.info("[master_proxy] Listening on #{scheme} with options: #{inspect(opts)}") + + [{Plug.Cowboy, scheme: scheme, plug: {nil, nil}, options: opts} | result] + end + end) + end + + defp build_opts(scheme, scheme_opts, handler_opts, callback_module) do + port = :proplists.get_value(:port, scheme_opts) + dispatch = [{:_, [{:_, MasterProxy.Cowboy2Handler, {nil, handler_opts}}]}] + + opts = + [ + port: port_to_integer(port), + dispatch: dispatch + ] + |> maybe_merge_config(scheme, callback_module) + + opts ++ :proplists.delete(:port, scheme_opts) + end + + defp maybe_merge_config(config, scheme, callback_module) do + if function_exported?(callback_module, :merge_config, 2) do + callback_module.merge_config(scheme, config) + else + config + end + end + + # :undefined is what :proplist.get_value returns + defp port_to_integer(:undefined), + do: raise("port is missing from the master_proxy configuration") + + defp port_to_integer(port) when is_binary(port), do: String.to_integer(port) + defp port_to_integer(port) when is_integer(port), do: port +end diff --git a/lib/master_proxy/application.ex b/lib/master_proxy/application.ex deleted file mode 100644 index d336678..0000000 --- a/lib/master_proxy/application.ex +++ /dev/null @@ -1,42 +0,0 @@ -defmodule MasterProxy.Application do - @moduledoc false - use Application - require Logger - - def start(_type, _args) do - import Supervisor.Spec, warn: false - - children = - Enum.reduce([:http, :https], [], fn scheme, result -> - case Application.get_env(:master_proxy, scheme) do - nil -> - # no config for this scheme, that's ok, just skip - result - - scheme_opts -> - port = :proplists.get_value(:port, scheme_opts) - dispatch = [{:_, [{:_, MasterProxy.Cowboy2Handler, {nil, nil}}]}] - - opts = - [ - port: port_to_integer(port), - dispatch: dispatch - ] ++ :proplists.delete(:port, scheme_opts) - - Logger.info("[master_proxy] Listening on #{scheme} with options: #{inspect(opts)}") - - [{Plug.Cowboy, scheme: scheme, plug: {nil, nil}, options: opts} | result] - end - end) - - opts = [strategy: :one_for_one, name: MasterProxy.Supervisor] - Supervisor.start_link(children, opts) - end - - # :undefined is what :proplist.get_value returns - defp port_to_integer(:undefined), - do: raise("port is missing from the master_proxy configuration") - - defp port_to_integer(port) when is_binary(port), do: String.to_integer(port) - defp port_to_integer(port) when is_integer(port), do: port -end diff --git a/lib/master_proxy/cowboy2_handler.ex b/lib/master_proxy/cowboy2_handler.ex index b8b7078..b5fa9b1 100644 --- a/lib/master_proxy/cowboy2_handler.ex +++ b/lib/master_proxy/cowboy2_handler.ex @@ -19,21 +19,33 @@ defmodule MasterProxy.Cowboy2Handler do # endpoint and opts are not passed in because they # are dynamically chosen - def init(req, {_endpoint, _opts}) do + def init(req, {_endpoint, opts}) do log_request("MasterProxy.Cowboy2Handler called with req: #{inspect(req)}") conn = connection().conn(req) - # extract this and pass in as a param somehow - backends = Application.get_env(:master_proxy, :backends) - - backend = choose_backend(conn, backends) + backend = choose_backend(conn, opts) log_request("Backend chosen: #{inspect(backend)}") dispatch(backend, req) end - defp choose_backend(conn, backends) do + defp choose_backend(conn, opts) do + callback_module = Keyword.get(opts, :callback_module) + backends = Keyword.fetch!(opts, :backends) + + if callback_module && function_exported?(callback_module, :choose_backend, 1) do + case callback_module.choose_backend(conn) do + :fallback -> choose_backend_from_config(conn, backends) + {:phoenix_endpoint, endpoint} -> %{phoenix_endpoint: endpoint} + {:plug, plug} -> %{plug: plug} + end + else + choose_backend_from_config(conn, backends) + end + end + + defp choose_backend_from_config(conn, backends) do Enum.find(backends, @not_found_backend, fn backend -> backend_matches?(conn, backend) end) @@ -72,12 +84,20 @@ defmodule MasterProxy.Cowboy2Handler do end defp backend_matches?(conn, backend) do + domain = Map.get(backend, :domain) verb = Map.get(backend, :verb) || ~r/.*/ host = Map.get(backend, :host) || ~r/.*/ path = Map.get(backend, :path) || ~r/.*/ - Regex.match?(host, conn.host) && Regex.match?(path, conn.request_path) && - Regex.match?(verb, conn.method) + verb_host_path_match = + Regex.match?(host, conn.host) && Regex.match?(path, conn.request_path) && + Regex.match?(verb, conn.method) + + if domain do + domain == conn.host && verb_host_path_match + else + verb_host_path_match + end end ## Websocket callbacks diff --git a/mix.exs b/mix.exs index 587cb52..6a90936 100644 --- a/mix.exs +++ b/mix.exs @@ -23,8 +23,7 @@ defmodule MasterProxy.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger], - mod: {MasterProxy.Application, []} + extra_applications: [:logger] ] end @@ -42,7 +41,7 @@ defmodule MasterProxy.MixProject do defp deps do [ {:plug_cowboy, "~> 2.0"}, - {:phoenix, "~> 1.4"}, + {:phoenix, "~> 1.6"}, # for hex.pm {:ex_doc, ">= 0.0.0", only: :dev}, diff --git a/mix.lock b/mix.lock index cc5e6da..90052b6 100644 --- a/mix.lock +++ b/mix.lock @@ -1,19 +1,22 @@ %{ - "cowboy": {:hex, :cowboy, "2.6.1", "f2e06f757c337b3b311f9437e6e072b678fcd71545a7b2865bdaa154d078593f", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "41c14336e40d78d3bb655e76817d9f29def522024a5bb53894c27e0ae338cda0"}, - "cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm", "59f952d504b9921a6f53226fdb8b4a671bb694277c7ccf1ddeaadcdc6d43d88e"}, + "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, "earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm", "f8b8820099caf0d5e72ae6482d2b0da96f213cbbe2b5b2191a37966e119eaa27"}, "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "dc87f778d8260da0189a622f62790f6202af72f2f3dee6e78d91a18dd2fcd137"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d7152ff93f2eac07905f510dfa03397134345ba4673a00fbf7119bab98632940"}, "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "4a36dd2d0d5c5f98d95b3f410d7071cd661d5af310472229dd0e92161f168a44"}, - "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, + "mime": {:hex, :mime, "2.0.1", "0de4c81303fe07806ebc2494d5321ce8fb4df106e34dd5f9d787b637ebadc256", [:mix], [], "hexpm", "7a86b920d2aedce5fb6280ac8261ac1a739ae6c1a1ad38f5eadf910063008942"}, "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm", "ebb595e19456a72786db6dcd370d320350cb624f0b6203fcc7e23161d49b0ffb"}, - "phoenix": {:hex, :phoenix, "1.4.0", "56fe9a809e0e735f3e3b9b31c1b749d4b436e466d8da627b8d82f90eaae714d2", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "22da8f659cf13d3ba73b767f66b8c389113ddf0ef7b94225cc84e94b85eac90e"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm", "a3d890aaa3156d51056179dcaaadaf32b844f71656bb27c58756f2b97875c36c"}, - "plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm", "daa5fee4209c12c3c48b05a96cf88c320b627c9575f987554dcdc1fdcdf2c15e"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.0.0", "ab0c92728f2ba43c544cce85f0f220d8d30fc0c90eaa1e6203683ab039655062", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "fa9087d93f4962d099b9c4246dbea97c2f2e6aab1029e8b8206e733a1274cecc"}, - "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, + "phoenix": {:hex, :phoenix, "1.6.2", "6cbd5c8ed7a797f25a919a37fafbc2fb1634c9cdb12a4448d7a5d0b26926f005", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7bbee475acae0c3abc229b7f189e210ea788e63bd168e585f60c299a4b2f9133"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, + "phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"}, + "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "stream_data": {:hex, :stream_data, "0.4.2", "fa86b78c88ec4eaa482c0891350fcc23f19a79059a687760ddcf8680aac2799b", [:mix], [], "hexpm", "54d6bf6f1e5e27fbf4a7784a2bffbb993446d0efd079debca0f27bf859c0d1cf"}, + "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}, }