├── test ├── test_helper.exs └── plug │ ├── cowboy │ ├── drainer_test.exs │ ├── websocket_handler_test.exs │ ├── translator_test.exs │ └── conn_test.exs │ └── cowboy_test.exs ├── .formatter.exs ├── .gitignore ├── config └── config.exs ├── LICENSE ├── mix.exs ├── .github └── workflows │ └── ci.yml ├── README.md ├── lib └── plug │ ├── cowboy │ ├── handler.ex │ ├── drainer.ex │ ├── translator.ex │ └── conn.ex │ └── cowboy.ex ├── CHANGELOG.md └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(assert_receive_timeout: 1000) 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /docs 4 | /doc 5 | erl_crash.dump 6 | *.ez 7 | /test/fixtures/ssl/ 8 | -------------------------------------------------------------------------------- /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 | 9 | config :logger, :console, 10 | colors: [enabled: false], 11 | metadata: [:request_id] 12 | end 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.5" 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 | -------------------------------------------------------------------------------- /.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-22.04 12 | env: 13 | MIX_ENV: test 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - pair: 19 | elixir: 1.14 20 | otp: 24.2 21 | - pair: 22 | elixir: 1.17 23 | otp: 27.3 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 | 49 | test_cowboy_latest: 50 | runs-on: ubuntu-22.04 51 | env: 52 | MIX_ENV: test 53 | strategy: 54 | fail-fast: false 55 | steps: 56 | - uses: actions/checkout@v2 57 | 58 | - uses: erlef/setup-beam@v1 59 | with: 60 | otp-version: 27.3 61 | elixir-version: 1.17 62 | 63 | - run: mix deps.unlock cowboy cowlib ranch && mix deps.get --only test 64 | 65 | - run: mix deps.compile 66 | 67 | - run: mix test 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v2.7.5 4 | 5 | ### Enhancements 6 | 7 | * Ensure path too large request errors are correctly translated 8 | 9 | ## v2.7.4 10 | 11 | ### Enhancements 12 | 13 | * Ensure errors from Ranch are correctly translated 14 | 15 | ## v2.7.3 16 | 17 | ### Enhancements 18 | 19 | * Ensure errors from Cowboy 2.13 are correctly translated 20 | 21 | ## v2.7.2 22 | 23 | ### Bug fixes 24 | 25 | * Ensure `crash_reason` in metadata is always a tuple 26 | 27 | ## v2.7.1 28 | 29 | ### Enhancements 30 | 31 | * Support Cowboy 2.11 32 | 33 | ## v2.7.0 34 | 35 | ### Enhancements 36 | 37 | * Do not allow Cowboy 2.11 due to backwards incompatible changes 38 | 39 | ## v2.6.2 40 | 41 | ### Enhancements 42 | 43 | * Fix warnings on Elixir v1.15+ 44 | 45 | ## v2.6.1 46 | 47 | ### Enhancements 48 | 49 | * Allow for opt-out of conn metadata on exception logs 50 | * Support `:check_interval` in drainer (in addition to `:drain_check_interval`) 51 | 52 | ## v2.6.0 53 | 54 | ### Enhancements 55 | 56 | * Support websocket upgrades 57 | * Require Plug v1.14+ and Elixir v1.10+ 58 | 59 | ## v2.5.2 60 | 61 | ### Enhancements 62 | 63 | * Fix warnings when running on telemetry 1.x 64 | 65 | ## v2.5.1 66 | 67 | ### Enhancements 68 | 69 | * Allow to configure which errors should be logged 70 | * Support telemetry 0.4.x or 1.x 71 | 72 | ## v2.5.0 73 | 74 | ### Enhancements 75 | 76 | * Return `:conn` as Logger metadata on translator 77 | * Support Ranch 2.0 78 | * Support the `:net` option so developers can work with keyword lists 79 | * Remove previously deprecated options 80 | 81 | ## v2.4.1 (2020-10-31) 82 | 83 | ### Bug fixes 84 | 85 | * Properly format linked exits 86 | 87 | ## v2.4.0 (2020-10-11) 88 | 89 | ### Bug fixes 90 | 91 | * Add [cowboy_telemetry](https://github.com/beam-telemetry/cowboy_telemetry/) as a dependency and enable it by default 92 | 93 | ## v2.3.0 (2020-06-11) 94 | 95 | Plug.Cowboy requires Elixir v1.7 or later. 96 | 97 | ### Bug fixes 98 | 99 | * 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. 100 | 101 | ## v2.2.2 (2020-05-25) 102 | 103 | ### Enhancements 104 | 105 | * Emit telemetry event for Cowboy early errors 106 | * Improve error messages for Cowboy early errors 107 | 108 | ## v2.2.1 (2020-04-21) 109 | 110 | ### Enhancements 111 | 112 | * Use proper telemetry metadata for exceptions 113 | 114 | ## v2.2.0 (2020-04-21) 115 | 116 | ### Enhancements 117 | 118 | * Include telemetry support 119 | 120 | ## v2.1.3 (2020-04-14) 121 | 122 | ### Bug fixes 123 | 124 | * Properly support the :options option before removal 125 | 126 | ## v2.1.2 (2020-01-28) 127 | 128 | ### Bug fixes 129 | 130 | * Properly deprecate the :timeout option before removal 131 | 132 | ## v2.1.1 (2020-01-08) 133 | 134 | ### Enhancement 135 | 136 | * Improve docs and simplify child spec API 137 | 138 | ## v2.1.0 (2019-06-27) 139 | 140 | ### Enhancement 141 | 142 | * Add `Plug.Cowboy.Drainer` for connection draining 143 | 144 | ## v2.0.2 (2019-03-18) 145 | 146 | ### Enhancements 147 | 148 | * Unwrap `Plug.Conn.WrapperError` on handler error 149 | * Include `crash_reason` as logger metadata 150 | 151 | ## v2.0.1 (2018-12-13) 152 | 153 | ### Bug fixes 154 | 155 | * Respect `:read_length` and `:read_timeout` in `read_body` with Cowboy 2 156 | 157 | ## v2.0.0 (2018-10-20) 158 | 159 | Extract `Plug.Adapters.Cowboy2` from Plug into `Plug.Cowboy` 160 | -------------------------------------------------------------------------------- /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/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 | crash_reason = 55 | case reason do 56 | {exception, _stack} when is_exception(exception) -> reason 57 | {{:nocatch, _value}, _stack} -> reason 58 | exit_reason -> {exit_reason, []} 59 | end 60 | 61 | metadata = 62 | [ 63 | crash_reason: crash_reason, 64 | domain: [:cowboy] 65 | ] ++ maybe_conn_metadata(conn) 66 | 67 | {:ok, message, metadata} 68 | else 69 | :skip 70 | end 71 | end 72 | 73 | defp translate_ranch(_min_level, ref, extra, pid, reason, stack) do 74 | {:ok, 75 | [ 76 | "Ranch protocol ", 77 | inspect(pid), 78 | " of listener ", 79 | inspect(ref), 80 | extra, 81 | " terminated\n" 82 | | Exception.format_exit({reason, stack}) 83 | ], crash_reason: {reason, stack}, domain: [:cowboy]} 84 | end 85 | 86 | defp log_exception?({%{__exception__: true} = exception, _}) do 87 | status_ranges = 88 | Application.get_env(:plug_cowboy, :log_exceptions_with_status_code, [500..599]) 89 | 90 | status = Plug.Exception.status(exception) 91 | 92 | Enum.any?(status_ranges, &(status in &1)) 93 | end 94 | 95 | defp log_exception?(_), do: true 96 | 97 | defp conn_info(_min_level, conn) do 98 | [server_info(conn), request_info(conn)] 99 | end 100 | 101 | defp server_info(%Plug.Conn{host: host, port: :undefined, scheme: scheme}) do 102 | ["Server: ", host, ?\s, ?(, Atom.to_string(scheme), ?), ?\n] 103 | end 104 | 105 | defp server_info(%Plug.Conn{host: host, port: port, scheme: scheme}) do 106 | ["Server: ", host, ":", Integer.to_string(port), ?\s, ?(, Atom.to_string(scheme), ?), ?\n] 107 | end 108 | 109 | defp request_info(%Plug.Conn{method: method, query_string: query_string} = conn) do 110 | ["Request: ", method, ?\s, path_to_iodata(conn.request_path, query_string), ?\n] 111 | end 112 | 113 | defp maybe_conn_metadata(conn) do 114 | if Application.get_env(:plug_cowboy, :conn_in_exception_metadata, true) do 115 | [conn: conn] 116 | else 117 | [] 118 | end 119 | end 120 | 121 | defp path_to_iodata(path, ""), do: path 122 | defp path_to_iodata(path, qs), do: [path, ??, qs] 123 | end 124 | -------------------------------------------------------------------------------- /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/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: 9898, protocol_options: @protocol_options) 31 | on_exit(fn -> :ok = Plug.Cowboy.shutdown(__MODULE__.HTTP) end) 32 | {:ok, port: 9898} 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 7 | "ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [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", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"}, 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.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [: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", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 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.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 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/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 | def call(%{path_info: ["exit"]}, _opts) do 23 | exit({:error, ["unfortunate shape"]}) 24 | end 25 | 26 | def call(%{path_info: ["throw"]}, _opts) do 27 | throw("catch!") 28 | end 29 | 30 | @metadata_log_opts format: {__MODULE__, :metadata}, metadata: [:conn, :crash_reason, :domain] 31 | 32 | def metadata(_log_level, _message, _timestamp, metadata) do 33 | inspect(metadata, limit: :infinity) 34 | end 35 | 36 | test "ranch/cowboy 500 logs" do 37 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9001) 38 | 39 | output = 40 | capture_log(fn -> 41 | :hackney.get("http://127.0.0.1:9001/error", [], "", []) 42 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 43 | end) 44 | 45 | assert output =~ ~r"#PID<0\.\d+\.0> running Plug\.Cowboy\.TranslatorTest \(.*\) terminated" 46 | assert output =~ "Server: 127.0.0.1:9001 (http)" 47 | assert output =~ "Request: GET /" 48 | assert output =~ "** (exit) an exception was raised:" 49 | assert output =~ "** (RuntimeError) oops" 50 | end 51 | 52 | test "ranch/cowboy non-500 skips" do 53 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9002) 54 | 55 | output = 56 | capture_log(fn -> 57 | :hackney.get("http://127.0.0.1:9002/warn", [], "", []) 58 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 59 | end) 60 | 61 | refute output =~ ~r"#PID<0\.\d+\.0> running Plug\.Cowboy\.TranslatorTest \(.*\) terminated" 62 | refute output =~ "Server: 127.0.0.1:9002 (http)" 63 | refute output =~ "Request: GET /" 64 | refute output =~ "** (exit) an exception was raised:" 65 | end 66 | 67 | test "ranch/cowboy logs configured statuses" do 68 | Application.put_env(:plug_cowboy, :log_exceptions_with_status_code, [400..499]) 69 | on_exit(fn -> Application.delete_env(:plug_cowboy, :log_exceptions_with_status_code) end) 70 | 71 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9002) 72 | 73 | output = 74 | capture_log(fn -> 75 | :hackney.get("http://127.0.0.1:9002/warn", [], "", []) 76 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 77 | end) 78 | 79 | assert output =~ ~r"#PID<0\.\d+\.0> running Plug\.Cowboy\.TranslatorTest \(.*\) terminated" 80 | assert output =~ "Server: 127.0.0.1:9002 (http)" 81 | assert output =~ "Request: GET /" 82 | assert output =~ "** (exit) an exception was raised:" 83 | assert output =~ "** (Plug.Parsers.UnsupportedMediaTypeError) unsupported media type foo/bar" 84 | 85 | output = 86 | capture_log(fn -> 87 | :hackney.get("http://127.0.0.1:9002/error", [], "", []) 88 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 89 | end) 90 | 91 | refute output =~ ~r"#PID<0\.\d+\.0> running Plug\.Cowboy\.TranslatorTest \(.*\) terminated" 92 | refute output =~ "Server: 127.0.0.1:9001 (http)" 93 | refute output =~ "Request: GET /" 94 | refute output =~ "** (exit) an exception was raised:" 95 | refute output =~ "** (RuntimeError) oops" 96 | end 97 | 98 | test "ranch/cowboy linked logs" do 99 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9003) 100 | 101 | output = 102 | capture_log(fn -> 103 | :hackney.get("http://127.0.0.1:9003/linked", [], "", []) 104 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 105 | end) 106 | 107 | assert output =~ 108 | ~r"Ranch protocol #PID<0\.\d+\.0> of listener Plug\.Cowboy\.TranslatorTest\.HTTP \(.*\) terminated" 109 | 110 | assert output =~ "exited in: GenServer.call" 111 | assert output =~ "** (EXIT) no process" 112 | end 113 | 114 | test "metadata in ranch/cowboy 500 logs" do 115 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9004) 116 | 117 | metadata = 118 | capture_log(@metadata_log_opts, fn -> 119 | :hackney.get("http://127.0.0.1:9004/error", [], "", []) 120 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 121 | end) 122 | 123 | assert metadata =~ "conn: %Plug.Conn{" 124 | assert metadata =~ "crash_reason:" 125 | assert metadata =~ "domain: [:cowboy]" 126 | end 127 | 128 | test "metadata opt-out ranch/cowboy 500 logs" do 129 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9004) 130 | Application.put_env(:plug_cowboy, :conn_in_exception_metadata, false) 131 | on_exit(fn -> Application.delete_env(:plug_cowboy, :conn_in_exception_metadata) end) 132 | 133 | metadata = 134 | capture_log(@metadata_log_opts, fn -> 135 | :hackney.get("http://127.0.0.1:9004/error", [], "", []) 136 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 137 | end) 138 | 139 | refute metadata =~ "conn: %Plug.Conn{" 140 | end 141 | 142 | test "metadata in ranch/cowboy linked logs" do 143 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9005) 144 | 145 | metadata = 146 | capture_log(@metadata_log_opts, fn -> 147 | :hackney.get("http://127.0.0.1:9005/linked", [], "", []) 148 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 149 | end) 150 | 151 | assert metadata =~ "crash_reason:" 152 | assert metadata =~ "{GenServer, :call" 153 | assert metadata =~ "domain: [:cowboy]" 154 | end 155 | 156 | test "metadata in ranch/cowboy exit logs" do 157 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9005) 158 | 159 | metadata = 160 | capture_log(@metadata_log_opts, fn -> 161 | :hackney.get("http://127.0.0.1:9005/exit", [], "", []) 162 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 163 | end) 164 | 165 | assert metadata =~ "crash_reason: {{:error, [\"unfortunate shape\"]}, []}" 166 | assert metadata =~ "domain: [:cowboy]" 167 | end 168 | 169 | test "metadata in ranch/cowboy throw logs" do 170 | {:ok, _pid} = Plug.Cowboy.http(__MODULE__, [], port: 9005) 171 | 172 | metadata = 173 | capture_log(@metadata_log_opts, fn -> 174 | :hackney.get("http://127.0.0.1:9005/throw", [], "", []) 175 | Plug.Cowboy.shutdown(__MODULE__.HTTP) 176 | end) 177 | 178 | assert metadata =~ "crash_reason: {{:nocatch, \"catch!\"}, " 179 | assert metadata =~ "domain: [:cowboy]" 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | %{ 436 | resp_status: 414, 437 | reason: {:connection_error, :limit_reached, specific_reason}, 438 | partial_req: partial_req 439 | }, 440 | _ 441 | ) do 442 | Logger.error(""" 443 | Cowboy returned 414 because the request path was too long. 444 | 445 | The more specific reason is: 446 | 447 | #{inspect(specific_reason)} 448 | 449 | You can customize those limits when configuring your http/https 450 | server. The configuration option and default values are shown below: 451 | 452 | protocol_options: [ 453 | max_request_line_length: 50_000 454 | ] 455 | 456 | Request info: 457 | 458 | peer: #{format_peer(partial_req.peer)} 459 | """) 460 | end 461 | 462 | def handle_event( 463 | [:cowboy, :request, :early_error], 464 | _, 465 | %{reason: {:connection_error, :limit_reached, specific_reason}, partial_req: partial_req}, 466 | _ 467 | ) do 468 | Logger.error(""" 469 | Cowboy returned 431 because it was unable to parse the request headers. 470 | 471 | This may happen because there are no headers, or there are too many headers 472 | or the header name or value are too large (such as a large cookie). 473 | 474 | More specific reason is: 475 | 476 | #{inspect(specific_reason)} 477 | 478 | You can customize those limits when configuring your http/https 479 | server. The configuration option and default values are shown below: 480 | 481 | protocol_options: [ 482 | max_header_name_length: 64, 483 | max_header_value_length: 4096, 484 | max_headers: 100 485 | ] 486 | 487 | Request info: 488 | 489 | peer: #{format_peer(partial_req.peer)} 490 | method: #{partial_req.method || ""} 491 | path: #{partial_req.path || ""} 492 | """) 493 | end 494 | 495 | def handle_event(_, _, _, _) do 496 | :ok 497 | end 498 | 499 | defp format_peer({addr, port}) do 500 | "#{:inet_parse.ntoa(addr)}:#{port}" 501 | end 502 | end 503 | -------------------------------------------------------------------------------- /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 | def telemetry_send(event, measurements, metadata, test) do 164 | send(test, {:telemetry, event, measurements, metadata}) 165 | end 166 | 167 | test "emits telemetry events for start/stop" do 168 | :telemetry.attach_many( 169 | :start_stop_test, 170 | [ 171 | [:cowboy, :request, :start], 172 | [:cowboy, :request, :stop], 173 | [:cowboy, :request, :exception] 174 | ], 175 | &__MODULE__.telemetry_send/4, 176 | self() 177 | ) 178 | 179 | assert {200, _, "TELEMETRY"} = request(:get, "/telemetry?foo=bar") 180 | 181 | assert_receive {:telemetry, [:cowboy, :request, :start], %{system_time: _}, 182 | %{streamid: _, req: req}} 183 | 184 | assert req.path == "/telemetry" 185 | 186 | assert_receive {:telemetry, [:cowboy, :request, :stop], %{duration: duration}, 187 | %{streamid: _, req: ^req}} 188 | 189 | duration_ms = System.convert_time_unit(duration, :native, :millisecond) 190 | 191 | assert duration_ms >= 30 192 | assert duration_ms < 100 193 | 194 | refute_received {:telemetry, [:cowboy, :request, :exception], _, _} 195 | 196 | :telemetry.detach(:start_stop_test) 197 | end 198 | 199 | @tag :capture_log 200 | test "emits telemetry events for exception" do 201 | :telemetry.attach_many( 202 | :exception_test, 203 | [ 204 | [:cowboy, :request, :start], 205 | [:cowboy, :request, :exception] 206 | ], 207 | &__MODULE__.telemetry_send/4, 208 | self() 209 | ) 210 | 211 | request(:get, "/telemetry_exception") 212 | 213 | assert_receive {:telemetry, [:cowboy, :request, :start], _, _} 214 | 215 | assert_receive {:telemetry, [:cowboy, :request, :exception], %{}, 216 | %{kind: :exit, reason: _reason, stacktrace: _stacktrace}} 217 | 218 | :telemetry.detach(:exception_test) 219 | end 220 | 221 | test "emits telemetry events for cowboy early_error" do 222 | :telemetry.attach( 223 | :early_error_test, 224 | [:cowboy, :request, :early_error], 225 | &__MODULE__.telemetry_send/4, 226 | self() 227 | ) 228 | 229 | assert capture_log(fn -> 230 | cookie = "bar=" <> String.duplicate("a", 8_000_000) 231 | response = request(:get, "/headers", [{"cookie", cookie}]) 232 | assert match?({431, _, _}, response) or match?({:error, :closed}, response) 233 | assert {200, _, _} = request(:get, "/headers", [{"foo", "bar"}, {"baz", "bat"}]) 234 | end) =~ "Cowboy returned 431 because it was unable to parse the request headers" 235 | 236 | assert_receive {:telemetry, [:cowboy, :request, :early_error], 237 | %{ 238 | system_time: _ 239 | }, 240 | %{ 241 | reason: {:connection_error, :limit_reached, _}, 242 | partial_req: %{} 243 | }} 244 | 245 | :telemetry.detach(:early_error_test) 246 | end 247 | 248 | test "emits telemetry events for cowboy early_error for paths that are too long" do 249 | :telemetry.attach( 250 | :early_error_test, 251 | [:cowboy, :request, :early_error], 252 | &__MODULE__.telemetry_send/4, 253 | self() 254 | ) 255 | 256 | assert capture_log(fn -> 257 | # Send a request line that's too long (exceeds max_request_line_length) 258 | long_path = String.duplicate("a", 10_000) 259 | response = request(:get, "/#{long_path}") 260 | assert match?({414, _, _}, response) or match?({:error, :closed}, response) 261 | end) =~ "Cowboy returned 414 because the request path was too long" 262 | 263 | assert_receive {:telemetry, [:cowboy, :request, :early_error], 264 | %{ 265 | system_time: _ 266 | }, 267 | %{ 268 | reason: {:connection_error, :limit_reached, _}, 269 | partial_req: %{} 270 | }} 271 | 272 | :telemetry.detach(:early_error_test) 273 | end 274 | 275 | def send_200(conn) do 276 | assert conn.state == :unset 277 | assert conn.resp_body == nil 278 | conn = send_resp(conn, 200, "OK") 279 | assert conn.state == :sent 280 | assert conn.resp_body == nil 281 | conn 282 | end 283 | 284 | def send_418(conn) do 285 | send_resp(conn, 418, "") 286 | end 287 | 288 | def send_998(conn) do 289 | send_resp(conn, 998, "") 290 | end 291 | 292 | def send_500(conn) do 293 | conn 294 | |> delete_resp_header("cache-control") 295 | |> put_resp_header("x-sample", "value") 296 | |> send_resp(500, ["ERR", ["OR"]]) 297 | end 298 | 299 | test "sends a response with status, headers and body" do 300 | assert {200, headers, "OK"} = request(:get, "/send_200") 301 | 302 | assert List.keyfind(headers, "cache-control", 0) == 303 | {"cache-control", "max-age=0, private, must-revalidate"} 304 | 305 | assert {500, headers, "ERROR"} = request(:get, "/send_500") 306 | assert List.keyfind(headers, "cache-control", 0) == nil 307 | assert List.keyfind(headers, "x-sample", 0) == {"x-sample", "value"} 308 | end 309 | 310 | test "allows customized statuses based on config" do 311 | assert {998, _headers, ""} = request(:get, "/send_998") 312 | {:ok, ref} = :hackney.get("http://127.0.0.1:8003/send_998", [], "", async: :once) 313 | assert_receive({:hackney_response, ^ref, {:status, 998, "Not An RFC Status Code"}}) 314 | :hackney.close(ref) 315 | end 316 | 317 | test "existing statuses can be customized" do 318 | assert {418, _headers, ""} = request(:get, "/send_418") 319 | {:ok, ref} = :hackney.get("http://127.0.0.1:8003/send_418", [], "", async: :once) 320 | assert_receive({:hackney_response, ^ref, {:status, 418, "Totally not a teapot"}}) 321 | :hackney.close(ref) 322 | end 323 | 324 | test "skips body on head" do 325 | assert {200, _, nil} = request(:head, "/send_200") 326 | end 327 | 328 | def send_file(conn) do 329 | conn = send_file(conn, 200, __ENV__.file) 330 | assert conn.state == :file 331 | assert conn.resp_body == nil 332 | conn 333 | end 334 | 335 | test "sends a file with status and headers" do 336 | assert {200, headers, body} = request(:get, "/send_file") 337 | assert body =~ "sends a file with status and headers" 338 | 339 | assert List.keyfind(headers, "cache-control", 0) == 340 | {"cache-control", "max-age=0, private, must-revalidate"} 341 | 342 | assert List.keyfind(headers, "content-length", 0) == 343 | { 344 | "content-length", 345 | __ENV__.file |> File.stat!() |> Map.fetch!(:size) |> Integer.to_string() 346 | } 347 | end 348 | 349 | test "skips file on head" do 350 | assert {200, _, nil} = request(:head, "/send_file") 351 | end 352 | 353 | def send_chunked(conn) do 354 | conn = send_chunked(conn, 200) 355 | assert conn.state == :chunked 356 | {:ok, conn} = chunk(conn, "HELLO\n") 357 | {:ok, conn} = chunk(conn, ["WORLD", ["\n"]]) 358 | conn 359 | end 360 | 361 | test "sends a chunked response with status and headers" do 362 | assert {200, headers, "HELLO\nWORLD\n"} = request(:get, "/send_chunked") 363 | 364 | assert List.keyfind(headers, "cache-control", 0) == 365 | {"cache-control", "max-age=0, private, must-revalidate"} 366 | 367 | assert List.keyfind(headers, "transfer-encoding", 0) == {"transfer-encoding", "chunked"} 368 | end 369 | 370 | def inform(conn) do 371 | conn 372 | |> inform(103, [{"link", "; rel=preload; as=style"}]) 373 | |> send_resp(200, "inform") 374 | end 375 | 376 | test "inform will not raise even though the adapter doesn't implement it" do 377 | # the _body in this response is actually garbled. this is a bug in the HTTP/1.1 client and not in plug 378 | assert {103, [{"link", "; rel=preload; as=style"}], _body} = 379 | request(:get, "/inform") 380 | end 381 | 382 | def upgrade_unsupported(conn) do 383 | conn 384 | |> upgrade_adapter(:unsupported, opt: :unsupported) 385 | end 386 | 387 | test "upgrade will not set the response" do 388 | assert {500, _, body} = request(:get, "/upgrade_unsupported") 389 | assert body =~ "upgrade to unsupported not supported by Plug.Cowboy.Conn" 390 | end 391 | 392 | defmodule NoopWebSocketHandler do 393 | @behaviour :cowboy_websocket 394 | 395 | # We never actually call this; it's just here to quell compiler warnings 396 | @impl true 397 | def init(req, state), do: {:cowboy_websocket, req, state} 398 | 399 | @impl true 400 | def websocket_handle(_frame, state), do: {:ok, state} 401 | 402 | @impl true 403 | def websocket_info(_msg, state), do: {:ok, state} 404 | end 405 | 406 | def upgrade_websocket(conn) do 407 | # In actual use, it's the caller's responsibility to ensure the upgrade is valid before 408 | # calling upgrade_adapter 409 | conn 410 | |> upgrade_adapter(:websocket, {NoopWebSocketHandler, [], %{}}) 411 | end 412 | 413 | test "upgrades the connection when the connection is a valid websocket" do 414 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 8003, active: false, mode: :binary) 415 | 416 | :gen_tcp.send(socket, """ 417 | GET /upgrade_websocket HTTP/1.1\r 418 | Host: server.example.com\r 419 | Upgrade: websocket\r 420 | Connection: Upgrade\r 421 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r 422 | Sec-WebSocket-Version: 13\r 423 | \r 424 | """) 425 | 426 | {:ok, response} = :gen_tcp.recv(socket, 234) 427 | 428 | assert [ 429 | "HTTP/1.1 101 Switching Protocols", 430 | "cache-control: max-age=0, private, must-revalidate", 431 | "connection: Upgrade", 432 | "date: " <> _date, 433 | "sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", 434 | "server: Cowboy", 435 | "upgrade: websocket", 436 | "", 437 | "" 438 | ] = String.split(response, "\r\n") 439 | end 440 | 441 | test "returns error in cases where an upgrade is indicated but the connection is not a valid upgrade" do 442 | assert {426, _headers, ""} = request(:get, "/upgrade_websocket") 443 | end 444 | 445 | def push(conn) do 446 | conn 447 | |> push("/static/assets.css") 448 | |> send_resp(200, "push") 449 | end 450 | 451 | test "push will not raise even though the adapter doesn't implement it" do 452 | assert {200, _headers, "push"} = request(:get, "/push") 453 | end 454 | 455 | def push_or_raise(conn) do 456 | conn 457 | |> push!("/static/assets.css") 458 | |> send_resp(200, "push or raise") 459 | end 460 | 461 | test "push will raise because it is not implemented" do 462 | assert {200, _headers, "push or raise"} = request(:get, "/push_or_raise") 463 | end 464 | 465 | def read_req_body(conn) do 466 | expected = :binary.copy("abcdefghij", 100_000) 467 | assert {:ok, ^expected, conn} = read_body(conn) 468 | assert {:ok, "", conn} = read_body(conn) 469 | resp(conn, 200, "ok") 470 | end 471 | 472 | def read_req_body_partial(conn) do 473 | # Read something even with no length 474 | assert {:more, body, conn} = read_body(conn, length: 0, read_length: 1_000) 475 | assert byte_size(body) > 0 476 | assert {:more, body, conn} = read_body(conn, length: 5_000, read_length: 1_000) 477 | assert byte_size(body) > 0 478 | assert {:more, body, conn} = read_body(conn, length: 20_000, read_length: 1_000) 479 | assert byte_size(body) > 0 480 | assert {:ok, body, conn} = read_body(conn, length: 2_000_000) 481 | assert byte_size(body) > 0 482 | 483 | # Once it is over, always returns :ok 484 | assert {:ok, "", conn} = read_body(conn, length: 2_000_000) 485 | assert {:ok, "", conn} = read_body(conn, length: 0) 486 | 487 | resp(conn, 200, "ok") 488 | end 489 | 490 | test "reads body" do 491 | body = :binary.copy("abcdefghij", 100_000) 492 | assert {200, _, "ok"} = request(:post, "/read_req_body_partial", [], body) 493 | assert {200, _, "ok"} = request(:get, "/read_req_body", [], body) 494 | assert {200, _, "ok"} = request(:post, "/read_req_body", [], body) 495 | end 496 | 497 | def multipart(conn) do 498 | opts = Plug.Parsers.init(parsers: [Plug.Parsers.MULTIPART], length: 8_000_000) 499 | conn = Plug.Parsers.call(conn, opts) 500 | assert conn.params["name"] == "hello" 501 | assert conn.params["status"] == ["choice1", "choice2"] 502 | assert conn.params["empty"] == nil 503 | 504 | assert %Plug.Upload{} = file = conn.params["pic"] 505 | assert File.read!(file.path) == "hello\n\n" 506 | assert file.content_type == "text/plain" 507 | assert file.filename == "foo.txt" 508 | 509 | resp(conn, 200, "ok") 510 | end 511 | 512 | test "parses multipart requests" do 513 | multipart = """ 514 | ------w58EW1cEpjzydSCq\r 515 | Content-Disposition: form-data; name=\"name\"\r 516 | \r 517 | hello\r 518 | ------w58EW1cEpjzydSCq\r 519 | Content-Disposition: form-data; name=\"pic\"; filename=\"foo.txt\"\r 520 | Content-Type: text/plain\r 521 | \r 522 | hello 523 | 524 | \r 525 | ------w58EW1cEpjzydSCq\r 526 | Content-Disposition: form-data; name=\"empty\"; filename=\"\"\r 527 | Content-Type: application/octet-stream\r 528 | \r 529 | \r 530 | ------w58EW1cEpjzydSCq\r 531 | Content-Disposition: form-data; name="status[]"\r 532 | \r 533 | choice1\r 534 | ------w58EW1cEpjzydSCq\r 535 | Content-Disposition: form-data; name="status[]"\r 536 | \r 537 | choice2\r 538 | ------w58EW1cEpjzydSCq\r 539 | Content-Disposition: form-data; name=\"commit\"\r 540 | \r 541 | Create User\r 542 | ------w58EW1cEpjzydSCq--\r 543 | """ 544 | 545 | headers = [ 546 | {"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"}, 547 | {"Content-Length", byte_size(multipart)} 548 | ] 549 | 550 | assert {200, _, _} = request(:post, "/multipart", headers, multipart) 551 | assert {200, _, _} = request(:post, "/multipart?name=overriden", headers, multipart) 552 | end 553 | 554 | def file_too_big(conn) do 555 | opts = Plug.Parsers.init(parsers: [Plug.Parsers.MULTIPART], length: 5) 556 | conn = Plug.Parsers.call(conn, opts) 557 | 558 | assert %Plug.Upload{} = file = conn.params["pic"] 559 | assert File.read!(file.path) == "hello\n\n" 560 | assert file.content_type == "text/plain" 561 | assert file.filename == "foo.txt" 562 | 563 | resp(conn, 200, "ok") 564 | end 565 | 566 | test "returns parse error when file pushed the boundaries in multipart requests" do 567 | multipart = """ 568 | ------w58EW1cEpjzydSCq\r 569 | Content-Disposition: form-data; name=\"pic\"; filename=\"foo.txt\"\r 570 | Content-Type: text/plain\r 571 | \r 572 | hello 573 | 574 | \r 575 | ------w58EW1cEpjzydSCq--\r 576 | """ 577 | 578 | headers = [ 579 | {"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"}, 580 | {"Content-Length", byte_size(multipart)} 581 | ] 582 | 583 | assert {500, _, body} = request(:post, "/file_too_big", headers, multipart) 584 | assert body =~ "the request is too large" 585 | end 586 | 587 | test "validates utf-8 on multipart requests" do 588 | multipart = """ 589 | ------w58EW1cEpjzydSCq\r 590 | Content-Disposition: form-data; name=\"name\"\r 591 | \r 592 | #{<<139>>}\r 593 | ------w58EW1cEpjzydSCq\r 594 | """ 595 | 596 | headers = [ 597 | {"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"}, 598 | {"Content-Length", byte_size(multipart)} 599 | ] 600 | 601 | assert {500, _, body} = request(:post, "/multipart", headers, multipart) 602 | assert body =~ "invalid UTF-8 on multipart body, got byte 139" 603 | end 604 | 605 | test "returns parse error when body is badly formatted in multipart requests" do 606 | multipart = """ 607 | ------w58EW1cEpjzydSCq\r 608 | Content-Disposition: form-data; name=\"name\"\r 609 | ------w58EW1cEpjzydSCq\r 610 | """ 611 | 612 | headers = [ 613 | {"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"}, 614 | {"Content-Length", byte_size(multipart)} 615 | ] 616 | 617 | assert {500, _, body} = request(:post, "/multipart", headers, multipart) 618 | 619 | assert body =~ 620 | "malformed request, a RuntimeError exception was raised with message \"invalid multipart" 621 | 622 | multipart = """ 623 | ------w58EW1cEpjzydSCq\r 624 | Content-Disposition: form-data; name=\"name\"\r 625 | \r 626 | hello 627 | """ 628 | 629 | headers = [ 630 | {"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"}, 631 | {"Content-Length", byte_size(multipart)} 632 | ] 633 | 634 | assert {500, _, body} = request(:post, "/multipart", headers, multipart) 635 | 636 | assert body =~ 637 | "malformed request, a RuntimeError exception was raised with message \"invalid multipart" 638 | end 639 | 640 | def http2(conn) do 641 | case conn.query_string do 642 | "noinfer" <> _ -> 643 | conn 644 | |> push("/static/assets.css", [{"accept", "text/plain"}]) 645 | |> send_resp(200, Atom.to_string(get_http_protocol(conn))) 646 | 647 | "earlyhints" <> _ -> 648 | conn 649 | |> inform(:early_hints, [{"link", "; rel=preload; as=style"}]) 650 | |> send_resp(200, Atom.to_string(get_http_protocol(conn))) 651 | 652 | _ -> 653 | conn 654 | |> push("/static/assets.css") 655 | |> send_resp(200, Atom.to_string(get_http_protocol(conn))) 656 | end 657 | end 658 | 659 | def peer_data(conn) do 660 | assert conn.scheme == :https 661 | %{address: address, port: port, ssl_cert: ssl_cert} = get_peer_data(conn) 662 | assert address == {127, 0, 0, 1} 663 | assert is_integer(port) 664 | assert is_binary(ssl_cert) 665 | send_resp(conn, 200, "OK") 666 | end 667 | 668 | test "exposes peer data" do 669 | pool = :client_ssl_pool 670 | pool_opts = [timeout: 150_000, max_connections: 10] 671 | :ok = :hackney_pool.start_pool(pool, pool_opts) 672 | 673 | opts = [ 674 | pool: :client_ssl_pool, 675 | ssl_options: [server_name_indication: ~c"localhost"] ++ @client_ssl_opts 676 | ] 677 | 678 | assert {:ok, 200, _headers, client} = 679 | :hackney.get("https://127.0.0.1:8004/peer_data", [], "", opts) 680 | 681 | assert {:ok, "OK"} = :hackney.body(client) 682 | :hackney.close(client) 683 | end 684 | 685 | ## Helpers 686 | 687 | defp request(:head = verb, path) do 688 | {:ok, status, headers} = :hackney.request(verb, "http://127.0.0.1:8003" <> path, [], "", []) 689 | {status, headers, nil} 690 | end 691 | 692 | defp request(verb, path, headers \\ [], body \\ "") do 693 | case :hackney.request(verb, "http://127.0.0.1:8003" <> path, headers, body, []) do 694 | {:ok, status, headers, client} -> 695 | {:ok, body} = :hackney.body(client) 696 | :hackney.close(client) 697 | {status, headers, body} 698 | 699 | {:error, _} = error -> 700 | error 701 | end 702 | end 703 | end 704 | --------------------------------------------------------------------------------