lib/bandit/phoenix_adapter.ex

defmodule Bandit.PhoenixAdapter do
  @moduledoc """
  A Bandit adapter for Phoenix.

  WebSocket support requires a version of Phoenix with Plug upgrade support, which is available
  as part of Phoenix 1.7 and later. This module will work fine on earlier versions of Phoenix,
  just without WebSocket support.

  To use this adapter, your project will need to include Bandit as a dependency; see
  https://hex.pm/bandit for details on the currently supported version of Bandit to include. Once
  Bandit is included as a dependency of your Phoenix project, add the following to your endpoint
  configuration in `config/config.exs`:

  ```
  config :your_app, YourAppWeb.Endpoint,
    adapter: Bandit.PhoenixAdapter
  ```

  ## Endpoint configuration

  Configuring Bandit within your Phoenix app is done in largely the same way as configuration for
  Cowboy works. For the most part, your existing configuration within your `/config/*.exs` files
  will work unchanged, although some of the more exotic options are different. Bandit supports the
  following parameters within the `:http` and `:https` parameters:

    * `:http`: the configuration for the HTTP server. Accepts the following options:
      * `port`: The port to run on. Defaults to 4000. Note that if a Unix domain socket is
        specified in the `ip` option, the value of `port` **must** be `0`.
      * `ip`: The address to bind to. Can be specified as a 4-element tuple such as `{127, 0, 0, 1}`
        for IPv4 addresses, an 8-element tuple for IPv6 addresses, or using `{:local, path}` to bind
        to a Unix domain socket. Defaults to the Bandit default of `{0, 0, 0, 0, 0, 0, 0, 0}`.
      * `transport_options`: Any valid value from `ThousandIsland.Transports.TCP`
      * `http_1_options`: Any valid value from the `http_1_options` section of `Bandit`'s config documentation
      * `http_2_options`: Any valid value from the `http_2_options` section of `Bandit`'s config documentation
      * `websocket_options`: Any valid value from the `websocket_options` section of `Bandit`'s config documentation

      Defaults to `false`, which will cause Bandit to not start an HTTP server.

    * `:https`: the configuration for the HTTPS server. Accepts the following options:
      * `port`: The port to run on. Defaults to 4040. Note that if a Unix domain socket is
        specified in the `ip` option, the value of `port` **must** be `0`.
      * `ip`: The address to bind to. Can be specified as a 4-element tuple such as `{127, 0, 0, 1}`
        for IPv4 addresses, an 8-element tuple for IPv6 addresses, or using `{:local, path}` to bind
        to a Unix domain socket. Defaults to the Bandit default of `{0, 0, 0, 0, 0, 0, 0, 0}`.
      * `transport_options`: Any valid value from `ThousandIsland.Transports.SSL`
      * `http_1_options`: Any valid value from the `http_1_options` section of `Bandit`'s config documentation
      * `http_2_options`: Any valid value from the `http_2_options` section of `Bandit`'s config documentation
      * `websocket_options`: Any valid value from the `websocket_options` section of `Bandit`'s config documentation

      Defaults to `false`, which will cause Bandit to not start an HTTPS server.
  """

  @doc false
  def child_specs(endpoint, config) do
    for {scheme, default_port} <- [http: 4000, https: 4040], opts = config[scheme] do
      plug = resolve_plug(config[:code_reloader], endpoint)

      opts
      |> build_options(default_port)
      |> Keyword.merge(plug: plug, display_plug: endpoint, scheme: scheme)
      |> Bandit.child_spec()
      |> Supervisor.child_spec(id: {endpoint, scheme})
    end
  end

  defp resolve_plug(code_reload?, endpoint) do
    if code_reload? &&
         Code.ensure_loaded?(Phoenix.Endpoint.SyncCodeReloadPlug) &&
         function_exported?(Phoenix.Endpoint.SyncCodeReloadPlug, :call, 2) do
      {Phoenix.Endpoint.SyncCodeReloadPlug, {endpoint, []}}
    else
      endpoint
    end
  end

  defp build_options(opts, default_port) do
    {ip_options, options} = Keyword.split(opts, [:ip])
    {nested_options, options} = Keyword.split(options, [:port, :transport_options])

    nested_options =
      nested_options
      |> Keyword.update!(:port, fn
        nil -> default_port
        port when is_binary(port) -> String.to_integer(port)
        port when is_integer(port) -> port
      end)
      |> Keyword.update(:transport_options, ip_options, &Keyword.merge(&1, ip_options))

    Keyword.put(options, :options, nested_options)
  end
end