├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib └── plug │ ├── cowboy.ex │ └── cowboy │ ├── conn.ex │ ├── drainer.ex │ ├── handler.ex │ └── translator.ex ├── mix.exs ├── mix.lock └── test ├── plug ├── cowboy │ ├── conn_test.exs │ ├── drainer_test.exs │ ├── translator_test.exs │ └── websocket_handler_test.exs └── cowboy_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-20.04 12 | env: 13 | MIX_ENV: test 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - pair: 19 | elixir: 1.11.4 20 | otp: 23.2.7 21 | - pair: 22 | elixir: 1.13.3 23 | otp: 24.2.2 24 | lint: lint 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - uses: erlef/setup-beam@v1 29 | with: 30 | otp-version: ${{matrix.pair.otp}} 31 | elixir-version: ${{matrix.pair.elixir}} 32 | 33 | - name: Install Dependencies 34 | run: mix deps.get --only test 35 | 36 | - run: mix format --check-formatted 37 | if: ${{ matrix.lint }} 38 | 39 | - run: mix deps.get && mix deps.unlock --check-unused 40 | if: ${{ matrix.lint }} 41 | 42 | - run: mix deps.compile 43 | 44 | - run: mix compile --warnings-as-errors 45 | if: ${{ matrix.lint }} 46 | 47 | - run: mix test 48 | test_cowboy_latest: 49 | runs-on: ubuntu-20.04 50 | env: 51 | MIX_ENV: test 52 | strategy: 53 | fail-fast: false 54 | steps: 55 | - uses: actions/checkout@v2 56 | 57 | - uses: erlef/setup-beam@v1 58 | with: 59 | otp-version: 24.2.2 60 | elixir-version: 1.13.3 61 | 62 | - run: mix deps.unlock cowboy cowlib ranch && mix deps.get --only test 63 | 64 | - run: mix deps.compile 65 | 66 | - run: mix test 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /docs 4 | /doc 5 | erl_crash.dump 6 | *.ez 7 | /test/fixtures/ssl/ 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v2.7.3 4 | 5 | ### Enhancements 6 | 7 | * Ensure errors from Cowboy 2.13 are correctly translated 8 | 9 | ## v2.7.2 10 | 11 | ### Bug fixes 12 | 13 | * Ensure `crash_reason` in metadata is always a tuple 14 | 15 | ## v2.7.1 16 | 17 | ### Enhancements 18 | 19 | * Support Cowboy 2.11 20 | 21 | ## v2.7.0 22 | 23 | ### Enhancements 24 | 25 | * Do not allow Cowboy 2.11 due to backwards incompatible changes 26 | 27 | ## v2.6.2 28 | 29 | ### Enhancements 30 | 31 | * Fix warnings on Elixir v1.15+ 32 | 33 | ## v2.6.1 34 | 35 | ### Enhancements 36 | 37 | * Allow for opt-out of conn metadata on exception logs 38 | * Support `:check_interval` in drainer (in addition to `:drain_check_interval`) 39 | 40 | ## v2.6.0 41 | 42 | ### Enhancements 43 | 44 | * Support websocket upgrades 45 | * Require Plug v1.14+ and Elixir v1.10+ 46 | 47 | ## v2.5.2 48 | 49 | ### Enhancements 50 | 51 | * Fix warnings when running on telemetry 1.x 52 | 53 | ## v2.5.1 54 | 55 | ### Enhancements 56 | 57 | * Allow to configure which errors should be logged 58 | * Support telemetry 0.4.x or 1.x 59 | 60 | ## v2.5.0 61 | 62 | ### Enhancements 63 | 64 | * Return `:conn` as Logger metadata on translator 65 | * Support Ranch 2.0 66 | * Support the `:net` option so developers can work with keyword lists 67 | * Remove previously deprecated options 68 | 69 | ## v2.4.1 (2020-10-31) 70 | 71 | ### Bug fixes 72 | 73 | * Properly format linked exits 74 | 75 | ## v2.4.0 (2020-10-11) 76 | 77 | ### Bug fixes 78 | 79 | * Add [cowboy_telemetry](https://github.com/beam-telemetry/cowboy_telemetry/) as a dependency and enable it by default 80 | 81 | ## v2.3.0 (2020-06-11) 82 | 83 | Plug.Cowboy requires Elixir v1.7 or later. 84 | 85 | ### Bug fixes 86 | 87 | * The telemetry events added in version v2.2.0 does not work as expected. The whole v2.2.x branch has been retired in favor of v2.3.0. 88 | 89 | ## v2.2.2 (2020-05-25) 90 | 91 | ### Enhancements 92 | 93 | * Emit telemetry event for Cowboy early errors 94 | * Improve error messages for Cowboy early errors 95 | 96 | ## v2.2.1 (2020-04-21) 97 | 98 | ### Enhancements 99 | 100 | * Use proper telemetry metadata for exceptions 101 | 102 | ## v2.2.0 (2020-04-21) 103 | 104 | ### Enhancements 105 | 106 | * Include telemetry support 107 | 108 | ## v2.1.3 (2020-04-14) 109 | 110 | ### Bug fixes 111 | 112 | * Properly support the :options option before removal 113 | 114 | ## v2.1.2 (2020-01-28) 115 | 116 | ### Bug fixes 117 | 118 | * Properly deprecate the :timeout option before removal 119 | 120 | ## v2.1.1 (2020-01-08) 121 | 122 | ### Enhancement 123 | 124 | * Improve docs and simplify child spec API 125 | 126 | ## v2.1.0 (2019-06-27) 127 | 128 | ### Enhancement 129 | 130 | * Add `Plug.Cowboy.Drainer` for connection draining 131 | 132 | ## v2.0.2 (2019-03-18) 133 | 134 | ### Enhancements 135 | 136 | * Unwrap `Plug.Conn.WrapperError` on handler error 137 | * Include `crash_reason` as logger metadata 138 | 139 | ## v2.0.1 (2018-12-13) 140 | 141 | ### Bug fixes 142 | 143 | * Respect `:read_length` and `:read_timeout` in `read_body` with Cowboy 2 144 | 145 | ## v2.0.0 (2018-10-20) 146 | 147 | Extract `Plug.Adapters.Cowboy2` from Plug into `Plug.Cowboy` 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Plataformatec. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plug.Cowboy 2 | 3 | [![Hex.pm Version](https://img.shields.io/hexpm/v/plug_cowboy.svg)](https://hex.pm/packages/plug_cowboy) 4 | [![Build Status](https://github.com/elixir-plug/plug_cowboy/workflows/CI/badge.svg)](https://github.com/elixir-plug/plug_cowboy/actions?query=workflow%3ACI) 5 | 6 | A Plug Adapter for the Erlang [Cowboy](https://github.com/ninenines/cowboy 7 | ) web server. 8 | 9 | ## Installation 10 | 11 | You can use `plug_cowboy` in your project by adding the dependency: 12 | 13 | ```elixir 14 | def deps do 15 | [ 16 | {:plug_cowboy, "~> 2.0"}, 17 | ] 18 | end 19 | ``` 20 | 21 | You can then start the adapter with: 22 | 23 | ```elixir 24 | Plug.Cowboy.http MyPlug, [] 25 | ``` 26 | 27 | ## Supervised handlers 28 | 29 | The `Plug.Cowboy` module can be started as part of a supervision tree like so: 30 | 31 | ```elixir 32 | defmodule MyApp do 33 | # See https://hexdocs.pm/elixir/Application.html 34 | # for more information on OTP Applications 35 | @moduledoc false 36 | 37 | use Application 38 | 39 | def start(_type, _args) do 40 | # List all child processes to be supervised 41 | children = [ 42 | {Plug.Cowboy, scheme: :http, plug: MyApp, port: 4040} 43 | ] 44 | 45 | # See https://hexdocs.pm/elixir/Supervisor.html 46 | # for other strategies and supported options 47 | opts = [strategy: :one_for_one, name: MyApp.Supervisor] 48 | Supervisor.start_link(children, opts) 49 | end 50 | end 51 | ``` 52 | 53 | ## Contributing 54 | 55 | We welcome everyone to contribute to Plug.Cowboy and help us tackle existing issues! 56 | 57 | - Use the [issue tracker](https://github.com/elixir-plug/plug_cowboy/issues) for bug reports or feature requests. 58 | - Open a [pull request](https://github.com/elixir-plug/plug_cowboy/pulls) when you are ready to contribute. 59 | - Do not update the `CHANGELOG.md` when submitting a pull request. 60 | 61 | ## License 62 | 63 | Plug.Cowboy source code is released under Apache License 2.0. 64 | Check the [LICENSE](./LICENSE) file for more information. 65 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if Mix.env() == :test do 4 | config :plug, :statuses, %{ 5 | 418 => "Totally not a teapot", 6 | 998 => "Not An RFC Status Code" 7 | } 8 | end 9 | -------------------------------------------------------------------------------- /lib/plug/cowboy.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Cowboy do 2 | @moduledoc """ 3 | Adapter interface to the [Cowboy webserver](https://github.com/ninenines/cowboy). 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. Must be one of: 10 | 11 | * a tuple in the format `{a, b, c, d}` with each value in `0..255` for IPv4, 12 | * a tuple in the format `{a, b, c, d, e, f, g, h}` with each value in `0..65_535` for IPv6, 13 | * or a tuple in the format `{:local, path}` for a Unix socket at the given `path`. 14 | 15 | If you set an IPv6, the `:net` option will be automatically set to `:inet6`. 16 | If both `:net` and `:ip` options are given, make sure they are compatible 17 | (that is, give a IPv4 for `:inet` and IPv6 for `:inet6`). 18 | Also, see the [*Loopback vs Public IP Addresses* 19 | section](#module-loopback-vs-public-ip-addresses). 20 | 21 | * `:port` - the port to run the server. 22 | Defaults to `4000` (HTTP) and `4040` (HTTPS). 23 | Must be `0` when `:ip` is a `{:local, path}` tuple. 24 | 25 | * `:dispatch` - manually configure Cowboy's dispatch. 26 | If this option is used, the given plug won't be initialized 27 | nor dispatched to (and doing so becomes the user's responsibility). 28 | 29 | * `:ref` - the reference name to be used. 30 | Defaults to `plug.HTTP` (HTTP) and `plug.HTTPS` (HTTPS). 31 | The default reference name does not contain the port, so in order 32 | to serve the same plug on multiple ports you need to set the `:ref` accordingly. 33 | For example, `ref: MyPlug_HTTP_4000`, `ref: MyPlug_HTTP_4001`, and so on. 34 | This is the value that needs to be given on shutdown. 35 | 36 | * `:compress` - if `true`, Cowboy will attempt to compress the response body. 37 | Defaults to `false`. 38 | 39 | * `:stream_handlers` - List of Cowboy `stream_handlers`, 40 | see [Cowboy docs](https://ninenines.eu/docs/en/cowboy/2.12/manual/cowboy_http/). 41 | 42 | * `:protocol_options` - Specifies remaining protocol options, 43 | see the [Cowboy docs](https://ninenines.eu/docs/en/cowboy/2.12/manual/cowboy_http/). 44 | 45 | * `:transport_options` - A keyword list specifying transport options, 46 | see [Ranch docs](https://ninenines.eu/docs/en/ranch/1.7/manual/ranch/). 47 | By default `:num_acceptors` will be set to `100` and `:max_connections` 48 | to `16_384`. 49 | 50 | All other options given at the top level must configure the underlying 51 | socket. For HTTP connections, those options are listed under 52 | [`ranch_tcp`](https://ninenines.eu/docs/en/ranch/1.7/manual/ranch_tcp/). 53 | For example, you can set `:ipv6_v6only` to true if you want to bind only 54 | on IPv6 addresses. 55 | 56 | For HTTPS (SSL) connections, those options are described in 57 | [`ranch_ssl`](https://ninenines.eu/docs/en/ranch/1.7/manual/ranch_ssl/). 58 | See `https/3` for an example and read `Plug.SSL.configure/1` to 59 | understand about our SSL defaults. 60 | 61 | When using a Unix socket, OTP 21+ is required for `Plug.Static` and 62 | `Plug.Conn.send_file/3` to behave correctly. 63 | 64 | ## Safety Limits 65 | 66 | Cowboy sets different limits on URL size, header length, number of 67 | headers, and so on to protect your application from attacks. For example, 68 | the request line length defaults to 10k, which means Cowboy will return 69 | `414` if a larger URL is given. You can change this under `:protocol_options`: 70 | 71 | protocol_options: [max_request_line_length: 50_000] 72 | 73 | Keep in mind that increasing those limits can pose a security risk. 74 | Other times, browsers and proxies along the way may have equally strict 75 | limits, which means the request will still fail or the URL will be 76 | pruned. You can [consult all limits here](https://ninenines.eu/docs/en/cowboy/2.12/manual/cowboy_http/). 77 | 78 | ## Loopback vs Public IP Addresses 79 | 80 | Should your application bind to a loopback address, such as `::1` (IPv6) or 81 | `127.0.0.1` (IPv4), or a public one, such as `::0` (IPv6) or `0.0.0.0` 82 | (IPv4)? It depends on how (and whether) you want it to be reachable from 83 | other machines. 84 | 85 | Loopback addresses are only reachable from the same host (`localhost` is 86 | usually configured to resolve to a loopback address). You may wish to use one if: 87 | 88 | * Your app is running in a development environment (such as your laptop) and 89 | you don't want others on the same network to access it. 90 | * Your app is running in production, but behind a reverse proxy. For 91 | example, you might have [nginx](https://nginx.org/en/) bound to a public 92 | address and serving HTTPS, but forwarding the traffic to your application 93 | running on the same host. In that case, having your app bind to the 94 | loopback address means that nginx can reach it, but outside traffic can 95 | only reach it via nginx. 96 | 97 | Public addresses are reachable from other hosts. You may wish to use one if: 98 | 99 | * Your app is running in a container. In this case, its loopback address is 100 | reachable only from within the container; to be accessible from outside the 101 | container, it needs to bind to a public IP address. 102 | * Your app is running in production without a reverse proxy, using Cowboy's 103 | SSL support. 104 | 105 | ## Logging 106 | 107 | You can configure which exceptions are logged via `:log_exceptions_with_status_code` 108 | application environment variable. If the status code returned by `Plug.Exception.status/1` 109 | for the exception falls into any of the configured ranges, the exception is logged. 110 | By default it's set to `[500..599]`. 111 | 112 | config :plug_cowboy, 113 | log_exceptions_with_status_code: [400..599] 114 | 115 | By default, `Plug.Cowboy` includes the entire `conn` to the log metadata for exceptions. 116 | However, this metadata may contain sensitive information such as security headers or 117 | cookies, which may be logged in plain text by certain logging backends. To prevent this, 118 | you can configure the `:conn_in_exception_metadata` option to not include the `conn` in the metadata. 119 | 120 | config :plug_cowboy, 121 | conn_in_exception_metadata: false 122 | 123 | ## Instrumentation 124 | 125 | `Plug.Cowboy` uses the [`telemetry` library](https://github.com/beam-telemetry/telemetry) 126 | for instrumentation. The following span events are published during each request: 127 | 128 | * `[:cowboy, :request, :start]` - dispatched at the beginning of the request 129 | * `[:cowboy, :request, :stop]` - dispatched at the end of the request 130 | * `[:cowboy, :request, :exception]` - dispatched at the end of a request that exits 131 | 132 | A single event is published when the request ends with an early error: 133 | * `[:cowboy, :request, :early_error]` - dispatched for requests terminated early by Cowboy 134 | 135 | See [`cowboy_telemetry`](https://github.com/beam-telemetry/cowboy_telemetry#telemetry-events) 136 | for more details on the events and their measurements and metadata. 137 | 138 | To opt-out of this default instrumentation, you can manually configure 139 | Cowboy with the option: 140 | 141 | stream_handlers: [:cowboy_stream_h] 142 | 143 | ## WebSocket support 144 | 145 | `Plug.Cowboy` supports upgrading HTTP requests to WebSocket connections via 146 | the use of the `Plug.Conn.upgrade_adapter/3` function, called with `:websocket` as the second 147 | argument. Applications should validate that the connection represents a valid WebSocket request 148 | before calling this function (Cowboy will validate the connection as part of the upgrade 149 | process, but does not provide any capacity for an application to be notified if the upgrade is 150 | not successful). If an application wishes to negotiate WebSocket subprotocols or otherwise set 151 | any response headers, it should do so before calling `Plug.Conn.upgrade_adapter/3`. 152 | 153 | The third argument to `Plug.Conn.upgrade_adapter/3` defines the details of how Plug.Cowboy 154 | should handle the WebSocket connection, and must take the form `{handler, handler_opts, 155 | connection_opts}`, where values are as follows: 156 | 157 | * `handler` is a module which implements the 158 | [`:cowboy_websocket`](https://ninenines.eu/docs/en/cowboy/2.6/manual/cowboy_websocket/) 159 | behaviour. Note that this module will NOT have its `c:cowboy_websocket.init/2` callback 160 | called; only the 'later' parts of the `:cowboy_websocket` lifecycle are supported 161 | * `handler_opts` is an arbitrary term which will be passed as the argument to 162 | `c:cowboy_websocket.websocket_init/1` 163 | * `connection_opts` is a map with any of [Cowboy's websockets options](https://ninenines.eu/docs/en/cowboy/2.6/manual/cowboy_websocket/#_opts) 164 | 165 | """ 166 | 167 | require Logger 168 | 169 | @doc false 170 | def start(_type, _args) do 171 | Logger.add_translator({Plug.Cowboy.Translator, :translate}) 172 | Supervisor.start_link([], strategy: :one_for_one) 173 | end 174 | 175 | # Made public with @doc false for testing. 176 | @doc false 177 | def args(scheme, plug, plug_opts, cowboy_options) do 178 | {cowboy_options, non_keyword_options} = Enum.split_with(cowboy_options, &match?({_, _}, &1)) 179 | 180 | cowboy_options 181 | |> normalize_cowboy_options(scheme) 182 | |> to_args(scheme, plug, plug_opts, non_keyword_options) 183 | end 184 | 185 | @doc """ 186 | Runs cowboy under HTTP. 187 | 188 | ## Example 189 | 190 | # Starts a new interface: 191 | Plug.Cowboy.http(MyPlug, [], port: 80) 192 | 193 | # The interface above can be shut down with: 194 | Plug.Cowboy.shutdown(MyPlug.HTTP) 195 | 196 | """ 197 | @spec http(module(), Keyword.t(), Keyword.t()) :: 198 | {:ok, pid} | {:error, :eaddrinuse} | {:error, term} 199 | def http(plug, opts, cowboy_options \\ []) do 200 | run(:http, plug, opts, cowboy_options) 201 | end 202 | 203 | @doc """ 204 | Runs cowboy under HTTPS. 205 | 206 | Besides the options described in the module documentation, 207 | this function sets defaults and accepts all options defined 208 | in `Plug.SSL.configure/1`. 209 | 210 | ## Example 211 | 212 | # Starts a new interface: 213 | Plug.Cowboy.https( 214 | MyPlug, 215 | [], 216 | port: 443, 217 | password: "SECRET", 218 | otp_app: :my_app, 219 | keyfile: "priv/ssl/key.pem", 220 | certfile: "priv/ssl/cert.pem", 221 | dhfile: "priv/ssl/dhparam.pem" 222 | ) 223 | 224 | # The interface above can be shut down with: 225 | Plug.Cowboy.shutdown(MyPlug.HTTPS) 226 | 227 | """ 228 | @spec https(module(), Keyword.t(), Keyword.t()) :: 229 | {:ok, pid} | {:error, :eaddrinuse} | {:error, term} 230 | def https(plug, opts, cowboy_options \\ []) do 231 | Application.ensure_all_started(:ssl) 232 | run(:https, plug, opts, cowboy_options) 233 | end 234 | 235 | @doc """ 236 | Shutdowns the given reference. 237 | """ 238 | @spec shutdown(:ranch.ref()) :: :ok | {:error, :not_found} 239 | def shutdown(ref) do 240 | :cowboy.stop_listener(ref) 241 | end 242 | 243 | @doc """ 244 | Returns a supervisor child spec to start Cowboy under a supervisor. 245 | 246 | It supports all options as specified in the module documentation plus it 247 | requires the following two options: 248 | 249 | * `:scheme` - either `:http` or `:https` 250 | * `:plug` - such as `MyPlug` or `{MyPlug, plug_opts}` 251 | 252 | ## Examples 253 | 254 | Assuming your Plug module is named `MyApp` you can add it to your 255 | supervision tree by using this function: 256 | 257 | children = [ 258 | {Plug.Cowboy, scheme: :http, plug: MyApp, options: [port: 4040]} 259 | ] 260 | 261 | Supervisor.start_link(children, strategy: :one_for_one) 262 | 263 | """ 264 | @spec child_spec(keyword()) :: Supervisor.child_spec() 265 | def child_spec(opts) do 266 | scheme = Keyword.fetch!(opts, :scheme) 267 | 268 | {plug, plug_opts} = 269 | case Keyword.fetch!(opts, :plug) do 270 | {_, _} = tuple -> tuple 271 | plug -> {plug, []} 272 | end 273 | 274 | # We support :options for backwards compatibility. 275 | cowboy_opts = 276 | opts 277 | |> Keyword.drop([:scheme, :plug, :options]) 278 | |> Kernel.++(Keyword.get(opts, :options, [])) 279 | 280 | cowboy_args = args(scheme, plug, plug_opts, cowboy_opts) 281 | [ref, transport_opts, proto_opts] = cowboy_args 282 | 283 | {ranch_module, cowboy_protocol, transport_opts} = 284 | case scheme do 285 | :http -> 286 | {:ranch_tcp, :cowboy_clear, transport_opts} 287 | 288 | :https -> 289 | %{socket_opts: socket_opts} = transport_opts 290 | 291 | socket_opts = 292 | socket_opts 293 | |> Keyword.put_new(:next_protocols_advertised, ["h2", "http/1.1"]) 294 | |> Keyword.put_new(:alpn_preferred_protocols, ["h2", "http/1.1"]) 295 | 296 | {:ranch_ssl, :cowboy_tls, %{transport_opts | socket_opts: socket_opts}} 297 | end 298 | 299 | case :ranch.child_spec(ref, ranch_module, transport_opts, cowboy_protocol, proto_opts) do 300 | {id, start, restart, shutdown, type, modules} -> 301 | %{ 302 | id: id, 303 | start: start, 304 | restart: restart, 305 | shutdown: shutdown, 306 | type: type, 307 | modules: modules 308 | } 309 | 310 | child_spec when is_map(child_spec) -> 311 | child_spec 312 | end 313 | end 314 | 315 | ## Helpers 316 | 317 | @protocol_options [:compress, :stream_handlers] 318 | 319 | defp run(scheme, plug, opts, cowboy_options) do 320 | case Application.ensure_all_started(:cowboy) do 321 | {:ok, _} -> 322 | nil 323 | 324 | {:error, {:cowboy, _}} -> 325 | raise "could not start the Cowboy application. Please ensure it is listed as a dependency in your mix.exs" 326 | end 327 | 328 | start = 329 | case scheme do 330 | :http -> :start_clear 331 | :https -> :start_tls 332 | other -> :erlang.error({:badarg, [other]}) 333 | end 334 | 335 | :telemetry.attach( 336 | :plug_cowboy, 337 | [:cowboy, :request, :early_error], 338 | &__MODULE__.handle_event/4, 339 | nil 340 | ) 341 | 342 | apply(:cowboy, start, args(scheme, plug, opts, cowboy_options)) 343 | end 344 | 345 | defp normalize_cowboy_options(cowboy_options, :http) do 346 | Keyword.put_new(cowboy_options, :port, 4000) 347 | end 348 | 349 | defp normalize_cowboy_options(cowboy_options, :https) do 350 | cowboy_options 351 | |> Keyword.put_new(:port, 4040) 352 | |> Plug.SSL.configure() 353 | |> case do 354 | {:ok, options} -> options 355 | {:error, message} -> fail(message) 356 | end 357 | end 358 | 359 | defp to_args(opts, scheme, plug, plug_opts, non_keyword_opts) do 360 | {timeout, opts} = Keyword.pop(opts, :timeout) 361 | 362 | if timeout do 363 | Logger.warning("the :timeout option for Cowboy webserver has no effect and must be removed") 364 | end 365 | 366 | opts = Keyword.delete(opts, :otp_app) 367 | {ref, opts} = Keyword.pop(opts, :ref) 368 | {dispatch, opts} = Keyword.pop(opts, :dispatch) 369 | {protocol_options, opts} = Keyword.pop(opts, :protocol_options, []) 370 | 371 | dispatch = :cowboy_router.compile(dispatch || dispatch_for(plug, plug_opts)) 372 | {extra_options, opts} = Keyword.split(opts, @protocol_options) 373 | 374 | extra_options = set_stream_handlers(extra_options) 375 | protocol_and_extra_options = :maps.from_list(protocol_options ++ extra_options) 376 | protocol_options = Map.merge(%{env: %{dispatch: dispatch}}, protocol_and_extra_options) 377 | {transport_options, socket_options} = Keyword.pop(opts, :transport_options, []) 378 | 379 | {net, socket_options} = Keyword.pop(socket_options, :net) 380 | socket_options = List.wrap(net) ++ non_keyword_opts ++ socket_options 381 | 382 | transport_options = 383 | transport_options 384 | |> Keyword.put_new(:num_acceptors, 100) 385 | |> Keyword.put_new(:max_connections, 16_384) 386 | |> Keyword.update( 387 | :socket_opts, 388 | socket_options, 389 | &(&1 ++ socket_options) 390 | ) 391 | |> Map.new() 392 | 393 | [ref || build_ref(plug, scheme), transport_options, protocol_options] 394 | end 395 | 396 | @default_stream_handlers [:cowboy_telemetry_h, :cowboy_stream_h] 397 | 398 | defp set_stream_handlers(opts) do 399 | compress = Keyword.get(opts, :compress) 400 | stream_handlers = Keyword.get(opts, :stream_handlers) 401 | 402 | case {compress, stream_handlers} do 403 | {true, nil} -> 404 | Keyword.put_new(opts, :stream_handlers, [:cowboy_compress_h | @default_stream_handlers]) 405 | 406 | {true, _} -> 407 | raise "cannot set both compress and stream_handlers at once. " <> 408 | "If you wish to set compress, please add `:cowboy_compress_h` to your stream handlers." 409 | 410 | {_, nil} -> 411 | Keyword.put_new(opts, :stream_handlers, @default_stream_handlers) 412 | 413 | {_, _} -> 414 | opts 415 | end 416 | end 417 | 418 | defp build_ref(plug, scheme) do 419 | Module.concat(plug, scheme |> to_string |> String.upcase()) 420 | end 421 | 422 | defp dispatch_for(plug, opts) do 423 | opts = plug.init(opts) 424 | [{:_, [{:_, Plug.Cowboy.Handler, {plug, opts}}]}] 425 | end 426 | 427 | defp fail(message) do 428 | raise ArgumentError, "could not start Cowboy2 adapter, " <> message 429 | end 430 | 431 | @doc false 432 | def handle_event( 433 | [:cowboy, :request, :early_error], 434 | _, 435 | %{reason: {:connection_error, :limit_reached, specific_reason}, partial_req: partial_req}, 436 | _ 437 | ) do 438 | Logger.error(""" 439 | Cowboy returned 431 because it was unable to parse the request headers. 440 | 441 | This may happen because there are no headers, or there are too many headers 442 | or the header name or value are too large (such as a large cookie). 443 | 444 | More specific reason is: 445 | 446 | #{inspect(specific_reason)} 447 | 448 | You can customize those limits when configuring your http/https 449 | server. The configuration option and default values are shown below: 450 | 451 | protocol_options: [ 452 | max_header_name_length: 64, 453 | max_header_value_length: 4096, 454 | max_headers: 100 455 | ] 456 | 457 | Request info: 458 | 459 | peer: #{format_peer(partial_req.peer)} 460 | method: #{partial_req.method || ""} 461 | path: #{partial_req.path || ""} 462 | """) 463 | end 464 | 465 | def handle_event(_, _, _, _) do 466 | :ok 467 | end 468 | 469 | defp format_peer({addr, port}) do 470 | "#{:inet_parse.ntoa(addr)}:#{port}" 471 | end 472 | end 473 | -------------------------------------------------------------------------------- /lib/plug/cowboy/conn.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Cowboy.Conn do 2 | @behaviour Plug.Conn.Adapter 3 | @moduledoc false 4 | 5 | @already_sent {:plug_conn, :sent} 6 | 7 | def conn(req) do 8 | %{ 9 | path: path, 10 | host: host, 11 | port: port, 12 | method: method, 13 | headers: headers, 14 | qs: qs, 15 | peer: {remote_ip, _} 16 | } = req 17 | 18 | %Plug.Conn{ 19 | adapter: {__MODULE__, Map.put(req, :plug_pid, self())}, 20 | host: host, 21 | method: method, 22 | owner: self(), 23 | path_info: split_path(path), 24 | port: port, 25 | remote_ip: remote_ip, 26 | query_string: qs, 27 | req_headers: to_headers_list(headers), 28 | request_path: path, 29 | scheme: String.to_atom(:cowboy_req.scheme(req)) 30 | } 31 | end 32 | 33 | @impl true 34 | def send_resp(req, status, headers, body) do 35 | req = to_headers_map(req, headers) 36 | status = Integer.to_string(status) <> " " <> Plug.Conn.Status.reason_phrase(status) 37 | req = :cowboy_req.reply(status, %{}, body, req) 38 | send(req.plug_pid, @already_sent) 39 | {:ok, nil, req} 40 | end 41 | 42 | @impl true 43 | def send_file(req, status, headers, path, offset, length) do 44 | %File.Stat{type: :regular, size: size} = File.stat!(path) 45 | 46 | length = 47 | cond do 48 | length == :all -> size 49 | is_integer(length) -> length 50 | end 51 | 52 | body = {:sendfile, offset, length, path} 53 | req = to_headers_map(req, headers) 54 | req = :cowboy_req.reply(status, %{}, body, req) 55 | send(req.plug_pid, @already_sent) 56 | {:ok, nil, req} 57 | end 58 | 59 | @impl true 60 | def send_chunked(req, status, headers) do 61 | req = to_headers_map(req, headers) 62 | req = :cowboy_req.stream_reply(status, %{}, req) 63 | send(req.plug_pid, @already_sent) 64 | {:ok, nil, req} 65 | end 66 | 67 | @impl true 68 | def chunk(req, body) do 69 | :cowboy_req.stream_body(body, :nofin, req) 70 | end 71 | 72 | @impl true 73 | def read_req_body(req, opts) do 74 | length = Keyword.get(opts, :length, 8_000_000) 75 | read_length = Keyword.get(opts, :read_length, 1_000_000) 76 | read_timeout = Keyword.get(opts, :read_timeout, 15_000) 77 | 78 | opts = %{length: read_length, period: read_timeout} 79 | read_req_body(req, opts, length, []) 80 | end 81 | 82 | defp read_req_body(req, opts, length, acc) when length >= 0 do 83 | case :cowboy_req.read_body(req, opts) do 84 | {:ok, data, req} -> {:ok, IO.iodata_to_binary([acc | data]), req} 85 | {:more, data, req} -> read_req_body(req, opts, length - byte_size(data), [acc | data]) 86 | end 87 | end 88 | 89 | defp read_req_body(req, _opts, _length, acc) do 90 | {:more, IO.iodata_to_binary(acc), req} 91 | end 92 | 93 | @impl true 94 | def inform(req, status, headers) do 95 | :cowboy_req.inform(status, to_headers_map(headers), req) 96 | end 97 | 98 | @impl true 99 | def upgrade(req, :websocket, args) do 100 | case args do 101 | {handler, _state, cowboy_opts} when is_atom(handler) and is_map(cowboy_opts) -> 102 | :ok 103 | 104 | _ -> 105 | raise ArgumentError, 106 | "expected websocket upgrade on Cowboy to be on the format {handler :: atom(), arg :: term(), opts :: map()}, got: " <> 107 | inspect(args) 108 | end 109 | 110 | {:ok, Map.put(req, :upgrade, {:websocket, args})} 111 | end 112 | 113 | def upgrade(_req, _protocol, _args), do: {:error, :not_supported} 114 | 115 | @impl true 116 | def push(req, path, headers) do 117 | opts = 118 | case {req.port, req.sock} do 119 | {:undefined, {_, port}} -> %{port: port} 120 | {port, _} when port in [80, 443] -> %{} 121 | {port, _} -> %{port: port} 122 | end 123 | 124 | req = to_headers_map(req, headers) 125 | :cowboy_req.push(path, %{}, req, opts) 126 | end 127 | 128 | @impl true 129 | def get_peer_data(%{peer: {ip, port}, cert: cert}) do 130 | %{ 131 | address: ip, 132 | port: port, 133 | ssl_cert: if(cert == :undefined, do: nil, else: cert) 134 | } 135 | end 136 | 137 | @impl true 138 | def get_http_protocol(req) do 139 | :cowboy_req.version(req) 140 | end 141 | 142 | ## Helpers 143 | 144 | defp to_headers_list(headers) when is_list(headers) do 145 | headers 146 | end 147 | 148 | defp to_headers_list(headers) when is_map(headers) do 149 | :maps.to_list(headers) 150 | end 151 | 152 | defp to_headers_map(req, headers) do 153 | headers = to_headers_map(headers) 154 | Map.update(req, :resp_headers, headers, &Map.merge(&1, headers)) 155 | end 156 | 157 | defp to_headers_map(headers) when is_list(headers) do 158 | # Group set-cookie headers into a list for a single `set-cookie` 159 | # key since cowboy 2 requires headers as a map. 160 | Enum.reduce(headers, %{}, fn 161 | {key = "set-cookie", value}, acc -> 162 | case acc do 163 | %{^key => existing} -> %{acc | key => [value | existing]} 164 | %{} -> Map.put(acc, key, [value]) 165 | end 166 | 167 | {key, value}, acc -> 168 | case acc do 169 | %{^key => existing} -> %{acc | key => existing <> ", " <> value} 170 | %{} -> Map.put(acc, key, value) 171 | end 172 | end) 173 | end 174 | 175 | defp split_path(path) do 176 | segments = :binary.split(path, "/", [:global]) 177 | for segment <- segments, segment != "", do: segment 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /lib/plug/cowboy/drainer.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Cowboy.Drainer do 2 | @moduledoc """ 3 | Process to drain cowboy connections at shutdown. 4 | 5 | When starting `Plug.Cowboy` in a supervision tree, it will create a listener that receives 6 | requests and creates a connection process to handle that request. During shutdown, a 7 | `Plug.Cowboy` process will immediately exit, closing the listener and any open connections 8 | that are still being served. However, in most cases, it is desirable to allow connections 9 | to complete before shutting down. 10 | 11 | This module provides a process that during shutdown will close listeners and wait 12 | for connections to complete. It should be placed after other supervised processes that 13 | handle cowboy connections. 14 | 15 | ## Options 16 | 17 | The following options can be given to the child spec: 18 | 19 | * `:refs` - A list of refs to drain. `:all` is also supported and will drain all cowboy 20 | listeners, including those started by means other than `Plug.Cowboy`. 21 | 22 | * `:id` - The ID for the process. 23 | Defaults to `Plug.Cowboy.Drainer`. 24 | 25 | * `:shutdown` - How long to wait for connections to drain. 26 | Defaults to 5000ms. 27 | 28 | * `:check_interval` - How frequently to check if a listener's 29 | connections have been drained. Defaults to 1000ms. 30 | 31 | ## Examples 32 | 33 | # In your application 34 | def start(_type, _args) do 35 | children = [ 36 | {Plug.Cowboy, scheme: :http, plug: MyApp, options: [port: 4040]}, 37 | {Plug.Cowboy, scheme: :https, plug: MyApp, options: [port: 4041]}, 38 | {Plug.Cowboy.Drainer, refs: [MyApp.HTTP, MyApp.HTTPS]} 39 | ] 40 | 41 | opts = [strategy: :one_for_one, name: MyApp.Supervisor] 42 | Supervisor.start_link(children, opts) 43 | end 44 | """ 45 | use GenServer 46 | 47 | @doc false 48 | @spec child_spec(opts :: Keyword.t()) :: Supervisor.child_spec() 49 | def child_spec(opts) when is_list(opts) do 50 | {spec_opts, opts} = Keyword.split(opts, [:id, :shutdown]) 51 | 52 | Supervisor.child_spec( 53 | %{ 54 | id: __MODULE__, 55 | start: {__MODULE__, :start_link, [opts]}, 56 | type: :worker 57 | }, 58 | spec_opts 59 | ) 60 | end 61 | 62 | @doc false 63 | def start_link(opts) do 64 | opts 65 | |> Keyword.fetch!(:refs) 66 | |> validate_refs!() 67 | 68 | GenServer.start_link(__MODULE__, opts) 69 | end 70 | 71 | @doc false 72 | @impl true 73 | def init(opts) do 74 | Process.flag(:trap_exit, true) 75 | {:ok, opts} 76 | end 77 | 78 | @doc false 79 | @impl true 80 | def terminate(_reason, opts) do 81 | opts 82 | |> Keyword.fetch!(:refs) 83 | |> drain(opts[:check_interval] || opts[:drain_check_interval] || 1_000) 84 | end 85 | 86 | defp drain(:all, check_interval) do 87 | :ranch.info() 88 | |> Enum.map(&elem(&1, 0)) 89 | |> drain(check_interval) 90 | end 91 | 92 | defp drain(refs, check_interval) do 93 | refs 94 | |> Enum.filter(&suspend_listener/1) 95 | |> Enum.each(&wait_for_connections(&1, check_interval)) 96 | end 97 | 98 | defp suspend_listener(ref) do 99 | :ranch.suspend_listener(ref) == :ok 100 | end 101 | 102 | defp wait_for_connections(ref, check_interval) do 103 | :ranch.wait_for_connections(ref, :==, 0, check_interval) 104 | end 105 | 106 | defp validate_refs!(:all), do: :ok 107 | defp validate_refs!(refs) when is_list(refs), do: :ok 108 | 109 | defp validate_refs!(refs) do 110 | raise ArgumentError, 111 | ":refs should be :all or a list of references, got: #{inspect(refs)}" 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/plug/cowboy/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Cowboy.Handler do 2 | @moduledoc false 3 | @connection Plug.Cowboy.Conn 4 | @already_sent {:plug_conn, :sent} 5 | 6 | def init(req, {plug, opts}) do 7 | conn = @connection.conn(req) 8 | 9 | try do 10 | conn 11 | |> plug.call(opts) 12 | |> maybe_send(plug) 13 | |> case do 14 | %Plug.Conn{adapter: {@connection, %{upgrade: {:websocket, websocket_args}} = req}} = conn -> 15 | {handler, state, cowboy_opts} = websocket_args 16 | {__MODULE__, copy_resp_headers(conn, req), {handler, state}, cowboy_opts} 17 | 18 | %Plug.Conn{adapter: {@connection, req}} -> 19 | {:ok, req, {plug, opts}} 20 | end 21 | catch 22 | kind, reason -> 23 | exit_on_error(kind, reason, __STACKTRACE__, {plug, :call, [conn, opts]}) 24 | after 25 | receive do 26 | @already_sent -> :ok 27 | after 28 | 0 -> :ok 29 | end 30 | end 31 | end 32 | 33 | def upgrade(req, env, __MODULE__, {handler, state}, opts) do 34 | :cowboy_websocket.upgrade(req, env, handler.module_info(:module), state, opts) 35 | end 36 | 37 | defp copy_resp_headers(%Plug.Conn{} = conn, req) do 38 | Enum.reduce(conn.resp_headers, req, fn {key, val}, acc -> 39 | :cowboy_req.set_resp_header(key, val, acc) 40 | end) 41 | end 42 | 43 | defp exit_on_error( 44 | :error, 45 | %Plug.Conn.WrapperError{kind: kind, reason: reason, stack: stack}, 46 | _stack, 47 | call 48 | ) do 49 | exit_on_error(kind, reason, stack, call) 50 | end 51 | 52 | defp exit_on_error(:error, value, stack, call) do 53 | exception = Exception.normalize(:error, value, stack) 54 | :erlang.raise(:exit, {{exception, stack}, call}, []) 55 | end 56 | 57 | defp exit_on_error(:throw, value, stack, call) do 58 | :erlang.raise(:exit, {{{:nocatch, value}, stack}, call}, []) 59 | end 60 | 61 | defp exit_on_error(:exit, value, _stack, call) do 62 | :erlang.raise(:exit, {value, call}, []) 63 | end 64 | 65 | defp maybe_send(%Plug.Conn{state: :unset}, _plug), do: raise(Plug.Conn.NotSentError) 66 | defp maybe_send(%Plug.Conn{state: :set} = conn, _plug), do: Plug.Conn.send_resp(conn) 67 | defp maybe_send(%Plug.Conn{} = conn, _plug), do: conn 68 | 69 | defp maybe_send(other, plug) do 70 | raise "Cowboy2 adapter expected #{inspect(plug)} to return Plug.Conn but got: " <> 71 | inspect(other) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/plug/cowboy/translator.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Cowboy.Translator do 2 | @moduledoc false 3 | 4 | # Cowboy 2.12.0 and below error format 5 | @doc """ 6 | The `translate/4` function expected by custom Logger translators. 7 | """ 8 | def translate( 9 | min_level, 10 | :error, 11 | :format, 12 | {~c"Ranch listener" ++ _, [ref, conn_pid, stream_id, stream_pid, reason, stack]} 13 | ) do 14 | extra = [" (connection ", inspect(conn_pid), ", stream id ", inspect(stream_id), ?)] 15 | translate_ranch(min_level, ref, extra, stream_pid, reason, stack) 16 | end 17 | 18 | # Cowboy 2.13.0 error format 19 | def translate( 20 | min_level, 21 | :error, 22 | :format, 23 | {~c"Ranch listener" ++ _, [ref, conn_pid, stream_id, stream_pid, {reason, stack}]} 24 | ) do 25 | extra = [" (connection ", inspect(conn_pid), ", stream id ", inspect(stream_id), ?)] 26 | translate_ranch(min_level, ref, extra, stream_pid, reason, stack) 27 | end 28 | 29 | def translate(_min_level, _level, _kind, _data) do 30 | :none 31 | end 32 | 33 | ## Ranch/Cowboy 34 | 35 | defp translate_ranch( 36 | min_level, 37 | _ref, 38 | extra, 39 | pid, 40 | {reason, {mod, :call, [%Plug.Conn{} = conn, _opts]}}, 41 | _stack 42 | ) do 43 | if log_exception?(reason) do 44 | message = [ 45 | inspect(pid), 46 | " running ", 47 | inspect(mod), 48 | extra, 49 | " terminated\n", 50 | conn_info(min_level, conn) 51 | | Exception.format(:exit, reason, []) 52 | ] 53 | 54 | metadata = 55 | [ 56 | crash_reason: reason, 57 | domain: [:cowboy] 58 | ] ++ maybe_conn_metadata(conn) 59 | 60 | {:ok, message, metadata} 61 | else 62 | :skip 63 | end 64 | end 65 | 66 | defp translate_ranch(_min_level, ref, extra, pid, reason, stack) do 67 | {:ok, 68 | [ 69 | "Ranch protocol ", 70 | inspect(pid), 71 | " of listener ", 72 | inspect(ref), 73 | extra, 74 | " terminated\n" 75 | | Exception.format_exit({reason, stack}) 76 | ], crash_reason: {reason, stack}, domain: [:cowboy]} 77 | end 78 | 79 | defp log_exception?({%{__exception__: true} = exception, _}) do 80 | status_ranges = 81 | Application.get_env(:plug_cowboy, :log_exceptions_with_status_code, [500..599]) 82 | 83 | status = Plug.Exception.status(exception) 84 | 85 | Enum.any?(status_ranges, &(status in &1)) 86 | end 87 | 88 | defp log_exception?(_), do: true 89 | 90 | defp conn_info(_min_level, conn) do 91 | [server_info(conn), request_info(conn)] 92 | end 93 | 94 | defp server_info(%Plug.Conn{host: host, port: :undefined, scheme: scheme}) do 95 | ["Server: ", host, ?\s, ?(, Atom.to_string(scheme), ?), ?\n] 96 | end 97 | 98 | defp server_info(%Plug.Conn{host: host, port: port, scheme: scheme}) do 99 | ["Server: ", host, ":", Integer.to_string(port), ?\s, ?(, Atom.to_string(scheme), ?), ?\n] 100 | end 101 | 102 | defp request_info(%Plug.Conn{method: method, query_string: query_string} = conn) do 103 | ["Request: ", method, ?\s, path_to_iodata(conn.request_path, query_string), ?\n] 104 | end 105 | 106 | defp maybe_conn_metadata(conn) do 107 | if Application.get_env(:plug_cowboy, :conn_in_exception_metadata, true) do 108 | [conn: conn] 109 | else 110 | [] 111 | end 112 | end 113 | 114 | defp path_to_iodata(path, ""), do: path 115 | defp path_to_iodata(path, qs), do: [path, ??, qs] 116 | end 117 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Cowboy.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/elixir-plug/plug_cowboy" 5 | @version "2.7.3" 6 | @description "A Plug adapter for Cowboy" 7 | 8 | def project do 9 | [ 10 | app: :plug_cowboy, 11 | version: @version, 12 | elixir: "~> 1.11", 13 | deps: deps(), 14 | package: package(), 15 | description: @description, 16 | name: "Plug.Cowboy", 17 | docs: [ 18 | main: "Plug.Cowboy", 19 | source_ref: "v#{@version}", 20 | source_url: @source_url, 21 | extras: ["CHANGELOG.md"] 22 | ], 23 | aliases: aliases() 24 | ] 25 | end 26 | 27 | def application do 28 | [ 29 | extra_applications: [:logger], 30 | mod: {Plug.Cowboy, []} 31 | ] 32 | end 33 | 34 | def deps do 35 | [ 36 | {:plug, "~> 1.14"}, 37 | {:cowboy, "~> 2.7"}, 38 | {:cowboy_telemetry, "~> 0.3"}, 39 | {:ex_doc, "~> 0.20", only: :docs}, 40 | {:hackney, "~> 1.2", only: :test}, 41 | {:x509, "~> 0.6", only: :test} 42 | ] 43 | end 44 | 45 | defp package do 46 | %{ 47 | licenses: ["Apache-2.0"], 48 | maintainers: ["José Valim", "Gary Rennie"], 49 | links: %{"GitHub" => @source_url} 50 | } 51 | end 52 | 53 | defp aliases do 54 | [ 55 | test: ["x509.gen.suite -f -p cowboy -o test/fixtures/ssl", "test"] 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, 3 | "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, 4 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, 5 | "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 7 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 8 | "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, 9 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 10 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 13 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 14 | "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, 15 | "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, 16 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 17 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 18 | "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, 19 | "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, 20 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, 21 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 22 | "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, 23 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 24 | "x509": {:hex, :x509, "0.8.8", "aaf5e58b19a36a8e2c5c5cff0ad30f64eef5d9225f0fd98fb07912ee23f7aba3", [:mix], [], "hexpm", "ccc3bff61406e5bb6a63f06d549f3dba3a1bbb456d84517efaaa210d8a33750f"}, 25 | } 26 | -------------------------------------------------------------------------------- /test/plug/cowboy/conn_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Cowboy.ConnTest do 2 | use ExUnit.Case, async: true 3 | import ExUnit.CaptureLog 4 | 5 | alias Plug.Conn 6 | import Plug.Conn 7 | 8 | ## Cowboy2 setup for testing 9 | # 10 | # We use hackney to perform an HTTP request against the cowboy/plug running 11 | # on port 8003. Plug then uses Kernel.apply/3 to dispatch based on the first 12 | # element of the URI's path. 13 | # 14 | # e.g. `assert {204, _, _} = request :get, "/build/foo/bar"` will perform a 15 | # GET http://127.0.0.1:8003/build/foo/bar and Plug will call build/1. 16 | 17 | @client_ssl_opts [ 18 | verify: :verify_peer, 19 | keyfile: Path.expand("../../fixtures/ssl/client_key.pem", __DIR__), 20 | certfile: Path.expand("../../fixtures/ssl/client.pem", __DIR__), 21 | cacertfile: Path.expand("../../fixtures/ssl/ca_and_chain.pem", __DIR__) 22 | ] 23 | 24 | @protocol_options [ 25 | idle_timeout: 1000, 26 | request_timeout: 1000 27 | ] 28 | 29 | @https_options [ 30 | port: 8004, 31 | password: "cowboy", 32 | verify: :verify_peer, 33 | keyfile: Path.expand("../../fixtures/ssl/server_key_enc.pem", __DIR__), 34 | certfile: Path.expand("../../fixtures/ssl/valid.pem", __DIR__), 35 | cacertfile: Path.expand("../../fixtures/ssl/ca_and_chain.pem", __DIR__), 36 | protocol_options: @protocol_options 37 | ] 38 | 39 | setup_all do 40 | {:ok, _} = Plug.Cowboy.http(__MODULE__, [], port: 8003, protocol_options: @protocol_options) 41 | {:ok, _} = Plug.Cowboy.https(__MODULE__, [], @https_options) 42 | 43 | on_exit(fn -> 44 | :ok = Plug.Cowboy.shutdown(__MODULE__.HTTP) 45 | :ok = Plug.Cowboy.shutdown(__MODULE__.HTTPS) 46 | end) 47 | 48 | :ok 49 | end 50 | 51 | @already_sent {:plug_conn, :sent} 52 | 53 | def init(opts) do 54 | opts 55 | end 56 | 57 | def call(conn, []) do 58 | # Assert we never have a lingering @already_sent entry in the inbox 59 | refute_received @already_sent 60 | 61 | function = String.to_atom(List.first(conn.path_info) || "root") 62 | apply(__MODULE__, function, [conn]) 63 | rescue 64 | exception -> 65 | receive do 66 | {:plug_conn, :sent} -> 67 | :erlang.raise(:error, exception, __STACKTRACE__) 68 | after 69 | 0 -> 70 | send_resp( 71 | conn, 72 | 500, 73 | Exception.message(exception) <> 74 | "\n" <> Exception.format_stacktrace(__STACKTRACE__) 75 | ) 76 | end 77 | end 78 | 79 | ## Tests 80 | 81 | def root(%Conn{} = conn) do 82 | assert conn.method == "HEAD" 83 | assert conn.path_info == [] 84 | assert conn.query_string == "foo=bar&baz=bat" 85 | assert conn.request_path == "/" 86 | resp(conn, 200, "ok") 87 | end 88 | 89 | def build(%Conn{} = conn) do 90 | assert {Plug.Cowboy.Conn, _} = conn.adapter 91 | assert conn.path_info == ["build", "foo", "bar"] 92 | assert conn.query_string == "" 93 | assert conn.scheme == :http 94 | assert conn.host == "127.0.0.1" 95 | assert conn.port == 8003 96 | assert conn.method == "GET" 97 | assert conn.remote_ip == {127, 0, 0, 1} 98 | assert get_http_protocol(conn) == :"HTTP/1.1" 99 | resp(conn, 200, "ok") 100 | end 101 | 102 | test "builds a connection" do 103 | assert {200, _, _} = request(:head, "/?foo=bar&baz=bat") 104 | assert {200, _, _} = request(:get, "/build/foo/bar") 105 | assert {200, _, _} = request(:get, "//build//foo//bar") 106 | end 107 | 108 | def return_request_path(%Conn{} = conn) do 109 | resp(conn, 200, conn.request_path) 110 | end 111 | 112 | test "request_path" do 113 | assert {200, _, "/return_request_path/foo"} = request(:get, "/return_request_path/foo?barbat") 114 | 115 | assert {200, _, "/return_request_path/foo/bar"} = 116 | request(:get, "/return_request_path/foo/bar?bar=bat") 117 | 118 | assert {200, _, "/return_request_path/foo/bar/"} = 119 | request(:get, "/return_request_path/foo/bar/?bar=bat") 120 | 121 | assert {200, _, "/return_request_path/foo//bar"} = 122 | request(:get, "/return_request_path/foo//bar") 123 | 124 | assert {200, _, "//return_request_path//foo//bar//"} = 125 | request(:get, "//return_request_path//foo//bar//") 126 | end 127 | 128 | def headers(conn) do 129 | assert get_req_header(conn, "foo") == ["bar"] 130 | assert get_req_header(conn, "baz") == ["bat"] 131 | resp(conn, 200, "ok") 132 | end 133 | 134 | test "stores request headers" do 135 | assert {200, _, _} = request(:get, "/headers", [{"foo", "bar"}, {"baz", "bat"}]) 136 | end 137 | 138 | def set_cookies(%Conn{} = conn) do 139 | conn 140 | |> put_resp_cookie("foo", "bar") 141 | |> put_resp_cookie("bar", "bat") 142 | |> resp(200, conn.request_path) 143 | end 144 | 145 | test "set cookies" do 146 | assert {200, headers, _} = request(:get, "/set_cookies") 147 | 148 | assert for({"set-cookie", value} <- headers, do: value) == 149 | ["bar=bat; path=/; HttpOnly", "foo=bar; path=/; HttpOnly"] 150 | end 151 | 152 | def telemetry(conn) do 153 | Process.sleep(30) 154 | send_resp(conn, 200, "TELEMETRY") 155 | end 156 | 157 | def telemetry_exception(conn) do 158 | # send first because of the `rescue` in `call` 159 | send_resp(conn, 200, "Fail") 160 | raise "BadTimes" 161 | end 162 | 163 | test "emits telemetry events for start/stop" do 164 | :telemetry.attach_many( 165 | :start_stop_test, 166 | [ 167 | [:cowboy, :request, :start], 168 | [:cowboy, :request, :stop], 169 | [:cowboy, :request, :exception] 170 | ], 171 | fn event, measurements, metadata, test -> 172 | send(test, {:telemetry, event, measurements, metadata}) 173 | end, 174 | self() 175 | ) 176 | 177 | assert {200, _, "TELEMETRY"} = request(:get, "/telemetry?foo=bar") 178 | 179 | assert_receive {:telemetry, [:cowboy, :request, :start], %{system_time: _}, 180 | %{streamid: _, req: req}} 181 | 182 | assert req.path == "/telemetry" 183 | 184 | assert_receive {:telemetry, [:cowboy, :request, :stop], %{duration: duration}, 185 | %{streamid: _, req: ^req}} 186 | 187 | duration_ms = System.convert_time_unit(duration, :native, :millisecond) 188 | 189 | assert duration_ms >= 30 190 | assert duration_ms < 100 191 | 192 | refute_received {:telemetry, [:cowboy, :request, :exception], _, _} 193 | 194 | :telemetry.detach(:start_stop_test) 195 | end 196 | 197 | test "emits telemetry events for exception" do 198 | :telemetry.attach_many( 199 | :exception_test, 200 | [ 201 | [:cowboy, :request, :start], 202 | [:cowboy, :request, :exception] 203 | ], 204 | fn event, measurements, metadata, test -> 205 | send(test, {:telemetry, event, measurements, metadata}) 206 | end, 207 | self() 208 | ) 209 | 210 | request(:get, "/telemetry_exception") 211 | 212 | assert_receive {:telemetry, [:cowboy, :request, :start], _, _} 213 | 214 | assert_receive {:telemetry, [:cowboy, :request, :exception], %{}, 215 | %{kind: :exit, reason: _reason, stacktrace: _stacktrace}} 216 | 217 | :telemetry.detach(:exception_test) 218 | end 219 | 220 | test "emits telemetry events for cowboy early_error" do 221 | :telemetry.attach( 222 | :early_error_test, 223 | [:cowboy, :request, :early_error], 224 | fn name, measurements, metadata, test -> 225 | send(test, {:event, name, measurements, metadata}) 226 | end, 227 | self() 228 | ) 229 | 230 | assert capture_log(fn -> 231 | cookie = "bar=" <> String.duplicate("a", 8_000_000) 232 | response = request(:get, "/headers", [{"cookie", cookie}]) 233 | assert match?({431, _, _}, response) or match?({:error, :closed}, response) 234 | assert {200, _, _} = request(:get, "/headers", [{"foo", "bar"}, {"baz", "bat"}]) 235 | end) =~ "Cowboy returned 431 because it was unable to parse the request headers" 236 | 237 | assert_receive {:event, [:cowboy, :request, :early_error], 238 | %{ 239 | system_time: _ 240 | }, 241 | %{ 242 | reason: {:connection_error, :limit_reached, _}, 243 | partial_req: %{} 244 | }} 245 | 246 | :telemetry.detach(:early_error_test) 247 | end 248 | 249 | def send_200(conn) do 250 | assert conn.state == :unset 251 | assert conn.resp_body == nil 252 | conn = send_resp(conn, 200, "OK") 253 | assert conn.state == :sent 254 | assert conn.resp_body == nil 255 | conn 256 | end 257 | 258 | def send_418(conn) do 259 | send_resp(conn, 418, "") 260 | end 261 | 262 | def send_998(conn) do 263 | send_resp(conn, 998, "") 264 | end 265 | 266 | def send_500(conn) do 267 | conn 268 | |> delete_resp_header("cache-control") 269 | |> put_resp_header("x-sample", "value") 270 | |> send_resp(500, ["ERR", ["OR"]]) 271 | end 272 | 273 | test "sends a response with status, headers and body" do 274 | assert {200, headers, "OK"} = request(:get, "/send_200") 275 | 276 | assert List.keyfind(headers, "cache-control", 0) == 277 | {"cache-control", "max-age=0, private, must-revalidate"} 278 | 279 | assert {500, headers, "ERROR"} = request(:get, "/send_500") 280 | assert List.keyfind(headers, "cache-control", 0) == nil 281 | assert List.keyfind(headers, "x-sample", 0) == {"x-sample", "value"} 282 | end 283 | 284 | test "allows customized statuses based on config" do 285 | assert {998, _headers, ""} = request(:get, "/send_998") 286 | {:ok, ref} = :hackney.get("http://127.0.0.1:8003/send_998", [], "", async: :once) 287 | assert_receive({:hackney_response, ^ref, {:status, 998, "Not An RFC Status Code"}}) 288 | :hackney.close(ref) 289 | end 290 | 291 | test "existing statuses can be customized" do 292 | assert {418, _headers, ""} = request(:get, "/send_418") 293 | {:ok, ref} = :hackney.get("http://127.0.0.1:8003/send_418", [], "", async: :once) 294 | assert_receive({:hackney_response, ^ref, {:status, 418, "Totally not a teapot"}}) 295 | :hackney.close(ref) 296 | end 297 | 298 | test "skips body on head" do 299 | assert {200, _, nil} = request(:head, "/send_200") 300 | end 301 | 302 | def send_file(conn) do 303 | conn = send_file(conn, 200, __ENV__.file) 304 | assert conn.state == :file 305 | assert conn.resp_body == nil 306 | conn 307 | end 308 | 309 | test "sends a file with status and headers" do 310 | assert {200, headers, body} = request(:get, "/send_file") 311 | assert body =~ "sends a file with status and headers" 312 | 313 | assert List.keyfind(headers, "cache-control", 0) == 314 | {"cache-control", "max-age=0, private, must-revalidate"} 315 | 316 | assert List.keyfind(headers, "content-length", 0) == 317 | { 318 | "content-length", 319 | __ENV__.file |> File.stat!() |> Map.fetch!(:size) |> Integer.to_string() 320 | } 321 | end 322 | 323 | test "skips file on head" do 324 | assert {200, _, nil} = request(:head, "/send_file") 325 | end 326 | 327 | def send_chunked(conn) do 328 | conn = send_chunked(conn, 200) 329 | assert conn.state == :chunked 330 | {:ok, conn} = chunk(conn, "HELLO\n") 331 | {:ok, conn} = chunk(conn, ["WORLD", ["\n"]]) 332 | conn 333 | end 334 | 335 | test "sends a chunked response with status and headers" do 336 | assert {200, headers, "HELLO\nWORLD\n"} = request(:get, "/send_chunked") 337 | 338 | assert List.keyfind(headers, "cache-control", 0) == 339 | {"cache-control", "max-age=0, private, must-revalidate"} 340 | 341 | assert List.keyfind(headers, "transfer-encoding", 0) == {"transfer-encoding", "chunked"} 342 | end 343 | 344 | def inform(conn) do 345 | conn 346 | |> inform(103, [{"link", "; rel=preload; as=style"}]) 347 | |> send_resp(200, "inform") 348 | end 349 | 350 | test "inform will not raise even though the adapter doesn't implement it" do 351 | # the _body in this response is actually garbled. this is a bug in the HTTP/1.1 client and not in plug 352 | assert {103, [{"link", "; rel=preload; as=style"}], _body} = 353 | request(:get, "/inform") 354 | end 355 | 356 | def upgrade_unsupported(conn) do 357 | conn 358 | |> upgrade_adapter(:unsupported, opt: :unsupported) 359 | end 360 | 361 | test "upgrade will not set the response" do 362 | assert {500, _, body} = request(:get, "/upgrade_unsupported") 363 | assert body =~ "upgrade to unsupported not supported by Plug.Cowboy.Conn" 364 | end 365 | 366 | defmodule NoopWebSocketHandler do 367 | @behaviour :cowboy_websocket 368 | 369 | # We never actually call this; it's just here to quell compiler warnings 370 | @impl true 371 | def init(req, state), do: {:cowboy_websocket, req, state} 372 | 373 | @impl true 374 | def websocket_handle(_frame, state), do: {:ok, state} 375 | 376 | @impl true 377 | def websocket_info(_msg, state), do: {:ok, state} 378 | end 379 | 380 | def upgrade_websocket(conn) do 381 | # In actual use, it's the caller's responsibility to ensure the upgrade is valid before 382 | # calling upgrade_adapter 383 | conn 384 | |> upgrade_adapter(:websocket, {NoopWebSocketHandler, [], %{}}) 385 | end 386 | 387 | test "upgrades the connection when the connection is a valid websocket" do 388 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 8003, active: false, mode: :binary) 389 | 390 | :gen_tcp.send(socket, """ 391 | GET /upgrade_websocket HTTP/1.1\r 392 | Host: server.example.com\r 393 | Upgrade: websocket\r 394 | Connection: Upgrade\r 395 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r 396 | Sec-WebSocket-Version: 13\r 397 | \r 398 | """) 399 | 400 | {:ok, response} = :gen_tcp.recv(socket, 234) 401 | 402 | assert [ 403 | "HTTP/1.1 101 Switching Protocols", 404 | "cache-control: max-age=0, private, must-revalidate", 405 | "connection: Upgrade", 406 | "date: " <> _date, 407 | "sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", 408 | "server: Cowboy", 409 | "upgrade: websocket", 410 | "", 411 | "" 412 | ] = String.split(response, "\r\n") 413 | end 414 | 415 | test "returns error in cases where an upgrade is indicated but the connection is not a valid upgrade" do 416 | assert {426, _headers, ""} = request(:get, "/upgrade_websocket") 417 | end 418 | 419 | def push(conn) do 420 | conn 421 | |> push("/static/assets.css") 422 | |> send_resp(200, "push") 423 | end 424 | 425 | test "push will not raise even though the adapter doesn't implement it" do 426 | assert {200, _headers, "push"} = request(:get, "/push") 427 | end 428 | 429 | def push_or_raise(conn) do 430 | conn 431 | |> push!("/static/assets.css") 432 | |> send_resp(200, "push or raise") 433 | end 434 | 435 | test "push will raise because it is not implemented" do 436 | assert {200, _headers, "push or raise"} = request(:get, "/push_or_raise") 437 | end 438 | 439 | def read_req_body(conn) do 440 | expected = :binary.copy("abcdefghij", 100_000) 441 | assert {:ok, ^expected, conn} = read_body(conn) 442 | assert {:ok, "", conn} = read_body(conn) 443 | resp(conn, 200, "ok") 444 | end 445 | 446 | def read_req_body_partial(conn) do 447 | # Read something even with no length 448 | assert {:more, body, conn} = read_body(conn, length: 0, read_length: 1_000) 449 | assert byte_size(body) > 0 450 | assert {:more, body, conn} = read_body(conn, length: 5_000, read_length: 1_000) 451 | assert byte_size(body) > 0 452 | assert {:more, body, conn} = read_body(conn, length: 20_000, read_length: 1_000) 453 | assert byte_size(body) > 0 454 | assert {:ok, body, conn} = read_body(conn, length: 2_000_000) 455 | assert byte_size(body) > 0 456 | 457 | # Once it is over, always returns :ok 458 | assert {:ok, "", conn} = read_body(conn, length: 2_000_000) 459 | assert {:ok, "", conn} = read_body(conn, length: 0) 460 | 461 | resp(conn, 200, "ok") 462 | end 463 | 464 | test "reads body" do 465 | body = :binary.copy("abcdefghij", 100_000) 466 | assert {200, _, "ok"} = request(:post, "/read_req_body_partial", [], body) 467 | assert {200, _, "ok"} = request(:get, "/read_req_body", [], body) 468 | assert {200, _, "ok"} = request(:post, "/read_req_body", [], body) 469 | end 470 | 471 | def multipart(conn) do 472 | opts = Plug.Parsers.init(parsers: [Plug.Parsers.MULTIPART], length: 8_000_000) 473 | conn = Plug.Parsers.call(conn, opts) 474 | assert conn.params["name"] == "hello" 475 | assert conn.params["status"] == ["choice1", "choice2"] 476 | assert conn.params["empty"] == nil 477 | 478 | assert %Plug.Upload{} = file = conn.params["pic"] 479 | assert File.read!(file.path) == "hello\n\n" 480 | assert file.content_type == "text/plain" 481 | assert file.filename == "foo.txt" 482 | 483 | resp(conn, 200, "ok") 484 | end 485 | 486 | test "parses multipart requests" do 487 | multipart = """ 488 | ------w58EW1cEpjzydSCq\r 489 | Content-Disposition: form-data; name=\"name\"\r 490 | \r 491 | hello\r 492 | ------w58EW1cEpjzydSCq\r 493 | Content-Disposition: form-data; name=\"pic\"; filename=\"foo.txt\"\r 494 | Content-Type: text/plain\r 495 | \r 496 | hello 497 | 498 | \r 499 | ------w58EW1cEpjzydSCq\r 500 | Content-Disposition: form-data; name=\"empty\"; filename=\"\"\r 501 | Content-Type: application/octet-stream\r 502 | \r 503 | \r 504 | ------w58EW1cEpjzydSCq\r 505 | Content-Disposition: form-data; name="status[]"\r 506 | \r 507 | choice1\r 508 | ------w58EW1cEpjzydSCq\r 509 | Content-Disposition: form-data; name="status[]"\r 510 | \r 511 | choice2\r 512 | ------w58EW1cEpjzydSCq\r 513 | Content-Disposition: form-data; name=\"commit\"\r 514 | \r 515 | Create User\r 516 | ------w58EW1cEpjzydSCq--\r 517 | """ 518 | 519 | headers = [ 520 | {"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"}, 521 | {"Content-Length", byte_size(multipart)} 522 | ] 523 | 524 | assert {200, _, _} = request(:post, "/multipart", headers, multipart) 525 | assert {200, _, _} = request(:post, "/multipart?name=overriden", headers, multipart) 526 | end 527 | 528 | def file_too_big(conn) do 529 | opts = Plug.Parsers.init(parsers: [Plug.Parsers.MULTIPART], length: 5) 530 | conn = Plug.Parsers.call(conn, opts) 531 | 532 | assert %Plug.Upload{} = file = conn.params["pic"] 533 | assert File.read!(file.path) == "hello\n\n" 534 | assert file.content_type == "text/plain" 535 | assert file.filename == "foo.txt" 536 | 537 | resp(conn, 200, "ok") 538 | end 539 | 540 | test "returns parse error when file pushed the boundaries in multipart requests" do 541 | multipart = """ 542 | ------w58EW1cEpjzydSCq\r 543 | Content-Disposition: form-data; name=\"pic\"; filename=\"foo.txt\"\r 544 | Content-Type: text/plain\r 545 | \r 546 | hello 547 | 548 | \r 549 | ------w58EW1cEpjzydSCq--\r 550 | """ 551 | 552 | headers = [ 553 | {"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"}, 554 | {"Content-Length", byte_size(multipart)} 555 | ] 556 | 557 | assert {500, _, body} = request(:post, "/file_too_big", headers, multipart) 558 | assert body =~ "the request is too large" 559 | end 560 | 561 | test "validates utf-8 on multipart requests" do 562 | multipart = """ 563 | ------w58EW1cEpjzydSCq\r 564 | Content-Disposition: form-data; name=\"name\"\r 565 | \r 566 | #{<<139>>}\r 567 | ------w58EW1cEpjzydSCq\r 568 | """ 569 | 570 | headers = [ 571 | {"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"}, 572 | {"Content-Length", byte_size(multipart)} 573 | ] 574 | 575 | assert {500, _, body} = request(:post, "/multipart", headers, multipart) 576 | assert body =~ "invalid UTF-8 on multipart body, got byte 139" 577 | end 578 | 579 | test "returns parse error when body is badly formatted in multipart requests" do 580 | multipart = """ 581 | ------w58EW1cEpjzydSCq\r 582 | Content-Disposition: form-data; name=\"name\"\r 583 | ------w58EW1cEpjzydSCq\r 584 | """ 585 | 586 | headers = [ 587 | {"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"}, 588 | {"Content-Length", byte_size(multipart)} 589 | ] 590 | 591 | assert {500, _, body} = request(:post, "/multipart", headers, multipart) 592 | 593 | assert body =~ 594 | "malformed request, a RuntimeError exception was raised with message \"invalid multipart" 595 | 596 | multipart = """ 597 | ------w58EW1cEpjzydSCq\r 598 | Content-Disposition: form-data; name=\"name\"\r 599 | \r 600 | hello 601 | """ 602 | 603 | headers = [ 604 | {"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"}, 605 | {"Content-Length", byte_size(multipart)} 606 | ] 607 | 608 | assert {500, _, body} = request(:post, "/multipart", headers, multipart) 609 | 610 | assert body =~ 611 | "malformed request, a RuntimeError exception was raised with message \"invalid multipart" 612 | end 613 | 614 | def https(conn) do 615 | assert conn.scheme == :https 616 | send_resp(conn, 200, "OK") 617 | end 618 | 619 | test "https" do 620 | pool = :https 621 | pool_opts = [timeout: 150_000, max_connections: 10] 622 | :ok = :hackney_pool.start_pool(pool, pool_opts) 623 | 624 | opts = [ 625 | pool: :https, 626 | ssl_options: [cacertfile: @https_options[:certfile], server_name_indication: ~c"localhost"] 627 | ] 628 | 629 | assert {:ok, 200, _headers, client} = 630 | :hackney.get("https://127.0.0.1:8004/https", [], "", opts) 631 | 632 | assert {:ok, "OK"} = :hackney.body(client) 633 | :hackney.close(client) 634 | end 635 | 636 | @http2_opts [ 637 | cacertfile: @https_options[:certfile], 638 | server_name_indication: ~c"localhost", 639 | port: 8004 640 | ] 641 | 642 | def http2(conn) do 643 | case conn.query_string do 644 | "noinfer" <> _ -> 645 | conn 646 | |> push("/static/assets.css", [{"accept", "text/plain"}]) 647 | |> send_resp(200, Atom.to_string(get_http_protocol(conn))) 648 | 649 | "earlyhints" <> _ -> 650 | conn 651 | |> inform(:early_hints, [{"link", "; rel=preload; as=style"}]) 652 | |> send_resp(200, Atom.to_string(get_http_protocol(conn))) 653 | 654 | _ -> 655 | conn 656 | |> push("/static/assets.css") 657 | |> send_resp(200, Atom.to_string(get_http_protocol(conn))) 658 | end 659 | end 660 | 661 | def peer_data(conn) do 662 | assert conn.scheme == :https 663 | %{address: address, port: port, ssl_cert: ssl_cert} = get_peer_data(conn) 664 | assert address == {127, 0, 0, 1} 665 | assert is_integer(port) 666 | assert is_binary(ssl_cert) 667 | send_resp(conn, 200, "OK") 668 | end 669 | 670 | test "exposes peer data" do 671 | pool = :client_ssl_pool 672 | pool_opts = [timeout: 150_000, max_connections: 10] 673 | :ok = :hackney_pool.start_pool(pool, pool_opts) 674 | 675 | opts = [ 676 | pool: :client_ssl_pool, 677 | ssl_options: [server_name_indication: ~c"localhost"] ++ @client_ssl_opts 678 | ] 679 | 680 | assert {:ok, 200, _headers, client} = 681 | :hackney.get("https://127.0.0.1:8004/peer_data", [], "", opts) 682 | 683 | assert {:ok, "OK"} = :hackney.body(client) 684 | :hackney.close(client) 685 | end 686 | 687 | ## Helpers 688 | 689 | defp request(:head = verb, path) do 690 | {:ok, status, headers} = :hackney.request(verb, "http://127.0.0.1:8003" <> path, [], "", []) 691 | {status, headers, nil} 692 | end 693 | 694 | defp request(verb, path, headers \\ [], body \\ "") do 695 | case :hackney.request(verb, "http://127.0.0.1:8003" <> path, headers, body, []) do 696 | {:ok, status, headers, client} -> 697 | {:ok, body} = :hackney.body(client) 698 | :hackney.close(client) 699 | {status, headers, body} 700 | 701 | {:error, _} = error -> 702 | error 703 | end 704 | end 705 | end 706 | -------------------------------------------------------------------------------- /test/plug/cowboy/drainer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Cowboy.DrainerTest do 2 | use ExUnit.Case, async: true 3 | 4 | def init(opts) do 5 | opts 6 | end 7 | 8 | def call(conn, []) do 9 | conn = Plug.Conn.send_chunked(conn, 200) 10 | Process.sleep(500) 11 | {:ok, conn} = Plug.Conn.chunk(conn, "ok") 12 | conn 13 | end 14 | 15 | def start_link(opts) do 16 | children = [ 17 | {Plug.Cowboy, scheme: :http, plug: __MODULE__, options: [port: 8005]}, 18 | {Plug.Cowboy.Drainer, opts} 19 | ] 20 | 21 | Supervisor.start_link(children, strategy: :one_for_one) 22 | end 23 | 24 | test "drainer drains connections correctly" do 25 | Process.register(self(), __MODULE__) 26 | 27 | # Supervisor and listener started 28 | assert {:ok, pid} = start_link(refs: :all, shutdown: 1000, drain_check_interval: 10) 29 | assert :running == get_status() 30 | 31 | # Start a request that will keep a connection open for a while 32 | observe_state_changes() 33 | observe_slow_request() 34 | 35 | # Slow request opened 36 | assert_receive {:request_status, 200, start_request_timestamp}, 2000 37 | 38 | # Stop the supervisor to start the request draining 39 | start_shutdown_timestamp = timestamp() 40 | assert :ok == GenServer.stop(pid) 41 | complete_shutdown_timestamp = timestamp() 42 | 43 | # Draining started, but one request still open 44 | assert_receive {:listener_status, :suspended, suspended_timestamp} 45 | assert_receive {:conn, 1, open_request_timestamp} 46 | 47 | # Request completed 48 | assert_receive {:request_body, "ok", complete_request_timestamp} 49 | 50 | # Requests drained 51 | assert_receive {:conn, 0, drained_requests_timestamp} 52 | 53 | assert start_request_timestamp < start_shutdown_timestamp 54 | assert start_shutdown_timestamp < suspended_timestamp 55 | assert suspended_timestamp < complete_request_timestamp 56 | assert open_request_timestamp < complete_request_timestamp 57 | assert complete_request_timestamp < drained_requests_timestamp 58 | assert complete_request_timestamp < complete_shutdown_timestamp 59 | end 60 | 61 | defp observe_state_changes() do 62 | this = __MODULE__ 63 | 64 | Task.async(fn -> 65 | wait_for_connections(1) 66 | wait_until_listener_suspended() 67 | 68 | send(this, {:listener_status, get_status(), timestamp()}) 69 | wait_for_connections(1) 70 | send(this, {:conn, 1, timestamp()}) 71 | 72 | wait_for_connections(0) 73 | send(this, {:conn, 0, timestamp()}) 74 | end) 75 | end 76 | 77 | test "raises when refs are not specified" do 78 | assert_raise KeyError, fn -> 79 | Plug.Cowboy.Drainer.start_link([]) 80 | end 81 | end 82 | 83 | test "raises when refs is not an expected argument type" do 84 | assert_raise ArgumentError, fn -> 85 | Plug.Cowboy.Drainer.start_link(refs: 1) 86 | end 87 | end 88 | 89 | defp observe_slow_request() do 90 | this = __MODULE__ 91 | 92 | Task.async(fn -> 93 | {:ok, status, _headers, client} = 94 | :hackney.request(:get, "http://127.0.0.1:8005/", [], "", [:stream]) 95 | 96 | send(this, {:request_status, status, timestamp()}) 97 | {:ok, body} = :hackney.stream_body(client) 98 | send(this, {:request_body, body, timestamp()}) 99 | end) 100 | end 101 | 102 | defp wait_for_connections(total) do 103 | :ranch.wait_for_connections(__MODULE__.HTTP, :==, total, 10) 104 | end 105 | 106 | defp wait_until_listener_suspended do 107 | Stream.repeatedly(&get_status/0) 108 | |> Stream.each(fn _ -> Process.sleep(5) end) 109 | |> Stream.take_while(fn status -> status == :running end) 110 | |> Stream.run() 111 | end 112 | 113 | defp get_status do 114 | :ranch.get_status(__MODULE__.HTTP) 115 | end 116 | 117 | defp timestamp, do: :os.system_time(:micro_seconds) 118 | end 119 | -------------------------------------------------------------------------------- /test/plug/cowboy/translator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Cowboy.TranslatorTest do 2 | use ExUnit.Case 3 | 4 | import ExUnit.CaptureLog 5 | 6 | def init(opts) do 7 | opts 8 | end 9 | 10 | def call(%{path_info: ["warn"]}, _opts) do 11 | raise Plug.Parsers.UnsupportedMediaTypeError, media_type: "foo/bar" 12 | end 13 | 14 | def call(%{path_info: ["error"]}, _opts) do 15 | raise "oops" 16 | end 17 | 18 | def call(%{path_info: ["linked"]}, _opts) do 19 | fn -> GenServer.call(:i_dont_exist, :ok) end |> Task.async() |> Task.await() 20 | end 21 | 22 | @metadata_log_opts format: {__MODULE__, :metadata}, metadata: [:conn, :crash_reason, :domain] 23 | 24 | def metadata(_log_level, _message, _timestamp, metadata) do 25 | inspect(metadata, limit: :infinity) 26 | end 27 | 28 | test "ranch/cowboy 500 logs" do 29 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9001) 30 | 31 | output = 32 | capture_log(fn -> 33 | :hackney.get("http://127.0.0.1:9001/error", [], "", []) 34 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 35 | end) 36 | 37 | assert output =~ ~r"#PID<0\.\d+\.0> running Plug\.Cowboy\.TranslatorTest \(.*\) terminated" 38 | assert output =~ "Server: 127.0.0.1:9001 (http)" 39 | assert output =~ "Request: GET /" 40 | assert output =~ "** (exit) an exception was raised:" 41 | assert output =~ "** (RuntimeError) oops" 42 | end 43 | 44 | test "ranch/cowboy non-500 skips" do 45 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9002) 46 | 47 | output = 48 | capture_log(fn -> 49 | :hackney.get("http://127.0.0.1:9002/warn", [], "", []) 50 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 51 | end) 52 | 53 | refute output =~ ~r"#PID<0\.\d+\.0> running Plug\.Cowboy\.TranslatorTest \(.*\) terminated" 54 | refute output =~ "Server: 127.0.0.1:9002 (http)" 55 | refute output =~ "Request: GET /" 56 | refute output =~ "** (exit) an exception was raised:" 57 | end 58 | 59 | test "ranch/cowboy logs configured statuses" do 60 | Application.put_env(:plug_cowboy, :log_exceptions_with_status_code, [400..499]) 61 | on_exit(fn -> Application.delete_env(:plug_cowboy, :log_exceptions_with_status_code) end) 62 | 63 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9002) 64 | 65 | output = 66 | capture_log(fn -> 67 | :hackney.get("http://127.0.0.1:9002/warn", [], "", []) 68 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 69 | end) 70 | 71 | assert output =~ ~r"#PID<0\.\d+\.0> running Plug\.Cowboy\.TranslatorTest \(.*\) terminated" 72 | assert output =~ "Server: 127.0.0.1:9002 (http)" 73 | assert output =~ "Request: GET /" 74 | assert output =~ "** (exit) an exception was raised:" 75 | assert output =~ "** (Plug.Parsers.UnsupportedMediaTypeError) unsupported media type foo/bar" 76 | 77 | output = 78 | capture_log(fn -> 79 | :hackney.get("http://127.0.0.1:9002/error", [], "", []) 80 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 81 | end) 82 | 83 | refute output =~ ~r"#PID<0\.\d+\.0> running Plug\.Cowboy\.TranslatorTest \(.*\) terminated" 84 | refute output =~ "Server: 127.0.0.1:9001 (http)" 85 | refute output =~ "Request: GET /" 86 | refute output =~ "** (exit) an exception was raised:" 87 | refute output =~ "** (RuntimeError) oops" 88 | end 89 | 90 | test "ranch/cowboy linked logs" do 91 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9003) 92 | 93 | output = 94 | capture_log(fn -> 95 | :hackney.get("http://127.0.0.1:9003/linked", [], "", []) 96 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 97 | end) 98 | 99 | assert output =~ 100 | ~r"Ranch protocol #PID<0\.\d+\.0> of listener Plug\.Cowboy\.TranslatorTest\.HTTP \(.*\) terminated" 101 | 102 | assert output =~ "exited in: GenServer.call" 103 | assert output =~ "** (EXIT) no process" 104 | end 105 | 106 | test "metadata in ranch/cowboy 500 logs" do 107 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9004) 108 | 109 | metadata = 110 | capture_log(@metadata_log_opts, fn -> 111 | :hackney.get("http://127.0.0.1:9004/error", [], "", []) 112 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 113 | end) 114 | 115 | assert metadata =~ "conn: %Plug.Conn{" 116 | assert metadata =~ "crash_reason:" 117 | assert metadata =~ "domain: [:cowboy]" 118 | end 119 | 120 | test "metadata opt-out ranch/cowboy 500 logs" do 121 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9004) 122 | Application.put_env(:plug_cowboy, :conn_in_exception_metadata, false) 123 | on_exit(fn -> Application.delete_env(:plug_cowboy, :conn_in_exception_metadata) end) 124 | 125 | metadata = 126 | capture_log(@metadata_log_opts, fn -> 127 | :hackney.get("http://127.0.0.1:9004/error", [], "", []) 128 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 129 | end) 130 | 131 | refute metadata =~ "conn: %Plug.Conn{" 132 | end 133 | 134 | test "metadata in ranch/cowboy linked logs" do 135 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9005) 136 | 137 | metadata = 138 | capture_log(@metadata_log_opts, fn -> 139 | :hackney.get("http://127.0.0.1:9005/linked", [], "", []) 140 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 141 | end) 142 | 143 | assert metadata =~ "crash_reason:" 144 | assert metadata =~ "{GenServer, :call" 145 | assert metadata =~ "domain: [:cowboy]" 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /test/plug/cowboy/websocket_handler_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WebSocketHandlerTest do 2 | use ExUnit.Case, async: true 3 | 4 | defmodule WebSocketHandler do 5 | @behaviour :cowboy_websocket 6 | 7 | # We never actually call this; it's just here to quell compiler warnings 8 | @impl true 9 | def init(req, state), do: {:cowboy_websocket, req, state} 10 | 11 | @impl true 12 | def websocket_init(_opts), do: {:ok, :init} 13 | 14 | @impl true 15 | def websocket_handle({:text, "state"}, state), do: {[{:text, inspect(state)}], state} 16 | 17 | def websocket_handle({:text, "whoami"}, state), 18 | do: {[{:text, :erlang.pid_to_list(self())}], state} 19 | 20 | @impl true 21 | def websocket_info(msg, state), do: {[{:text, inspect(msg)}], state} 22 | end 23 | 24 | @protocol_options [ 25 | idle_timeout: 1000, 26 | request_timeout: 1000 27 | ] 28 | 29 | setup_all do 30 | {:ok, _} = Plug.Cowboy.http(__MODULE__, [], port: 0, protocol_options: @protocol_options) 31 | on_exit(fn -> :ok = Plug.Cowboy.shutdown(__MODULE__.HTTP) end) 32 | {:ok, port: :ranch.get_port(__MODULE__.HTTP)} 33 | end 34 | 35 | @behaviour Plug 36 | 37 | @impl Plug 38 | def init(arg), do: arg 39 | 40 | @impl Plug 41 | def call(conn, _opts) do 42 | conn = Plug.Conn.fetch_query_params(conn) 43 | handler = conn.query_params["handler"] |> String.to_atom() 44 | Plug.Conn.upgrade_adapter(conn, :websocket, {handler, [], %{idle_timeout: 1000}}) 45 | end 46 | 47 | test "websocket_init and websocket_handle are called", context do 48 | client = tcp_client(context) 49 | http1_handshake(client, WebSocketHandler) 50 | 51 | send_text_frame(client, "state") 52 | {:ok, result} = recv_text_frame(client) 53 | assert result == inspect(:init) 54 | end 55 | 56 | test "websocket_info is called", context do 57 | client = tcp_client(context) 58 | http1_handshake(client, WebSocketHandler) 59 | 60 | send_text_frame(client, "whoami") 61 | {:ok, pid} = recv_text_frame(client) 62 | pid = pid |> String.to_charlist() |> :erlang.list_to_pid() 63 | 64 | Process.send(pid, "hello info", []) 65 | 66 | {:ok, response} = recv_text_frame(client) 67 | assert response == inspect("hello info") 68 | end 69 | 70 | # Simple WebSocket client 71 | 72 | def tcp_client(context) do 73 | {:ok, socket} = :gen_tcp.connect(~c"localhost", context[:port], active: false, mode: :binary) 74 | 75 | socket 76 | end 77 | 78 | def http1_handshake(client, module, params \\ []) do 79 | params = params |> Keyword.put(:handler, module) 80 | 81 | :gen_tcp.send(client, """ 82 | GET /?#{URI.encode_query(params)} HTTP/1.1\r 83 | Host: server.example.com\r 84 | Upgrade: websocket\r 85 | Connection: Upgrade\r 86 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r 87 | Sec-WebSocket-Version: 13\r 88 | \r 89 | """) 90 | 91 | {:ok, response} = :gen_tcp.recv(client, 234) 92 | 93 | [ 94 | "HTTP/1.1 101 Switching Protocols", 95 | "cache-control: max-age=0, private, must-revalidate", 96 | "connection: Upgrade", 97 | "date: " <> _date, 98 | "sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", 99 | "server: Cowboy", 100 | "upgrade: websocket", 101 | "", 102 | "" 103 | ] = String.split(response, "\r\n") 104 | end 105 | 106 | defp recv_text_frame(client) do 107 | {:ok, 0x8, 0x1, body} = recv_frame(client) 108 | {:ok, body} 109 | end 110 | 111 | defp recv_frame(client) do 112 | {:ok, header} = :gen_tcp.recv(client, 2) 113 | <> = header 114 | 115 | {:ok, data} = 116 | case length do 117 | 0 -> 118 | {:ok, <<>>} 119 | 120 | 126 -> 121 | {:ok, <>} = :gen_tcp.recv(client, 2) 122 | :gen_tcp.recv(client, length) 123 | 124 | 127 -> 125 | {:ok, <>} = :gen_tcp.recv(client, 8) 126 | :gen_tcp.recv(client, length) 127 | 128 | length -> 129 | :gen_tcp.recv(client, length) 130 | end 131 | 132 | {:ok, flags, opcode, data} 133 | end 134 | 135 | defp send_text_frame(client, data, flags \\ 0x8) do 136 | send_frame(client, flags, 0x1, data) 137 | end 138 | 139 | defp send_frame(client, flags, opcode, data) do 140 | mask = :rand.uniform(1_000_000) 141 | masked_data = mask(data, mask) 142 | 143 | mask_flag_and_size = 144 | case byte_size(masked_data) do 145 | size when size <= 125 -> <<1::1, size::7>> 146 | size when size <= 65_535 -> <<1::1, 126::7, size::16>> 147 | size -> <<1::1, 127::7, size::64>> 148 | end 149 | 150 | :gen_tcp.send(client, [<>, mask_flag_and_size, <>, masked_data]) 151 | end 152 | 153 | # Note that masking is an involution, so we don't need a separate unmask function 154 | defp mask(payload, mask, acc \\ <<>>) 155 | 156 | defp mask(payload, mask, acc) when is_integer(mask), do: mask(payload, <>, acc) 157 | 158 | defp mask(<>, <>, acc) do 159 | mask(rest, mask, acc <> <>) 160 | end 161 | 162 | defp mask(<>, <>, acc) do 163 | mask(rest, <>, acc <> <>) 164 | end 165 | 166 | defp mask(<<>>, _mask, acc), do: acc 167 | end 168 | -------------------------------------------------------------------------------- /test/plug/cowboy_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.CowboyTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Plug.Cowboy 5 | 6 | def init([]) do 7 | [foo: :bar] 8 | end 9 | 10 | handler = {:_, [], Plug.Cowboy.Handler, {Plug.CowboyTest, [foo: :bar]}} 11 | @dispatch [{:_, [], [handler]}] 12 | 13 | test "supports Elixir child specs" do 14 | spec = {Plug.Cowboy, [scheme: :http, plug: __MODULE__, port: 4040]} 15 | 16 | ranch_listener_mod = ranch_listener_for_version() 17 | 18 | assert %{ 19 | id: {^ranch_listener_mod, Plug.CowboyTest.HTTP}, 20 | start: {^ranch_listener_mod, :start_link, _}, 21 | type: :supervisor 22 | } = Supervisor.child_spec(spec, []) 23 | 24 | # For backwards compatibility: 25 | spec = {Plug.Cowboy, [scheme: :http, plug: __MODULE__, options: [port: 4040]]} 26 | 27 | assert %{ 28 | id: {^ranch_listener_mod, Plug.CowboyTest.HTTP}, 29 | start: {^ranch_listener_mod, :start_link, _}, 30 | type: :supervisor 31 | } = Supervisor.child_spec(spec, []) 32 | 33 | spec = 34 | {Plug.Cowboy, 35 | [scheme: :http, plug: __MODULE__, parent: :key, options: [:inet6, port: 4040]]} 36 | 37 | assert %{ 38 | id: {^ranch_listener_mod, Plug.CowboyTest.HTTP}, 39 | start: {^ranch_listener_mod, :start_link, _}, 40 | type: :supervisor 41 | } = Supervisor.child_spec(spec, []) 42 | end 43 | 44 | test "the h2 alpn settings are added when using https" do 45 | options = [ 46 | port: 4040, 47 | password: "cowboy", 48 | keyfile: Path.expand("../fixtures/ssl/server_key_enc.pem", __DIR__), 49 | certfile: Path.expand("../fixtures/ssl/valid.pem", __DIR__) 50 | ] 51 | 52 | spec = {Plug.Cowboy, [scheme: :https, plug: __MODULE__] ++ options} 53 | 54 | ranch_listener_mod = ranch_listener_for_version() 55 | %{start: {^ranch_listener_mod, :start_link, opts}} = Supervisor.child_spec(spec, []) 56 | 57 | assert [ 58 | Plug.CowboyTest.HTTPS, 59 | :ranch_ssl, 60 | %{socket_opts: socket_opts}, 61 | :cowboy_tls, 62 | _proto_opts 63 | ] = opts 64 | 65 | assert Keyword.get(socket_opts, :alpn_preferred_protocols) == ["h2", "http/1.1"] 66 | assert Keyword.get(socket_opts, :next_protocols_advertised) == ["h2", "http/1.1"] 67 | end 68 | 69 | test "builds args for cowboy dispatch" do 70 | assert [ 71 | Plug.CowboyTest.HTTP, 72 | %{num_acceptors: 100, socket_opts: [port: 4000], max_connections: 16_384}, 73 | %{env: %{dispatch: @dispatch}} 74 | ] = args(:http, __MODULE__, [], []) 75 | end 76 | 77 | test "builds args with custom options" do 78 | assert [ 79 | Plug.CowboyTest.HTTP, 80 | %{ 81 | num_acceptors: 100, 82 | max_connections: 16_384, 83 | socket_opts: [port: 3000, other: true] 84 | }, 85 | %{env: %{dispatch: @dispatch}} 86 | ] = args(:http, __MODULE__, [], port: 3000, other: true) 87 | end 88 | 89 | test "builds args with non 2-element tuple options" do 90 | assert [ 91 | Plug.CowboyTest.HTTP, 92 | %{ 93 | num_acceptors: 100, 94 | max_connections: 16_384, 95 | socket_opts: [:inet6, {:raw, 1, 2, 3}, port: 3000, other: true] 96 | }, 97 | %{env: %{dispatch: @dispatch}} 98 | ] = args(:http, __MODULE__, [], [:inet6, {:raw, 1, 2, 3}, port: 3000, other: true]) 99 | end 100 | 101 | test "builds args with protocol option" do 102 | assert [ 103 | Plug.CowboyTest.HTTP, 104 | %{num_acceptors: 100, max_connections: 16_384, socket_opts: [port: 3000]}, 105 | %{env: %{dispatch: @dispatch}, compress: true} 106 | ] = args(:http, __MODULE__, [], port: 3000, compress: true) 107 | 108 | assert [ 109 | Plug.CowboyTest.HTTP, 110 | %{num_acceptors: 100, max_connections: 16_384, socket_opts: [port: 3000]}, 111 | %{env: %{dispatch: @dispatch}, timeout: 30_000} 112 | ] = args(:http, __MODULE__, [], port: 3000, protocol_options: [timeout: 30_000]) 113 | end 114 | 115 | test "builds args with compress option" do 116 | assert [ 117 | Plug.CowboyTest.HTTP, 118 | %{num_acceptors: 100, max_connections: 16_384, socket_opts: [port: 3000]}, 119 | %{ 120 | env: %{dispatch: @dispatch}, 121 | stream_handlers: [:cowboy_compress_h, :cowboy_telemetry_h, :cowboy_stream_h] 122 | } 123 | ] = args(:http, __MODULE__, [], port: 3000, compress: true) 124 | end 125 | 126 | test "builds args with net option" do 127 | assert [ 128 | Plug.CowboyTest.HTTP, 129 | %{num_acceptors: 100, max_connections: 16_384, socket_opts: [:inet6, port: 3000]}, 130 | %{ 131 | env: %{dispatch: @dispatch}, 132 | stream_handlers: [:cowboy_telemetry_h, :cowboy_stream_h] 133 | } 134 | ] = args(:http, __MODULE__, [], port: 3000, net: :inet6) 135 | end 136 | 137 | test "builds args with transport options" do 138 | assert [ 139 | Plug.CowboyTest.HTTP, 140 | %{ 141 | num_acceptors: 50, 142 | max_connections: 16_384, 143 | shutdown: :brutal_kill, 144 | socket_opts: [:inets, priority: 1, port: 3000] 145 | }, 146 | %{ 147 | env: %{dispatch: @dispatch} 148 | } 149 | ] = 150 | args(:http, __MODULE__, [], 151 | port: 3000, 152 | transport_options: [ 153 | shutdown: :brutal_kill, 154 | num_acceptors: 50, 155 | socket_opts: [:inets, priority: 1] 156 | ] 157 | ) 158 | end 159 | 160 | test "builds args with compress option fails if stream_handlers are set" do 161 | assert_raise(RuntimeError, ~r/set both compress and stream_handlers/, fn -> 162 | args(:http, __MODULE__, [], port: 3000, compress: true, stream_handlers: [:cowboy_stream_h]) 163 | end) 164 | end 165 | 166 | test "builds args with single-atom protocol option" do 167 | assert [ 168 | Plug.CowboyTest.HTTP, 169 | %{num_acceptors: 100, max_connections: 16_384, socket_opts: [:inet6, port: 3000]}, 170 | %{env: %{dispatch: @dispatch}} 171 | ] = args(:http, __MODULE__, [], [:inet6, port: 3000]) 172 | end 173 | 174 | test "builds child specs" do 175 | ranch_listener_mod = ranch_listener_for_version() 176 | 177 | assert %{ 178 | id: {^ranch_listener_mod, Plug.CowboyTest.HTTP}, 179 | start: {^ranch_listener_mod, :start_link, _}, 180 | type: :supervisor 181 | } = child_spec(scheme: :http, plug: __MODULE__, options: []) 182 | end 183 | 184 | defp ranch_listener_for_version() do 185 | case Version.parse!("#{Application.spec(:ranch, :vsn)}") |> Version.compare("2.2.0") do 186 | :lt -> :ranch_listener_sup 187 | _ -> :ranch_embedded_sup 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(assert_receive_timeout: 1000) 2 | Logger.configure_backend(:console, colors: [enabled: false], metadata: [:request_id]) 3 | --------------------------------------------------------------------------------