Packages
A Plug adapter for Cowboy
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/plug/cowboy.ex
defmodule Plug.Cowboy do
@moduledoc """
Adapter interface to the Cowboy2 webserver.
## Options
* `: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`.
* `: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.6/manual/ranch/).
By default `:num_acceptors` will be set to `100` and `:max_connections`
to `16_384`.
All other options are given as `:socket_opts` to the underlying transport.
When running on HTTPS, any SSL configuration should be given directly to the
adapter. 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.
## Instrumentation
PlugCowboy 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]`.
"""
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/2`.
## 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
@transport_options [
:connection_type,
:handshake_timeout,
:max_connections,
:logger,
:num_acceptors,
:shutdown,
:socket,
:socket_opts,
# Special cases supported by plug but not ranch
:acceptors
]
@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 follow 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
{id, start, restart, shutdown, type, modules} =
:ranch.child_spec(ref, ranch_module, transport_opts, cowboy_protocol, proto_opts)
%{id: id, start: start, restart: restart, shutdown: shutdown, type: type, modules: modules}
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],
&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.warn("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, [])
option_keys = Keyword.keys(socket_options)
for opt <- @transport_options, opt in option_keys do
option_deprecation_warning(opt)
end
{num_acceptors, socket_options} = Keyword.pop(socket_options, :num_acceptors, 100)
{num_acceptors, socket_options} = Keyword.pop(socket_options, :acceptors, num_acceptors)
{max_connections, socket_options} = Keyword.pop(socket_options, :max_connections, 16_384)
socket_options = non_keyword_opts ++ socket_options
transport_options =
transport_options
|> Keyword.put_new(:num_acceptors, num_acceptors)
|> Keyword.put_new(:max_connections, max_connections)
|> 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
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
defp option_deprecation_warning(:acceptors),
do: option_deprecation_warning(:acceptors, :num_acceptors)
defp option_deprecation_warning(option),
do: option_deprecation_warning(option, option)
defp option_deprecation_warning(option, expected_option) do
warning =
"using :#{option} in options is deprecated. Please pass " <>
":#{expected_option} to the :transport_options keyword list instead"
IO.warn(warning)
end
end