defmodule Bandit do
@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.
## Using Bandit With Phoenix
Bandit fully supports Phoenix. Phoenix applications which use WebSockets for
features such as Channels or LiveView require Phoenix 1.7 or later.
Using Bandit to host your Phoenix application couldn't be simpler:
1. Add Bandit as a dependency in your Phoenix application's `mix.exs`:
```elixir
{:bandit, ">= 0.7.7"}
```
2. Add the following `adapter:` line to your endpoint configuration in `config/config.exs`:
```elixir
config :your_app, YourAppWeb.Endpoint,
adapter: Bandit.PhoenixAdapter
```
3. That's it! You should now see messages at startup indicating that Phoenix is
using Bandit to serve your endpoint, and everything should 'just work'. Note
that if you have set any exotic configuration options within your endpoint,
you may need to update that configuration to work with Bandit; see the
`Bandit.PhoenixAdapter` documentation for more information.
## Using Bandit With Plug Applications
Using Bandit to host your own Plug is very straightforward. Assuming you have a Plug module
implemented already, you can host it within Bandit by adding something similar to the following
to your application's `Application.start/2` function:
```elixir
def start(_type, _args) do
children = [
{Bandit, plug: MyPlug}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
```
For details about writing Plug based applications, consult the excellent [Plug
documentation](https://hexdocs.pm/plug/) for plenty of examples & tips to get started.
Bandit supports the complete Plug API & should work correctly with any Plug-based
application. If you encounter errors using Bandit your Plug app, please do get in touch by
filing an issue on the Bandit [GitHub project](https://github.com/mtrudel/bandit) (especially if
the error does not occur with another HTTP server such as Cowboy).
## Configuration
A number of options are defined when starting a server. The complete list is
defined by the `t:Bandit.options/0` type.
## Setting up an HTTPS Server
By far the most common stumbling block encountered when setting up an HTTPS server involves
configuring key and certificate data. Bandit is comparatively easy to set up in this regard,
with a working example looking similar to the following:
```elixir
def start(_type, _args) do
children = [
{Bandit,
plug: MyPlug,
scheme: :https,
certfile: "/absolute/path/to/cert.pem",
keyfile: "/absolute/path/to/key.pem"
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
```
## WebSocket Support
Bandit supports WebSocket implementations via the
[WebSock](https://hexdocs.pm/websock/WebSock.html) and
[WebSockAdapter](https://hexdocs.pm/websock_adapter/WebSockAdapter.html) libraries, which
provide a generic abstraction for WebSockets (very similar to how Plug is a generic abstraction
on top of HTTP). Bandit fully supports all aspects of these libraries.
Applications should validate that the connection represents a valid WebSocket request
before attempting an upgrade (Bandit will validate the connection as part of the upgrade
process, but does not provide any capacity for an application to be notified if the upgrade is
not successful). If an application wishes to negotiate WebSocket subprotocols or otherwise set
any response headers, it should do so before upgrading.
"""
@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 ia 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`
* `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_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(),
keyfile: binary(),
certfile: binary(),
otp_app: binary() | atom(),
cipher_suite: :strong | :compatible,
display_plug: module(),
startup_log: Logger.level() | false,
thousand_island_options: ThousandIsland.options(),
http_1_options: http_1_options(),
http_2_options: http_2_options(),
websocket_options: websocket_options()
]
@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)
* `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`
"""
@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(),
compress: boolean(),
deflate_opions: deflate_options()
]
@typedoc """
Options to configure the HTTP/2 stack in Bandit
* `enabled`: Whether or not to serve HTTP/2 requests. Defaults to true
* `max_header_key_length`: The maximum permitted length of any single header key
(expressed as the number of decompressed bytes) in an HTTP/2 request. Defaults to 10_000 bytes
* `max_header_value_length`: The maximum permitted length of any single header value
(expressed as the number of decompressed bytes) in an HTTP/2 request. Defaults to 10_000 bytes
* `max_header_count`: The maximum permitted number of headers in an HTTP/2 request.
Defaults to 50 headers
* `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
* `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`
"""
@type http_2_options :: [
enabled: boolean(),
max_header_key_length: pos_integer(),
max_header_value_length: pos_integer(),
max_header_count: pos_integer(),
max_requests: pos_integer(),
default_local_settings: Bandit.HTTP2.Settings.t(),
compress: boolean(),
deflate_options: deflate_options()
]
@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()
]
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
@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
arg =
arg
|> validate_options(
~w(plug scheme port ip keyfile certfile otp_app cipher_suite display_plug startup_log thousand_island_options http_1_options http_2_options websocket_options)a,
"top level"
)
thousand_island_options =
arg
|> Keyword.get(:thousand_island_options, [])
|> validate_options(
ThousandIsland.ServerConfig.__struct__() |> Map.from_struct() |> Map.keys(),
:thousand_island_options
)
http_1_options =
arg
|> Keyword.get(:http_1_options, [])
|> validate_options(
~w(enabled max_request_line_length max_header_length max_header_count max_requests compress deflate_options)a,
:http_1_options
)
http_2_options =
arg
|> Keyword.get(:http_2_options, [])
|> validate_options(
~w(enabled max_header_key_length max_header_value_length max_header_count max_requests default_local_settings compress deflate_options)a,
:http_2_options
)
websocket_options =
arg
|> Keyword.get(:websocket_options, [])
|> validate_options(
~w(enabled max_frame_size validate_text_frames compress)a,
:websocket_options
)
{plug_mod, _} = plug = plug(arg)
display_plug = Keyword.get(arg, :display_plug, plug_mod)
startup_log = Keyword.get(arg, :startup_log, :info)
handler_options = %{
plug: plug,
handler_module: Bandit.InitialHandler,
opts: %{http_1: http_1_options, http_2: http_2_options, websocket: websocket_options}
}
scheme = Keyword.get(arg, :scheme, :http)
{transport_module, transport_options, default_port} =
case scheme do
:http ->
transport_options =
Keyword.take(arg, [:ip])
|> Keyword.merge(Keyword.get(thousand_island_options, :transport_options, []))
{ThousandIsland.Transports.TCP, transport_options, 4000}
:https ->
transport_options =
Keyword.take(arg, [:ip, :keyfile, :certfile, :otp_app, :cipher_suite])
|> Keyword.merge(alpn_preferred_protocols: ["h2", "http/1.1"])
|> Keyword.merge(Keyword.get(thousand_island_options, :transport_options, []))
|> Plug.SSL.configure()
|> case do
{:ok, options} -> options
{:error, message} -> raise "Plug.SSL.configure/1 encountered error: #{message}"
end
|> Keyword.delete(: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))
{:ok, pid}
{:error, {:shutdown, {:failed_to_start_child, :listener, :eaddrinuse}}} = error ->
Logger.error([info(scheme, display_plug, nil), " failed, port already in use"])
error
{:error, _} = error ->
error
end
end
defp validate_options(options, valid_values, name) do
case Keyword.split(options, valid_values) do
{options, []} ->
options
{_, illegal_options} ->
raise "Unsupported keys(s) in #{name} config: #{inspect(Keyword.keys(illegal_options))}"
end
end
defp plug(arg) do
arg
|> Keyword.get(:plug)
|> case do
nil -> raise "A value for is required for :plug"
{plug, plug_options} -> {plug, plug.init(plug_options)}
plug -> {plug, plug.init([])}
end
end
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
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
defp bound_address(scheme, nil), do: scheme
defp bound_address(scheme, pid) do
{:ok, %{address: address, port: port}} = ThousandIsland.listener_info(pid)
case address do
{:local, unix_path} -> "#{unix_path} (#{scheme}+unix)"
address -> "#{:inet.ntoa(address)}:#{port} (#{scheme})"
end
end
end