462 lines
16 KiB
Elixir
462 lines
16 KiB
Elixir
defmodule Plug.Cowboy do
|
|
@moduledoc """
|
|
Adapter interface to the Cowboy2 webserver.
|
|
|
|
## Options
|
|
|
|
* `:net` - If using `:inet` (IPv4 only - the default) or `:inet6` (IPv6)
|
|
|
|
* `:ip` - the ip to bind the server to.
|
|
Must be either a tuple in the format `{a, b, c, d}` with each value in `0..255` for IPv4,
|
|
or a tuple in the format `{a, b, c, d, e, f, g, h}` with each value in `0..65535` for IPv6,
|
|
or a tuple in the format `{:local, path}` for a unix socket at the given `path`.
|
|
If you set an IPv6, the `:net` option will be automatically set to `:inet6`.
|
|
If both `:net` and `:ip` options are given, make sure they are compatible
|
|
(i.e. give a IPv4 for `:inet` and IPv6 for `:inet6`).
|
|
Also, see "Loopback vs Public IP Addresses".
|
|
|
|
* `:port` - the port to run the server.
|
|
Defaults to 4000 (http) and 4040 (https).
|
|
Must be 0 when `:ip` is a `{:local, path}` tuple.
|
|
|
|
* `:dispatch` - manually configure Cowboy's dispatch.
|
|
If this option is used, the given plug won't be initialized
|
|
nor dispatched to (and doing so becomes the user's responsibility).
|
|
|
|
* `:ref` - the reference name to be used.
|
|
Defaults to `plug.HTTP` (http) and `plug.HTTPS` (https).
|
|
Note, the default reference name does not contain the port so in order
|
|
to serve the same plug on multiple ports you need to set the `:ref` accordingly,
|
|
e.g.: `ref: MyPlug_HTTP_4000`, `ref: MyPlug_HTTP_4001`, etc.
|
|
This is the value that needs to be given on shutdown.
|
|
|
|
* `:compress` - Cowboy will attempt to compress the response body.
|
|
Defaults to false.
|
|
|
|
* `:stream_handlers` - List of Cowboy `stream_handlers`,
|
|
see [Cowboy docs](https://ninenines.eu/docs/en/cowboy/2.5/manual/cowboy_http/).
|
|
|
|
* `:protocol_options` - Specifies remaining protocol options,
|
|
see [Cowboy docs](https://ninenines.eu/docs/en/cowboy/2.5/manual/cowboy_http/).
|
|
|
|
* `:transport_options` - A keyword list specifying transport options,
|
|
see [Ranch docs](https://ninenines.eu/docs/en/ranch/1.7/manual/ranch/).
|
|
By default `:num_acceptors` will be set to `100` and `:max_connections`
|
|
to `16_384`.
|
|
|
|
All other options given at the top level must configure the underlying
|
|
socket. For HTTP connections, those options are listed under
|
|
[`ranch_tcp`](https://ninenines.eu/docs/en/ranch/1.7/manual/ranch_tcp/).
|
|
For example, you can set `:ipv6_v6only` to true if you want to bind only
|
|
on IPv6 addresses.
|
|
|
|
For HTTPS (SSL) connections, those options are described in
|
|
[`ranch_ssl`](https://ninenines.eu/docs/en/ranch/1.7/manual/ranch_ssl/).
|
|
See `https/3` for an example and read `Plug.SSL.configure/1` to
|
|
understand about our SSL defaults.
|
|
|
|
When using a Unix socket, OTP 21+ is required for `Plug.Static` and
|
|
`Plug.Conn.send_file/3` to behave correctly.
|
|
|
|
## Safety limits
|
|
|
|
Cowboy sets different limits on URL size, header length, number of
|
|
headers and so on to protect your application from attacks. For example,
|
|
the request line length defaults to 10k, which means Cowboy will return
|
|
414 if a larger URL is given. You can change this under `:protocol_options`:
|
|
|
|
protocol_options: [max_request_line_length: 50_000]
|
|
|
|
Keep in mind though increasing those limits can pose a security risk.
|
|
Other times, browsers and proxies along the way may have equally strict
|
|
limits, which means the request will still fail or the URL will be
|
|
pruned. You can [consult all limits here](https://ninenines.eu/docs/en/cowboy/2.5/manual/cowboy_http/).
|
|
|
|
## Loopback vs Public IP Addresses
|
|
|
|
Should your application bind to a loopback address, such as `::1` (IPv6) or
|
|
`127.0.0.1` (IPv4), or a public one, such as `::0` (IPv6) or `0.0.0.0`
|
|
(IPv4)? It depends on how (and whether) you want it to be reachable from
|
|
other machines.
|
|
|
|
Loopback addresses are only reachable from the same host (`localhost` is
|
|
usually configured to resolve to a loopback address). You may wish to use one if:
|
|
|
|
- Your app is running in a development environment (such as your laptop) and
|
|
you don't want others on the same network to access it.
|
|
- Your app is running in production, but behind a reverse proxy. For example,
|
|
you might have Nginx bound to a public address and serving HTTPS, but
|
|
forwarding the traffic to your application running on the same host. In that
|
|
case, having your app bind to the loopback address means that Nginx can reach
|
|
it, but outside traffic can only reach it via Nginx.
|
|
|
|
Public addresses are reachable from other hosts. You may wish to use one if:
|
|
|
|
- Your app is running in a container. In this case, its loopback address is
|
|
reachable only from within the container; to be accessible from outside the
|
|
container, it needs to bind to a public IP address.
|
|
- Your app is running in production without a reverse proxy, using Cowboy's
|
|
SSL support.
|
|
|
|
## Logging
|
|
|
|
You can configure which exceptions are logged via `:log_exceptions_with_status_code`
|
|
application environment variable. If the status code returned by `Plug.Exception.status/1`
|
|
for the exception falls into any of the configured ranges, the exception is logged.
|
|
By default it's set to `[500..599]`.
|
|
|
|
config :plug_cowboy,
|
|
log_exceptions_with_status_code: [400..599]
|
|
|
|
By default, `Plug.Cowboy` includes the entire `conn` to the log metadata for exceptions.
|
|
However, this metadata may contain sensitive information such as security headers or
|
|
cookies, which may be logged in plain text by certain logging backends. To prevent this,
|
|
you can configure the `:conn_in_exception_metadata` option to not include the `conn` in the metadata.
|
|
|
|
config :plug_cowboy,
|
|
conn_in_exception_metadata: false
|
|
|
|
## Instrumentation
|
|
|
|
Plug.Cowboy uses the `:telemetry` library for instrumentation. The following
|
|
span events are published during each request:
|
|
|
|
* `[:cowboy, :request, :start]` - dispatched at the beginning of the request
|
|
* `[:cowboy, :request, :stop]` - dispatched at the end of the request
|
|
* `[:cowboy, :request, :exception]` - dispatched at the end of a request that exits
|
|
|
|
A single event is published when the request ends with an early error:
|
|
* `[:cowboy, :request, :early_error]` - dispatched for requests terminated early by Cowboy
|
|
|
|
See [`cowboy_telemetry`](https://github.com/beam-telemetry/cowboy_telemetry#telemetry-events)
|
|
for more details on the events.
|
|
|
|
To opt-out of this default instrumentation, you can manually configure
|
|
cowboy with the option `stream_handlers: [:cowboy_stream_h]`.
|
|
|
|
## WebSocket support
|
|
|
|
Plug.Cowboy supports upgrading HTTP requests to WebSocket connections via
|
|
the use of the `Plug.Conn.upgrade_adapter/3` function, called with `:websocket` as the second
|
|
argument. Applications should validate that the connection represents a valid WebSocket request
|
|
before calling this function (Cowboy 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 calling `Plug.Conn.upgrade_adapter/3`.
|
|
|
|
The third argument to `Plug.Conn.upgrade_adapter/3` defines the details of how Plug.Cowboy
|
|
should handle the WebSocket connection, and must take the form `{handler, handler_opts,
|
|
connection_opts}`, where values are as follows:
|
|
|
|
* `handler` is a module which implements the
|
|
[`:cowboy_websocket`](https://ninenines.eu/docs/en/cowboy/2.6/manual/cowboy_websocket/)
|
|
behaviour. Note that this module will NOT have its `c:cowboy_websocket.init/2` callback
|
|
called; only the 'later' parts of the `:cowboy_websocket` lifecycle are supported
|
|
* `handler_opts` is an arbitrary term which will be passed as the argument to
|
|
`c:cowboy_websocket.websocket_init/1`
|
|
* `connection_opts` is a map with any of [Cowboy's websockets options](https://ninenines.eu/docs/en/cowboy/2.6/manual/cowboy_websocket/#_opts)
|
|
|
|
"""
|
|
|
|
require Logger
|
|
|
|
@doc false
|
|
def start(_type, _args) do
|
|
Logger.add_translator({Plug.Cowboy.Translator, :translate})
|
|
Supervisor.start_link([], strategy: :one_for_one)
|
|
end
|
|
|
|
# Made public with @doc false for testing.
|
|
@doc false
|
|
def args(scheme, plug, plug_opts, cowboy_options) do
|
|
{cowboy_options, non_keyword_options} = Enum.split_with(cowboy_options, &match?({_, _}, &1))
|
|
|
|
cowboy_options
|
|
|> normalize_cowboy_options(scheme)
|
|
|> to_args(scheme, plug, plug_opts, non_keyword_options)
|
|
end
|
|
|
|
@doc """
|
|
Runs cowboy under http.
|
|
|
|
## Example
|
|
|
|
# Starts a new interface
|
|
Plug.Cowboy.http MyPlug, [], port: 80
|
|
|
|
# The interface above can be shutdown with
|
|
Plug.Cowboy.shutdown MyPlug.HTTP
|
|
|
|
"""
|
|
@spec http(module(), Keyword.t(), Keyword.t()) ::
|
|
{:ok, pid} | {:error, :eaddrinuse} | {:error, term}
|
|
def http(plug, opts, cowboy_options \\ []) do
|
|
run(:http, plug, opts, cowboy_options)
|
|
end
|
|
|
|
@doc """
|
|
Runs cowboy under https.
|
|
|
|
Besides the options described in the module documentation,
|
|
this function sets defaults and accepts all options defined
|
|
in `Plug.SSL.configure/1`.
|
|
|
|
## Example
|
|
|
|
# Starts a new interface
|
|
Plug.Cowboy.https MyPlug, [],
|
|
port: 443,
|
|
password: "SECRET",
|
|
otp_app: :my_app,
|
|
keyfile: "priv/ssl/key.pem",
|
|
certfile: "priv/ssl/cert.pem",
|
|
dhfile: "priv/ssl/dhparam.pem"
|
|
|
|
# The interface above can be shutdown with
|
|
Plug.Cowboy.shutdown MyPlug.HTTPS
|
|
|
|
"""
|
|
@spec https(module(), Keyword.t(), Keyword.t()) ::
|
|
{:ok, pid} | {:error, :eaddrinuse} | {:error, term}
|
|
def https(plug, opts, cowboy_options \\ []) do
|
|
Application.ensure_all_started(:ssl)
|
|
run(:https, plug, opts, cowboy_options)
|
|
end
|
|
|
|
@doc """
|
|
Shutdowns the given reference.
|
|
"""
|
|
def shutdown(ref) do
|
|
:cowboy.stop_listener(ref)
|
|
end
|
|
|
|
@doc """
|
|
A function for starting a Cowboy2 server under Elixir v1.5+ supervisors.
|
|
|
|
It supports all options as specified in the module documentation plus it
|
|
requires the following two options:
|
|
|
|
* `:scheme` - either `:http` or `:https`
|
|
* `:plug` - such as `MyPlug` or `{MyPlug, plug_opts}`
|
|
|
|
## Examples
|
|
|
|
Assuming your Plug module is named `MyApp` you can add it to your
|
|
supervision tree by using this function:
|
|
|
|
children = [
|
|
{Plug.Cowboy, scheme: :http, plug: MyApp, options: [port: 4040]}
|
|
]
|
|
|
|
Supervisor.start_link(children, strategy: :one_for_one)
|
|
|
|
"""
|
|
def child_spec(opts) do
|
|
scheme = Keyword.fetch!(opts, :scheme)
|
|
|
|
{plug, plug_opts} =
|
|
case Keyword.fetch!(opts, :plug) do
|
|
{_, _} = tuple -> tuple
|
|
plug -> {plug, []}
|
|
end
|
|
|
|
# We support :options for backwards compatibility.
|
|
cowboy_opts =
|
|
opts
|
|
|> Keyword.drop([:scheme, :plug, :options])
|
|
|> Kernel.++(Keyword.get(opts, :options, []))
|
|
|
|
cowboy_args = args(scheme, plug, plug_opts, cowboy_opts)
|
|
[ref, transport_opts, proto_opts] = cowboy_args
|
|
|
|
{ranch_module, cowboy_protocol, transport_opts} =
|
|
case scheme do
|
|
:http ->
|
|
{:ranch_tcp, :cowboy_clear, transport_opts}
|
|
|
|
:https ->
|
|
%{socket_opts: socket_opts} = transport_opts
|
|
|
|
socket_opts =
|
|
socket_opts
|
|
|> Keyword.put_new(:next_protocols_advertised, ["h2", "http/1.1"])
|
|
|> Keyword.put_new(:alpn_preferred_protocols, ["h2", "http/1.1"])
|
|
|
|
{:ranch_ssl, :cowboy_tls, %{transport_opts | socket_opts: socket_opts}}
|
|
end
|
|
|
|
case :ranch.child_spec(ref, ranch_module, transport_opts, cowboy_protocol, proto_opts) do
|
|
{id, start, restart, shutdown, type, modules} ->
|
|
%{
|
|
id: id,
|
|
start: start,
|
|
restart: restart,
|
|
shutdown: shutdown,
|
|
type: type,
|
|
modules: modules
|
|
}
|
|
|
|
child_spec when is_map(child_spec) ->
|
|
child_spec
|
|
end
|
|
end
|
|
|
|
## Helpers
|
|
|
|
@protocol_options [:compress, :stream_handlers]
|
|
|
|
defp run(scheme, plug, opts, cowboy_options) do
|
|
case Application.ensure_all_started(:cowboy) do
|
|
{:ok, _} ->
|
|
nil
|
|
|
|
{:error, {:cowboy, _}} ->
|
|
raise "could not start the Cowboy application. Please ensure it is listed as a dependency in your mix.exs"
|
|
end
|
|
|
|
start =
|
|
case scheme do
|
|
:http -> :start_clear
|
|
:https -> :start_tls
|
|
other -> :erlang.error({:badarg, [other]})
|
|
end
|
|
|
|
:telemetry.attach(
|
|
:plug_cowboy,
|
|
[:cowboy, :request, :early_error],
|
|
&__MODULE__.handle_event/4,
|
|
nil
|
|
)
|
|
|
|
apply(:cowboy, start, args(scheme, plug, opts, cowboy_options))
|
|
end
|
|
|
|
defp normalize_cowboy_options(cowboy_options, :http) do
|
|
Keyword.put_new(cowboy_options, :port, 4000)
|
|
end
|
|
|
|
defp normalize_cowboy_options(cowboy_options, :https) do
|
|
cowboy_options
|
|
|> Keyword.put_new(:port, 4040)
|
|
|> Plug.SSL.configure()
|
|
|> case do
|
|
{:ok, options} -> options
|
|
{:error, message} -> fail(message)
|
|
end
|
|
end
|
|
|
|
defp to_args(opts, scheme, plug, plug_opts, non_keyword_opts) do
|
|
{timeout, opts} = Keyword.pop(opts, :timeout)
|
|
|
|
if timeout do
|
|
Logger.warning("the :timeout option for Cowboy webserver has no effect and must be removed")
|
|
end
|
|
|
|
opts = Keyword.delete(opts, :otp_app)
|
|
{ref, opts} = Keyword.pop(opts, :ref)
|
|
{dispatch, opts} = Keyword.pop(opts, :dispatch)
|
|
{protocol_options, opts} = Keyword.pop(opts, :protocol_options, [])
|
|
|
|
dispatch = :cowboy_router.compile(dispatch || dispatch_for(plug, plug_opts))
|
|
{extra_options, opts} = Keyword.split(opts, @protocol_options)
|
|
|
|
extra_options = set_stream_handlers(extra_options)
|
|
protocol_and_extra_options = :maps.from_list(protocol_options ++ extra_options)
|
|
protocol_options = Map.merge(%{env: %{dispatch: dispatch}}, protocol_and_extra_options)
|
|
{transport_options, socket_options} = Keyword.pop(opts, :transport_options, [])
|
|
|
|
{net, socket_options} = Keyword.pop(socket_options, :net)
|
|
socket_options = List.wrap(net) ++ non_keyword_opts ++ socket_options
|
|
|
|
transport_options =
|
|
transport_options
|
|
|> Keyword.put_new(:num_acceptors, 100)
|
|
|> Keyword.put_new(:max_connections, 16_384)
|
|
|> Keyword.update(
|
|
:socket_opts,
|
|
socket_options,
|
|
&(&1 ++ socket_options)
|
|
)
|
|
|> Map.new()
|
|
|
|
[ref || build_ref(plug, scheme), transport_options, protocol_options]
|
|
end
|
|
|
|
@default_stream_handlers [:cowboy_telemetry_h, :cowboy_stream_h]
|
|
|
|
defp set_stream_handlers(opts) do
|
|
compress = Keyword.get(opts, :compress)
|
|
stream_handlers = Keyword.get(opts, :stream_handlers)
|
|
|
|
case {compress, stream_handlers} do
|
|
{true, nil} ->
|
|
Keyword.put_new(opts, :stream_handlers, [:cowboy_compress_h | @default_stream_handlers])
|
|
|
|
{true, _} ->
|
|
raise "cannot set both compress and stream_handlers at once. " <>
|
|
"If you wish to set compress, please add `:cowboy_compress_h` to your stream handlers."
|
|
|
|
{_, nil} ->
|
|
Keyword.put_new(opts, :stream_handlers, @default_stream_handlers)
|
|
|
|
{_, _} ->
|
|
opts
|
|
end
|
|
end
|
|
|
|
defp build_ref(plug, scheme) do
|
|
Module.concat(plug, scheme |> to_string |> String.upcase())
|
|
end
|
|
|
|
defp dispatch_for(plug, opts) do
|
|
opts = plug.init(opts)
|
|
[{:_, [{:_, Plug.Cowboy.Handler, {plug, opts}}]}]
|
|
end
|
|
|
|
defp fail(message) do
|
|
raise ArgumentError, "could not start Cowboy2 adapter, " <> message
|
|
end
|
|
|
|
@doc false
|
|
def handle_event(
|
|
[:cowboy, :request, :early_error],
|
|
_,
|
|
%{reason: {:connection_error, :limit_reached, specific_reason}, partial_req: partial_req},
|
|
_
|
|
) do
|
|
Logger.error("""
|
|
Cowboy returned 431 because it was unable to parse the request headers.
|
|
|
|
This may happen because there are no headers, or there are too many headers
|
|
or the header name or value are too large (such as a large cookie).
|
|
|
|
More specific reason is:
|
|
|
|
#{inspect(specific_reason)}
|
|
|
|
You can customize those limits when configuring your http/https
|
|
server. The configuration option and default values are shown below:
|
|
|
|
protocol_options: [
|
|
max_header_name_length: 64,
|
|
max_header_value_length: 4096,
|
|
max_headers: 100
|
|
]
|
|
|
|
Request info:
|
|
|
|
peer: #{format_peer(partial_req.peer)}
|
|
method: #{partial_req.method || "<unable to parse>"}
|
|
path: #{partial_req.path || "<unable to parse>"}
|
|
""")
|
|
end
|
|
|
|
def handle_event(_, _, _, _) do
|
|
:ok
|
|
end
|
|
|
|
defp format_peer({addr, port}) do
|
|
"#{:inet_parse.ntoa(addr)}:#{port}"
|
|
end
|
|
end
|