Repository: phoenixframework/phoenix (elixir)

Issue:
`force_ssl` doesn't work in releases.exs 
We're trying to enforce ssl and following the [docs](https://hexdocs.pm/phoenix/endpoint.html#force-ssl), we added the `force_ssl` config, but we are also using elixir releases, so we (arbitrarily) put it in our releases.exs file instead of our prod.exs file and it is silently ignored.

I think this happens because `Plug.SSL` is injected by a macro seen [here](https://github.com/phoenixframework/phoenix/blob/029098e38ca830c35e832b44e58a0cd641fe24f1/lib/phoenix/endpoint.ex#L453). Since releases.exs is for runtime configs, the `force_ssl` configuration isn't present during build time when the macros are evaluated and `Plug.SSL` is never injected.

I'm not sure if "fixing" this is the right way to go or if we should just update the documentation to mention that the config must not be in a runtime config such as releases.exs. 

I'm happy to help make the change if you let me know how you'd like to proceed. Thanks again for a great library!

* Elixir version (elixir -v): 1.9.2
* Phoenix version (mix deps): 1.4.10

Pull request:
Checks force_ssl has not changed from compile time to runtime
Resolves #3586

Patch:
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 147102cc2..a0e212866 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -386,6 +386,12 @@ defmodule Phoenix.Endpoint do
       @otp_app unquote(opts)[:otp_app] || raise "endpoint expects :otp_app to be given"
       var!(config) = Phoenix.Endpoint.Supervisor.config(@otp_app, __MODULE__)
       var!(code_reloading?) = var!(config)[:code_reloader]
+      @compile_config Keyword.take(var!(config), Phoenix.Endpoint.Supervisor.compile_config_keys)
+
+      @doc false
+      def __compile_config__ do
+        @compile_config
+      end
 
       # Avoid unused variable warnings
       _ = var!(code_reloading?)
diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex
index 5704a4c71..99b4e5988 100644
--- a/lib/phoenix/endpoint/supervisor.ex
+++ b/lib/phoenix/endpoint/supervisor.ex
@@ -22,6 +22,48 @@ defmodule Phoenix.Endpoint.Supervisor do
     end
   end
 
+  defp check_compile_configs(mod, runtime_configs) do
+    compile_configs = mod.__compile_config__()
+
+    bad_keys =
+      Phoenix.Endpoint.Supervisor.compile_config_keys()
+      |> Enum.map(fn key ->
+        compile_config = Keyword.get(compile_configs, key)
+        runtime_config = Keyword.get(runtime_configs, key)
+
+        if compile_config == runtime_config do
+          {:ok, key}
+        else
+          {:error, {key, compile_config, runtime_config}}
+        end
+      end)
+      |> Enum.filter(fn {status, _} -> status == :error end)
+      |> Enum.map(fn {:error, {key, compile_config, runtime_config}} ->
+        require Logger
+
+        Logger.error("""
+        Failed :#{key} check for Phoenix.Endpoint.Supervisor.
+
+        Compile time configuration: #{inspect(compile_config)}
+        Runtime configuration     : #{inspect(runtime_config)}
+
+        This happens when the :#{key} config at compile time does
+        not match the :#{key} config at runtime. For example, if
+        your releases.exs file sets :#{key} to something different
+        than your config.exs file. To fix this issue, you should
+        move the :#{key} config from releases.exs to prod.exs.
+        """)
+
+        key
+      end)
+
+    if Enum.empty?(bad_keys) do
+      {:ok, nil}
+    else
+      {:error, bad_keys}
+    end
+  end
+
   @doc false
   def init({otp_app, mod, opts}) do
     default_conf = Phoenix.Config.merge(defaults(otp_app, mod), opts)
@@ -53,6 +95,16 @@ defmodule Phoenix.Endpoint.Supervisor do
     conf = Keyword.drop(secret_conf, [:secret_key_base])
     server? = server?(conf)
 
+    {:ok, _} =
+      case check_compile_configs(mod, conf) do
+        {:error, keys} ->
+          raise ArgumentError,
+                "expected these options to be unchanged from compile time: #{inspect(keys)}"
+
+        other ->
+          other
+      end
+
     if conf[:instrumenters] do
       Logger.warn(":instrumenters configuration for #{inspect(mod)} is deprecated and has no effect")
     end
@@ -433,4 +485,14 @@ defmodule Phoenix.Endpoint.Supervisor do
       Logger.info("Access #{inspect(endpoint)} at #{endpoint.url()}")
     end
   end
+
+  @doc """
+  List of keys which we ensure are unchanged from compile time to runtime. For
+  example, the :force_ssl option must be available at compile time in order to 
+  work properly. We check these keys so we can warn the user that changing the
+  option at runtime may lead to undesirable behavior.
+  """
+  def compile_config_keys do
+    [:force_ssl]
+  end
 end


Test patch:
diff --git a/test/phoenix/endpoint/endpoint_test.exs b/test/phoenix/endpoint/endpoint_test.exs
index c07e4368f..ac83e94d1 100644
--- a/test/phoenix/endpoint/endpoint_test.exs
+++ b/test/phoenix/endpoint/endpoint_test.exs
@@ -19,6 +19,7 @@ defmodule Phoenix.Endpoint.EndpointTest do
     assert is_list(config)
     assert @otp_app == :phoenix
     assert code_reloading? == false
+    assert @compile_config == [force_ssl: [subdomains: true]]
   end
 
   defmodule NoConfigEndpoint do
@@ -221,4 +222,8 @@ defmodule Phoenix.Endpoint.EndpointTest do
       Endpoint.static_integrity("//invalid_path")
     end
   end
+
+  test "__compile_config__/0 returns selected configs from compile time" do
+    assert Endpoint.__compile_config__() == [force_ssl: [subdomains: true]]
+  end
 end
diff --git a/test/phoenix/endpoint/supervisor_test.exs b/test/phoenix/endpoint/supervisor_test.exs
index e365072be..fc8992511 100644
--- a/test/phoenix/endpoint/supervisor_test.exs
+++ b/test/phoenix/endpoint/supervisor_test.exs
@@ -52,6 +52,11 @@ defmodule Phoenix.Endpoint.SupervisorTest do
     def config(:static_url), do: [host: "static.example.com"]
   end
 
+  defmodule ForceSslEndpoint do
+    def init(:supervisor, config), do: {:ok, config}
+    def __compile_config__(), do: [force_ssl: [rewrite_on: [:x_forwarded_proto]]]
+  end
+
   test "generates the static url based on the static host configuration" do
     static_host = {:cache, "http://static.example.com"}
     assert Supervisor.static_url(StaticURLEndpoint) == static_host
@@ -74,4 +79,16 @@ defmodule Phoenix.Endpoint.SupervisorTest do
     assert {:nocache, {"/images/unknown.png", nil}} =
              Supervisor.static_lookup(HTTPEndpoint, "/images/unknown.png")
   end
+
+  test "compile_config_keys/0 returns config keys we want to store for runtime checks" do
+    assert Supervisor.compile_config_keys() == [:force_ssl]
+  end
+
+  test "init/1 fails when force_ssl check fails" do
+    Application.put_env(:phoenix, ForceSslEndpoint, force_ssl: [hsts: true])
+
+    assert_raise ArgumentError,
+                 "expected these options to be unchanged from compile time: [:force_ssl]",
+                 fn -> Supervisor.init({:phoenix, ForceSslEndpoint, []}) end
+  end
 end