414 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
			
		
		
	
	
			414 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
defmodule Bandit do
 | 
						|
  @external_resource Path.join([__DIR__, "../README.md"])
 | 
						|
 | 
						|
  @moduledoc """
 | 
						|
  Bandit is an HTTP server for Plug and WebSock apps.
 | 
						|
 | 
						|
  As an HTTP server, Bandit's primary goal is to act as 'glue' between client connections managed
 | 
						|
  by [Thousand Island](https://github.com/mtrudel/thousand_island) and application code defined
 | 
						|
  via the [Plug](https://github.com/elixir-plug/plug) and/or
 | 
						|
  [WebSock](https://github.com/phoenixframework/websock) APIs. As such there really isn't a whole lot of
 | 
						|
  user-visible surface area to Bandit, and as a consequence the API documentation presented here
 | 
						|
  is somewhat sparse. This is by design! Bandit is intended to 'just work' in almost all cases;
 | 
						|
  the only thought users typically have to put into Bandit comes in the choice of which options (if
 | 
						|
  any) they would like to change when starting a Bandit server. The sparseness of the Bandit API
 | 
						|
  should not be taken as an indicator of the comprehensiveness or robustness of the project.
 | 
						|
 | 
						|
  #{@external_resource |> File.read!() |> String.split("<!-- MDOC -->") |> Enum.fetch!(1)}
 | 
						|
  """
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  Possible top-level options to configure a Bandit server
 | 
						|
 | 
						|
  * `plug`: The Plug to use to handle connections. Can be specified as `MyPlug` or `{MyPlug, plug_opts}`
 | 
						|
  * `scheme`: One of `:http` or `:https`. If `:https` is specified, you will also need to specify
 | 
						|
    valid `certfile` and `keyfile` values (or an equivalent value within
 | 
						|
    `thousand_island_options.transport_options`). Defaults to `:http`
 | 
						|
  * `port`: The TCP port to listen on. This option is offered as a convenience and actually sets
 | 
						|
    the option of the same name within `thousand_island_options`. If a string value is passed, it
 | 
						|
    will be parsed as an integer. Defaults to 4000 if `scheme` is `:http`, and 4040 if `scheme` is
 | 
						|
    `:https`
 | 
						|
  * `ip`:  The interface(s) to listen on. This option is offered as a convenience and actually sets the
 | 
						|
    option of the same name within `thousand_island_options.transport_options`. Can be specified as:
 | 
						|
      * `{1, 2, 3, 4}` for IPv4 addresses
 | 
						|
      * `{1, 2, 3, 4, 5, 6, 7, 8}` for IPv6 addresses
 | 
						|
      * `:loopback` for local loopback (ie: `127.0.0.1`)
 | 
						|
      * `:any` for all interfaces (ie: `0.0.0.0`)
 | 
						|
      * `{:local, "/path/to/socket"}` for a Unix domain socket. If this option is used, the `port`
 | 
						|
        option *must* be set to `0`
 | 
						|
  * `inet`: Only bind to IPv4 interfaces. This option is offered as a convenience and actually sets the
 | 
						|
    option of the same name within `thousand_island_options.transport_options`. Must be specified
 | 
						|
    as a bare atom `:inet`
 | 
						|
  * `inet6`: Only bind to IPv6 interfaces. This option is offered as a convenience and actually sets the
 | 
						|
    option of the same name within `thousand_island_options.transport_options`. Must be specified
 | 
						|
    as a bare atom `:inet6`
 | 
						|
  * `keyfile`: The path to a file containing the SSL key to use for this server. This option is
 | 
						|
    offered as a convenience and actually sets the option of the same name within
 | 
						|
    `thousand_island_options.transport_options`. If a relative path is used here, you will also
 | 
						|
    need to set the `otp_app` parameter and ensure that the named file is part of your application
 | 
						|
    build
 | 
						|
  * `certfile`: The path to a file containing the SSL certificate to use for this server. This option is
 | 
						|
    offered as a convenience and actually sets the option of the same name within
 | 
						|
    `thousand_island_options.transport_options`. If a relative path is used here, you will also
 | 
						|
    need to set the `otp_app` parameter and ensure that the named file is part of your application
 | 
						|
    build
 | 
						|
  * `otp_app`: Provided as a convenience when using relative paths for `keyfile` and `certfile`
 | 
						|
  * `cipher_suite`: Used to define a pre-selected set of ciphers, as described by
 | 
						|
    `Plug.SSL.configure/1`. Optional, can be either `:strong` or `:compatible`
 | 
						|
  * `display_plug`: The plug to use when describing the connection in logs. Useful for situations
 | 
						|
    such as Phoenix code reloading where you have a 'wrapper' plug but wish to refer to the
 | 
						|
    connection by the endpoint name
 | 
						|
  * `startup_log`: The log level at which Bandit should log startup info.
 | 
						|
    Defaults to `:info` log level, can be set to false to disable it
 | 
						|
  * `thousand_island_options`: A list of options to pass to Thousand Island. Bandit sets some
 | 
						|
    default values in this list based on your top-level configuration; these values will be
 | 
						|
    overridden by values appearing here. A complete list can be found at
 | 
						|
    `t:ThousandIsland.options/0`
 | 
						|
  * `http_options`: A list of options to configure the shared aspects of Bandit's HTTP stack. A
 | 
						|
    complete list can be found at `t:http_options/0`
 | 
						|
  * `http_1_options`: A list of options to configure Bandit's HTTP/1 stack. A complete list can
 | 
						|
    be found at `t:http_1_options/0`
 | 
						|
  * `http_2_options`: A list of options to configure Bandit's HTTP/2 stack. A complete list can
 | 
						|
    be found at `t:http_2_options/0`
 | 
						|
  * `websocket_options`: A list of options to configure Bandit's WebSocket stack. A complete list can
 | 
						|
    be found at `t:websocket_options/0`
 | 
						|
  """
 | 
						|
  @type options :: [
 | 
						|
          {:plug, module() | {module(), Plug.opts()}}
 | 
						|
          | {:scheme, :http | :https}
 | 
						|
          | {:port, :inet.port_number()}
 | 
						|
          | {:ip, :inet.socket_address()}
 | 
						|
          | :inet
 | 
						|
          | :inet6
 | 
						|
          | {:keyfile, binary()}
 | 
						|
          | {:certfile, binary()}
 | 
						|
          | {:otp_app, Application.app()}
 | 
						|
          | {:cipher_suite, :strong | :compatible}
 | 
						|
          | {:display_plug, module()}
 | 
						|
          | {:startup_log, Logger.level() | false}
 | 
						|
          | {:thousand_island_options, ThousandIsland.options()}
 | 
						|
          | {:http_options, http_options()}
 | 
						|
          | {:http_1_options, http_1_options()}
 | 
						|
          | {:http_2_options, http_2_options()}
 | 
						|
          | {:websocket_options, websocket_options()}
 | 
						|
        ]
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  Options to configure shared aspects of the HTTP stack in Bandit
 | 
						|
 | 
						|
  * `compress`: Whether or not to attempt compression of responses via content-encoding
 | 
						|
    negotiation as described in
 | 
						|
    [RFC9110§8.4](https://www.rfc-editor.org/rfc/rfc9110.html#section-8.4). Defaults to true
 | 
						|
  * `deflate_options`: A keyword list of options to set on the deflate library. A complete list can
 | 
						|
    be found at `t:deflate_options/0`. Note that these options only affect the behaviour of the
 | 
						|
    'deflate' content encoding; 'gzip' does not have any configurable options (this is a
 | 
						|
    limitation of the underlying `:zlib` library)
 | 
						|
  * `log_exceptions_with_status_codes`: Which exceptions to log. Bandit will log only those
 | 
						|
    exceptions whose status codes (as determined by `Plug.Exception.status/1`) match the specified
 | 
						|
    list or range. Defaults to `500..599`
 | 
						|
  * `log_protocol_errors`: How to log protocol errors such as malformed requests. `:short` will
 | 
						|
    log a single-line summary, while `:verbose` will log full stack traces. The value of `false`
 | 
						|
    will disable protocol error logging entirely. Defaults to `:short`
 | 
						|
  * `log_client_closures`: How to log cases where the client closes the connection. These happen
 | 
						|
    routinely in the real world and so the handling of them is configured separately since they
 | 
						|
    can be quite noisy. Takes the same options as `log_protocol_errors`, but defaults to `false`
 | 
						|
  """
 | 
						|
  @type http_options :: [
 | 
						|
          {:compress, boolean()}
 | 
						|
          | {:deflate_options, deflate_options()}
 | 
						|
          | {:log_exceptions_with_status_codes, list() | Range.t()}
 | 
						|
          | {:log_protocol_errors, :short | :verbose | false}
 | 
						|
          | {:log_client_closures, :short | :verbose | false}
 | 
						|
        ]
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  Options to configure the HTTP/1 stack in Bandit
 | 
						|
 | 
						|
  * `enabled`: Whether or not to serve HTTP/1 requests. Defaults to true
 | 
						|
  * `max_request_line_length`: The maximum permitted length of the request line
 | 
						|
    (expressed as the number of bytes on the wire) in an HTTP/1.1 request. Defaults to 10_000 bytes
 | 
						|
  * `max_header_length`: The maximum permitted length of any single header (combined
 | 
						|
    key & value, expressed as the number of bytes on the wire) in an HTTP/1.1 request. Defaults to 10_000 bytes
 | 
						|
  * `max_header_count`: The maximum permitted number of headers in an HTTP/1.1 request.
 | 
						|
    Defaults to 50 headers
 | 
						|
  * `max_requests`: The maximum number of requests to serve in a single
 | 
						|
    HTTP/1.1 connection before closing the connection. Defaults to 0 (no limit)
 | 
						|
  * `clear_process_dict`: Whether to clear the process dictionary of all non-internal entries
 | 
						|
    between subsequent keepalive requests. If set, all keys not starting with `$` are removed from
 | 
						|
    the process dictionary between requests. Defaults to `true`
 | 
						|
  * `gc_every_n_keepalive_requests`: How often to run a full garbage collection pass between subsequent
 | 
						|
    keepalive requests on the same HTTP/1.1 connection. Defaults to 5 (garbage collect between
 | 
						|
    every 5 requests). This option is currently experimental, and may change at any time
 | 
						|
  * `log_unknown_messages`: Whether or not to log unknown messages sent to the handler process.
 | 
						|
    Defaults to `false`
 | 
						|
  """
 | 
						|
  @type http_1_options :: [
 | 
						|
          {:enabled, boolean()}
 | 
						|
          | {:max_request_line_length, pos_integer()}
 | 
						|
          | {:max_header_length, pos_integer()}
 | 
						|
          | {:max_header_count, pos_integer()}
 | 
						|
          | {:max_requests, pos_integer()}
 | 
						|
          | {:clear_process_dict, boolean()}
 | 
						|
          | {:gc_every_n_keepalive_requests, pos_integer()}
 | 
						|
          | {:log_unknown_messages, boolean()}
 | 
						|
        ]
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  Options to configure the HTTP/2 stack in Bandit
 | 
						|
 | 
						|
  * `enabled`: Whether or not to serve HTTP/2 requests. Defaults to true
 | 
						|
  * `max_header_block_size`: The maximum permitted length of a field block of an HTTP/2 request
 | 
						|
    (expressed as the number of compressed bytes). Includes any concatenated block fragments from
 | 
						|
    continuation frames. Defaults to 50_000 bytes
 | 
						|
  * `max_requests`: The maximum number of requests to serve in a single
 | 
						|
    HTTP/2 connection before closing the connection. Defaults to 0 (no limit)
 | 
						|
  * `default_local_settings`: Options to override the default values for local HTTP/2
 | 
						|
    settings. Values provided here will override the defaults specified in RFC9113§6.5.2
 | 
						|
  """
 | 
						|
  @type http_2_options :: [
 | 
						|
          {:enabled, boolean()}
 | 
						|
          | {:max_header_block_size, pos_integer()}
 | 
						|
          | {:max_requests, pos_integer()}
 | 
						|
          | {:default_local_settings, keyword()}
 | 
						|
        ]
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  Options to configure the WebSocket stack in Bandit
 | 
						|
 | 
						|
  * `enabled`: Whether or not to serve WebSocket upgrade requests. Defaults to true
 | 
						|
  * `max_frame_size`: The maximum size of a single WebSocket frame (expressed as
 | 
						|
    a number of bytes on the wire). Defaults to 0 (no limit)
 | 
						|
  * `validate_text_frames`: Whether or not to validate text frames as being UTF-8. Strictly
 | 
						|
    speaking this is required per RFC6455§5.6, however it can be an expensive operation and one
 | 
						|
    that may be safely skipped in some situations. Defaults to true
 | 
						|
  * `compress`: Whether or not to allow per-message deflate compression globally. Note that
 | 
						|
    upgrade requests still need to set the `compress: true` option in `connection_opts` on
 | 
						|
    a per-upgrade basis for compression to be negotiated (see 'WebSocket Support' section below
 | 
						|
    for details). Defaults to `true`
 | 
						|
  """
 | 
						|
  @type websocket_options :: [
 | 
						|
          {:enabled, boolean()}
 | 
						|
          | {:max_frame_size, pos_integer()}
 | 
						|
          | {:validate_text_frames, boolean()}
 | 
						|
          | {:compress, boolean()}
 | 
						|
        ]
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  Options to configure the deflate library used for HTTP compression
 | 
						|
  """
 | 
						|
  @type deflate_options :: [
 | 
						|
          {:level, :zlib.zlevel()}
 | 
						|
          | {:window_bits, :zlib.zwindowbits()}
 | 
						|
          | {:memory_level, :zlib.zmemlevel()}
 | 
						|
          | {:strategy, :zlib.zstrategy()}
 | 
						|
        ]
 | 
						|
 | 
						|
  @typep scheme :: :http | :https
 | 
						|
 | 
						|
  require Logger
 | 
						|
 | 
						|
  @doc false
 | 
						|
  @spec child_spec(options()) :: Supervisor.child_spec()
 | 
						|
  def child_spec(arg) do
 | 
						|
    %{
 | 
						|
      id: {__MODULE__, make_ref()},
 | 
						|
      start: {__MODULE__, :start_link, [arg]},
 | 
						|
      type: :supervisor,
 | 
						|
      restart: :permanent
 | 
						|
    }
 | 
						|
  end
 | 
						|
 | 
						|
  @top_level_keys ~w(plug scheme port ip keyfile certfile otp_app cipher_suite display_plug startup_log thousand_island_options http_options http_1_options http_2_options websocket_options)a
 | 
						|
  @http_keys ~w(compress deflate_options log_exceptions_with_status_codes log_protocol_errors log_client_closures)a
 | 
						|
  @http_1_keys ~w(enabled max_request_line_length max_header_length max_header_count max_requests clear_process_dict gc_every_n_keepalive_requests log_unknown_messages)a
 | 
						|
  @http_2_keys ~w(enabled max_header_block_size max_requests default_local_settings)a
 | 
						|
  @websocket_keys ~w(enabled max_frame_size validate_text_frames compress primitive_ops_module)a
 | 
						|
  @thousand_island_keys ThousandIsland.ServerConfig.__struct__()
 | 
						|
                        |> Map.from_struct()
 | 
						|
                        |> Map.keys()
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Starts a Bandit server using the provided arguments. See `t:options/0` for specific options to
 | 
						|
  pass to this function.
 | 
						|
  """
 | 
						|
  @spec start_link(options()) :: Supervisor.on_start()
 | 
						|
  def start_link(arg) do
 | 
						|
    # Special case top-level `:inet` and `:inet6` options so we can use keyword logic everywhere else
 | 
						|
    arg = arg |> special_case_inet_options() |> validate_options(@top_level_keys, "top level")
 | 
						|
 | 
						|
    thousand_island_options =
 | 
						|
      Keyword.get(arg, :thousand_island_options, [])
 | 
						|
      |> validate_options(@thousand_island_keys, :thousand_island_options)
 | 
						|
 | 
						|
    http_options =
 | 
						|
      Keyword.get(arg, :http_options, [])
 | 
						|
      |> validate_options(@http_keys, :http_options)
 | 
						|
 | 
						|
    http_1_options =
 | 
						|
      Keyword.get(arg, :http_1_options, [])
 | 
						|
      |> validate_options(@http_1_keys, :http_1_options)
 | 
						|
 | 
						|
    http_2_options =
 | 
						|
      Keyword.get(arg, :http_2_options, [])
 | 
						|
      |> validate_options(@http_2_keys, :http_2_options)
 | 
						|
 | 
						|
    websocket_options =
 | 
						|
      Keyword.get(arg, :websocket_options, [])
 | 
						|
      |> validate_options(@websocket_keys, :websocket_options)
 | 
						|
 | 
						|
    {plug_mod, _} = plug = plug(arg)
 | 
						|
    display_plug = Keyword.get(arg, :display_plug, plug_mod)
 | 
						|
    startup_log = Keyword.get(arg, :startup_log, :info)
 | 
						|
 | 
						|
    {http_1_enabled, http_1_options} = Keyword.pop(http_1_options, :enabled, true)
 | 
						|
    {http_2_enabled, http_2_options} = Keyword.pop(http_2_options, :enabled, true)
 | 
						|
 | 
						|
    handler_options = %{
 | 
						|
      plug: plug,
 | 
						|
      handler_module: Bandit.InitialHandler,
 | 
						|
      opts: %{
 | 
						|
        http: http_options,
 | 
						|
        http_1: http_1_options,
 | 
						|
        http_2: http_2_options,
 | 
						|
        websocket: websocket_options
 | 
						|
      },
 | 
						|
      http_1_enabled: http_1_enabled,
 | 
						|
      http_2_enabled: http_2_enabled
 | 
						|
    }
 | 
						|
 | 
						|
    scheme = Keyword.get(arg, :scheme, :http)
 | 
						|
 | 
						|
    {transport_module, transport_options, default_port} =
 | 
						|
      case scheme do
 | 
						|
        :http ->
 | 
						|
          transport_options =
 | 
						|
            Keyword.take(arg, [:ip])
 | 
						|
            |> then(&(Keyword.get(thousand_island_options, :transport_options, []) ++ &1))
 | 
						|
 | 
						|
          {ThousandIsland.Transports.TCP, transport_options, 4000}
 | 
						|
 | 
						|
        :https ->
 | 
						|
          supported_protocols =
 | 
						|
            if(http_2_enabled, do: ["h2"], else: []) ++
 | 
						|
              if http_1_enabled, do: ["http/1.1"], else: []
 | 
						|
 | 
						|
          transport_options =
 | 
						|
            Keyword.take(arg, [:ip, :keyfile, :certfile, :otp_app, :cipher_suite])
 | 
						|
            |> Keyword.merge(alpn_preferred_protocols: supported_protocols)
 | 
						|
            |> then(&(Keyword.get(thousand_island_options, :transport_options, []) ++ &1))
 | 
						|
            |> Plug.SSL.configure()
 | 
						|
            |> case do
 | 
						|
              {:ok, options} -> options
 | 
						|
              {:error, message} -> raise "Plug.SSL.configure/1 encountered error: #{message}"
 | 
						|
            end
 | 
						|
            |> Enum.reject(&(is_tuple(&1) and elem(&1, 0) == :otp_app))
 | 
						|
 | 
						|
          {ThousandIsland.Transports.SSL, transport_options, 4040}
 | 
						|
      end
 | 
						|
 | 
						|
    port = Keyword.get(arg, :port, default_port) |> parse_as_number()
 | 
						|
 | 
						|
    thousand_island_options
 | 
						|
    |> Keyword.put_new(:port, port)
 | 
						|
    |> Keyword.put_new(:transport_module, transport_module)
 | 
						|
    |> Keyword.put(:transport_options, transport_options)
 | 
						|
    |> Keyword.put_new(:handler_module, Bandit.DelegatingHandler)
 | 
						|
    |> Keyword.put_new(:handler_options, handler_options)
 | 
						|
    |> ThousandIsland.start_link()
 | 
						|
    |> case do
 | 
						|
      {:ok, pid} ->
 | 
						|
        startup_log &&
 | 
						|
          Logger.log(startup_log, info(scheme, display_plug, pid), domain: [:bandit], plug: plug)
 | 
						|
 | 
						|
        {:ok, pid}
 | 
						|
 | 
						|
      {:error, {:shutdown, {:failed_to_start_child, :listener, :eaddrinuse}}} = error ->
 | 
						|
        Logger.error([info(scheme, display_plug, nil), " failed, port #{port} already in use"],
 | 
						|
          domain: [:bandit],
 | 
						|
          plug: plug
 | 
						|
        )
 | 
						|
 | 
						|
        error
 | 
						|
 | 
						|
      {:error, _} = error ->
 | 
						|
        error
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  @spec special_case_inet_options(options()) :: options()
 | 
						|
  defp special_case_inet_options(opts) do
 | 
						|
    {inet_opts, opts} = Enum.split_with(opts, &(&1 in [:inet, :inet6]))
 | 
						|
 | 
						|
    if inet_opts == [] do
 | 
						|
      opts
 | 
						|
    else
 | 
						|
      Keyword.update(
 | 
						|
        opts,
 | 
						|
        :thousand_island_options,
 | 
						|
        [transport_options: inet_opts],
 | 
						|
        fn thousand_island_opts ->
 | 
						|
          Keyword.update(thousand_island_opts, :transport_options, inet_opts, &(&1 ++ inet_opts))
 | 
						|
        end
 | 
						|
      )
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  @spec validate_options(Keyword.t(), [atom(), ...], String.t() | atom()) ::
 | 
						|
          Keyword.t() | no_return()
 | 
						|
  defp validate_options(options, valid_values, name) do
 | 
						|
    case Keyword.split(options, valid_values) do
 | 
						|
      {options, []} ->
 | 
						|
        options
 | 
						|
 | 
						|
      {_, illegal_options} ->
 | 
						|
        raise "Unsupported key(s) in #{name} config: #{inspect(Keyword.keys(illegal_options))}"
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  @spec plug(options()) :: {module(), Plug.opts()}
 | 
						|
  defp plug(arg) do
 | 
						|
    arg
 | 
						|
    |> Keyword.get(:plug)
 | 
						|
    |> case do
 | 
						|
      nil -> raise "A value is required for :plug"
 | 
						|
      {plug_fn, plug_options} when is_function(plug_fn, 2) -> {plug_fn, plug_options}
 | 
						|
      plug_fn when is_function(plug_fn) -> {plug_fn, []}
 | 
						|
      {plug, plug_options} when is_atom(plug) -> validate_plug(plug, plug_options)
 | 
						|
      plug when is_atom(plug) -> validate_plug(plug, [])
 | 
						|
      other -> raise "Invalid value for plug: #{inspect(other)}"
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp validate_plug(plug, plug_options) do
 | 
						|
    Code.ensure_loaded!(plug)
 | 
						|
    if !function_exported?(plug, :init, 1), do: raise("plug module does not define init/1")
 | 
						|
    if !function_exported?(plug, :call, 2), do: raise("plug module does not define call/2")
 | 
						|
 | 
						|
    {plug, plug.init(plug_options)}
 | 
						|
  end
 | 
						|
 | 
						|
  @spec parse_as_number(binary() | integer()) :: integer()
 | 
						|
  defp parse_as_number(value) when is_binary(value), do: String.to_integer(value)
 | 
						|
  defp parse_as_number(value) when is_integer(value), do: value
 | 
						|
 | 
						|
  @spec info(scheme(), module(), nil | pid()) :: String.t()
 | 
						|
  defp info(scheme, plug, pid) do
 | 
						|
    server_vsn = Application.spec(:bandit)[:vsn]
 | 
						|
    "Running #{inspect(plug)} with Bandit #{server_vsn} at #{bound_address(scheme, pid)}"
 | 
						|
  end
 | 
						|
 | 
						|
  @spec bound_address(scheme(), nil | pid()) :: String.t() | scheme()
 | 
						|
  defp bound_address(scheme, nil), do: scheme
 | 
						|
 | 
						|
  defp bound_address(scheme, pid) do
 | 
						|
    {:ok, {address, port}} = ThousandIsland.listener_info(pid)
 | 
						|
 | 
						|
    case address do
 | 
						|
      :local -> "#{_unix_path = port} (#{scheme}+unix)"
 | 
						|
      :undefined -> "#{inspect(port)} (#{scheme}+undefined)"
 | 
						|
      :unspec -> "unspec (#{scheme})"
 | 
						|
      address -> "#{:inet.ntoa(address)}:#{port} (#{scheme})"
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |