zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

commit 21ae3b43bcbeb1baa2a53633e28b395dd92b673e
parent b17125d85520a2e4f043777be6439c6e0ca0878b
Author: srfsh <dev@srf.sh>
Date:   Tue,  8 Nov 2022 14:20:11 +0300

dep: update plug_cowboy to v2.6.0

Diffstat:
M.deps/plug/.hex | 0
M.deps/plug/CHANGELOG.md | 19+++++++++++++++++++
M.deps/plug/README.md | 39+++++++++++++++++++++++++++------------
M.deps/plug/hex_metadata.config | 4++--
M.deps/plug/lib/plug/adapters/test/conn.ex | 10++++++++++
M.deps/plug/lib/plug/builder.ex | 6+++---
M.deps/plug/lib/plug/conn.ex | 274++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
M.deps/plug/lib/plug/conn/adapter.ex | 14++++++++++++++
M.deps/plug/lib/plug/conn/query.ex | 31++++++++++++++++++++++---------
M.deps/plug/lib/plug/conn/status.ex | 2+-
M.deps/plug/lib/plug/conn/wrapper_error.ex | 5+----
M.deps/plug/lib/plug/debugger.ex | 94++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
M.deps/plug/lib/plug/mime.ex | 2+-
M.deps/plug/lib/plug/parsers.ex | 4++--
M.deps/plug/lib/plug/parsers/multipart.ex | 4++--
M.deps/plug/lib/plug/ssl.ex | 14+++++++++++---
M.deps/plug/lib/plug/static.ex | 2+-
M.deps/plug/lib/plug/templates/debugger.html.eex | 102++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
M.deps/plug/lib/plug/templates/debugger.md.eex | 18++++++++++++++++++
M.deps/plug/lib/plug/test.ex | 28++++++++++++++++++++++++++++
M.deps/plug/lib/plug/upload.ex | 44++++++++++++++++++--------------------------
M.deps/plug/mix.exs | 6+++---
M.deps/plug_cowboy/.hex | 0
M.deps/plug_cowboy/CHANGELOG.md | 7+++++++
M.deps/plug_cowboy/hex_metadata.config | 6+++---
M.deps/plug_cowboy/lib/plug/cowboy.ex | 23+++++++++++++++++++++++
M.deps/plug_cowboy/lib/plug/cowboy/conn.ex | 17+++++++++++++++++
M.deps/plug_cowboy/lib/plug/cowboy/handler.ex | 25++++++++++++++++++++-----
M.deps/plug_cowboy/mix.exs | 10+++++-----
Mmix.exs | 2+-
Mmix.lock | 4++--
31 files changed, 613 insertions(+), 203 deletions(-)

