cowboy.ex (16176B)
1 defmodule Plug.Cowboy do 2 @moduledoc """ 3 Adapter interface to the Cowboy2 webserver. 4 5 ## Options 6 7 * `:net` - If using `:inet` (IPv4 only - the default) or `:inet6` (IPv6) 8 9 * `:ip` - the ip to bind the server to. 10 Must be either a tuple in the format `{a, b, c, d}` with each value in `0..255` for IPv4, 11 or a tuple in the format `{a, b, c, d, e, f, g, h}` with each value in `0..65535` for IPv6, 12 or a tuple in the format `{:local, path}` for a unix socket at the given `path`. 13 If you set an IPv6, the `:net` option will be automatically set to `:inet6`. 14 If both `:net` and `:ip` options are given, make sure they are compatible 15 (i.e. give a IPv4 for `:inet` and IPv6 for `:inet6`). 16 Also, see "Loopback vs Public IP Addresses". 17 18 * `:port` - the port to run the server. 19 Defaults to 4000 (http) and 4040 (https). 20 Must be 0 when `:ip` is a `{:local, path}` tuple. 21 22 * `:dispatch` - manually configure Cowboy's dispatch. 23 If this option is used, the given plug won't be initialized 24 nor dispatched to (and doing so becomes the user's responsibility). 25 26 * `:ref` - the reference name to be used. 27 Defaults to `plug.HTTP` (http) and `plug.HTTPS` (https). 28 Note, the default reference name does not contain the port so in order 29 to serve the same plug on multiple ports you need to set the `:ref` accordingly, 30 e.g.: `ref: MyPlug_HTTP_4000`, `ref: MyPlug_HTTP_4001`, etc. 31 This is the value that needs to be given on shutdown. 32 33 * `:compress` - Cowboy will attempt to compress the response body. 34 Defaults to false. 35 36 * `:stream_handlers` - List of Cowboy `stream_handlers`, 37 see [Cowboy docs](https://ninenines.eu/docs/en/cowboy/2.5/manual/cowboy_http/). 38 39 * `:protocol_options` - Specifies remaining protocol options, 40 see [Cowboy docs](https://ninenines.eu/docs/en/cowboy/2.5/manual/cowboy_http/). 41 42 * `:transport_options` - A keyword list specifying transport options, 43 see [Ranch docs](https://ninenines.eu/docs/en/ranch/1.7/manual/ranch/). 44 By default `:num_acceptors` will be set to `100` and `:max_connections` 45 to `16_384`. 46 47 All other options given at the top level must configure the underlying 48 socket. For HTTP connections, those options are listed under 49 [`ranch_tcp`](https://ninenines.eu/docs/en/ranch/1.7/manual/ranch_tcp/). 50 For example, you can set `:ipv6_v6only` to true if you want to bind only 51 on IPv6 addresses. 52 53 For HTTPS (SSL) connections, those options are described in 54 [`ranch_ssl`](https://ninenines.eu/docs/en/ranch/1.7/manual/ranch_ssl/). 55 See `https/3` for an example and read `Plug.SSL.configure/1` to 56 understand about our SSL defaults. 57 58 When using a Unix socket, OTP 21+ is required for `Plug.Static` and 59 `Plug.Conn.send_file/3` to behave correctly. 60 61 ## Safety limits 62 63 Cowboy sets different limits on URL size, header length, number of 64 headers and so on to protect your application from attacks. For example, 65 the request line length defaults to 10k, which means Cowboy will return 66 414 if a larger URL is given. You can change this under `:protocol_options`: 67 68 protocol_options: [max_request_line_length: 50_000] 69 70 Keep in mind though increasing those limits can pose a security risk. 71 Other times, browsers and proxies along the way may have equally strict 72 limits, which means the request will still fail or the URL will be 73 pruned. You can [consult all limits here](https://ninenines.eu/docs/en/cowboy/2.5/manual/cowboy_http/). 74 75 ## Loopback vs Public IP Addresses 76 77 Should your application bind to a loopback address, such as `::1` (IPv6) or 78 `127.0.0.1` (IPv4), or a public one, such as `::0` (IPv6) or `0.0.0.0` 79 (IPv4)? It depends on how (and whether) you want it to be reachable from 80 other machines. 81 82 Loopback addresses are only reachable from the same host (`localhost` is 83 usually configured to resolve to a loopback address). You may wish to use one if: 84 85 - Your app is running in a development environment (such as your laptop) and 86 you don't want others on the same network to access it. 87 - Your app is running in production, but behind a reverse proxy. For example, 88 you might have Nginx bound to a public address and serving HTTPS, but 89 forwarding the traffic to your application running on the same host. In that 90 case, having your app bind to the loopback address means that Nginx can reach 91 it, but outside traffic can only reach it via Nginx. 92 93 Public addresses are reachable from other hosts. You may wish to use one if: 94 95 - Your app is running in a container. In this case, its loopback address is 96 reachable only from within the container; to be accessible from outside the 97 container, it needs to bind to a public IP address. 98 - Your app is running in production without a reverse proxy, using Cowboy's 99 SSL support. 100 101 ## Logging 102 103 You can configure which exceptions are logged via `:log_exceptions_with_status_code` 104 application environment variable. If the status code returned by `Plug.Exception.status/1` 105 for the exception falls into any of the configured ranges, the exception is logged. 106 By default it's set to `[500..599]`. 107 108 config :plug_cowboy, 109 log_exceptions_with_status_code: [400..599] 110 111 ## Instrumentation 112 113 Plug.Cowboy uses the `:telemetry` library for instrumentation. The following 114 span events are published during each request: 115 116 * `[:cowboy, :request, :start]` - dispatched at the beginning of the request 117 * `[:cowboy, :request, :stop]` - dispatched at the end of the request 118 * `[:cowboy, :request, :exception]` - dispatched at the end of a request that exits 119 120 A single event is published when the request ends with an early error: 121 * `[:cowboy, :request, :early_error]` - dispatched for requests terminated early by Cowboy 122 123 See [`cowboy_telemetry`](https://github.com/beam-telemetry/cowboy_telemetry#telemetry-events) 124 for more details on the events. 125 126 To opt-out of this default instrumentation, you can manually configure 127 cowboy with the option `stream_handlers: [:cowboy_stream_h]`. 128 129 ## WebSocket support 130 131 Plug.Cowboy supports upgrading HTTP requests to WebSocket connections via 132 the use of the `Plug.Conn.upgrade_adapter/3` function, called with `:websocket` as the second 133 argument. Applications should validate that the connection represents a valid WebSocket request 134 before calling this function (Cowboy will validate the connection as part of the upgrade 135 process, but does not provide any capacity for an application to be notified if the upgrade is 136 not successful). If an application wishes to negotiate WebSocket subprotocols or otherwise set 137 any response headers, it should do so before calling `Plug.Conn.upgrade_adapter/3`. 138 139 The third argument to `Plug.Conn.upgrade_adapter/3` defines the details of how Plug.Cowboy 140 should handle the WebSocket connection, and must take the form `{handler, handler_opts, 141 connection_opts}`, where values are as follows: 142 143 * `handler` is a module which implements the 144 [`:cowboy_websocket`](https://ninenines.eu/docs/en/cowboy/2.6/manual/cowboy_websocket/) 145 behaviour. Note that this module will NOT have its `c:cowboy_websocket.init/2` callback 146 called; only the 'later' parts of the `:cowboy_websocket` lifecycle are supported 147 * `handler_opts` is an arbitrary term which will be passed as the argument to 148 `c:cowboy_websocket.websocket_init/1` 149 * `connection_opts` is a map with any of [Cowboy's websockets options](https://ninenines.eu/docs/en/cowboy/2.6/manual/cowboy_websocket/#_opts) 150 151 """ 152 153 require Logger 154 155 @doc false 156 def start(_type, _args) do 157 Logger.add_translator({Plug.Cowboy.Translator, :translate}) 158 Supervisor.start_link([], strategy: :one_for_one) 159 end 160 161 # Made public with @doc false for testing. 162 @doc false 163 def args(scheme, plug, plug_opts, cowboy_options) do 164 {cowboy_options, non_keyword_options} = Enum.split_with(cowboy_options, &match?({_, _}, &1)) 165 166 cowboy_options 167 |> normalize_cowboy_options(scheme) 168 |> to_args(scheme, plug, plug_opts, non_keyword_options) 169 end 170 171 @doc """ 172 Runs cowboy under http. 173 174 ## Example 175 176 # Starts a new interface 177 Plug.Cowboy.http MyPlug, [], port: 80 178 179 # The interface above can be shutdown with 180 Plug.Cowboy.shutdown MyPlug.HTTP 181 182 """ 183 @spec http(module(), Keyword.t(), Keyword.t()) :: 184 {:ok, pid} | {:error, :eaddrinuse} | {:error, term} 185 def http(plug, opts, cowboy_options \\ []) do 186 run(:http, plug, opts, cowboy_options) 187 end 188 189 @doc """ 190 Runs cowboy under https. 191 192 Besides the options described in the module documentation, 193 this function sets defaults and accepts all options defined 194 in `Plug.SSL.configure/1`. 195 196 ## Example 197 198 # Starts a new interface 199 Plug.Cowboy.https MyPlug, [], 200 port: 443, 201 password: "SECRET", 202 otp_app: :my_app, 203 keyfile: "priv/ssl/key.pem", 204 certfile: "priv/ssl/cert.pem", 205 dhfile: "priv/ssl/dhparam.pem" 206 207 # The interface above can be shutdown with 208 Plug.Cowboy.shutdown MyPlug.HTTPS 209 210 """ 211 @spec https(module(), Keyword.t(), Keyword.t()) :: 212 {:ok, pid} | {:error, :eaddrinuse} | {:error, term} 213 def https(plug, opts, cowboy_options \\ []) do 214 Application.ensure_all_started(:ssl) 215 run(:https, plug, opts, cowboy_options) 216 end 217 218 @doc """ 219 Shutdowns the given reference. 220 """ 221 def shutdown(ref) do 222 :cowboy.stop_listener(ref) 223 end 224 225 @doc """ 226 A function for starting a Cowboy2 server under Elixir v1.5+ supervisors. 227 228 It supports all options as specified in the module documentation plus it 229 requires the following two options: 230 231 * `:scheme` - either `:http` or `:https` 232 * `:plug` - such as `MyPlug` or `{MyPlug, plug_opts}` 233 234 ## Examples 235 236 Assuming your Plug module is named `MyApp` you can add it to your 237 supervision tree by using this function: 238 239 children = [ 240 {Plug.Cowboy, scheme: :http, plug: MyApp, options: [port: 4040]} 241 ] 242 243 Supervisor.start_link(children, strategy: :one_for_one) 244 245 """ 246 def child_spec(opts) do 247 scheme = Keyword.fetch!(opts, :scheme) 248 249 {plug, plug_opts} = 250 case Keyword.fetch!(opts, :plug) do 251 {_, _} = tuple -> tuple 252 plug -> {plug, []} 253 end 254 255 # We support :options for backwards compatibility. 256 cowboy_opts = 257 opts 258 |> Keyword.drop([:scheme, :plug, :options]) 259 |> Kernel.++(Keyword.get(opts, :options, [])) 260 261 cowboy_args = args(scheme, plug, plug_opts, cowboy_opts) 262 [ref, transport_opts, proto_opts] = cowboy_args 263 264 {ranch_module, cowboy_protocol, transport_opts} = 265 case scheme do 266 :http -> 267 {:ranch_tcp, :cowboy_clear, transport_opts} 268 269 :https -> 270 %{socket_opts: socket_opts} = transport_opts 271 272 socket_opts = 273 socket_opts 274 |> Keyword.put_new(:next_protocols_advertised, ["h2", "http/1.1"]) 275 |> Keyword.put_new(:alpn_preferred_protocols, ["h2", "http/1.1"]) 276 277 {:ranch_ssl, :cowboy_tls, %{transport_opts | socket_opts: socket_opts}} 278 end 279 280 case :ranch.child_spec(ref, ranch_module, transport_opts, cowboy_protocol, proto_opts) do 281 {id, start, restart, shutdown, type, modules} -> 282 %{ 283 id: id, 284 start: start, 285 restart: restart, 286 shutdown: shutdown, 287 type: type, 288 modules: modules 289 } 290 291 child_spec when is_map(child_spec) -> 292 child_spec 293 end 294 end 295 296 ## Helpers 297 298 @protocol_options [:compress, :stream_handlers] 299 300 defp run(scheme, plug, opts, cowboy_options) do 301 case Application.ensure_all_started(:cowboy) do 302 {:ok, _} -> 303 nil 304 305 {:error, {:cowboy, _}} -> 306 raise "could not start the Cowboy application. Please ensure it is listed as a dependency in your mix.exs" 307 end 308 309 start = 310 case scheme do 311 :http -> :start_clear 312 :https -> :start_tls 313 other -> :erlang.error({:badarg, [other]}) 314 end 315 316 :telemetry.attach( 317 :plug_cowboy, 318 [:cowboy, :request, :early_error], 319 &__MODULE__.handle_event/4, 320 nil 321 ) 322 323 apply(:cowboy, start, args(scheme, plug, opts, cowboy_options)) 324 end 325 326 defp normalize_cowboy_options(cowboy_options, :http) do 327 Keyword.put_new(cowboy_options, :port, 4000) 328 end 329 330 defp normalize_cowboy_options(cowboy_options, :https) do 331 cowboy_options 332 |> Keyword.put_new(:port, 4040) 333 |> Plug.SSL.configure() 334 |> case do 335 {:ok, options} -> options 336 {:error, message} -> fail(message) 337 end 338 end 339 340 defp to_args(opts, scheme, plug, plug_opts, non_keyword_opts) do 341 {timeout, opts} = Keyword.pop(opts, :timeout) 342 343 if timeout do 344 Logger.warn("the :timeout option for Cowboy webserver has no effect and must be removed") 345 end 346 347 opts = Keyword.delete(opts, :otp_app) 348 {ref, opts} = Keyword.pop(opts, :ref) 349 {dispatch, opts} = Keyword.pop(opts, :dispatch) 350 {protocol_options, opts} = Keyword.pop(opts, :protocol_options, []) 351 352 dispatch = :cowboy_router.compile(dispatch || dispatch_for(plug, plug_opts)) 353 {extra_options, opts} = Keyword.split(opts, @protocol_options) 354 355 extra_options = set_stream_handlers(extra_options) 356 protocol_and_extra_options = :maps.from_list(protocol_options ++ extra_options) 357 protocol_options = Map.merge(%{env: %{dispatch: dispatch}}, protocol_and_extra_options) 358 {transport_options, socket_options} = Keyword.pop(opts, :transport_options, []) 359 360 {net, socket_options} = Keyword.pop(socket_options, :net) 361 socket_options = List.wrap(net) ++ non_keyword_opts ++ socket_options 362 363 transport_options = 364 transport_options 365 |> Keyword.put_new(:num_acceptors, 100) 366 |> Keyword.put_new(:max_connections, 16_384) 367 |> Keyword.update( 368 :socket_opts, 369 socket_options, 370 &(&1 ++ socket_options) 371 ) 372 |> Map.new() 373 374 [ref || build_ref(plug, scheme), transport_options, protocol_options] 375 end 376 377 @default_stream_handlers [:cowboy_telemetry_h, :cowboy_stream_h] 378 379 defp set_stream_handlers(opts) do 380 compress = Keyword.get(opts, :compress) 381 stream_handlers = Keyword.get(opts, :stream_handlers) 382 383 case {compress, stream_handlers} do 384 {true, nil} -> 385 Keyword.put_new(opts, :stream_handlers, [:cowboy_compress_h | @default_stream_handlers]) 386 387 {true, _} -> 388 raise "cannot set both compress and stream_handlers at once. " <> 389 "If you wish to set compress, please add `:cowboy_compress_h` to your stream handlers." 390 391 {_, nil} -> 392 Keyword.put_new(opts, :stream_handlers, @default_stream_handlers) 393 394 {_, _} -> 395 opts 396 end 397 end 398 399 defp build_ref(plug, scheme) do 400 Module.concat(plug, scheme |> to_string |> String.upcase()) 401 end 402 403 defp dispatch_for(plug, opts) do 404 opts = plug.init(opts) 405 [{:_, [{:_, Plug.Cowboy.Handler, {plug, opts}}]}] 406 end 407 408 defp fail(message) do 409 raise ArgumentError, "could not start Cowboy2 adapter, " <> message 410 end 411 412 @doc false 413 def handle_event( 414 [:cowboy, :request, :early_error], 415 _, 416 %{reason: {:connection_error, :limit_reached, specific_reason}, partial_req: partial_req}, 417 _ 418 ) do 419 Logger.error(""" 420 Cowboy returned 431 because it was unable to parse the request headers. 421 422 This may happen because there are no headers, or there are too many headers 423 or the header name or value are too large (such as a large cookie). 424 425 More specific reason is: 426 427 #{inspect(specific_reason)} 428 429 You can customize those limits when configuring your http/https 430 server. The configuration option and default values are shown below: 431 432 protocol_options: [ 433 max_header_name_length: 64, 434 max_header_value_length: 4096, 435 max_headers: 100 436 ] 437 438 Request info: 439 440 peer: #{format_peer(partial_req.peer)} 441 method: #{partial_req.method || "<unable to parse>"} 442 path: #{partial_req.path || "<unable to parse>"} 443 """) 444 end 445 446 def handle_event(_, _, _, _) do 447 :ok 448 end 449 450 defp format_peer({addr, port}) do 451 "#{:inet_parse.ntoa(addr)}:#{port}" 452 end 453 end