cat-bookmarker/deps/plug_cowboy/lib/plug/cowboy.ex

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