diff --git a/.deps/plug/.hex b/.deps/plug/.hex Binary files differ. diff --git a/.deps/plug/CHANGELOG.md b/.deps/plug/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## v1.14.0 (2022-10-31) + +Require Elixir v1.10+. + +### Enhancements + + * Add `Plug.Conn.prepend_req_headers/2` and `Plug.Conn.merge_req_headers/2` + * Support adapter upgrades with `Plug.Conn.upgrade_adapter/3` + * Add "Copy to Markdown" button in exception page + * Support exclusive use of tlsv1.3 + +### Bug fixes + + * Make sure last parameter works within maps + +### Deprecations + + * Deprecate server pushes as they are no longer supported by browsers + ## v1.13.6 (2022-04-14) ### Bug fixes diff --git a/.deps/plug/README.md b/.deps/plug/README.md @@ -25,6 +25,8 @@ end ## Hello world ```elixir +Mix.install([:plug, :plug_cowboy]) + defmodule MyPlug do import Plug.Conn @@ -39,25 +41,36 @@ defmodule MyPlug do |> send_resp(200, "Hello world") end end -``` -The snippet above shows a very simple example on how to use Plug. Save that snippet to a file and run it inside the plug application with: +require Logger +{:ok, _} = Plug.Cowboy.http(MyPlug, []) +Logger.info("Plug now running on localhost:4000") +``` - $ iex -S mix - iex> c "path/to/file.ex" - [MyPlug] - iex> {:ok, _} = Plug.Cowboy.http MyPlug, [] - {:ok, #PID<...>} +The snippet above shows a very simple example on how to use Plug. Save that snippet to a file and execute it as `elixir --no-halt hello_world.exs`. Access <http://localhost:4000/> and you should be greeted! -Access [http://localhost:4000/](http://localhost:4000/) and we are done! For now, we have directly started the server in our terminal but, for production deployments, you likely want to start it in your supervision tree. See the [Supervised handlers](#supervised-handlers) section next. +For now, we have directly started the server in a single file but, for production deployments, you likely want to start it in your supervision tree. See the [Supervised handlers](#supervised-handlers) section next. ## Supervised handlers On a production system, you likely want to start your Plug pipeline under your application's supervision tree. Start a new Elixir project with the `--sup` flag: - $ mix new my_app --sup +```shell +$ mix new my_app --sup +``` + +Add both `:plug` and `:plug_cowboy` as dependencies in your `mix.exs`: + +```elixir +def deps do + [ + {:plug, "~> 1.13"}, + {:plug_cowboy, "~> 2.0"} + ] +end +``` -and then update `lib/my_app/application.ex` as follows: +Now update `lib/my_app/application.ex` as follows: ```elixir defmodule MyApp.Application do @@ -81,12 +94,14 @@ defmodule MyApp.Application do end ``` -Now run `mix run --no-halt` and it will start your application with a web server running at `localhost:4001`. +Finally create `lib/my_app/my_plug.ex` with the `MyPlug` module. + +Now run `mix run --no-halt` and it will start your application with a web server running at <http://localhost:4001>. ## Supported Versions | Branch | Support | -| ------ | ------------------------ | +|--------|--------------------------| | v1.13 | Bug fixes | | v1.12 | Security patches only | | v1.11 | Security patches only | diff --git a/.deps/plug/hex_metadata.config b/.deps/plug/hex_metadata.config @@ -1,7 +1,7 @@ {<<"app">>,<<"plug">>}. {<<"build_tools">>,[<<"mix">>]}. {<<"description">>,<<"Compose web applications with functions">>}. -{<<"elixir">>,<<"~> 1.7">>}. +{<<"elixir">>,<<"~> 1.10">>}. {<<"files">>, [<<"lib">>,<<"lib/plug">>,<<"lib/plug/html.ex">>, <<"lib/plug/csrf_protection.ex">>,<<"lib/plug/rewrite_on.ex">>, @@ -48,4 +48,4 @@ {<<"optional">>,false}, {<<"repository">>,<<"hexpm">>}, {<<"requirement">>,<<"~> 0.4.3 or ~> 1.0">>}]]}. -{<<"version">>,<<"1.13.6">>}. +{<<"version">>,<<"1.14.0">>}. diff --git a/.deps/plug/lib/plug/adapters/test/conn.ex b/.deps/plug/lib/plug/adapters/test/conn.ex @@ -119,6 +119,16 @@ defmodule Plug.Adapters.Test.Conn do :ok end + def upgrade(%{owner: owner, ref: ref}, :not_supported = protocol, opts) do + send(owner, {ref, :upgrade, {protocol, opts}}) + {:error, :not_supported} + end + + def upgrade(%{owner: owner, ref: ref} = state, protocol, opts) do + send(owner, {ref, :upgrade, {protocol, opts}}) + {:ok, state} + end + def push(%{owner: owner, ref: ref}, path, headers) do send(owner, {ref, :push, {path, headers}}) :ok diff --git a/.deps/plug/lib/plug/builder.ex b/.deps/plug/lib/plug/builder.ex @@ -339,9 +339,9 @@ defmodule Plug.Builder do end defp quote_plug({:module, plug, opts, guards}, :compile, acc, env, builder_opts) do - # require no longer adds a compile time dependency, which is - # required by Plug.Builder. So we build the alias an we expand it. - parts = [:Elixir | Enum.map(Module.split(plug), &String.to_atom/1)] + # Elixir v1.13/1.14 do not add a compile time dependency on require, + # so we build the alias and expand it to simulate the behaviour. + parts = [:"Elixir" | Enum.map(Module.split(plug), &String.to_atom/1)] alias = {:__aliases__, [line: env.line], parts} _ = Macro.expand(alias, env) diff --git a/.deps/plug/lib/plug/conn.ex b/.deps/plug/lib/plug/conn.ex @@ -46,6 +46,41 @@ defmodule Plug.Conn do `:body_params` on top of `:query_params` * `req_cookies` - the request cookies (without the response ones) + ## Session vs Assigns + + HTTP is stateless. + This means that a server begins each request cycle with no knowledge about + the client except the request itself. + Its response may include one or more `"Set-Cookie"` headers, asking the client + to send that value back in a `"Cookie"` header on subsequent requests. + This is the basis for stateful interactions with a client, so that the server + can remember the client's name, the contents of their shopping cart, and so on. + + In Plug, a "session" is a place to store data that persists from one request + to the next. + Typically, this data is stored in a cookie using `Plug.Session.COOKIE`. + A minimal approach would be to store only a user's id in the session, then + use that during the request cycle to look up other information (in a database + or elsewhere). + More can be stored in a session cookie, but be careful: this makes requests + and responses heavier, and clients may reject cookies beyond a certain size. + Also, whatever is stored in a session cookie is not shared between a user's + different browsers or devices. + + If the session is stored elsewhere, such as with `Plug.Session.ETS`, + something like a user id would still be needed to look it up on each request. + + Unlike data in a session, data in the `assigns` field lasts only for a single + request. + A typical use case would be for an authentication plug to look up a + user by id and store the user's details in the assigns for later plugs to + access during the same request. + When the next request happens, this data will be gone. + + To summarize: `assigns` is for storing data to be accessed during the current + request, and the session is for storing data to be accessed in subsequent + requests. + ## Response fields These fields contain response information: @@ -73,8 +108,8 @@ defmodule Plug.Conn do The connection state is used to track the connection lifecycle. It starts as `:unset` but is changed to `:set` (via `resp/3`) or `:set_chunked` (used only for `before_send` callbacks by `send_chunked/2`) or `:file` - (when invoked via `send_file/3`). Its final result is `:sent`, `:file` or - `:chunked` depending on the response model. + (when invoked via `send_file/3`). Its final result is `:sent`, `:file`, `:chunked` + or `:upgraded` depending on the response model. ## Private fields @@ -115,6 +150,17 @@ defmodule Plug.Conn do Even though 404 has been overridden, the `:not_found` atom can still be used to set the status to 404 as well as the new atom `:actually_this_was_found` inflected from the reason phrase "Actually This Was Found". + + ## Protocol Upgrades + + Plug provides basic support for protocol upgrades via the `upgrade_adapter/3` + function to facilitate connection upgrades to protocols such as WebSockets. + As the name suggests, this functionality is adapter-dependent and the + functionality & requirements of a given upgrade require explicit coordination + between a Plug application & the underlying adapter. Plug provides upgrade + related functionality only to the extent necessary to allow a Plug application + to request protocol upgrades from the underlying adapter. See the documentation + for `upgrade_adapter/3` for details. """ @type adapter :: {module, term} @@ -137,7 +183,7 @@ defmodule Plug.Conn do @type scheme :: :http | :https @type secret_key_base :: binary | nil @type segments :: [binary] - @type state :: :unset | :set | :set_chunked | :set_file | :file | :chunked | :sent + @type state :: :unset | :set | :set_chunked | :set_file | :file | :chunked | :sent | :upgraded @type status :: atom | int_status @type t :: %__MODULE__{ @@ -355,7 +401,7 @@ defmodule Plug.Conn do atoms is available in `Plug.Conn.Status`. Raises a `Plug.Conn.AlreadySentError` if the connection has already been - `:sent` or `:chunked`. + `:sent`, `:chunked` or `:upgraded`. ## Examples @@ -364,7 +410,10 @@ defmodule Plug.Conn do """ @spec put_status(t, status) :: t - def put_status(%Conn{state: :sent}, _status), do: raise(AlreadySentError) + def put_status(%Conn{state: state}, _status) when state not in @unsent do + raise AlreadySentError + end + def put_status(%Conn{} = conn, nil), do: %{conn | status: nil} def put_status(%Conn{} = conn, status), do: %{conn | status: Plug.Conn.Status.code(status)} @@ -373,7 +422,7 @@ defmodule Plug.Conn do It expects the connection state to be `:set`, otherwise raises an `ArgumentError` for `:unset` connections or a `Plug.Conn.AlreadySentError` for - already `:sent` connections. + already `:sent`, `:chunked` or `:upgraded` connections. At the end sets the connection state to `:sent`. @@ -416,7 +465,7 @@ defmodule Plug.Conn do If available, the file is sent directly over the socket using the operating system `sendfile` operation. - It expects a connection that has not been `:sent` yet and sets its + It expects a connection that has not been `:sent`, `:chunked` or `:upgraded` yet and sets its state to `:file` afterwards. Otherwise raises `Plug.Conn.AlreadySentError`. ## Examples @@ -459,7 +508,7 @@ defmodule Plug.Conn do @doc """ Sends the response headers as a chunked response. - It expects a connection that has not been `:sent` yet and sets its + It expects a connection that has not been `:sent` or `:upgraded` yet and sets its state to `:chunked` afterwards. Otherwise, raises `Plug.Conn.AlreadySentError`. After `send_chunked/2` is called, chunks can be sent to the client via the `chunk/2` function. @@ -555,7 +604,7 @@ defmodule Plug.Conn do Sets the response to the given `status` and `body`. It sets the connection state to `:set` (if not already `:set`) - and raises `Plug.Conn.AlreadySentError` if it was already `:sent`. + and raises `Plug.Conn.AlreadySentError` if it was already `:sent`, `:chunked` or `:upgraded`. If you also want to send the response, use `send_resp/1` after this or use `send_resp/3`. @@ -620,10 +669,96 @@ defmodule Plug.Conn do for {^key, value} <- headers, do: value end + @doc ~S""" + Prepends the list of headers to the connection request headers. + + Similar to `put_req_header` this functions adds a new request header + (`key`) but rather than replacing the existing one it prepends another + header with the same `key`. + + The "host" header will be overridden by `conn.host` and should not be set + with this method. Instead, do `%Plug.Conn{conn | host: value}`. + + Because header keys are case-insensitive in both HTTP/1.1 and HTTP/2, + it is recommended for header keys to be in lowercase, to avoid sending + duplicate keys in a request. + Additionally, requests with mixed-case headers served over HTTP/2 are not + considered valid by common clients, resulting in dropped requests. + As a convenience, when using the `Plug.Adapters.Conn.Test` adapter, any + headers that aren't lowercase will raise a `Plug.Conn.InvalidHeaderError`. + + Raises a `Plug.Conn.AlreadySentError` if the connection has already been + `:sent`, `:chunked` or `:upgraded`. + + ## Examples + + Plug.Conn.prepend_req_headers(conn, [{"accept", "application/json"}]) + + """ + @spec prepend_req_headers(t, headers) :: t + def prepend_req_headers(conn, headers) + + def prepend_req_headers(%Conn{state: state}, _headers) when state not in @unsent do + raise AlreadySentError + end + + def prepend_req_headers(%Conn{adapter: adapter, req_headers: req_headers} = conn, headers) + when is_list(headers) do + for {key, _value} <- headers do + validate_req_header!(adapter, key) + end + + %{conn | req_headers: headers ++ req_headers} + end + + @doc """ + Merges a series of request headers into the connection. + + The "host" header will be overridden by `conn.host` and should not be set + with this method. Instead, do `%Plug.Conn{conn | host: value}`. + + Because header keys are case-insensitive in both HTTP/1.1 and HTTP/2, + it is recommended for header keys to be in lowercase, to avoid sending + duplicate keys in a request. + Additionally, requests with mixed-case headers served over HTTP/2 are not + considered valid by common clients, resulting in dropped requests. + As a convenience, when using the `Plug.Adapters.Conn.Test` adapter, any + headers that aren't lowercase will raise a `Plug.Conn.InvalidHeaderError`. + + ## Example + + Plug.Conn.merge_req_headers(conn, [{"accept", "text/plain"}, {"X-1337", "5P34K"}]) + + """ + @spec merge_req_headers(t, Enum.t()) :: t + def merge_req_headers(conn, headers) + + def merge_req_headers(%Conn{state: state}, _headers) when state not in @unsent do + raise AlreadySentError + end + + def merge_req_headers(conn, headers) when headers == %{} do + conn + end + + def merge_req_headers(%Conn{req_headers: current, adapter: adapter} = conn, headers) do + headers = + Enum.reduce(headers, current, fn {key, value}, acc + when is_binary(key) and is_binary(value) -> + validate_req_header!(adapter, key) + List.keystore(acc, key, 0, {key, value}) + end) + + %{conn | req_headers: headers} + end + @doc """ Adds a new request header (`key`) if not present, otherwise replaces the previous value of that header with `value`. + The "host" header will be overridden by `conn.host` and should not be set + with this method. Instead, do `%Plug.Conn{conn | host: value}`. + Because header keys are case-insensitive in both HTTP/1.1 and HTTP/2, it is recommended for header keys to be in lowercase, to avoid sending duplicate keys in a request. @@ -633,7 +768,7 @@ defmodule Plug.Conn do headers that aren't lowercase will raise a `Plug.Conn.InvalidHeaderError`. Raises a `Plug.Conn.AlreadySentError` if the connection has already been - `:sent` or `:chunked`. + `:sent`, `:chunked` or `:upgraded`. ## Examples @@ -643,13 +778,13 @@ defmodule Plug.Conn do @spec put_req_header(t, binary, binary) :: t def put_req_header(conn, key, value) - def put_req_header(%Conn{state: :sent}, _key, _value) do + def put_req_header(%Conn{state: state}, _key, _value) when state not in @unsent do raise AlreadySentError end def put_req_header(%Conn{adapter: adapter, req_headers: headers} = conn, key, value) when is_binary(key) and is_binary(value) do - validate_header_key_if_test!(adapter, key) + validate_req_header!(adapter, key) %{conn | req_headers: List.keystore(headers, key, 0, {key, value})} end @@ -657,7 +792,7 @@ defmodule Plug.Conn do Deletes a request header if present. Raises a `Plug.Conn.AlreadySentError` if the connection has already been - `:sent` or `:chunked`. + `:sent`, `:chunked` or `:upgraded`. ## Examples @@ -667,11 +802,7 @@ defmodule Plug.Conn do @spec delete_req_header(t, binary) :: t def delete_req_header(conn, key) - def delete_req_header(%Conn{state: :sent}, _key) do - raise AlreadySentError - end - - def delete_req_header(%Conn{state: :chunked}, _key) do + def delete_req_header(%Conn{state: state}, _key) when state not in @unsent do raise AlreadySentError end @@ -685,7 +816,7 @@ defmodule Plug.Conn do value. Raises a `Plug.Conn.AlreadySentError` if the connection has already been - `:sent` or `:chunked`. + `:sent`, `:chunked` or `:upgraded`. Only the first value of the header `key` is updated if present. @@ -702,11 +833,7 @@ defmodule Plug.Conn do @spec update_req_header(t, binary, binary, (binary -> binary)) :: t def update_req_header(conn, key, initial, fun) - def update_req_header(%Conn{state: :sent}, _key, _initial, _fun) do - raise AlreadySentError - end - - def update_req_header(%Conn{state: :chunked}, _key, _initial, _fun) do + def update_req_header(%Conn{state: state}, _key, _initial, _fun) when state not in @unsent do raise AlreadySentError end @@ -746,7 +873,7 @@ defmodule Plug.Conn do headers that aren't lowercase will raise a `Plug.Conn.InvalidHeaderError`. Raises a `Plug.Conn.AlreadySentError` if the connection has already been - `:sent` or `:chunked`. + `:sent`, `:chunked` or `:upgraded`. Raises a `Plug.Conn.InvalidHeaderError` if the header value contains control feed (`\r`) or newline (`\n`) characters. @@ -757,11 +884,7 @@ defmodule Plug.Conn do """ @spec put_resp_header(t, binary, binary) :: t - def put_resp_header(%Conn{state: :sent}, _key, _value) do - raise AlreadySentError - end - - def put_resp_header(%Conn{state: :chunked}, _key, _value) do + def put_resp_header(%Conn{state: state}, _key, _value) when state not in @unsent do raise AlreadySentError end @@ -776,7 +899,7 @@ defmodule Plug.Conn do Prepends the list of headers to the connection response headers. Similar to `put_resp_header` this functions adds a new response header - (`key`) but rather then replacing the existing one it prepends another header + (`key`) but rather than replacing the existing one it prepends another header with the same `key`. It is recommended for header keys to be in lowercase, to avoid sending @@ -787,7 +910,7 @@ defmodule Plug.Conn do headers that aren't lowercase will raise a `Plug.Conn.InvalidHeaderError`. Raises a `Plug.Conn.AlreadySentError` if the connection has already been - `:sent` or `:chunked`. + `:sent`, `:chunked` or `:upgraded`. Raises a `Plug.Conn.InvalidHeaderError` if the header value contains control feed (`\r`) or newline (`\n`) characters. @@ -800,11 +923,7 @@ defmodule Plug.Conn do @spec prepend_resp_headers(t, headers) :: t def prepend_resp_headers(conn, headers) - def prepend_resp_headers(%Conn{state: :sent}, _headers) do - raise AlreadySentError - end - - def prepend_resp_headers(%Conn{state: :chunked}, _headers) do + def prepend_resp_headers(%Conn{state: state}, _headers) when state not in @unsent do raise AlreadySentError end @@ -836,11 +955,7 @@ defmodule Plug.Conn do @spec merge_resp_headers(t, Enum.t()) :: t def merge_resp_headers(conn, headers) - def merge_resp_headers(%Conn{state: :sent}, _headers) do - raise AlreadySentError - end - - def merge_resp_headers(%Conn{state: :chunked}, _headers) do + def merge_resp_headers(%Conn{state: state}, _headers) when state not in @unsent do raise AlreadySentError end @@ -864,7 +979,7 @@ defmodule Plug.Conn do Deletes a response header if present. Raises a `Plug.Conn.AlreadySentError` if the connection has already been - `:sent` or `:chunked`. + `:sent`, `:chunked` or `:upgraded`. ## Examples @@ -872,11 +987,7 @@ defmodule Plug.Conn do """ @spec delete_resp_header(t, binary) :: t - def delete_resp_header(%Conn{state: :sent}, _key) do - raise AlreadySentError - end - - def delete_resp_header(%Conn{state: :chunked}, _key) do + def delete_resp_header(%Conn{state: state}, _key) when state not in @unsent do raise AlreadySentError end @@ -890,7 +1001,7 @@ defmodule Plug.Conn do value. Raises a `Plug.Conn.AlreadySentError` if the connection has already been - `:sent` or `:chunked`. + `:sent`, `:chunked` or `:upgraded`. Only the first value of the header `key` is updated if present. @@ -907,11 +1018,7 @@ defmodule Plug.Conn do @spec update_resp_header(t, binary, binary, (binary -> binary)) :: t def update_resp_header(conn, key, initial, fun) - def update_resp_header(%Conn{state: :sent}, _key, _initial, _fun) do - raise AlreadySentError - end - - def update_resp_header(%Conn{state: :chunked}, _key, _initial, _fun) do + def update_resp_header(%Conn{state: state}, _key, _initial, _fun) when state not in @unsent do raise AlreadySentError end @@ -1229,7 +1336,7 @@ defmodule Plug.Conn do @doc """ Sends an information response to a client but raises if the adapter does not support inform. - See `inform/1` for more information. + See `inform/3` for more information. """ @spec inform!(t, status, Keyword.t()) :: t def inform!(%Conn{adapter: {adapter, _}} = conn, status, headers \\ []) do @@ -1260,6 +1367,46 @@ defmodule Plug.Conn do do: adapter.inform(payload, status, headers) @doc """ + Request a protocol upgrade from the underlying adapter. + + The precise semantics of an upgrade are deliberately left unspecified here in order to + support arbitrary upgrades, even to protocols which may not exist today. The primary intent of + this function is solely to allow an application to issue an upgrade request, not to manage how + a given protocol upgrade takes place or what APIs the application must support in order to serve + this updated protocol. For details in this regard, consult the documentation of the underlying + adapter (such a Plug.Cowboy or Bandit). + + Takes an argument describing the requested upgrade (for example, `:websocket`), and an argument + which contains arbitrary data which the underlying adapter is expected to interpret in the + context of the requested upgrade. + + If the upgrade is accepted by the adapter, the returned `Plug.Conn` will have a `state` of + `:upgraded`. This state is considered equivalently to a 'sent' state, and is subject to the same + limitation on subsequent mutating operations. Note that there is no guarantee or expectation + that the actual upgrade process is undertaken within this function; it is entirely possible that + the server will only do the actual upgrade later in the connection lifecycle. + + If the adapter does not support the requested upgrade then this is a noop and the returned + `Plug.Conn` will be unchanged. The application can detect this and operate on the conn as it + normally would in order to indicate an upgrade failure to the client. + """ + @spec upgrade_adapter(t, atom, term) :: t + def upgrade_adapter(%Conn{adapter: {adapter, payload}, state: state} = conn, protocol, args) + when state in @unsent do + case adapter.upgrade(payload, protocol, args) do + {:ok, payload} -> + %{conn | adapter: {adapter, payload}, state: :upgraded} + + {:error, :not_supported} -> + raise ArgumentError, "upgrade to #{protocol} not supported by #{inspect(adapter)}" + end + end + + def upgrade_adapter(_conn, _protocol, _args) do + raise AlreadySentError + end + + @doc """ Pushes a resource to the client. Server pushes must happen prior to a response being sent. If a server @@ -1272,6 +1419,7 @@ defmodule Plug.Conn do resource if your certificate is not trusted. In the case of Chrome this means a valid cert with a SAN. See https://www.chromestatus.com/feature/4981025180483584 """ + @deprecated "Most browsers and clients have removed push support" @spec push(t, String.t(), Keyword.t()) :: t def push(%Conn{} = conn, path, headers \\ []) do adapter_push(conn, path, headers) @@ -1282,6 +1430,7 @@ defmodule Plug.Conn do Pushes a resource to the client but raises if the adapter does not support server push. """ + @deprecated "Most browsers and clients have removed push support" @spec push!(t, String.t(), Keyword.t()) :: t def push!(%Conn{adapter: {adapter, _}} = conn, path, headers \\ []) do case adapter_push(conn, path, headers) do @@ -1716,8 +1865,10 @@ defmodule Plug.Conn do validate_header_value!("set-cookie", cookie) end - defp update_cookies(%Conn{state: :sent}, _fun), do: raise(AlreadySentError) - defp update_cookies(%Conn{state: :chunked}, _fun), do: raise(AlreadySentError) + defp update_cookies(%Conn{state: state}, _fun) when state not in @unsent do + raise AlreadySentError + end + defp update_cookies(%Conn{cookies: %Unfetched{}} = conn, _fun), do: conn defp update_cookies(%Conn{cookies: cookies} = conn, fun), do: %{conn | cookies: fun.(cookies)} @@ -1733,6 +1884,15 @@ defmodule Plug.Conn do %{conn | private: private} end + # host is an HTTP header, but if you store it in the main list it will be + # overridden by conn.host. + defp validate_req_header!(_adapter, "host") do + raise InvalidHeaderError, + "set the host header with %Plug.Conn{conn | host: \"example.com\"}" + end + + defp validate_req_header!(adapter, key), do: validate_header_key_if_test!(adapter, key) + defp validate_header_key_if_test!({Plug.Adapters.Test.Conn, _}, key) do if Application.fetch_env!(:plug, :validate_header_keys_during_test) and not valid_header_key?(key) do diff --git a/.deps/plug/lib/plug/conn/adapter.ex b/.deps/plug/lib/plug/conn/adapter.ex @@ -134,6 +134,20 @@ defmodule Plug.Conn.Adapter do :ok | {:error, term} @doc """ + Attempt to upgrade the connection with the client. + + If the adapter does not support the indicated upgrade, then `{:error, :not_supported}` should be + be returned. + + If the adapter supports the indicated upgrade but is unable to proceed with it (due to + a negotiation error, invalid opts being passed to this function, or some other reason), then an + arbitrary error may be returned. Note that an adapter does not need to process the actual + upgrade within this function; it is a wholly supported failure mode for an adapter to attempt + the upgrade process later in the connection lifecycle and fail at that point. + """ + @callback upgrade(payload, protocol :: atom, opts :: term) :: {:ok, payload} | {:error, term} + + @doc """ Returns peer information such as the address, port and ssl cert. """ @callback get_peer_data(payload) :: peer_data() diff --git a/.deps/plug/lib/plug/conn/query.ex b/.deps/plug/lib/plug/conn/query.ex @@ -1,8 +1,8 @@ defmodule Plug.Conn.Query do @moduledoc """ - Conveniences for decoding and encoding url encoded queries. + Conveniences for decoding and encoding URL-encoded queries. - Plug allows a developer to build query strings that map to + Plug allows developers to build query strings that map to Elixir structures in order to make manipulation of such structures easier on the server side. Here are some examples: @@ -59,11 +59,18 @@ defmodule Plug.Conn.Query do """ @doc """ - Decodes the given binary. + Decodes the given `query`. - The binary is assumed to be encoded in "x-www-form-urlencoded" format. - The format is decoded and then validated for proper UTF-8 encoding. + The `query` is assumed to be encoded in the "x-www-form-urlencoded" format. + The format is decoded at first. Then, if `validate_utf8` is `true`, the decoded + result is validated for proper UTF-8 encoding. + + `initial` is the initial "accumulator" where decoded values will be added. + + `invalid_exception` is the exception module for the exception to raise on + errors with decoding. """ + @spec decode(String.t(), map(), module(), boolean()) :: %{optional(String.t()) => term()} def decode( query, initial \\ %{}, @@ -75,7 +82,8 @@ defmodule Plug.Conn.Query do initial end - def decode(query, initial, invalid_exception, validate_utf8) do + def decode(query, initial, invalid_exception, validate_utf8) + when is_binary(query) do parts = :binary.split(query, "&", [:global]) Enum.reduce( @@ -121,7 +129,7 @@ defmodule Plug.Conn.Query do end @doc """ - Decodes the given tuple and stores it in the accumulator. + Decodes the given tuple and stores it in the given accumulator. It parses the key and stores the value into the current accumulator. The keys and values are not assumed to be @@ -130,7 +138,8 @@ defmodule Plug.Conn.Query do Parameter lists are added to the accumulator in reverse order, so be sure to pass the parameters in reverse order. """ - def decode_pair({key, value}, acc) do + @spec decode_pair({String.t(), term()}, acc) :: acc when acc: term() + def decode_pair({key, value} = _pair, acc) do if key != "" and :binary.last(key) == ?] do # Remove trailing ] subkey = :binary.part(key, 0, byte_size(key) - 1) @@ -159,9 +168,12 @@ defmodule Plug.Conn.Query do parts = :binary.split(rest, pattern) case acc do - %{^key => current} -> + %{^key => current} when is_list(current) or is_map(current) -> Map.put(acc, key, assign_split(parts, value, current, pattern)) + %{^key => _} -> + acc + %{} -> Map.put(acc, key, assign_split(parts, value, :none, pattern)) @@ -200,6 +212,7 @@ defmodule Plug.Conn.Query do @doc """ Encodes the given map or list of tuples. """ + @spec encode(Enumerable.t(), (term() -> binary())) :: binary() def encode(kv, encoder \\ &to_string/1) do IO.iodata_to_binary(encode_pair("", kv, encoder)) end diff --git a/.deps/plug/lib/plug/conn/status.ex b/.deps/plug/lib/plug/conn/status.ex @@ -3,7 +3,7 @@ defmodule Plug.Conn.Status do Conveniences for working with status codes. """ - custom_statuses = Application.get_env(:plug, :statuses, %{}) + custom_statuses = Application.compile_env(:plug, :statuses, %{}) statuses = %{ 100 => "Continue", diff --git a/.deps/plug/lib/plug/conn/wrapper_error.ex b/.deps/plug/lib/plug/conn/wrapper_error.ex @@ -20,10 +20,7 @@ defmodule Plug.Conn.WrapperError do @deprecated "Use reraise/1 or reraise/4 instead" def reraise(conn, kind, reason) do - # Please do not submit a PR to remove System.stacktrace() - # See https://github.com/elixir-plug/plug/pull/991 - # TODO: Use an empty list instead of System.stacktrace/0 when we depend on Elixir 1.10+ - reraise(conn, kind, reason, System.stacktrace()) + reraise(conn, kind, reason, []) end def reraise(_conn, :error, %__MODULE__{stack: stack} = reason, _stack) do diff --git a/.deps/plug/lib/plug/debugger.ex b/.deps/plug/lib/plug/debugger.ex @@ -187,8 +187,17 @@ defmodule Plug.Debugger do session = maybe_fetch_session(conn) params = maybe_fetch_query_params(conn) {title, message} = info(kind, reason) - style = Enum.into(opts[:style] || [], @default_style) - banner = banner(conn, status, kind, reason, stack, opts) + + assigns = [ + conn: conn, + title: title, + formatted: Exception.format(kind, reason, stack), + session: session, + params: params, + frames: frames(:md, stack, opts) + ] + + markdown = template_markdown(assigns) if accepts_html?(get_req_header(conn, "accept")) do conn = @@ -198,35 +207,25 @@ defmodule Plug.Debugger do actions = encoded_actions_for_exception(reason, conn) last_path = actions_redirect_path(conn) - - assigns = [ - conn: conn, - frames: frames(stack, opts), - title: title, - message: message, - session: session, - params: params, - style: style, - banner: banner, - actions: actions, - last_path: last_path - ] + style = Enum.into(opts[:style] || [], @default_style) + banner = banner(conn, status, kind, reason, stack, opts) + + assigns = + Keyword.merge(assigns, + conn: conn, + message: message, + markdown: markdown, + style: style, + banner: banner, + actions: actions, + frames: frames(:html, stack, opts), + last_path: last_path + ) send_resp(conn, status, template_html(assigns)) else - {reason, stack} = Exception.blame(kind, reason, stack) - conn = put_resp_content_type(conn, "text/markdown") - - assigns = [ - conn: conn, - title: title, - formatted: Exception.format(kind, reason, stack), - session: session, - params: params - ] - - send_resp(conn, status, template_markdown(assigns)) + send_resp(conn, status, markdown) end end @@ -267,8 +266,16 @@ defmodule Plug.Debugger do end end - defp actions_redirect_path(%Plug.Conn{method: "GET", request_path: request_path}), - do: request_path + defp actions_redirect_path(%Plug.Conn{ + method: "GET", + request_path: request_path, + query_string: query_string + }) do + case query_string do + "" -> request_path + query_string -> "#{request_path}?#{query_string}" + end + end defp actions_redirect_path(conn) do case get_req_header(conn, "referer") do @@ -308,21 +315,21 @@ defmodule Plug.Debugger do defp info(:throw, thrown), do: {"unhandled throw", inspect(thrown)} defp info(:exit, reason), do: {"unhandled exit", Exception.format_exit(reason)} - defp frames(stacktrace, opts) do + defp frames(renderer, stacktrace, opts) do app = opts[:otp_app] editor = System.get_env("PLUG_EDITOR") stacktrace - |> Enum.map_reduce(0, &each_frame(&1, &2, app, editor)) + |> Enum.map_reduce(0, &each_frame(&1, &2, renderer, app, editor)) |> elem(0) end - defp each_frame(entry, index, root, editor) do + defp each_frame(entry, index, renderer, root, editor) do {module, info, location, app, fun, arity, args} = get_entry(entry) {file, line} = {to_string(location[:file] || "nofile"), location[:line]} doc = module && get_doc(module, fun, arity, app) - clauses = module && get_clauses(module, fun, args) + clauses = module && get_clauses(renderer, module, fun, args) source = get_source(app, module, file) context = get_context(root, app) snippet = get_snippet(source, line) @@ -402,21 +409,21 @@ defmodule Plug.Debugger do defp has_doc_matcher?(name, arity) do &match?( {{kind, ^name, ^arity}, _, _, doc, _} - when kind in [:function, :macro] and doc != :hidden, + when kind in [:function, :macro] and doc != :hidden and doc != :none, &1 ) end - defp get_clauses(module, fun, args) do + defp get_clauses(renderer, module, fun, args) do with true <- is_list(args), {:ok, kind, clauses} <- Exception.blame_mfa(module, fun, args) do top_10 = clauses |> Enum.take(10) |> Enum.map(fn {args, guards} -> - args = Enum.map_join(args, ", ", &blame_match/1) + args = Enum.map_join(args, ", ", &blame_match(renderer, &1)) base = "#{kind} #{fun}(#{args})" - Enum.reduce(guards, base, &"#{&2} when #{blame_clause(&1)}") + Enum.reduce(guards, base, &"#{&2} when #{blame_clause(renderer, &1)}") end) {length(top_10), length(clauses), top_10} @@ -425,16 +432,19 @@ defmodule Plug.Debugger do end end - defp blame_match(%{match?: true, node: node}), + defp blame_match(:html, %{match?: true, node: node}), do: ~s(<i class="green">) <> h(Macro.to_string(node)) <> "</i>" - defp blame_match(%{match?: false, node: node}), + defp blame_match(:html, %{match?: false, node: node}), do: ~s(<i class="red">) <> h(Macro.to_string(node)) <> "</i>" - defp blame_clause({op, _, [left, right]}), - do: blame_clause(left) <> " #{op} " <> blame_clause(right) + defp blame_match(_md, %{node: node}), + do: h(Macro.to_string(node)) + + defp blame_clause(renderer, {op, _, [left, right]}), + do: blame_clause(renderer, left) <> " #{op} " <> blame_clause(renderer, right) - defp blame_clause(node), do: blame_match(node) + defp blame_clause(renderer, node), do: blame_match(renderer, node) defp get_context(app, app) when app != nil, do: :app defp get_context(_app1, _app2), do: :all diff --git a/.deps/plug/lib/plug/mime.ex b/.deps/plug/lib/plug/mime.ex @@ -1,7 +1,7 @@ defmodule Plug.MIME do @moduledoc false - if Application.get_env(:plug, :mimes) do + if Application.compile_env(:plug, :mimes) do IO.puts(:stderr, """ warning: you have set the :mimes configuration for the :plug application but it is no longer supported. Instead of: diff --git a/.deps/plug/lib/plug/parsers.ex b/.deps/plug/lib/plug/parsers.ex @@ -139,7 +139,7 @@ defmodule Plug.Parsers do uploads, you can do: plug Plug.Parsers, - parsers: [:url_encoded, :multipart], + parsers: [:urlencoded, :multipart], length: 20_000_000 However, the above will increase the maximum length of all request @@ -148,7 +148,7 @@ defmodule Plug.Parsers do plug Plug.Parsers, parsers: [ - :url_encoded, + :urlencoded, {:multipart, length: 20_000_000} # Increase to 20MB max upload ] diff --git a/.deps/plug/lib/plug/parsers/multipart.ex b/.deps/plug/lib/plug/parsers/multipart.ex @@ -76,8 +76,8 @@ defmodule Plug.Parsers.MULTIPART do end def parse(conn, "multipart", subtype, headers, opts) do - limit = [limit: System.fetch_env!("UPLOAD_LIMIT")] - opts = @multipart.init([limit: limit] ++ opts) + length = System.fetch_env!("UPLOAD_LIMIT") |> String.to_integer + opts = @multipart.init([length: length] ++ opts) @multipart.parse(conn, "multipart", subtype, headers, opts) end diff --git a/.deps/plug/lib/plug/ssl.ex b/.deps/plug/lib/plug/ssl.ex @@ -242,9 +242,17 @@ defmodule Plug.SSL do end defp set_secure_defaults(options) do - options - |> Keyword.put_new(:secure_renegotiate, true) - |> Keyword.put_new(:reuse_sessions, true) + versions = options[:versions] || :ssl.versions()[:supported] + + if Enum.any?([:tlsv1, :"tlsv1.1", :"tlsv1.2"], &(&1 in versions)) do + options + |> Keyword.put_new(:secure_renegotiate, true) + |> Keyword.put_new(:reuse_sessions, true) + else + options + |> Keyword.delete(:secure_renegotiate) + |> Keyword.delete(:reuse_sessions) + end end defp configure_managed_tls(options) do diff --git a/.deps/plug/lib/plug/static.ex b/.deps/plug/lib/plug/static.ex @@ -185,7 +185,7 @@ defmodule Plug.Static do segments = Enum.map(segments, &uri_decode/1) if invalid_path?(segments) do - raise InvalidPathError + raise InvalidPathError, "invalid path for static asset: #{conn.request_path}" end path = path(from, segments) diff --git a/.deps/plug/lib/plug/templates/debugger.html.eex b/.deps/plug/lib/plug/templates/debugger.html.eex @@ -202,6 +202,11 @@ padding-left: 32px; } + .code-explorer .hidden-contents { + position: absolute; + left: -999em; + } + /* Collapse to single-column */ @media (max-width: 960px) { .code-explorer > .code-snippets { @@ -224,6 +229,7 @@ .frame-info { background: white; + border-radius: 4px; box-shadow: 0 1px 3px rgba(80, 100, 140, .1), 0 8px 15px rgba(80, 100, 140, .05); @@ -232,7 +238,9 @@ .frame-info > .meta, .frame-info > .file { padding: 12px 16px; - white-space: no-wrap; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-size: <%= :math.pow(1.2, -1) %>em; } @@ -399,26 +407,19 @@ */ .stack-trace-heading { + display: flex; + align-items: center; + justify-content: space-between; padding-top: 8px; } - .stack-trace-heading:after { - content: ''; - display: block; - clear: both; - zoom: 1; - border-bottom: solid 1px <%= @style.line_color %>; - padding-top: 12px; - margin-bottom: 16px; - } - .stack-trace-heading > h3 { display: none; } .stack-trace-heading > label { - display: block; - padding-left: 8px; + display: inline-block; + padding-left: 5px; line-height: 1.9; font-size: <%= :math.pow(1.2, -1) %>em; -webkit-user-select: none; @@ -428,7 +429,32 @@ } .stack-trace-heading > label > input { - margin-right: .3em; + margin: -0.2em 0.5em 0 0; + vertical-align: middle; + } + + .stack-trace-heading .copy-markdown { + color: <%= @style.text_color %>; + background-color: transparent; + display: inline-flex; + align-items: center; + font-size: <%= :math.pow(1.2, -1) %>em; + line-height: 1.9; + border-width: 0; + } + + .stack-trace-heading .copy-markdown:active { + cursor: pointer; + } + + .stack-trace-heading .copy-markdown:hover { + cursor: pointer; + } + + .stack-trace-heading .copy-markdown-icon { + height: 1rem; + width: 1rem; + margin: 0.1em 0.3rem 0; } @media (max-width: 480px) { @@ -478,6 +504,7 @@ .stack-trace-item:hover, .stack-trace-item:focus { background-color: rgba(80, 100, 140, 0.05); + border-radius: 4px; } .stack-trace-item, @@ -490,7 +517,8 @@ } .stack-trace-item.-active { - background-color: white; + background-color: rgba(80, 100, 140, 0.1); + border-radius: 4px; } /* Circle */ @@ -502,11 +530,15 @@ background: #a0b0c0; border-radius: 50%; margin-right: 8px; + opacity: 0.5; + } + + .stack-trace-item.-active > .left:before { + opacity: 1; } .stack-trace-item.-app > .left:before { background: <%= @style.primary %>; - opacity: 1; } .stack-trace-item.-app > .left > .app { @@ -560,6 +592,10 @@ white-space: normal; } + .code-quote > pre { + margin: 0; + } + .code.-padded { padding: 0 16px 16px 16px; } @@ -661,6 +697,7 @@ background-color: inherit; background-color: #f2f2f4; } + </style> </head> <body> @@ -697,11 +734,12 @@ <% end %> <div class="code-explorer"> + <textarea class="hidden-contents" role="copy-contents"><%= @markdown %></textarea> <div class="code-snippets"> <%= for frame <- @frames do %> <div class="frame-info" data-index="<%= frame.index %>" role="stack-trace-details"> <div class="file"> - <a href="<%= frame.link %>"><%= h frame.file %></a> + <a href="<%= frame.link %>"><%= h frame.file %></a> </div> <%= if (snippet = frame.snippet) && snippet != [] do %> @@ -715,7 +753,7 @@ <div class="frame-mfa"> <%= h frame.info %> <%= if doc = frame.doc do %> - <a class="docs right" href="<%= h doc %>">docs</a> + <a class="docs right" href="<%= h doc %>">Docs</a> <% end %> <%= if app = frame.app do %> <span class="app right"><%= h app %></span> @@ -752,6 +790,10 @@ <div class="stack-trace"> <div class="stack-trace-heading"> <label><input type="checkbox" role="show-all-toggle">Show only app frames</label> + <button class="copy-markdown" role="copy-to-markdown" type="button"> + <span role="copy-to-markdown-text">Copy markdown</span> + <svg xmlns="http://www.w3.org/2000/svg" class="copy-markdown-icon" viewBox="0 0 115.77 122.88"><g><path d="M89.62,13.96v7.73h12.19h0.01v0.02c3.85,0.01,7.34,1.57,9.86,4.1c2.5,2.51,4.06,5.98,4.07,9.82h0.02v0.02 v73.27v0.01h-0.02c-0.01,3.84-1.57,7.33-4.1,9.86c-2.51,2.5-5.98,4.06-9.82,4.07v0.02h-0.02h-61.7H40.1v-0.02 c-3.84-0.01-7.34-1.57-9.86-4.1c-2.5-2.51-4.06-5.98-4.07-9.82h-0.02v-0.02V92.51H13.96h-0.01v-0.02c-3.84-0.01-7.34-1.57-9.86-4.1 c-2.5-2.51-4.06-5.98-4.07-9.82H0v-0.02V13.96v-0.01h0.02c0.01-3.85,1.58-7.34,4.1-9.86c2.51-2.5,5.98-4.06,9.82-4.07V0h0.02h61.7 h0.01v0.02c3.85,0.01,7.34,1.57,9.86,4.1c2.5,2.51,4.06,5.98,4.07,9.82h0.02V13.96L89.62,13.96z M79.04,21.69v-7.73v-0.02h0.02 c0-0.91-0.39-1.75-1.01-2.37c-0.61-0.61-1.46-1-2.37-1v0.02h-0.01h-61.7h-0.02v-0.02c-0.91,0-1.75,0.39-2.37,1.01 c-0.61,0.61-1,1.46-1,2.37h0.02v0.01v64.59v0.02h-0.02c0,0.91,0.39,1.75,1.01,2.37c0.61,0.61,1.46,1,2.37,1v-0.02h0.01h12.19V35.65 v-0.01h0.02c0.01-3.85,1.58-7.34,4.1-9.86c2.51-2.5,5.98-4.06,9.82-4.07v-0.02h0.02H79.04L79.04,21.69z M105.18,108.92V35.65v-0.02 h0.02c0-0.91-0.39-1.75-1.01-2.37c-0.61-0.61-1.46-1-2.37-1v0.02h-0.01h-61.7h-0.02v-0.02c-0.91,0-1.75,0.39-2.37,1.01 c-0.61,0.61-1,1.46-1,2.37h0.02v0.01v73.27v0.02h-0.02c0,0.91,0.39,1.75,1.01,2.37c0.61,0.61,1.46,1,2.37,1v-0.02h0.01h61.7h0.02 v0.02c0.91,0,1.75-0.39,2.37-1.01c0.61-0.61,1-1.46,1-2.37h-0.02V108.92L105.18,108.92z"/></g></svg> + </button> </div> <ul class="stack-trace-list -show-all" role="stack-trace-list"> @@ -820,7 +862,7 @@ <%= for {key, value} <- @session do %> <dl> <dt><%= h key %></dt> - <dd><pre><%= h inspect(value) %></pre></dd> + <dd class="code-quote"><pre><%= h inspect(value) %></pre></dd> </dl> <% end %> </details> @@ -831,12 +873,34 @@ var $items = document.querySelectorAll('[role~="stack-trace-item"]') var $toggle = document.querySelector('[role~="show-all-toggle"]') var $list = document.querySelector('[role~="stack-trace-list"]') + var $copyBtn = document.querySelector('[role~="copy-to-markdown"]') + var $copyBtnText = document.querySelector('[role~="copy-to-markdown-text"]') + var $copy = document.querySelector('[role~="copy-contents"]') each($items, function ($item) { on($item, 'click', itemOnclick) }) on($toggle, 'click', toggleOnclick) + on($copyBtn, 'click', copyToClipboard) + + function copyToClipboard () { + if(navigator.clipboard) { + // For those working on localhost or HTTPS + navigator.clipboard.writeText($copy.innerHTML).then(copiedClipboard).catch(() => {}) + } else { + // For those working on HTTP + $copy.select() + if (document.execCommand("copy")) copiedClipboard() + } + } + + function copiedClipboard () { + $copyBtnText.innerText = "Copied!" + setTimeout(function () { + $copyBtnText.innerText = "Copy markdown" + }, 5000) + } function toggleOnclick () { if (this.checked) { diff --git a/.deps/plug/lib/plug/templates/debugger.md.eex b/.deps/plug/lib/plug/templates/debugger.md.eex @@ -4,6 +4,24 @@ Exception: <%= String.replace(@formatted, "\n", "\n ") %> +Code: +<%= for frame <- @frames do %> +`<%= h frame.file %>` +<%= if (snippet = frame.snippet) && snippet != [] do %> + <%= for {index, line, highlight} <- snippet do %><%= if highlight do %><%= h index %>> <% else %><%= h index %> <% end %><%= h String.trim_trailing(line) %> + <% end %><% else %> + No code available. +<% end %><%= if frame.args do %> + Called with <%= length(frame.args) %> arguments + + <%= for arg <- frame.args do %>* `<%= h inspect arg %>` + <% end %><% end %><%= if frame.clauses do %><% {min, max, clauses} = frame.clauses %> + Attempted function clauses (showing <%= min %> out of <%= max %>) + + <%= for clause <- clauses do %> <%= clause %> + <% end %> +<% end %><% end %> + ## Connection details ### Params diff --git a/.deps/plug/lib/plug/test.ex b/.deps/plug/lib/plug/test.ex @@ -130,6 +130,33 @@ defmodule Plug.Test do end @doc """ + Returns the upgrade requests that have been sent. + + This function depends on gathering the messages sent by the test adapter when + upgrade requests are sent. Calling this function will clear the upgrade request messages from the inbox for the + process. + + ## Examples + + conn = conn(:get, "/foo", "bar=10") + upgrades = Plug.Test.send_upgrades(conn) + assert {:websocket, [opt: :value]} in upgrades + + """ + def sent_upgrades(%Conn{adapter: {Plug.Adapters.Test.Conn, %{ref: ref}}}) do + Enum.reverse(receive_upgrades(ref, [])) + end + + defp receive_upgrades(ref, upgrades) do + receive do + {^ref, :upgrade, response} -> + receive_upgrades(ref, [response | upgrades]) + after + 0 -> upgrades + end + end + + @doc """ Returns the assets that have been pushed. This function depends on gathering the messages sent by the test adapter @@ -145,6 +172,7 @@ defmodule Plug.Test do assert {"/static/application.js", [{"accept", "application/javascript"}]} in pushes """ + @deprecated "Most browsers and clients have removed push support" def sent_pushes(%Conn{adapter: {Plug.Adapters.Test.Conn, %{ref: ref}}}) do Enum.reverse(receive_pushes(ref, [])) end diff --git a/.deps/plug/lib/plug/upload.ex b/.deps/plug/lib/plug/upload.ex @@ -76,7 +76,7 @@ defmodule Plug.Upload do def give_away(path, to_pid, from_pid) when is_binary(path) and is_pid(to_pid) and is_pid(from_pid) do with [{^from_pid, _tmp}] <- :ets.lookup(@dir_table, from_pid), - true <- is_path_owner?(from_pid, path) do + true <- path_owner?(from_pid, path) do case :ets.lookup(@dir_table, to_pid) do [{^to_pid, _tmp}] -> :ets.insert(@path_table, {to_pid, path}) @@ -86,13 +86,9 @@ defmodule Plug.Upload do [] -> server = plug_server() - - {:ok, tmps} = GenServer.call(server, :roots) - {:ok, tmp} = generate_tmp_dir(tmps) + {:ok, tmp} = generate_tmp_dir() :ok = GenServer.call(server, {:give_away, to_pid, tmp, path}) - :ets.delete_object(@path_table, {from_pid, path}) - :ok end else @@ -110,17 +106,17 @@ defmodule Plug.Upload do [] -> server = plug_server() + GenServer.cast(server, {:monitor, pid}) - {:ok, tmps} = GenServer.call(server, {:monitor, pid}) - - with {:ok, tmp} <- generate_tmp_dir(tmps) do + with {:ok, tmp} <- generate_tmp_dir() do true = :ets.insert_new(@dir_table, {pid, tmp}) {:ok, tmp} end end end - defp generate_tmp_dir(tmp_roots) do + defp generate_tmp_dir() do + tmp_roots = :persistent_term.get(__MODULE__) {mega, _, _} = :os.timestamp() subdir = "/plug-" <> i(mega) @@ -157,14 +153,13 @@ defmodule Plug.Upload do defp path(prefix, tmp) do sec = :os.system_time(:second) - rand = :rand.uniform(999_999_999_999_999) + rand = :rand.uniform(999_999_999_999) scheduler_id = :erlang.system_info(:scheduler_id) tmp <> "/" <> prefix <> "-" <> i(sec) <> "-" <> i(rand) <> "-" <> i(scheduler_id) end - defp is_path_owner?(pid, path) do + defp path_owner?(pid, path) do owned_paths = :ets.lookup(@path_table, pid) - Enum.any?(owned_paths, fn {_pid, p} -> p == path end) end @@ -211,31 +206,28 @@ defmodule Plug.Upload do Process.flag(:trap_exit, true) tmp = Enum.find_value(@temp_env_vars, "/tmp", &System.get_env/1) |> Path.expand() cwd = Path.join(File.cwd!(), "tmp") + :persistent_term.put(__MODULE__, [tmp, cwd]) :ets.new(@dir_table, [:named_table, :public, :set]) :ets.new(@path_table, [:named_table, :public, :duplicate_bag]) - - {:ok, [tmp, cwd]} + {:ok, %{}} end @impl true - def handle_call({:monitor, pid}, _from, dirs) do - Process.monitor(pid) - {:reply, {:ok, dirs}, dirs} - end - - def handle_call(:roots, _from, dirs) do - {:reply, {:ok, dirs}, dirs} - end - - def handle_call({:give_away, pid, tmp, path}, _from, dirs) do + def handle_call({:give_away, pid, tmp, path}, _from, state) do # Since we are writing in behalf of another process, we need to make sure # the monitor and writing to the tables happen within the same operation. Process.monitor(pid) :ets.insert_new(@dir_table, {pid, tmp}) :ets.insert(@path_table, {pid, path}) - {:reply, :ok, dirs} + {:reply, :ok, state} + end + + @impl true + def handle_cast({:monitor, pid}, state) do + Process.monitor(pid) + {:noreply, state} end @impl true diff --git a/.deps/plug/mix.exs b/.deps/plug/mix.exs @@ -1,15 +1,15 @@ defmodule Plug.MixProject do use Mix.Project - @version "1.13.6" + @version "1.14.0" @description "Compose web applications with functions" - @xref_exclude [Plug.Cowboy, :telemetry] + @xref_exclude [Plug.Cowboy, :telemetry, :ssl] def project do [ app: :plug, version: @version, - elixir: "~> 1.7", + elixir: "~> 1.10", deps: deps(), package: package(), description: @description, diff --git a/.deps/plug_cowboy/.hex b/.deps/plug_cowboy/.hex Binary files differ. diff --git a/.deps/plug_cowboy/CHANGELOG.md b/.deps/plug_cowboy/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v2.6.0 + +### Enhancements + + * Support websocket upgrades + * Require Plug v1.14+ and Elixir v1.10+ + ## v2.5.2 ### Enhancements diff --git a/.deps/plug_cowboy/hex_metadata.config b/.deps/plug_cowboy/hex_metadata.config @@ -1,7 +1,7 @@ {<<"app">>,<<"plug_cowboy">>}. {<<"build_tools">>,[<<"mix">>]}. {<<"description">>,<<"A Plug adapter for Cowboy">>}. -{<<"elixir">>,<<"~> 1.7">>}. +{<<"elixir">>,<<"~> 1.10">>}. {<<"files">>, [<<"lib">>,<<"lib/plug">>,<<"lib/plug/cowboy.ex">>,<<"lib/plug/cowboy">>, <<"lib/plug/cowboy/handler.ex">>,<<"lib/plug/cowboy/drainer.ex">>, @@ -17,7 +17,7 @@ {<<"name">>,<<"plug">>}, {<<"optional">>,false}, {<<"repository">>,<<"hexpm">>}, - {<<"requirement">>,<<"~> 1.7">>}], + {<<"requirement">>,<<"~> 1.14">>}], [{<<"app">>,<<"cowboy">>}, {<<"name">>,<<"cowboy">>}, {<<"optional">>,false}, @@ -28,4 +28,4 @@ {<<"optional">>,false}, {<<"repository">>,<<"hexpm">>}, {<<"requirement">>,<<"~> 0.3">>}]]}. -{<<"version">>,<<"2.5.2">>}. +{<<"version">>,<<"2.6.0">>}. diff --git a/.deps/plug_cowboy/lib/plug/cowboy.ex b/.deps/plug_cowboy/lib/plug/cowboy.ex @@ -125,6 +125,29 @@ defmodule Plug.Cowboy do To opt-out of this default instrumentation, you can manually configure cowboy with the option `stream_handlers: [:cowboy_stream_h]`. + + ## WebSocket support + + Plug.Cowboy supports upgrading HTTP requests to WebSocket connections via + the use of the `Plug.Conn.upgrade_adapter/3` function, called with `:websocket` as the second + argument. Applications should validate that the connection represents a valid WebSocket request + before calling this function (Cowboy will validate the connection as part of the upgrade + process, but does not provide any capacity for an application to be notified if the upgrade is + not successful). If an application wishes to negotiate WebSocket subprotocols or otherwise set + any response headers, it should do so before calling `Plug.Conn.upgrade_adapter/3`. + + The third argument to `Plug.Conn.upgrade_adapter/3` defines the details of how Plug.Cowboy + should handle the WebSocket connection, and must take the form `{handler, handler_opts, + connection_opts}`, where values are as follows: + + * `handler` is a module which implements the + [`:cowboy_websocket`](https://ninenines.eu/docs/en/cowboy/2.6/manual/cowboy_websocket/) + behaviour. Note that this module will NOT have its `c:cowboy_websocket.init/2` callback + called; only the 'later' parts of the `:cowboy_websocket` lifecycle are supported + * `handler_opts` is an arbitrary term which will be passed as the argument to + `c:cowboy_websocket.websocket_init/1` + * `connection_opts` is a map with any of [Cowboy's websockets options](https://ninenines.eu/docs/en/cowboy/2.6/manual/cowboy_websocket/#_opts) + """ require Logger diff --git a/.deps/plug_cowboy/lib/plug/cowboy/conn.ex b/.deps/plug_cowboy/lib/plug/cowboy/conn.ex @@ -91,6 +91,23 @@ defmodule Plug.Cowboy.Conn do end @impl true + def upgrade(req, :websocket, args) do + case args do + {handler, _state, cowboy_opts} when is_atom(handler) and is_map(cowboy_opts) -> + :ok + + _ -> + raise ArgumentError, + "expected websocket upgrade on Cowboy to be on the format {handler :: atom(), arg :: term(), opts :: map()}, got: " <> + inspect(args) + end + + {:ok, Map.put(req, :upgrade, {:websocket, args})} + end + + def upgrade(_req, _protocol, _args), do: {:error, :not_supported} + + @impl true def push(req, path, headers) do opts = case {req.port, req.sock} do diff --git a/.deps/plug_cowboy/lib/plug/cowboy/handler.ex b/.deps/plug_cowboy/lib/plug/cowboy/handler.ex @@ -7,12 +7,17 @@ defmodule Plug.Cowboy.Handler do conn = @connection.conn(req) try do - %{adapter: {@connection, req}} = - conn - |> plug.call(opts) - |> maybe_send(plug) + conn + |> plug.call(opts) + |> maybe_send(plug) + |> case do + %Plug.Conn{adapter: {@connection, %{upgrade: {:websocket, websocket_args}} = req}} = conn -> + {handler, state, cowboy_opts} = websocket_args + {__MODULE__, copy_resp_headers(conn, req), {handler, state}, cowboy_opts} - {:ok, req, {plug, opts}} + %Plug.Conn{adapter: {@connection, req}} -> + {:ok, req, {plug, opts}} + end catch kind, reason -> exit_on_error(kind, reason, __STACKTRACE__, {plug, :call, [conn, opts]}) @@ -25,6 +30,16 @@ defmodule Plug.Cowboy.Handler do end end + def upgrade(req, env, __MODULE__, {handler, state}, opts) do + :cowboy_websocket.upgrade(req, env, handler.module_info(:module), state, opts) + end + + defp copy_resp_headers(%Plug.Conn{} = conn, req) do + Enum.reduce(conn.resp_headers, req, fn {key, val}, acc -> + :cowboy_req.set_resp_header(key, val, acc) + end) + end + defp exit_on_error( :error, %Plug.Conn.WrapperError{kind: kind, reason: reason, stack: stack}, diff --git a/.deps/plug_cowboy/mix.exs b/.deps/plug_cowboy/mix.exs @@ -2,14 +2,14 @@ defmodule Plug.Cowboy.MixProject do use Mix.Project @source_url "https://github.com/elixir-plug/plug_cowboy" - @version "2.5.2" + @version "2.6.0" @description "A Plug adapter for Cowboy" def project do [ app: :plug_cowboy, version: @version, - elixir: "~> 1.7", + elixir: "~> 1.10", deps: deps(), package: package(), description: @description, @@ -33,13 +33,13 @@ defmodule Plug.Cowboy.MixProject do def deps do [ - {:plug, "~> 1.7"}, + {:plug, "~> 1.14"}, {:cowboy, "~> 2.7"}, {:cowboy_telemetry, "~> 0.3"}, {:ex_doc, "~> 0.20", only: :docs}, - {:hackney, "~> 1.2.0", only: :test}, + {:hackney, "~> 1.2", only: :test}, {:kadabra, "0.3.4", only: :test}, - {:x509, "~> 0.6.0", only: :test} + {:x509, "~> 0.6", only: :test} ] end diff --git a/mix.exs b/mix.exs @@ -62,7 +62,7 @@ defp deps() do {:plug_crypto, "~> 1.2"}, # http - {:plug_cowboy, "~> 2.5"}, + {:plug_cowboy, "~> 2.6"}, {:mint, "~> 1.4"}, {:castore, "~> 0.1"}, diff --git a/mix.lock b/mix.lock @@ -26,8 +26,8 @@ "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [: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", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, + "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"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"}, "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},