Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support top-level :inet and :inet6 options in Phoenix #337

Merged
merged 1 commit into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 65 additions & 38 deletions lib/bandit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ defmodule Bandit do
* `: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
Expand Down Expand Up @@ -68,21 +74,23 @@ defmodule Bandit do
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: 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()
{: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 """
Expand All @@ -97,9 +105,9 @@ defmodule Bandit do
Defaults to `true`
"""
@type http_options :: [
compress: boolean(),
deflate_opions: deflate_options(),
log_protocol_errors: boolean()
{:compress, boolean()}
| {:deflate_opions, deflate_options()}
| {:log_protocol_errors, boolean()}
]

@typedoc """
Expand All @@ -121,13 +129,13 @@ defmodule Bandit do
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(),
gc_every_n_keepalive_requests: pos_integer(),
log_unknown_messages: boolean()
{:enabled, boolean()}
| {:max_request_line_length, pos_integer()}
| {:max_header_length, pos_integer()}
| {:max_header_count, pos_integer()}
| {:max_requests, pos_integer()}
| {:gc_every_n_keepalive_requests, pos_integer()}
| {:log_unknown_messages, boolean()}
]

@typedoc """
Expand All @@ -143,10 +151,10 @@ defmodule Bandit do
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: Bandit.HTTP2.Settings.t()
{:enabled, boolean()}
| {:max_header_block_size, pos_integer()}
| {:max_requests, pos_integer()}
| {:default_local_settings, Bandit.HTTP2.Settings.t()}
]

@typedoc """
Expand All @@ -164,20 +172,20 @@ defmodule Bandit do
for details). Defaults to `true`
"""
@type websocket_options :: [
enabled: boolean(),
max_frame_size: pos_integer(),
validate_text_frames: boolean(),
compress: boolean()
{: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()
{:level, :zlib.zlevel()}
| {:window_bits, :zlib.zwindowbits()}
| {:memory_level, :zlib.zmemlevel()}
| {:strategy, :zlib.zstrategy()}
]

@typep scheme :: :http | :https
Expand Down Expand Up @@ -210,7 +218,8 @@ defmodule Bandit do
"""
@spec start_link(options()) :: Supervisor.on_start()
def start_link(arg) do
arg = validate_options(arg, @top_level_keys, "top level")
# 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, [])
Expand Down Expand Up @@ -305,6 +314,24 @@ defmodule Bandit do
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
Expand Down
3 changes: 1 addition & 2 deletions lib/bandit/phoenix_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ defmodule Bandit.PhoenixAdapter do
plug = resolve_plug(config[:code_reloader], endpoint)

for scheme <- [:http, :https], opts = config[scheme] do
opts
|> Keyword.merge(plug: plug, display_plug: endpoint, scheme: scheme, otp_app: otp_app)
([plug: plug, display_plug: endpoint, scheme: scheme, otp_app: otp_app] ++ opts)
|> Bandit.child_spec()
|> Supervisor.child_spec(id: {endpoint, scheme})
end
Expand Down
18 changes: 18 additions & 0 deletions test/bandit/server_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ defmodule ServerTest do
"Running ServerTest with Bandit #{Application.spec(:bandit)[:vsn]} at http failed, port #{port} already in use"
end

test "special cases :inet option" do
logs =
capture_log(fn ->
start_supervised({Bandit, [{:plug, __MODULE__}, :inet, {:port, 0}, {:ip, :loopback}]})
end)

assert logs =~ "at 127.0.0.1"
end

test "special cases :inet6 option" do
logs =
capture_log(fn ->
start_supervised({Bandit, [{:plug, __MODULE__}, :inet6, {:port, 0}, {:ip, :loopback}]})
end)

assert logs =~ "at ::1"
end

test "can run multiple instances of Bandit" do
start_supervised({Bandit, plug: __MODULE__, port: 4000})
start_supervised({Bandit, plug: __MODULE__, port: 4001})
Expand Down