update
This commit is contained in:
BIN
phoenix/deps/bandit/.hex
Normal file
BIN
phoenix/deps/bandit/.hex
Normal file
Binary file not shown.
839
phoenix/deps/bandit/CHANGELOG.md
Normal file
839
phoenix/deps/bandit/CHANGELOG.md
Normal file
@@ -0,0 +1,839 @@
|
||||
## 1.10.4 (25 Mar 2026)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Support `{:shutdown, :disconnected}` as a normal WebSocket result code (#576, thanks @wwitek-whatnot!)
|
||||
|
||||
## 1.10.3 (22 Feb 2026)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Support authority form requests for CONNECT requests (#571)
|
||||
* Narrow acceptance of asterisk form requests to OPTIONS requests (#571)
|
||||
* Detect client disconnect on timeout in ensure_completed (#566, thanks @pepicrft!)
|
||||
* Improve http2 sendfile streaming (#565, thanks @elibosley!)
|
||||
|
||||
## 1.10.2 (22 Jan 2026)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Distinguish client disconnects from genuine body read timeouts (#564, thanks @pepicrft!)
|
||||
|
||||
## 1.10.1 (5 Jan 2026)
|
||||
|
||||
### Changes
|
||||
|
||||
* Change default preference order for compression methods to be 'zstd (if present), gzip, deflate' (#562)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Allow `:zstd_options` key to be set in config (#558, thanks @Fudoshiki!)
|
||||
* Fix error where deflate responses weren't always completely sent (#559, thanks @josevalim!)
|
||||
|
||||
## 1.10.0 (29 Dec 2025)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Expose `response_encodings` to allow specifying an explicit preference order to compression encodings (#555)
|
||||
|
||||
## 1.9.0 (12 Dec 2025)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Skip body draining when Connection: close is set (#546, thanks @pepicrft!)
|
||||
* Make deflate options for WebSockets configurable (#540, thanks @proxima!)
|
||||
* Mitigate HTTP/2 rapid reset attacks (#533, thanks @NelsonVides!)
|
||||
* Implement improved respect for SETTINGS_MAX_CONCURRENT_STREAMS (#524, thanks @NelsonVides!)
|
||||
* Support zstd HTTP compression (#514, thanks @mattmatters!)
|
||||
|
||||
## 1.8.0 (18 Aug 2025)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* If the user has set a `content-length` header when calling `send_chunked/3`,
|
||||
the response is streamed via content-length delimited framing and not chunked (#510)
|
||||
|
||||
## 1.7.0 (29 May 2025)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add support for new `get_sock_data/1` and `get_ssl_data/1` callbacks from Plug 1.18 (#497)
|
||||
* Honour server-sent `Connection: close` headers (#495, thanks @ruslandoga!)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Don't overwrite non-default HTTP/2 settings when receiving HTTP/2 settings (#494, thanks @ns-blee!)
|
||||
* Fix handling of early-connection error handling in HTTP/2 (#486)
|
||||
|
||||
## 1.6.11 (31 Mar 2025)
|
||||
|
||||
### Changes
|
||||
|
||||
* Ensure that HTTP/1 request headers are sent to the Plug in the order they're
|
||||
sent (#482)
|
||||
* Do not populate the `cookies` header with an empty string if no cookies were
|
||||
sent in HTTP/2 (#483)
|
||||
|
||||
## 1.6.10 (25 Mar 2025)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix bug introduced when closing compressed websock connections in certain circumstances (#478)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Standardize & document the format of messages sent to HTTP/2 Stream processes (#481)
|
||||
|
||||
## 1.6.9 (21 Mar 2025)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Do not close compression context before calling websock close callback (#462,
|
||||
thanks @thiagopromano!)
|
||||
|
||||
## 1.6.8 (5 Mar 2025)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Do not send stream WINDOW_UPDATEs on the last data frame of a stream
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `status` to the telemetry metadata emitted on WebSocket upgrades (#466)
|
||||
|
||||
## 1.6.7 (30 Jan 2025)
|
||||
|
||||
### Changes
|
||||
|
||||
* Consider timeouts when reading HTTP/1 headers as a connection error and not an HTTP error
|
||||
* Enhance logging for WebSocket deflation errors
|
||||
|
||||
## 1.6.6 (25 Jan 2025)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Consider closures during HTTP/1 header reading as a socket error to silence them by default via `log_client_closures` config flag
|
||||
* Send `connection: close` when closing connection on error per RFC9112§9.6
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add experimental opt-in trace logging to help diagnose hard to reproduce errors
|
||||
* Move CI to 1.18 & improve tests (#459, #461, thanks @grzuy!)
|
||||
|
||||
## 1.6.5 (15 Jan 2025)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix regression introduced in 1.6.1 where we would not send headers set by the Plug during WebSocket upgrades (#458)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Properly normalize Erlang errors before emitting telemetry and logged crash_reason (#455, thanks @grzuy!)
|
||||
|
||||
## 1.6.4 (11 Jan 2025)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix error in socket setup error handling introduced in 1.6.2 (thanks @danielspofford!)
|
||||
|
||||
## 1.6.3 (8 Jan 2025)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Always close HTTP/1 connection in any case where an error comes out of the plug (#452, thanks @zookzook!)
|
||||
* Fix dialyzer warning introduced by Thousand Island 1.3.9
|
||||
|
||||
## 1.6.2 (4 Jan 2025)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Send telemetry events on Plugs that throw or exit (#443)
|
||||
* Improve test robustness & speed (#446)
|
||||
* Read a minimal number of bytes when sniffing for protocol (#449)
|
||||
* Add `plug` and `websock` to logging metadata whenever possible (#448)
|
||||
* Add `plug` and `websock` to telemetry metadata whenever possible (#447)
|
||||
* Silently eat Bandit.TransportError errors during HTTP/1 error fallback handling
|
||||
|
||||
### Fixes
|
||||
|
||||
* Bump hpax to 1.0.2, fixes https://github.com/phoenixframework/phoenix/issues/6020 (thanks @krainboltgreene!)
|
||||
* Fix cases where we would desync on pipelined POST requests (#442)
|
||||
|
||||
### Changes
|
||||
|
||||
* Unwrap Plug.Conn.WrapperErrors raised by Plug and handle the wrapped error per policy
|
||||
* Surface socket setup errors as Bandit.TransportError for consistency in logging
|
||||
|
||||
## 1.6.1 (6 Dec 2024)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add deflate support when sending chunked responses (#429)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Bring in updated HPAX to fix HTTP/2 error cases seen in AWS load balancing
|
||||
environments (#392)
|
||||
* Improve handle of pipelined HTTP/1.1 requests (#437)
|
||||
* Improve error handling when dealing with socket errors (#433)
|
||||
|
||||
### Changes
|
||||
|
||||
* Use `Plug.Call.inform/2` to send websocket upgrades (#428)
|
||||
|
||||
## 1.6.0 (18 Nov 2024)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add framework for supporting optimized native code on various hot paths (#394,
|
||||
thanks @alisinabh!)
|
||||
* Pass conn and exception data as logger metadata (#417 & #420, thanks @grzuy!)
|
||||
* Loosen hpax dependency requirements
|
||||
* Add `log_client_closures` http option, defaulting to false (#397, thanks @goncalotomas!)
|
||||
* Handle plugs that throw a result (#411, thanks @grzuy!)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Improve content-length send logic per RFC9110§8.6/8.7
|
||||
* Explicitly signal keepalives in HTTP/1.0 requests
|
||||
|
||||
### Changes
|
||||
|
||||
* Fix typo & clarify docs
|
||||
* Update security policy
|
||||
|
||||
## 1.5.7 (1 Aug 2024)
|
||||
|
||||
### Changes
|
||||
|
||||
* Timeouts encountered while reading a request body will now result in a `408
|
||||
Request Timeout` being returned to the client by way of a `Bandit.HTTPError`
|
||||
being raised. Previously, a `:more` tuple was returned (#385, thanks
|
||||
@martosaur!)
|
||||
|
||||
## 1.5.6 (1 Aug 2024)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Improve handling of the end of stream condition for HTTP/2 requests that send
|
||||
a body which isn't read by the Plug (#387, thanks @fekle!)
|
||||
|
||||
## 1.5.5 (19 Jun 2024)
|
||||
|
||||
### Changes
|
||||
|
||||
* Add `domain: [:bandit]` to the metadata of all logger calls
|
||||
* Bring logging of early-connect HTTP2 errors under the `log_protocol_errors` umbrella
|
||||
|
||||
## 1.5.4 (14 Jun 2024)
|
||||
|
||||
### Changes
|
||||
|
||||
* Raise HTTP/2 send window timeouts as stream errors so that they're logged as
|
||||
protocol errors (thanks @hunterboerner!)
|
||||
|
||||
## 1.5.3 (7 Jun 2024)
|
||||
|
||||
### Changes
|
||||
|
||||
* Add `:short` and `:verbose` options to `log_protocol_errors` configuration
|
||||
option. **Change default value to `:short`, which will log protocol
|
||||
errors as a single summary line instead of a full stack trace**
|
||||
* Raise `Bandit.HTTPError` errors when attempting to write to a closed client
|
||||
connection (except for chunk/2 calls, which now return `{:error, reason}`).
|
||||
Unless otherwise caught by the user, these errors will bubble out past the
|
||||
configured plug and terminate the plug process. This closely mimics the
|
||||
behaviour of Cowboy in this regard (#359)
|
||||
* Respect the plug-provided content-length on HEAD responses (#353, thanks
|
||||
@meeq!)
|
||||
* Minor changes to how 'non-system process dictionary entries' are identified
|
||||
|
||||
### Fixes
|
||||
|
||||
* No longer closes on HTTP/1 requests smaller than the size of the HTTP/2
|
||||
preamble
|
||||
* Close deflate contexts more eagerly for reduced memory use
|
||||
|
||||
## 1.5.2 (10 May 2024)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Don't crash on non-stringable process dictionary keys (#350, thanks
|
||||
@ryanwinchester, @chrismccord!)
|
||||
|
||||
## 1.5.1 (10 May 2024)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Process dictionary is now cleared of all non-system process dictionary entries
|
||||
between keepalive requests (#349)
|
||||
* Explicitly run a GC before upgrading a connection to websocket (#348)
|
||||
* Improve docs around deflate options (thanks @kotsius!)
|
||||
|
||||
## 1.5.0 (21 Apr 2024)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Bandit now respects an exception's conformance to `Plug.Exception` when
|
||||
determining which status code to return to the client (if the plug did not
|
||||
already send one). Previously they were always returned as 500 (for HTTP/1)
|
||||
or an 'internal error' stream error (for HTTP/2)
|
||||
* Bandit now only logs the stacktrace of plug-generated exceptions whose status
|
||||
code (as determined by `Plug.Exception.status/1`) is contained within the new
|
||||
`log_exceptions_with_status_codes` configuration option (defaulting to
|
||||
`500..599`)
|
||||
* As a corollary to the above, Bandit request handler processes no longer exit
|
||||
abnormally in the case of plug-generated exceptions
|
||||
|
||||
### Changes
|
||||
|
||||
* HTTP semantic errors encountered in an HTTP/2 request are returned to the
|
||||
client using their proper status code instead of as a 'protocol error' stream
|
||||
error
|
||||
|
||||
## 1.4.2 (2 Apr 2024)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Support top-level :inet and :inet6 options for Plug.Cowboy compatibility (#337)
|
||||
|
||||
## 1.4.1 (27 Mar 2024)
|
||||
|
||||
### Changes
|
||||
|
||||
* **BREAKING CHANGE** Move `log_protocol_errors` configuration option into
|
||||
shared `http_options` top-level config (and apply it to HTTP/2 errors as well)
|
||||
* **BREAKING CHANGE** Remove `origin_telemetry_span_context` from WebSocket
|
||||
telemetry events
|
||||
* **BREAKING CHANGE** Remove `stream_id` from HTTP/2 telemetry events
|
||||
* Add `conn` to the metadata of telemetry start events for HTTP requests
|
||||
* Stop sending WebSocket upgrade failure reasons to the client (they're still
|
||||
logged)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Return HTTP semantic errors to HTTP/2 clients as protocol errors instead of
|
||||
internal errors
|
||||
|
||||
## 1.4.0 (26 Mar 2024)
|
||||
|
||||
> [!WARNING]
|
||||
> **IMPORTANT** Phoenix users MUST upgrade to WebSockAdapter `0.5.6` or newer when
|
||||
> upgrading to Bandit `1.4.0` or newer as some internal module names have changed
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Complete refactor of HTTP/2. Improved process model is MUCH easier to
|
||||
understand and yields about a 10% performance boost to HTTP/2 requests (#286 /
|
||||
#307)
|
||||
* Substantial refactor of the HTTP/1 and HTTP/2 stacks to share a common code
|
||||
path for much of their implementations, with the protocol-specific parts being
|
||||
factored out to a minimal `Bandit.HTTPTransport` protocol internally, which
|
||||
allows each protocol to define its own implementation for the minimal set of
|
||||
things that are different between the two stacks (#297 / #329)
|
||||
|
||||
### Changes
|
||||
|
||||
* **BREAKING CHANGE** Move configuration options that are common between HTTP/1
|
||||
and HTTP/2 stacks into a shared `http_options` top-level config
|
||||
* **BREAKING CHANGE** The HTTP/2 header size limit options have been deprecated,
|
||||
and have been replaced with a single `max_header_block_size` option. The setting
|
||||
defaults to 50k bytes, and refers to the size of the compressed header block
|
||||
as sent on the wire (including any continuation frames)
|
||||
* **BREAKING CHANGE** Remove `req_line_bytes`, `req_header_bytes`, `resp_line_bytes` and
|
||||
`resp_header_bytes` from HTTP/1 request telemetry measurements
|
||||
* **BREAKING CHANGE** Remove `status`, `method` and `request_target` from
|
||||
telemetry metadata. All of this information can be obtained from the `conn`
|
||||
struct attached to most telemetry events
|
||||
* **BREAKING CHANGE** Re-reading a body that has already been read returns `{:ok,
|
||||
"", conn}` instead of raising a `Bandit.BodyAlreadyReadError`
|
||||
* **BREAKING CHANGE** Remove `Bandit.BodyAlreadyReadError`
|
||||
* **BREAKING CHANGE** Remove h2c support via Upgrade header. This was deprecated
|
||||
in RFC9113 and never in widespread use. We continue to support h2c via prior
|
||||
knowledge, which remains the only supported mechanism for h2c in RFC9113
|
||||
* Treat trailing bytes beyond the indicated content-length on HTTP/1 requests as
|
||||
an error
|
||||
* Surface request body read timeouts on HTTP/1 requests as `{:more...}` tuples
|
||||
and not errors
|
||||
* Socket sending errors are no longer surfaced on chunk sends in HTTP/1
|
||||
* We no longer log if processes that are linked to an HTTP/2 stream process
|
||||
terminate unexpectedly. This has always been unspecified behaviour so is not
|
||||
considered a breaking change
|
||||
* Calls of `Plug.Conn` functions for an HTTP/2 connection must now come from the
|
||||
stream process; any other process will raise an error. Again, this has always
|
||||
been unspecified behaviour
|
||||
* We now send an empty DATA frame for explicitly zero byte bodies instead of
|
||||
optimizing to a HEADERS frame with end_stream set (we still do so for cases
|
||||
such as 204/304 and HEAD requests)
|
||||
* We now send RST_STREAM frames if we complete a stream and the remote end is
|
||||
still open. This optimizes cases where the client may still be sending a body
|
||||
that we never consumed and don't care about
|
||||
* We no longer explicitly close the connection when we receive a GOAWAY frame
|
||||
|
||||
## 1.3.0 (8 Mar 2024)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Run an explicit garbage collection between every 'n' keepalive requests on the same HTTP/1.1 connection in order to keep reported (but not actual!) memory usage from growing over time. Add `gc_every_n_keepalive_requests` option to configure this (default value of
|
||||
`5`). #322, thanks @ianko & @Nilsonn!)
|
||||
* Add `log_protocol_errors` option to optionally quell console logging of 4xx errors generated by Bandit. Defaults to `true` for now; may switch to `false` in the future based on adoption (#321, thanks @Stroemgren!)
|
||||
|
||||
### Changes
|
||||
|
||||
* Don't send a `transfer-encoding` header for 1xx or 204 responses (#317, thanks
|
||||
@mwhitworth!)
|
||||
|
||||
## 1.2.3 (23 Feb 2024)
|
||||
|
||||
### Changes
|
||||
|
||||
* Log port number when listen fails (#312, thanks @jonatanklosko!)
|
||||
* Accept mixed-case keepalive directives (#308, thanks @gregors!)
|
||||
|
||||
## 1.2.2 (16 Feb 2024)
|
||||
|
||||
### Changes
|
||||
|
||||
* Reset Logger metadata on every request
|
||||
|
||||
## 1.2.1 (12 Feb 2024)
|
||||
|
||||
### Changes
|
||||
|
||||
* Disable logging of unknown messages received by an idle HTTP/1 handler to
|
||||
avoid noise on long polling clients. This can be changed via the
|
||||
`log_unknown_messages` http_1 option (#299)
|
||||
|
||||
## 1.2.0 (31 Jan 2024)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Automatically pull in `:otp_app` value in Bandit.PhoenixAdapter (thanks
|
||||
@krns!)
|
||||
* Include response body metrics for HTTP/1 chunk responses
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix broken HTTP/1 inform/3 return value (thanks @wojtekmach!)
|
||||
* Maintain HTTP/1 read timeout after receiving unknown messages
|
||||
|
||||
## 1.1.3 (12 Jan 2024)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Do not send a fallback response if the plug has already sent one (#288 & #289, thanks @jclem!)
|
||||
|
||||
### Changes
|
||||
|
||||
* Packagaing improvements (#283, thanks @wojtekmach!)
|
||||
|
||||
## 1.1.2 (20 Dec 2023)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix support for proplist-style arguments (#277, thanks @jjcarstens!)
|
||||
* Speed up WebSocket framing (#272, thanks @crertel!)
|
||||
* Fix off-by-one error in HTTP2 sendfile (#269, thanks @OrangeDrangon!)
|
||||
* Improve mix file packaging (#266, thanks @patrickjaberg!)
|
||||
|
||||
## 1.1.1 (14 Nov 2023)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Do not advertise disabled protocols via ALPN (#263)
|
||||
|
||||
## 1.1.0 (2 Nov 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Messages sent to Bandit HTTP/1 handlers no longer intentionally crash the
|
||||
handler process but are now logged in the same manner as messages sent to a
|
||||
no-op GenServer (#259)
|
||||
* Messages regarding normal termination of monitored processes are no longer
|
||||
handled by the WebSocket handler, but are now passed to the configured
|
||||
`c:WebSock.handle_info/2` callback (#259)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add support for `Phoenix.Endpoint.server_info/1` (now in Phoenix main; #258)
|
||||
* Add support for `:max_heap_size` option in WebSocket handler (introduced in
|
||||
websock_adapter 0.5.5; #255, thanks @v0idpwn!)
|
||||
|
||||
## 1.0.0 (18 Oct 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Remove internal tracking of remote `max_concurrent_streams` setting (#248)
|
||||
|
||||
## 1.0.0-pre.18 (10 Oct 2023)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix startup when plug module has not yet been loaded by the BEAM
|
||||
|
||||
## 1.0.0-pre.17 (9 Oct 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Support function based plugs & improve startup analysis of plug configuration
|
||||
(#236)
|
||||
* Improve keepalive support when Plug does not read request bodies (#244)
|
||||
* Improve logic around not sending bodies on HEAD requests (#242)
|
||||
|
||||
### Changes
|
||||
|
||||
* Internal refactor of WebSocket validation (#229)
|
||||
|
||||
|
||||
## 1.0.0-pre.16 (18 Sep 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Use protocol default port in the event that no port is provided in host header (#228)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Improve handling of iolist response bodies (#231, thanks @travelmassive!)
|
||||
|
||||
## 1.0.0-pre.15 (9 Sep 2023)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix issue with setting remote IP at connection startup (#227, thanks @jimc64!)
|
||||
|
||||
## 1.0.0-pre.14 (28 Aug 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `Bandit.PhoenixAdapter.bandit_pid/2` (#212)
|
||||
* Return errors to `Plug.Conn.Adapter.chunk/2` HTTP/1 calls (#216)
|
||||
|
||||
### Changes
|
||||
|
||||
* `Plug.Conn` function calls must come from the process on which `Plug.call/2` was called (#217, reverts #117)
|
||||
|
||||
## 1.0.0-pre.13 (15 Aug 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add ability to send preamble frames when closing a WebSock connection (#211)
|
||||
|
||||
## 1.0.0-pre.12 (12 Aug 2023)
|
||||
|
||||
## Fixes
|
||||
|
||||
* Bump ThousandIsland to 1.0.0-pre.7 to fix leaking file descriptors on
|
||||
`Plug.Conn.sendfile/5` calls (thanks @Hermanverschooten!)
|
||||
|
||||
## 1.0.0-pre.11 (11 Aug 2023)
|
||||
|
||||
## Changes
|
||||
|
||||
* **BREAKING CHANGE** Move `conn` value in telemetry events from measurements to metadata
|
||||
|
||||
## Enhancements
|
||||
|
||||
* Add `method`, `request_target` and `status` fields to telemetry metadata on HTTP stop events
|
||||
* Improve RFC compliance regarding cache-related headers on deflated responses (#207, thanks @tanguilp!)
|
||||
* Bump to Thousand Island `1.0.0-pre.6`
|
||||
* Doc improvements (particularly around implementation notes)
|
||||
* Typespec improvements (thanks @moogle19!)
|
||||
|
||||
## 1.0.0-pre.10 (28 Jun 2023)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* Add support for `Plug.Conn.inform/3` on HTTP/1 connections (#180)
|
||||
* Add support for h2c upgrades (#186, thanks @alisinabh!)
|
||||
* Internal refactoring of HTTP/1 content-length encoded body reads (#184, #190,
|
||||
thanks @asakura & @moogle19!)
|
||||
|
||||
## Changes
|
||||
|
||||
* Bump Thousand Island to 1.0.0-pre.6 (gaining support for suspend/resume API)
|
||||
* Drop Elixir 1.12 as a supported target (it should continue to work, but is no
|
||||
longer covered by CI)
|
||||
|
||||
## Fixes
|
||||
|
||||
* Fix crash when Plug used `Plug.Conn.get_peer_data/1` function on HTTP/1
|
||||
connections (#170, thanks @moogle19!)
|
||||
* Fix port behaviour when connecting over unix socket (#176, thanks @asakura
|
||||
& @ibarchenkov!)
|
||||
|
||||
## 1.0.0-pre.9 (16 Jun 2023)
|
||||
|
||||
## Changes
|
||||
|
||||
* Use new ThousandIsland APIs for socket info (#167, thanks @asakura!)
|
||||
|
||||
## Fixes
|
||||
|
||||
* Handle nil connection close reason when closing a WebSocket
|
||||
|
||||
## 1.0.0-pre.8 (15 Jun 2023)
|
||||
|
||||
## Fixes
|
||||
|
||||
* Further improve logging on WebSocket upgrade errors (#149)
|
||||
|
||||
## 1.0.0-pre.7 (14 Jun 2023)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* Refactor HTTP/1 read routines (#158 & #166, thanks @asakura!)
|
||||
* Improve logging on WebSocket upgrade errors (#149)
|
||||
|
||||
## Changes
|
||||
|
||||
* Override any content-length headers that may have been set by Plug (#165)
|
||||
* Send content-length on HTTP/2 responses where appropriate (#165)
|
||||
|
||||
## Fixes
|
||||
|
||||
* Send correct content-length header when sending deflated response (#151)
|
||||
* Do not attempt to deflate if Plug sends a content-encoding header (#165)
|
||||
* Improve corner case handling of content-length request header (#163, thanks
|
||||
@ryanwinchester!)
|
||||
* Handle case where ThousandIsland returns error tuples on some helper routines
|
||||
(#162)
|
||||
|
||||
## 1.0.0-pre.6 (8 Jun 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Always use the declaed scheme if declared in a request-line or `:scheme`
|
||||
pseudo-header (#159)
|
||||
* Internal tidying (thanks @asakura!)
|
||||
|
||||
## 1.0.0-pre.5 (2 Jun 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Total overhaul of typespecs throughout the library (thanks @asakura!)
|
||||
|
||||
## 1.0.0-pre.4 (23 May 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Performance / correctness improvements to header length validation (#143,
|
||||
thanks @moogle19!)
|
||||
* Performance improvements to host header port parsing (#145 & #147, thanks
|
||||
@ryanwinchester!)
|
||||
* Improve WebSocket upgrade failure error messages to aid in diagnosis (#152)
|
||||
|
||||
### Changes
|
||||
|
||||
* Consolidate credo config (#146, thanks @ryanwinchester!)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix error in suggested version dependencies during 1.0-pre series (#142,
|
||||
thanks @cvkmohan!)
|
||||
|
||||
## 1.0.0-pre.3 (3 May 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Respect read timeout for HTTP/1 keepalives (#140)
|
||||
* Support Websock 0.5.1, including support for optional `c:WebSock.terminate/2`
|
||||
(#131)
|
||||
|
||||
### Changes
|
||||
|
||||
* Use Req instead of Finch in tests (#137)
|
||||
* Improve a few corner cases in tests (#136)
|
||||
|
||||
## 1.0.0-pre.2 (24 Apr 2023)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Don't require transport_options to be a keyword list (#130, thanks @justinludwig!)
|
||||
|
||||
## 1.0.0-pre.1 (21 Apr 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Update Thousand Island dependency to 1.0-pre
|
||||
|
||||
# Changelog for 0.7.x
|
||||
|
||||
## 0.7.7 (11 Apr 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Bandit will now raise an error at startup if no plug is specified in config
|
||||
(thanks @moogle19!)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix crash at startup when using `otp_app` option (thanks @moogle19!)
|
||||
* Minor doc formatting fixes
|
||||
|
||||
## 0.7.6 (9 Apr 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* **BREAKING CHANGE** Rename top-level `options` field to `thousand_island_options`
|
||||
* **BREAKING CHANGE** Rename `deflate_opts` to `deflate_options` where used
|
||||
* Massive overhaul of documentation to use types where possible
|
||||
* Bandit now uses a term of the form `{Bandit, ref()}` for `id` in our child spec
|
||||
* Bumped to Thousand Island 0.6.7. `num_connections` is now 16384 by default
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Added top level support for the following convenience parameters:
|
||||
* `port` can now be set at the top level of your configuration
|
||||
* `ip` can now be set at the top level of your configuration
|
||||
* `keyfile` and `certfile` can now be set at the top level of your configuration
|
||||
* Transport options are now validated by `Plug.SSL.configure/1` when starting
|
||||
an HTTPS server
|
||||
* Rely on Thousand Island to validate options specified in `thousand_island_options`. This should avoid cases like #125 in the future.
|
||||
|
||||
## 0.7.5 (4 Apr 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Drop explicit support for Elixir 1.11 since we no longer test it in CI (should
|
||||
still work, just that it's now at-your-own-risk)
|
||||
* Add logo to ex_doc and README
|
||||
|
||||
### Fixes
|
||||
|
||||
* Allow access to Thousand Island's underlying `shutdown_timeout` option
|
||||
* Fix test errors that cropped up in OTP 26
|
||||
|
||||
|
||||
## 0.7.4 (27 Mar 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Calling `Plug.Conn` adapter functions for HTTP/2 based requests are no longer
|
||||
restricted to being called from the process which called `c:Plug.call/2`
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Added `startup_log` to control whether / how Bandit logs the bound host & port
|
||||
at startup (Thanks @danschultzer)
|
||||
* Improved logging when the configured port is in use at startup (Thanks
|
||||
@danschultzer)
|
||||
* Update to Thousand Island 0.6.5
|
||||
|
||||
## 0.7.3 (20 Mar 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Added advanced `handler_module` configuration option to `options`
|
||||
|
||||
### Fixes
|
||||
|
||||
* Support returning `x-gzip` as negotiated `content-encoding` (previously would
|
||||
negotiate a request for `x-gzip` as `gzip`)
|
||||
|
||||
## 0.7.2 (18 Mar 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Added HTTP compression via 'Content-Encoding' negotiation, enabled by default.
|
||||
Configuration is available; see [Bandit
|
||||
docs](https://hexdocs.pm/bandit/Bandit.html#module-config-options) for details
|
||||
|
||||
### Changes
|
||||
|
||||
* Minor refactor of internal HTTP/2 plumbing. No user visible changes
|
||||
|
||||
## 0.7.1 (17 Mar 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Update documentation & messaging to refer to RFC911x RFCs where appropriate
|
||||
* Validate top-level config options at startup
|
||||
* Revise Phoenix adapter to support new config options
|
||||
* Doc updates
|
||||
|
||||
## 0.7.0 (17 Mar 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add configuration points for various parameters within the HTTP/1, HTTP/2 and
|
||||
WebSocket stacks. See [Bandit
|
||||
docs](https://hexdocs.pm/bandit/Bandit.html#module-config-options) for details
|
||||
|
||||
# Changelog for 0.6.x
|
||||
|
||||
## 0.6.11 (17 Mar 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Modified telemetry event payloads to match the conventions espoused by
|
||||
`:telemetry.span/3`
|
||||
* Default shutdown timeout is now 15s (up from 5s)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Update to Thosuand Island 0.6.4 (from 0.6.2)
|
||||
|
||||
## 0.6.10 (10 Mar 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Support explicit setting of WebSocket close codes & reasons as added in WebSock
|
||||
0.5.0
|
||||
|
||||
## 0.6.9 (20 Feb 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add comprehensive Telemetry support within Bandit, as documented in the
|
||||
`Bandit.Telemetry` module
|
||||
* Update our ThousandIsland dependnecy to pull in Thousand Island's newly
|
||||
updated Telemetry support as documented in the `ThousandIsland.Telemetry`
|
||||
module
|
||||
* Fix parsing of host / request headers which contain IPv6 addresses (#97).
|
||||
Thanks @derekkraan!
|
||||
|
||||
# Changes
|
||||
|
||||
* Use Plug's list of response code reason phrases (#96). Thanks @jclem!
|
||||
* Minor doc updates
|
||||
|
||||
## 0.6.8 (31 Jan 2023)
|
||||
|
||||
### Changes
|
||||
|
||||
* Close WebSocket connections with a code of 1000 (instead of 1001) when
|
||||
shutting down the server (#89)
|
||||
* Use 100 acceptor processes by default (instead of 10)
|
||||
* Improvements to make WebSocket frame masking faster
|
||||
|
||||
## 0.6.7 (17 Jan 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Remove logging entirely when client connections do not contain a valid protocol
|
||||
* Refactor WebSocket support for about a 20% performance lift
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add `nodelay` option to test suite to fix artificially slow WebSocket perf tests
|
||||
|
||||
## 0.6.6 (11 Jan 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Log useful message when a TLS connection is made to plaintext server (#74)
|
||||
|
||||
## 0.6.5 (10 Jan 2023)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Update Thousand Island to 0.5.15 (quiets logging in timeout cases)
|
||||
* Quiet logging in when client connections do not contain a valid protocol
|
||||
* Refactor HTTP/1 for about a 20% performance lift
|
||||
* Add WebSocket support to CI benchmark workflow
|
||||
* Doc updates
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Allow multiple instances of Bandit to be started in the same node (#75)
|
||||
* Improve error handling in HTTP/1 when protocol errors are encountered (#74)
|
||||
21
phoenix/deps/bandit/LICENSE
Normal file
21
phoenix/deps/bandit/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Mat Trudel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
245
phoenix/deps/bandit/README.md
Normal file
245
phoenix/deps/bandit/README.md
Normal file
@@ -0,0 +1,245 @@
|
||||

|
||||

|
||||
|
||||
[](https://github.com/mtrudel/bandit/actions)
|
||||
[](https://hexdocs.pm/bandit)
|
||||
[](https://hex.pm/packages/bandit)
|
||||
|
||||
Bandit is an HTTP server for Plug and WebSock apps.
|
||||
|
||||
Bandit is written entirely in Elixir and is built atop [Thousand
|
||||
Island](https://github.com/mtrudel/thousand_island). It can serve HTTP/1.x,
|
||||
HTTP/2 and WebSocket clients over both HTTP and HTTPS. It is written with
|
||||
correctness, clarity & performance as fundamental goals. It is the default HTTP
|
||||
server for [Phoenix](https://github.com/phoenixframework/phoenix) since release 1.7.11 of the framework.
|
||||
|
||||
In [ongoing automated performance
|
||||
tests](https://github.com/mtrudel/bandit/actions/workflows/manual_benchmark.yml),
|
||||
Bandit's HTTP/1.x engine is up to 4x faster than Cowboy depending on the number of concurrent
|
||||
requests. When comparing HTTP/2 performance, Bandit is up to 1.5x faster than Cowboy. This is
|
||||
possible because Bandit has been built from the ground up for use with Plug applications; this
|
||||
focus pays dividends in both performance and also in the approachability of the code base.
|
||||
|
||||
Bandit also emphasizes correctness. Its HTTP/2 implementation scores 100% on the
|
||||
[h2spec](https://github.com/summerwind/h2spec) suite in strict mode, and its
|
||||
WebSocket implementation scores 100% on the
|
||||
[Autobahn](https://github.com/crossbario/autobahn-testsuite) test suite, both of
|
||||
which run as part of Bandit's comprehensive CI suite. Extensive unit test,
|
||||
credo, dialyzer, and performance regression test coverage round out a test suite
|
||||
that ensures that Bandit is and will remain a platform you can count on.
|
||||
|
||||
Lastly, Bandit exists to demystify the lower layers of infrastructure code. In a world where
|
||||
The New Thing is nearly always adding abstraction on top of abstraction, it's important to have
|
||||
foundational work that is approachable & understandable by users above it in the stack.
|
||||
|
||||
## Project Goals
|
||||
|
||||
* Implement comprehensive support for HTTP/1.0 through HTTP/2 & WebSockets (and
|
||||
beyond) backed by obsessive RFC literacy and automated conformance testing
|
||||
* Aim for minimal internal policy and HTTP-level configuration. Delegate to Plug & WebSock as much as
|
||||
possible, and only interpret requests to the extent necessary to safely manage a connection
|
||||
& fulfill the requirements of safely supporting protocol correctness
|
||||
* Prioritize (in order): correctness, clarity, performance. Seek to remove the mystery of
|
||||
infrastructure code by being approachable and easy to understand
|
||||
* Along with our companion library [Thousand
|
||||
Island](https://github.com/mtrudel/thousand_island), become the go-to HTTP
|
||||
& low-level networking stack of choice for the Elixir community by being
|
||||
reliable, efficient, and approachable
|
||||
|
||||
## Project Status
|
||||
|
||||
* Complete support for [Phoenix](https://github.com/phoenixframework/phoenix) applications (WebSocket
|
||||
support requires Phoenix 1.7+)
|
||||
* Complete support of the [Plug API](https://github.com/elixir-plug/plug)
|
||||
* Complete support of the [WebSock API](https://github.com/phoenixframework/websock)
|
||||
* Complete server support for HTTP/1.x as defined in [RFC
|
||||
9112](https://datatracker.ietf.org/doc/html/rfc9112) & [RFC
|
||||
9110](https://datatracker.ietf.org/doc/html/rfc9110)
|
||||
* Complete server support for HTTP/2 as defined in [RFC
|
||||
9113](https://datatracker.ietf.org/doc/html/rfc9113) & [RFC
|
||||
9110](https://datatracker.ietf.org/doc/html/rfc9110), comprehensively covered
|
||||
by automated [h2spec](https://github.com/summerwind/h2spec) conformance testing
|
||||
* Support for HTTP content encoding compression on both HTTP/1.x and HTTP/2.
|
||||
gzip and deflate methods are supported per
|
||||
[RFC9110§8.4.1.{2,3}](https://www.rfc-editor.org/rfc/rfc9110.html#section-8.4.1.2)
|
||||
* Complete server support for WebSockets as defined in [RFC
|
||||
6455](https://datatracker.ietf.org/doc/html/rfc6455), comprehensively covered by automated
|
||||
[Autobahn](https://github.com/crossbario/autobahn-testsuite) conformance testing. Per-message
|
||||
compression as defined in [RFC 7692](https://datatracker.ietf.org/doc/html/rfc7692) is also
|
||||
supported
|
||||
* Extremely scalable and performant client handling at a rate up to 4x that of Cowboy for the same
|
||||
workload with as-good-or-better memory use
|
||||
|
||||
Any Phoenix or Plug app should work with Bandit as a drop-in replacement for
|
||||
Cowboy; exceptions to this are errors (if you find one, please [file an
|
||||
issue!](https://github.com/mtrudel/bandit/issues)).
|
||||
|
||||
<!-- MDOC -->
|
||||
|
||||
## Using Bandit With Phoenix
|
||||
|
||||
Bandit fully supports Phoenix. Phoenix applications which use WebSockets for
|
||||
features such as Channels or LiveView require Phoenix 1.7 or later.
|
||||
|
||||
Using Bandit to host your Phoenix application couldn't be simpler:
|
||||
|
||||
1. Add Bandit as a dependency in your Phoenix application's `mix.exs`:
|
||||
|
||||
```elixir
|
||||
{:bandit, "~> 1.8"}
|
||||
```
|
||||
2. Add the following `adapter:` line to your endpoint configuration in `config/config.exs`, as in the following example:
|
||||
|
||||
```elixir
|
||||
# config/config.exs
|
||||
|
||||
config :your_app, YourAppWeb.Endpoint,
|
||||
adapter: Bandit.PhoenixAdapter, # <---- ADD THIS LINE
|
||||
url: [host: "localhost"],
|
||||
render_errors: ...
|
||||
```
|
||||
3. That's it! **You should now see messages at startup indicating that Phoenix is
|
||||
using Bandit to serve your endpoint**, and everything should 'just work'. Note
|
||||
that if you have set any exotic configuration options within your endpoint,
|
||||
you may need to update that configuration to work with Bandit; see the
|
||||
[Bandit.PhoenixAdapter](https://hexdocs.pm/bandit/Bandit.PhoenixAdapter.html)
|
||||
documentation for more information.
|
||||
|
||||
## Using Bandit With Plug Applications
|
||||
|
||||
Using Bandit to host your own Plug is very straightforward. Assuming you have
|
||||
a Plug module implemented already, you can host it within Bandit by adding
|
||||
something similar to the following to your application's `Application.start/2`
|
||||
function:
|
||||
|
||||
```elixir
|
||||
# lib/my_app/application.ex
|
||||
|
||||
defmodule MyApp.Application do
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
{Bandit, plug: MyApp.MyPlug}
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
For less formal usage, you can also start Bandit using the same configuration
|
||||
options via the `Bandit.start_link/1` function:
|
||||
|
||||
```elixir
|
||||
# Start an http server on the default port 4000, serving MyApp.MyPlug
|
||||
Bandit.start_link(plug: MyPlug)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
A number of options are defined when starting a server. The complete list is
|
||||
defined by the [`t:Bandit.options/0`](https://hexdocs.pm/bandit/Bandit.html#summary) type.
|
||||
|
||||
## Setting up an HTTPS Server
|
||||
|
||||
By far the most common stumbling block encountered when setting up an HTTPS
|
||||
server involves configuring key and certificate data. Bandit is comparatively
|
||||
easy to set up in this regard, with a working example looking similar to the
|
||||
following:
|
||||
|
||||
```elixir
|
||||
# lib/my_app/application.ex
|
||||
|
||||
defmodule MyApp.Application do
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
{Bandit,
|
||||
plug: MyApp.MyPlug,
|
||||
scheme: :https,
|
||||
certfile: "/absolute/path/to/cert.pem",
|
||||
keyfile: "/absolute/path/to/key.pem"}
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## WebSocket Support
|
||||
|
||||
If you're using Bandit to run a Phoenix application as suggested above, there is
|
||||
nothing more for you to do; WebSocket support will 'just work'.
|
||||
|
||||
If you wish to interact with WebSockets at a more fundamental level, the
|
||||
[WebSock](https://hexdocs.pm/websock/WebSock.html) and
|
||||
[WebSockAdapter](https://hexdocs.pm/websock_adapter/WebSockAdapter.html) libraries
|
||||
provides a generic abstraction for WebSockets (very similar to how Plug is
|
||||
a generic abstraction on top of HTTP). Bandit fully supports all aspects of
|
||||
these libraries.
|
||||
|
||||
## Receiving messages in your Plug process: A word of warning
|
||||
|
||||
The Plug specification is concerned only with the shape of the `c:Plug.init/1`
|
||||
and `c:Plug.call/2` functions; it says nothing about the process model that
|
||||
underlies the call, nor about how the Plug function should respond to any
|
||||
messages it may receive. Although it is occasionally necessary to receive
|
||||
messages from within your Plug call, this must be done with caution as Bandit
|
||||
makes extensive use of messaging internally, especially with HTTP/2 based
|
||||
requests.
|
||||
|
||||
In particular, you must ensure that your code *never* receives messages that
|
||||
match the patterns `{:bandit, _}` or `{:plug_conn, :sent}`. Any `receive` calls
|
||||
you make should be appropriately guarded to ensure that these messages remain in
|
||||
the process' mailbox for Bandit to process them when required.
|
||||
|
||||
<!-- MDOC -->
|
||||
|
||||
## Implementation Details
|
||||
|
||||
Bandit primarily consists of three protocol-specific implementations, one each
|
||||
for [HTTP/1][], [HTTP/2][] and [WebSockets][]. Each of these implementations is
|
||||
largely distinct from one another, and is described in its own README linked
|
||||
above.
|
||||
|
||||
If you're just taking a casual look at Bandit or trying to understand how an
|
||||
HTTP server works, the [HTTP/1][] implementation is likely the best place to
|
||||
start exploring.
|
||||
|
||||
[HTTP/1]: lib/bandit/http1/README.md
|
||||
[HTTP/2]: lib/bandit/http2/README.md
|
||||
[WebSockets]: lib/bandit/websocket/README.md
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to Bandit are very much welcome! Before undertaking any substantial work, please
|
||||
open an issue on the project to discuss ideas and planned approaches so we can ensure we keep
|
||||
progress moving in the same direction.
|
||||
|
||||
All contributors must agree and adhere to the project's [Code of
|
||||
Conduct](https://github.com/mtrudel/bandit/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
||||
Security disclosures should be handled per Bandit's published [security policy](https://github.com/mtrudel/bandit/blob/main/SECURITY.md).
|
||||
|
||||
## Installation
|
||||
|
||||
Bandit is [available in Hex](https://hex.pm/docs/publish). The package can be installed
|
||||
by adding `bandit` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:bandit, "~> 1.8"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Documentation can be found at [https://hexdocs.pm/bandit](https://hexdocs.pm/bandit).
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
82
phoenix/deps/bandit/hex_metadata.config
Normal file
82
phoenix/deps/bandit/hex_metadata.config
Normal file
@@ -0,0 +1,82 @@
|
||||
{<<"links">>,
|
||||
[{<<"Changelog">>,<<"https://hexdocs.pm/bandit/changelog.html">>},
|
||||
{<<"GitHub">>,<<"https://github.com/mtrudel/bandit">>}]}.
|
||||
{<<"name">>,<<"bandit">>}.
|
||||
{<<"version">>,<<"1.10.4">>}.
|
||||
{<<"description">>,
|
||||
<<"A pure-Elixir HTTP server built for Plug & WebSock apps">>}.
|
||||
{<<"elixir">>,<<"~> 1.13">>}.
|
||||
{<<"app">>,<<"bandit">>}.
|
||||
{<<"licenses">>,[<<"MIT">>]}.
|
||||
{<<"files">>,
|
||||
[<<"lib">>,<<"lib/bandit.ex">>,<<"lib/bandit">>,
|
||||
<<"lib/bandit/primitive_ops">>,<<"lib/bandit/primitive_ops/websocket.ex">>,
|
||||
<<"lib/bandit/phoenix_adapter.ex">>,<<"lib/bandit/http_transport.ex">>,
|
||||
<<"lib/bandit/logger.ex">>,<<"lib/bandit/http1">>,
|
||||
<<"lib/bandit/http1/README.md">>,<<"lib/bandit/http1/handler.ex">>,
|
||||
<<"lib/bandit/http1/socket.ex">>,<<"lib/bandit/initial_handler.ex">>,
|
||||
<<"lib/bandit/socket_helpers.ex">>,<<"lib/bandit/extractor.ex">>,
|
||||
<<"lib/bandit/http2">>,<<"lib/bandit/http2/README.md">>,
|
||||
<<"lib/bandit/http2/stream_collection.ex">>,<<"lib/bandit/http2/frame.ex">>,
|
||||
<<"lib/bandit/http2/stream_process.ex">>,<<"lib/bandit/http2/settings.ex">>,
|
||||
<<"lib/bandit/http2/connection.ex">>,<<"lib/bandit/http2/handler.ex">>,
|
||||
<<"lib/bandit/http2/errors.ex">>,<<"lib/bandit/http2/stream.ex">>,
|
||||
<<"lib/bandit/http2/frame">>,<<"lib/bandit/http2/frame/data.ex">>,
|
||||
<<"lib/bandit/http2/frame/window_update.ex">>,
|
||||
<<"lib/bandit/http2/frame/goaway.ex">>,
|
||||
<<"lib/bandit/http2/frame/rst_stream.ex">>,
|
||||
<<"lib/bandit/http2/frame/push_promise.ex">>,
|
||||
<<"lib/bandit/http2/frame/settings.ex">>,
|
||||
<<"lib/bandit/http2/frame/priority.ex">>,
|
||||
<<"lib/bandit/http2/frame/unknown.ex">>,
|
||||
<<"lib/bandit/http2/frame/ping.ex">>,
|
||||
<<"lib/bandit/http2/frame/continuation.ex">>,
|
||||
<<"lib/bandit/http2/frame/headers.ex">>,
|
||||
<<"lib/bandit/http2/flow_control.ex">>,<<"lib/bandit/compression.ex">>,
|
||||
<<"lib/bandit/application.ex">>,<<"lib/bandit/http_error.ex">>,
|
||||
<<"lib/bandit/websocket">>,<<"lib/bandit/websocket/README.md">>,
|
||||
<<"lib/bandit/websocket/frame.ex">>,
|
||||
<<"lib/bandit/websocket/permessage_deflate.ex">>,
|
||||
<<"lib/bandit/websocket/connection.ex">>,
|
||||
<<"lib/bandit/websocket/handler.ex">>,
|
||||
<<"lib/bandit/websocket/handshake.ex">>,
|
||||
<<"lib/bandit/websocket/socket.ex">>,<<"lib/bandit/websocket/frame">>,
|
||||
<<"lib/bandit/websocket/frame/connection_close.ex">>,
|
||||
<<"lib/bandit/websocket/frame/pong.ex">>,
|
||||
<<"lib/bandit/websocket/frame/binary.ex">>,
|
||||
<<"lib/bandit/websocket/frame/ping.ex">>,
|
||||
<<"lib/bandit/websocket/frame/text.ex">>,
|
||||
<<"lib/bandit/websocket/frame/continuation.ex">>,
|
||||
<<"lib/bandit/websocket/upgrade_validation.ex">>,
|
||||
<<"lib/bandit/telemetry.ex">>,<<"lib/bandit/transport_error.ex">>,
|
||||
<<"lib/bandit/pipeline.ex">>,<<"lib/bandit/adapter.ex">>,
|
||||
<<"lib/bandit/delegating_handler.ex">>,<<"lib/bandit/headers.ex">>,
|
||||
<<"lib/bandit/trace.ex">>,<<"lib/bandit/clock.ex">>,<<"mix.exs">>,
|
||||
<<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>]}.
|
||||
{<<"requirements">>,
|
||||
[[{<<"name">>,<<"thousand_island">>},
|
||||
{<<"app">>,<<"thousand_island">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 1.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"plug">>},
|
||||
{<<"app">>,<<"plug">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 1.18">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"websock">>},
|
||||
{<<"app">>,<<"websock">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 0.5">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"hpax">>},
|
||||
{<<"app">>,<<"hpax">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 1.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"telemetry">>},
|
||||
{<<"app">>,<<"telemetry">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 0.4 or ~> 1.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}]]}.
|
||||
{<<"build_tools">>,[<<"mix">>]}.
|
||||
436
phoenix/deps/bandit/lib/bandit.ex
Normal file
436
phoenix/deps/bandit/lib/bandit.ex
Normal file
@@ -0,0 +1,436 @@
|
||||
defmodule Bandit do
|
||||
@external_resource Path.join([__DIR__, "../README.md"])
|
||||
|
||||
@moduledoc """
|
||||
Bandit is an HTTP server for Plug and WebSock apps.
|
||||
|
||||
As an HTTP server, Bandit's primary goal is to act as 'glue' between client connections managed
|
||||
by [Thousand Island](https://github.com/mtrudel/thousand_island) and application code defined
|
||||
via the [Plug](https://github.com/elixir-plug/plug) and/or
|
||||
[WebSock](https://github.com/phoenixframework/websock) APIs. As such there really isn't a whole lot of
|
||||
user-visible surface area to Bandit, and as a consequence the API documentation presented here
|
||||
is somewhat sparse. This is by design! Bandit is intended to 'just work' in almost all cases;
|
||||
the only thought users typically have to put into Bandit comes in the choice of which options (if
|
||||
any) they would like to change when starting a Bandit server. The sparseness of the Bandit API
|
||||
should not be taken as an indicator of the comprehensiveness or robustness of the project.
|
||||
|
||||
#{@external_resource |> File.read!() |> String.split("<!-- MDOC -->") |> Enum.fetch!(1)}
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
Possible top-level options to configure a Bandit server
|
||||
|
||||
* `plug`: The Plug to use to handle connections. Can be specified as `MyPlug` or `{MyPlug, plug_opts}`
|
||||
* `scheme`: One of `:http` or `:https`. If `:https` is specified, you will also need to specify
|
||||
valid `certfile` and `keyfile` values (or an equivalent value within
|
||||
`thousand_island_options.transport_options`). Defaults to `:http`
|
||||
* `port`: The TCP port to listen on. This option is offered as a convenience and actually sets
|
||||
the option of the same name within `thousand_island_options`. If a string value is passed, it
|
||||
will be parsed as an integer. Defaults to 4000 if `scheme` is `:http`, and 4040 if `scheme` is
|
||||
`:https`
|
||||
* `ip`: The interface(s) to listen on. This option is offered as a convenience and actually sets the
|
||||
option of the same name within `thousand_island_options.transport_options`. Can be specified as:
|
||||
* `{1, 2, 3, 4}` for IPv4 addresses
|
||||
* `{1, 2, 3, 4, 5, 6, 7, 8}` for IPv6 addresses
|
||||
* `:loopback` for local loopback (ie: `127.0.0.1`)
|
||||
* `:any` for all interfaces (ie: `0.0.0.0`)
|
||||
* `{:local, "/path/to/socket"}` for a Unix domain socket. If this option is used, the `port`
|
||||
option *must* be set to `0`
|
||||
* `inet`: Only bind to IPv4 interfaces. This option is offered as a convenience and actually sets the
|
||||
option of the same name within `thousand_island_options.transport_options`. Must be specified
|
||||
as a bare atom `:inet`
|
||||
* `inet6`: Only bind to IPv6 interfaces. This option is offered as a convenience and actually sets the
|
||||
option of the same name within `thousand_island_options.transport_options`. Must be specified
|
||||
as a bare atom `:inet6`
|
||||
* `keyfile`: The path to a file containing the SSL key to use for this server. This option is
|
||||
offered as a convenience and actually sets the option of the same name within
|
||||
`thousand_island_options.transport_options`. If a relative path is used here, you will also
|
||||
need to set the `otp_app` parameter and ensure that the named file is part of your application
|
||||
build
|
||||
* `certfile`: The path to a file containing the SSL certificate to use for this server. This option is
|
||||
offered as a convenience and actually sets the option of the same name within
|
||||
`thousand_island_options.transport_options`. If a relative path is used here, you will also
|
||||
need to set the `otp_app` parameter and ensure that the named file is part of your application
|
||||
build
|
||||
* `otp_app`: Provided as a convenience when using relative paths for `keyfile` and `certfile`
|
||||
* `cipher_suite`: Used to define a pre-selected set of ciphers, as described by
|
||||
`Plug.SSL.configure/1`. Optional, can be either `:strong` or `:compatible`
|
||||
* `display_plug`: The plug to use when describing the connection in logs. Useful for situations
|
||||
such as Phoenix code reloading where you have a 'wrapper' plug but wish to refer to the
|
||||
connection by the endpoint name
|
||||
* `startup_log`: The log level at which Bandit should log startup info.
|
||||
Defaults to `:info` log level, can be set to false to disable it
|
||||
* `thousand_island_options`: A list of options to pass to Thousand Island. Bandit sets some
|
||||
default values in this list based on your top-level configuration; these values will be
|
||||
overridden by values appearing here. A complete list can be found at
|
||||
`t:ThousandIsland.options/0`
|
||||
* `http_options`: A list of options to configure the shared aspects of Bandit's HTTP stack. A
|
||||
complete list can be found at `t:http_options/0`
|
||||
* `http_1_options`: A list of options to configure Bandit's HTTP/1 stack. A complete list can
|
||||
be found at `t:http_1_options/0`
|
||||
* `http_2_options`: A list of options to configure Bandit's HTTP/2 stack. A complete list can
|
||||
be found at `t:http_2_options/0`
|
||||
* `websocket_options`: A list of options to configure Bandit's WebSocket stack. A complete list can
|
||||
be found at `t:websocket_options/0`
|
||||
"""
|
||||
@type options :: [
|
||||
{:plug, module() | {module(), Plug.opts()}}
|
||||
| {:scheme, :http | :https}
|
||||
| {:port, :inet.port_number()}
|
||||
| {:ip, :inet.socket_address()}
|
||||
| :inet
|
||||
| :inet6
|
||||
| {:keyfile, binary()}
|
||||
| {:certfile, binary()}
|
||||
| {:otp_app, Application.app()}
|
||||
| {:cipher_suite, :strong | :compatible}
|
||||
| {:display_plug, module()}
|
||||
| {:startup_log, Logger.level() | false}
|
||||
| {:thousand_island_options, ThousandIsland.options()}
|
||||
| {:http_options, http_options()}
|
||||
| {:http_1_options, http_1_options()}
|
||||
| {:http_2_options, http_2_options()}
|
||||
| {:websocket_options, websocket_options()}
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options to configure shared aspects of the HTTP stack in Bandit
|
||||
|
||||
* `compress`: Whether or not to attempt compression of responses via content-encoding
|
||||
negotiation as described in
|
||||
[RFC9110§8.4](https://www.rfc-editor.org/rfc/rfc9110.html#section-8.4). Defaults to true
|
||||
* `response_encodings`: A list of compression encodings, expressed in order of preference.
|
||||
Defaults to `~w(zstd gzip x-gzip deflate)`, with `zstd` only being present on platforms which
|
||||
have the zstd library compiled in
|
||||
* `deflate_options`: A keyword list of options to set on the deflate library. A complete list can
|
||||
be found at `t:deflate_options/0`. Note that these options only affect the behaviour of the
|
||||
'deflate' content encoding; 'gzip' does not have any configurable options (this is a
|
||||
limitation of the underlying `:zlib` library)
|
||||
* `zstd_options`: A map of options passed verbatim to :zstd, review the options [here](https://www.erlang.org/doc/apps/stdlib/zstd.html#t:compress_parameters/0)
|
||||
* `log_exceptions_with_status_codes`: Which exceptions to log. Bandit will log only those
|
||||
exceptions whose status codes (as determined by `Plug.Exception.status/1`) match the specified
|
||||
list or range. Defaults to `500..599`
|
||||
* `log_protocol_errors`: How to log protocol errors such as malformed requests. `:short` will
|
||||
log a single-line summary, while `:verbose` will log full stack traces. The value of `false`
|
||||
will disable protocol error logging entirely. Defaults to `:short`
|
||||
* `log_client_closures`: How to log cases where the client closes the connection. These happen
|
||||
routinely in the real world and so the handling of them is configured separately since they
|
||||
can be quite noisy. Takes the same options as `log_protocol_errors`, but defaults to `false`
|
||||
"""
|
||||
@type http_options :: [
|
||||
{:compress, boolean()}
|
||||
| {:response_encodings, list()}
|
||||
| {:deflate_options, deflate_options()}
|
||||
| {:zstd_options, zstd_options()}
|
||||
| {:log_exceptions_with_status_codes, list() | Range.t()}
|
||||
| {:log_protocol_errors, :short | :verbose | false}
|
||||
| {:log_client_closures, :short | :verbose | false}
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options to configure the HTTP/1 stack in Bandit
|
||||
|
||||
* `enabled`: Whether or not to serve HTTP/1 requests. Defaults to true
|
||||
* `max_request_line_length`: The maximum permitted length of the request line
|
||||
(expressed as the number of bytes on the wire) in an HTTP/1.1 request. Defaults to 10_000 bytes
|
||||
* `max_header_length`: The maximum permitted length of any single header (combined
|
||||
key & value, expressed as the number of bytes on the wire) in an HTTP/1.1 request. Defaults to 10_000 bytes
|
||||
* `max_header_count`: The maximum permitted number of headers in an HTTP/1.1 request.
|
||||
Defaults to 50 headers
|
||||
* `max_requests`: The maximum number of requests to serve in a single
|
||||
HTTP/1.1 connection before closing the connection. Defaults to 0 (no limit)
|
||||
* `clear_process_dict`: Whether to clear the process dictionary of all non-internal entries
|
||||
between subsequent keepalive requests. If set, all keys not starting with `$` are removed from
|
||||
the process dictionary between requests. Defaults to `true`
|
||||
* `gc_every_n_keepalive_requests`: How often to run a full garbage collection pass between subsequent
|
||||
keepalive requests on the same HTTP/1.1 connection. Defaults to 5 (garbage collect between
|
||||
every 5 requests). This option is currently experimental, and may change at any time
|
||||
* `log_unknown_messages`: Whether or not to log unknown messages sent to the handler process.
|
||||
Defaults to `false`
|
||||
"""
|
||||
@type http_1_options :: [
|
||||
{:enabled, boolean()}
|
||||
| {:max_request_line_length, pos_integer()}
|
||||
| {:max_header_length, pos_integer()}
|
||||
| {:max_header_count, pos_integer()}
|
||||
| {:max_requests, pos_integer()}
|
||||
| {:clear_process_dict, boolean()}
|
||||
| {:gc_every_n_keepalive_requests, pos_integer()}
|
||||
| {:log_unknown_messages, boolean()}
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options to configure the HTTP/2 stack in Bandit
|
||||
|
||||
* `enabled`: Whether or not to serve HTTP/2 requests. Defaults to true
|
||||
* `max_header_block_size`: The maximum permitted length of a field block of an HTTP/2 request
|
||||
(expressed as the number of compressed bytes). Includes any concatenated block fragments from
|
||||
continuation frames. Defaults to 50_000 bytes
|
||||
* `max_requests`: The maximum number of requests to serve in a single
|
||||
HTTP/2 connection before closing the connection. Defaults to 0 (no limit)
|
||||
* `max_reset_stream_rate`: The maximum rate of stream resets (RST_STREAM frames) allowed.
|
||||
Specified as a tuple of `{count, milliseconds}` where `count` is the maximum number of
|
||||
RST_STREAM frames allowed within the time window of `milliseconds`. Defaults to `{500, 10_000}`
|
||||
(500 resets per 10 seconds). Setting this to `nil` disables rate limiting
|
||||
* `sendfile_chunk_size`: The maximum number of bytes read per sendfile chunk when streaming
|
||||
HTTP/2 responses. Defaults to 1_048_576 (1 MiB)
|
||||
* `default_local_settings`: Options to override the default values for local HTTP/2
|
||||
settings. Values provided here will override the defaults specified in RFC9113§6.5.2
|
||||
"""
|
||||
@type http_2_options :: [
|
||||
{:enabled, boolean()}
|
||||
| {:max_header_block_size, pos_integer()}
|
||||
| {:max_requests, pos_integer()}
|
||||
| {:max_reset_stream_rate, {pos_integer(), pos_integer()} | nil}
|
||||
| {:sendfile_chunk_size, pos_integer()}
|
||||
| {:default_local_settings, keyword()}
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options to configure the WebSocket stack in Bandit
|
||||
|
||||
* `enabled`: Whether or not to serve WebSocket upgrade requests. Defaults to true
|
||||
* `max_frame_size`: The maximum size of a single WebSocket frame (expressed as
|
||||
a number of bytes on the wire). Defaults to 0 (no limit)
|
||||
* `validate_text_frames`: Whether or not to validate text frames as being UTF-8. Strictly
|
||||
speaking this is required per RFC6455§5.6, however it can be an expensive operation and one
|
||||
that may be safely skipped in some situations. Defaults to true
|
||||
* `compress`: Whether or not to allow per-message deflate compression globally. Note that
|
||||
upgrade requests still need to set the `compress: true` option in `connection_opts` on
|
||||
a per-upgrade basis for compression to be negotiated (see 'WebSocket Support' section below
|
||||
for details). Defaults to `true`
|
||||
* `deflate_options`: A keyword list of options to set on the deflate library when using the
|
||||
per-message deflate extension. A complete list can be found at `t:deflate_options/0`.
|
||||
`window_bits` is currently ignored and left to negotiation.
|
||||
"""
|
||||
@type websocket_options :: [
|
||||
{:enabled, boolean()}
|
||||
| {:max_frame_size, pos_integer()}
|
||||
| {:validate_text_frames, boolean()}
|
||||
| {:compress, boolean()}
|
||||
| {:deflate_options, deflate_options()}
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options to configure the deflate library used for HTTP and WebSocket compression
|
||||
"""
|
||||
@type deflate_options :: [
|
||||
{:level, :zlib.zlevel()}
|
||||
| {:window_bits, :zlib.zwindowbits()}
|
||||
| {:memory_level, :zlib.zmemlevel()}
|
||||
| {:strategy, :zlib.zstrategy()}
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options to configure the zstd library used for HTTP compression
|
||||
"""
|
||||
@type zstd_options :: map
|
||||
|
||||
@typep scheme :: :http | :https
|
||||
|
||||
require Logger
|
||||
|
||||
@doc false
|
||||
@spec child_spec(options()) :: Supervisor.child_spec()
|
||||
def child_spec(arg) do
|
||||
%{
|
||||
id: {__MODULE__, make_ref()},
|
||||
start: {__MODULE__, :start_link, [arg]},
|
||||
type: :supervisor,
|
||||
restart: :permanent
|
||||
}
|
||||
end
|
||||
|
||||
@top_level_keys ~w(plug scheme port ip keyfile certfile otp_app cipher_suite display_plug startup_log thousand_island_options http_options http_1_options http_2_options websocket_options)a
|
||||
@http_keys ~w(compress response_encodings deflate_options zstd_options log_exceptions_with_status_codes log_protocol_errors log_client_closures)a
|
||||
@http_1_keys ~w(enabled max_request_line_length max_header_length max_header_count max_requests clear_process_dict gc_every_n_keepalive_requests log_unknown_messages)a
|
||||
@http_2_keys ~w(enabled max_header_block_size max_requests max_reset_stream_rate sendfile_chunk_size default_local_settings)a
|
||||
@websocket_keys ~w(enabled max_frame_size validate_text_frames compress deflate_options primitive_ops_module)a
|
||||
@thousand_island_keys ThousandIsland.ServerConfig.__struct__()
|
||||
|> Map.from_struct()
|
||||
|> Map.keys()
|
||||
|
||||
@doc """
|
||||
Starts a Bandit server using the provided arguments. See `t:options/0` for specific options to
|
||||
pass to this function.
|
||||
"""
|
||||
@spec start_link(options()) :: Supervisor.on_start()
|
||||
def start_link(arg) do
|
||||
# Special case top-level `:inet` and `:inet6` options so we can use keyword logic everywhere else
|
||||
arg = arg |> special_case_inet_options() |> validate_options(@top_level_keys, "top level")
|
||||
|
||||
thousand_island_options =
|
||||
Keyword.get(arg, :thousand_island_options, [])
|
||||
|> validate_options(@thousand_island_keys, :thousand_island_options)
|
||||
|
||||
http_options =
|
||||
Keyword.get(arg, :http_options, [])
|
||||
|> validate_options(@http_keys, :http_options)
|
||||
|
||||
http_1_options =
|
||||
Keyword.get(arg, :http_1_options, [])
|
||||
|> validate_options(@http_1_keys, :http_1_options)
|
||||
|
||||
http_2_options =
|
||||
Keyword.get(arg, :http_2_options, [])
|
||||
|> validate_options(@http_2_keys, :http_2_options)
|
||||
|
||||
websocket_options =
|
||||
Keyword.get(arg, :websocket_options, [])
|
||||
|> validate_options(@websocket_keys, :websocket_options)
|
||||
|
||||
{plug_mod, _} = plug = plug(arg)
|
||||
display_plug = Keyword.get(arg, :display_plug, plug_mod)
|
||||
startup_log = Keyword.get(arg, :startup_log, :info)
|
||||
|
||||
{http_1_enabled, http_1_options} = Keyword.pop(http_1_options, :enabled, true)
|
||||
{http_2_enabled, http_2_options} = Keyword.pop(http_2_options, :enabled, true)
|
||||
|
||||
handler_options = %{
|
||||
plug: plug,
|
||||
handler_module: Bandit.InitialHandler,
|
||||
opts: %{
|
||||
http: http_options,
|
||||
http_1: http_1_options,
|
||||
http_2: http_2_options,
|
||||
websocket: websocket_options
|
||||
},
|
||||
http_1_enabled: http_1_enabled,
|
||||
http_2_enabled: http_2_enabled
|
||||
}
|
||||
|
||||
scheme = Keyword.get(arg, :scheme, :http)
|
||||
|
||||
{transport_module, transport_options, default_port} =
|
||||
case scheme do
|
||||
:http ->
|
||||
transport_options =
|
||||
Keyword.take(arg, [:ip])
|
||||
|> then(&(Keyword.get(thousand_island_options, :transport_options, []) ++ &1))
|
||||
|
||||
{ThousandIsland.Transports.TCP, transport_options, 4000}
|
||||
|
||||
:https ->
|
||||
supported_protocols =
|
||||
if(http_2_enabled, do: ["h2"], else: []) ++
|
||||
if http_1_enabled, do: ["http/1.1"], else: []
|
||||
|
||||
transport_options =
|
||||
Keyword.take(arg, [:ip, :keyfile, :certfile, :otp_app, :cipher_suite])
|
||||
|> Keyword.merge(alpn_preferred_protocols: supported_protocols)
|
||||
|> then(&(Keyword.get(thousand_island_options, :transport_options, []) ++ &1))
|
||||
|> Plug.SSL.configure()
|
||||
|> case do
|
||||
{:ok, options} -> options
|
||||
{:error, message} -> raise "Plug.SSL.configure/1 encountered error: #{message}"
|
||||
end
|
||||
|> Enum.reject(&(is_tuple(&1) and elem(&1, 0) == :otp_app))
|
||||
|
||||
{ThousandIsland.Transports.SSL, transport_options, 4040}
|
||||
end
|
||||
|
||||
port = Keyword.get(arg, :port, default_port) |> parse_as_number()
|
||||
|
||||
thousand_island_options
|
||||
|> Keyword.put_new(:port, port)
|
||||
|> Keyword.put_new(:transport_module, transport_module)
|
||||
|> Keyword.put(:transport_options, transport_options)
|
||||
|> Keyword.put_new(:handler_module, Bandit.DelegatingHandler)
|
||||
|> Keyword.put_new(:handler_options, handler_options)
|
||||
|> ThousandIsland.start_link()
|
||||
|> case do
|
||||
{:ok, pid} ->
|
||||
startup_log &&
|
||||
Logger.log(startup_log, info(scheme, display_plug, pid), domain: [:bandit], plug: plug)
|
||||
|
||||
{:ok, pid}
|
||||
|
||||
{:error, {:shutdown, {:failed_to_start_child, :listener, :eaddrinuse}}} = error ->
|
||||
Logger.error([info(scheme, display_plug, nil), " failed, port #{port} already in use"],
|
||||
domain: [:bandit],
|
||||
plug: plug
|
||||
)
|
||||
|
||||
error
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@spec special_case_inet_options(options()) :: options()
|
||||
defp special_case_inet_options(opts) do
|
||||
{inet_opts, opts} = Enum.split_with(opts, &(&1 in [:inet, :inet6]))
|
||||
|
||||
if inet_opts == [] do
|
||||
opts
|
||||
else
|
||||
Keyword.update(
|
||||
opts,
|
||||
:thousand_island_options,
|
||||
[transport_options: inet_opts],
|
||||
fn thousand_island_opts ->
|
||||
Keyword.update(thousand_island_opts, :transport_options, inet_opts, &(&1 ++ inet_opts))
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@spec validate_options(Keyword.t(), [atom(), ...], String.t() | atom()) ::
|
||||
Keyword.t() | no_return()
|
||||
defp validate_options(options, valid_values, name) do
|
||||
case Keyword.split(options, valid_values) do
|
||||
{options, []} ->
|
||||
options
|
||||
|
||||
{_, illegal_options} ->
|
||||
raise "Unsupported key(s) in #{name} config: #{inspect(Keyword.keys(illegal_options))}"
|
||||
end
|
||||
end
|
||||
|
||||
@spec plug(options()) :: {module(), Plug.opts()}
|
||||
defp plug(arg) do
|
||||
arg
|
||||
|> Keyword.get(:plug)
|
||||
|> case do
|
||||
nil -> raise "A value is required for :plug"
|
||||
{plug_fn, plug_options} when is_function(plug_fn, 2) -> {plug_fn, plug_options}
|
||||
plug_fn when is_function(plug_fn) -> {plug_fn, []}
|
||||
{plug, plug_options} when is_atom(plug) -> validate_plug(plug, plug_options)
|
||||
plug when is_atom(plug) -> validate_plug(plug, [])
|
||||
other -> raise "Invalid value for plug: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_plug(plug, plug_options) do
|
||||
Code.ensure_loaded!(plug)
|
||||
if !function_exported?(plug, :init, 1), do: raise("plug module does not define init/1")
|
||||
if !function_exported?(plug, :call, 2), do: raise("plug module does not define call/2")
|
||||
|
||||
{plug, plug.init(plug_options)}
|
||||
end
|
||||
|
||||
@spec parse_as_number(binary() | integer()) :: integer()
|
||||
defp parse_as_number(value) when is_binary(value), do: String.to_integer(value)
|
||||
defp parse_as_number(value) when is_integer(value), do: value
|
||||
|
||||
@spec info(scheme(), module(), nil | pid()) :: String.t()
|
||||
defp info(scheme, plug, pid) do
|
||||
server_vsn = Application.spec(:bandit)[:vsn]
|
||||
"Running #{inspect(plug)} with Bandit #{server_vsn} at #{bound_address(scheme, pid)}"
|
||||
end
|
||||
|
||||
@spec bound_address(scheme(), nil | pid()) :: String.t() | scheme()
|
||||
defp bound_address(scheme, nil), do: scheme
|
||||
|
||||
defp bound_address(scheme, pid) do
|
||||
{:ok, {address, port}} = ThousandIsland.listener_info(pid)
|
||||
|
||||
case address do
|
||||
:local -> "#{_unix_path = port} (#{scheme}+unix)"
|
||||
:undefined -> "#{inspect(port)} (#{scheme}+undefined)"
|
||||
:unspec -> "unspec (#{scheme})"
|
||||
address -> "#{:inet.ntoa(address)}:#{port} (#{scheme})"
|
||||
end
|
||||
end
|
||||
end
|
||||
294
phoenix/deps/bandit/lib/bandit/adapter.ex
Normal file
294
phoenix/deps/bandit/lib/bandit/adapter.ex
Normal file
@@ -0,0 +1,294 @@
|
||||
defmodule Bandit.Adapter do
|
||||
@moduledoc false
|
||||
# Implements the Plug-facing `Plug.Conn.Adapter` behaviour. These functions provide the primary
|
||||
# mechanism for Plug applications to interact with a client, including functions to read the
|
||||
# client body (if sent) and send response information back to the client. The concerns in this
|
||||
# module are broadly about the semantics of HTTP in general, and less about transport-specific
|
||||
# concerns, which are managed by the underlying `Bandit.HTTPTransport` implementation
|
||||
|
||||
@behaviour Plug.Conn.Adapter
|
||||
@already_sent {:plug_conn, :sent}
|
||||
|
||||
defstruct transport: nil,
|
||||
owner_pid: nil,
|
||||
method: nil,
|
||||
status: nil,
|
||||
content_encoding: nil,
|
||||
compression_context: nil,
|
||||
upgrade: nil,
|
||||
metrics: %{},
|
||||
opts: []
|
||||
|
||||
@typedoc "A struct for backing a Plug.Conn.Adapter"
|
||||
@type t :: %__MODULE__{
|
||||
transport: Bandit.HTTPTransport.t(),
|
||||
owner_pid: pid() | nil,
|
||||
method: Plug.Conn.method() | nil,
|
||||
status: Plug.Conn.status() | nil,
|
||||
content_encoding: String.t(),
|
||||
compression_context: Bandit.Compression.t() | nil,
|
||||
upgrade: nil | {:websocket, opts :: keyword(), websocket_opts :: keyword()},
|
||||
metrics: %{},
|
||||
opts: %{
|
||||
required(:http) => Bandit.http_options(),
|
||||
required(:websocket) => Bandit.websocket_options()
|
||||
}
|
||||
}
|
||||
|
||||
def init(owner_pid, transport, method, headers, opts) do
|
||||
content_encoding =
|
||||
Bandit.Compression.negotiate_content_encoding(
|
||||
Bandit.Headers.get_header(headers, "accept-encoding"),
|
||||
opts.http
|
||||
)
|
||||
|
||||
%__MODULE__{
|
||||
transport: transport,
|
||||
owner_pid: owner_pid,
|
||||
method: method,
|
||||
content_encoding: content_encoding,
|
||||
metrics: %{req_header_end_time: Bandit.Telemetry.monotonic_time()},
|
||||
opts: opts
|
||||
}
|
||||
end
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def read_req_body(%__MODULE__{} = adapter, opts) do
|
||||
validate_calling_process!(adapter)
|
||||
|
||||
metrics =
|
||||
adapter.metrics
|
||||
|> Map.put_new_lazy(:req_body_start_time, &Bandit.Telemetry.monotonic_time/0)
|
||||
|
||||
case Bandit.HTTPTransport.read_data(adapter.transport, opts) do
|
||||
{:ok, body, transport} ->
|
||||
body = IO.iodata_to_binary(body)
|
||||
|
||||
metrics =
|
||||
metrics
|
||||
|> Map.update(:req_body_bytes, byte_size(body), &(&1 + byte_size(body)))
|
||||
|> Map.put(:req_body_end_time, Bandit.Telemetry.monotonic_time())
|
||||
|
||||
{:ok, body, %{adapter | transport: transport, metrics: metrics}}
|
||||
|
||||
{:more, body, transport} ->
|
||||
body = IO.iodata_to_binary(body)
|
||||
|
||||
metrics =
|
||||
metrics
|
||||
|> Map.update(:req_body_bytes, byte_size(body), &(&1 + byte_size(body)))
|
||||
|
||||
{:more, body, %{adapter | transport: transport, metrics: metrics}}
|
||||
end
|
||||
end
|
||||
|
||||
##################
|
||||
# Response Sending
|
||||
##################
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def send_resp(%__MODULE__{} = adapter, status, headers, body) do
|
||||
validate_calling_process!(adapter)
|
||||
start_time = Bandit.Telemetry.monotonic_time()
|
||||
|
||||
# Save an extra iodata_length by checking common cases
|
||||
empty_body? = Bandit.SocketHelpers.iodata_empty?(body)
|
||||
{headers, compression_context} = Bandit.Compression.new(adapter, status, headers, empty_body?)
|
||||
|
||||
{compress_chunk, compression_context} =
|
||||
Bandit.Compression.compress_chunk(body, compression_context)
|
||||
|
||||
{close_chunk, compression_metrics} = Bandit.Compression.close(compression_context)
|
||||
|
||||
encoded_body = [compress_chunk | close_chunk]
|
||||
encoded_length = IO.iodata_length(encoded_body)
|
||||
headers = Bandit.Headers.add_content_length(headers, encoded_length, status, adapter.method)
|
||||
|
||||
metrics =
|
||||
adapter.metrics
|
||||
|> Map.put(:resp_start_time, start_time)
|
||||
|> Map.merge(compression_metrics)
|
||||
|
||||
adapter =
|
||||
%{adapter | metrics: metrics}
|
||||
|> send_headers(status, headers, :raw)
|
||||
|> send_data(encoded_body, true)
|
||||
|
||||
send(adapter.owner_pid, @already_sent)
|
||||
{:ok, nil, adapter}
|
||||
end
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def send_file(
|
||||
%__MODULE__{} = adapter,
|
||||
status,
|
||||
headers,
|
||||
path,
|
||||
offset,
|
||||
length
|
||||
) do
|
||||
validate_calling_process!(adapter)
|
||||
start_time = Bandit.Telemetry.monotonic_time()
|
||||
{:ok, fileinfo} = :file.read_file_info(path, [:raw, time: :universal])
|
||||
%File.Stat{type: :regular, size: size} = File.Stat.from_record(fileinfo)
|
||||
length = if length == :all, do: size - offset, else: length
|
||||
|
||||
if offset + length <= size do
|
||||
headers = Bandit.Headers.add_content_length(headers, length, status, adapter.method)
|
||||
adapter = send_headers(adapter, status, headers, :raw)
|
||||
|
||||
{socket, bytes_actually_written} =
|
||||
if send_resp_body?(adapter),
|
||||
do: {Bandit.HTTPTransport.sendfile(adapter.transport, path, offset, length), length},
|
||||
else: {adapter.transport, 0}
|
||||
|
||||
metrics =
|
||||
adapter.metrics
|
||||
|> Map.put(:resp_body_bytes, bytes_actually_written)
|
||||
|> Map.put(:resp_start_time, start_time)
|
||||
|> Map.put(:resp_end_time, Bandit.Telemetry.monotonic_time())
|
||||
|
||||
send(adapter.owner_pid, @already_sent)
|
||||
{:ok, nil, %{adapter | transport: socket, metrics: metrics}}
|
||||
else
|
||||
raise "Cannot read #{length} bytes starting at #{offset} as #{path} is only #{size} octets in length"
|
||||
end
|
||||
end
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def send_chunked(%__MODULE__{} = adapter, status, headers) do
|
||||
validate_calling_process!(adapter)
|
||||
start_time = Bandit.Telemetry.monotonic_time()
|
||||
metrics = Map.put(adapter.metrics, :resp_start_time, start_time)
|
||||
|
||||
{headers, compression_context} = Bandit.Compression.new(adapter, status, headers, false, true)
|
||||
adapter = %{adapter | metrics: metrics, compression_context: compression_context}
|
||||
send(adapter.owner_pid, @already_sent)
|
||||
{:ok, nil, send_headers(adapter, status, headers, :chunk_encoded)}
|
||||
end
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def chunk(%__MODULE__{} = adapter, chunk) do
|
||||
# Sending an empty chunk implicitly ends the response. This is a bit of an undefined corner of
|
||||
# the Plug.Conn.Adapter behaviour (see https://github.com/elixir-plug/plug/pull/535 for
|
||||
# details) and ending the response here carves closest to the underlying HTTP/1.1 behaviour
|
||||
# (RFC9112§7.1). Since there is no notion of chunked encoding is in HTTP/2 anyway (RFC9113§8.1)
|
||||
# this entire section of the API is a bit slanty regardless.
|
||||
|
||||
validate_calling_process!(adapter)
|
||||
|
||||
# chunk/2 is unique among Plug.Conn.Adapter's sending callbacks in that it can return an error
|
||||
# tuple instead of just raising or dying on error. Rescue here to implement this
|
||||
try do
|
||||
if Bandit.SocketHelpers.iodata_empty?(chunk) do
|
||||
{encoded_chunk, compression_metrics} =
|
||||
Bandit.Compression.close(adapter.compression_context)
|
||||
|
||||
adapter = %{adapter | metrics: Map.merge(adapter.metrics, compression_metrics)}
|
||||
|
||||
adapter =
|
||||
if encoded_chunk != [] do
|
||||
send_data(adapter, encoded_chunk, false)
|
||||
else
|
||||
adapter
|
||||
end
|
||||
|
||||
{:ok, nil, send_data(adapter, "", true)}
|
||||
else
|
||||
{encoded_chunk, compression_context} =
|
||||
Bandit.Compression.compress_chunk(chunk, adapter.compression_context)
|
||||
|
||||
adapter = %{adapter | compression_context: compression_context}
|
||||
{:ok, nil, send_data(adapter, encoded_chunk, false)}
|
||||
end
|
||||
rescue
|
||||
error in Bandit.TransportError -> {:error, error.error}
|
||||
error -> {:error, Exception.message(error)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def inform(%__MODULE__{} = adapter, status, headers) do
|
||||
validate_calling_process!(adapter)
|
||||
# It's a bit weird to be casing on the underlying version here, but whether or not to send
|
||||
# an informational response is actually defined in RFC9110§15.2 so we consider it as an aspect
|
||||
# of semantics that belongs here and not in the underlying transport
|
||||
if get_http_protocol(adapter) == :"HTTP/1.0" do
|
||||
{:error, :not_supported}
|
||||
else
|
||||
# inform/3 is unique in that headers comes in as a keyword list
|
||||
headers = Enum.map(headers, fn {header, value} -> {to_string(header), value} end)
|
||||
{:ok, send_headers(adapter, status, headers, :inform)}
|
||||
end
|
||||
end
|
||||
|
||||
defp send_headers(adapter, status, headers, body_disposition) do
|
||||
headers =
|
||||
if is_nil(Bandit.Headers.get_header(headers, "date")) do
|
||||
[Bandit.Clock.date_header() | headers]
|
||||
else
|
||||
headers
|
||||
end
|
||||
|
||||
adapter = %{adapter | status: status}
|
||||
|
||||
body_disposition = if send_resp_body?(adapter), do: body_disposition, else: :no_body
|
||||
|
||||
socket =
|
||||
Bandit.HTTPTransport.send_headers(adapter.transport, status, headers, body_disposition)
|
||||
|
||||
%{adapter | transport: socket}
|
||||
end
|
||||
|
||||
defp send_data(adapter, data, end_request) do
|
||||
socket =
|
||||
if send_resp_body?(adapter),
|
||||
do: Bandit.HTTPTransport.send_data(adapter.transport, data, end_request),
|
||||
else: adapter.transport
|
||||
|
||||
data_size = IO.iodata_length(data)
|
||||
metrics = Map.update(adapter.metrics, :resp_body_bytes, data_size, &(&1 + data_size))
|
||||
|
||||
metrics =
|
||||
if end_request,
|
||||
do: Map.put(metrics, :resp_end_time, Bandit.Telemetry.monotonic_time()),
|
||||
else: metrics
|
||||
|
||||
%{adapter | transport: socket, metrics: metrics}
|
||||
end
|
||||
|
||||
defp send_resp_body?(%{method: "HEAD"}), do: false
|
||||
defp send_resp_body?(%{status: 204}), do: false
|
||||
defp send_resp_body?(%{status: 304}), do: false
|
||||
defp send_resp_body?(_adapter), do: true
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def upgrade(%__MODULE__{} = adapter, protocol, opts) do
|
||||
if Keyword.get(adapter.opts.websocket, :enabled, true) &&
|
||||
Bandit.HTTPTransport.supported_upgrade?(adapter.transport, protocol),
|
||||
do: {:ok, %{adapter | upgrade: {protocol, opts, adapter.opts.websocket}}},
|
||||
else: {:error, :not_supported}
|
||||
end
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def push(_adapter, _path, _headers), do: {:error, :not_supported}
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def get_peer_data(%__MODULE__{} = adapter),
|
||||
do: Bandit.HTTPTransport.peer_data(adapter.transport)
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def get_sock_data(%__MODULE__{} = adapter),
|
||||
do: Bandit.HTTPTransport.sock_data(adapter.transport)
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def get_ssl_data(%__MODULE__{} = adapter),
|
||||
do: Bandit.HTTPTransport.ssl_data(adapter.transport)
|
||||
|
||||
@impl Plug.Conn.Adapter
|
||||
def get_http_protocol(%__MODULE__{} = adapter),
|
||||
do: Bandit.HTTPTransport.version(adapter.transport)
|
||||
|
||||
defp validate_calling_process!(%{owner_pid: owner}) when owner == self(), do: :ok
|
||||
defp validate_calling_process!(_), do: raise("Adapter functions must be called by stream owner")
|
||||
end
|
||||
14
phoenix/deps/bandit/lib/bandit/application.ex
Normal file
14
phoenix/deps/bandit/lib/bandit/application.ex
Normal file
@@ -0,0 +1,14 @@
|
||||
defmodule Bandit.Application do
|
||||
@moduledoc false
|
||||
|
||||
use Application
|
||||
|
||||
@impl Application
|
||||
@spec start(Application.start_type(), start_args :: term) ::
|
||||
{:ok, pid}
|
||||
| {:error, {:already_started, pid} | {:shutdown, term} | term}
|
||||
def start(_type, _args) do
|
||||
children = [Bandit.Clock]
|
||||
Supervisor.start_link(children, strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
56
phoenix/deps/bandit/lib/bandit/clock.ex
Normal file
56
phoenix/deps/bandit/lib/bandit/clock.ex
Normal file
@@ -0,0 +1,56 @@
|
||||
defmodule Bandit.Clock do
|
||||
@moduledoc false
|
||||
# Task which updates an ETS table with the current pre-formatted HTTP header
|
||||
# timestamp once a second. This saves the individual request processes from
|
||||
# having to construct this themselves, since it is a surprisingly expensive
|
||||
# operation
|
||||
|
||||
use Task, restart: :permanent
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Returns the current timestamp according to RFC9110§5.6.7.
|
||||
|
||||
If the timestamp doesn't exist in the ETS table or the table doesn't exist
|
||||
the timestamp is newly created for every request
|
||||
"""
|
||||
@spec date_header() :: {header :: binary(), date :: binary()}
|
||||
def date_header do
|
||||
date =
|
||||
try do
|
||||
:ets.lookup_element(__MODULE__, :date_header, 2)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
Logger.warning("Header timestamp couldn't be fetched from ETS cache", domain: [:bandit])
|
||||
get_date_header()
|
||||
end
|
||||
|
||||
{"date", date}
|
||||
end
|
||||
|
||||
@spec start_link(any()) :: {:ok, pid()}
|
||||
def start_link(_opts) do
|
||||
Task.start_link(__MODULE__, :init, [])
|
||||
end
|
||||
|
||||
@spec init :: no_return()
|
||||
def init do
|
||||
__MODULE__ = :ets.new(__MODULE__, [:set, :protected, :named_table, {:read_concurrency, true}])
|
||||
|
||||
run()
|
||||
end
|
||||
|
||||
@spec run() :: no_return()
|
||||
defp run do
|
||||
_ = update_header()
|
||||
Process.sleep(1_000)
|
||||
run()
|
||||
end
|
||||
|
||||
@spec get_date_header() :: String.t()
|
||||
defp get_date_header, do: Calendar.strftime(DateTime.utc_now(), "%a, %d %b %Y %X GMT")
|
||||
|
||||
@spec update_header() :: true
|
||||
defp update_header, do: :ets.insert(__MODULE__, {:date_header, get_date_header()})
|
||||
end
|
||||
168
phoenix/deps/bandit/lib/bandit/compression.ex
Normal file
168
phoenix/deps/bandit/lib/bandit/compression.ex
Normal file
@@ -0,0 +1,168 @@
|
||||
defmodule Bandit.Compression do
|
||||
@moduledoc false
|
||||
|
||||
defstruct method: nil, bytes_in: 0, lib_context: nil
|
||||
|
||||
@typedoc "A struct containing the context for response compression"
|
||||
@type t :: %__MODULE__{
|
||||
method: :deflate | :gzip | :identity | :zstd,
|
||||
bytes_in: non_neg_integer(),
|
||||
lib_context: term()
|
||||
}
|
||||
|
||||
@accepted_encodings ~w(gzip x-gzip deflate)
|
||||
|
||||
if Code.ensure_loaded?(:zstd) do
|
||||
@accepted_encodings ~w(zstd) ++ @accepted_encodings
|
||||
end
|
||||
|
||||
@spec negotiate_content_encoding(nil | binary(), keyword()) :: String.t() | nil
|
||||
def negotiate_content_encoding(nil, _), do: nil
|
||||
|
||||
def negotiate_content_encoding(accept_encoding, http_opts) do
|
||||
if Keyword.get(http_opts, :compress, true) do
|
||||
client_accept_encoding = Plug.Conn.Utils.list(accept_encoding)
|
||||
|
||||
Keyword.get(http_opts, :response_encodings, @accepted_encodings)
|
||||
|> Enum.find(&(&1 in client_accept_encoding))
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def new(adapter, status, headers, empty_body?, streamable \\ false) do
|
||||
response_content_encoding_header = Bandit.Headers.get_header(headers, "content-encoding")
|
||||
|
||||
headers = maybe_add_vary_header(adapter, status, headers)
|
||||
|
||||
if status not in [204, 304] && not is_nil(adapter.content_encoding) &&
|
||||
is_nil(response_content_encoding_header) &&
|
||||
!response_has_strong_etag(headers) && !response_indicates_no_transform(headers) &&
|
||||
!empty_body? do
|
||||
case start_stream(adapter.content_encoding, adapter.opts.http, streamable) do
|
||||
{:ok, context} -> {[{"content-encoding", adapter.content_encoding} | headers], context}
|
||||
{:error, :unsupported_encoding} -> {headers, %__MODULE__{method: :identity}}
|
||||
end
|
||||
else
|
||||
{headers, %__MODULE__{method: :identity}}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_add_vary_header(adapter, status, headers) do
|
||||
if status != 204 && Keyword.get(adapter.opts.http, :compress, true),
|
||||
do: [{"vary", "accept-encoding"} | headers],
|
||||
else: headers
|
||||
end
|
||||
|
||||
defp response_has_strong_etag(headers) do
|
||||
case Bandit.Headers.get_header(headers, "etag") do
|
||||
nil -> false
|
||||
"\W" <> _rest -> false
|
||||
_strong_etag -> true
|
||||
end
|
||||
end
|
||||
|
||||
defp response_indicates_no_transform(headers) do
|
||||
case Bandit.Headers.get_header(headers, "cache-control") do
|
||||
nil -> false
|
||||
header -> "no-transform" in Plug.Conn.Utils.list(header)
|
||||
end
|
||||
end
|
||||
|
||||
defp start_stream("deflate", http_opts, _streamable) do
|
||||
opts = Keyword.get(http_opts, :deflate_options, [])
|
||||
deflate_context = :zlib.open()
|
||||
|
||||
:zlib.deflateInit(
|
||||
deflate_context,
|
||||
Keyword.get(opts, :level, :default),
|
||||
:deflated,
|
||||
Keyword.get(opts, :window_bits, 15),
|
||||
Keyword.get(opts, :mem_level, 8),
|
||||
Keyword.get(opts, :strategy, :default)
|
||||
)
|
||||
|
||||
{:ok, %__MODULE__{method: :deflate, lib_context: deflate_context}}
|
||||
end
|
||||
|
||||
defp start_stream("x-gzip", _opts, false), do: {:ok, %__MODULE__{method: :gzip}}
|
||||
defp start_stream("gzip", _opts, false), do: {:ok, %__MODULE__{method: :gzip}}
|
||||
|
||||
if Code.ensure_loaded?(:zstd) do
|
||||
defp start_stream("zstd", http_opts, false) do
|
||||
opts = Keyword.get(http_opts, :zstd_options, %{})
|
||||
{:ok, zstd_context} = :zstd.context(:compress, opts)
|
||||
|
||||
{:ok, %__MODULE__{method: :zstd, lib_context: zstd_context}}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_stream(_encoding, _opts, _streamable), do: {:error, :unsupported_encoding}
|
||||
|
||||
def compress_chunk(chunk, %__MODULE__{method: :deflate} = context) do
|
||||
result = :zlib.deflate(context.lib_context, chunk, :sync)
|
||||
|
||||
context =
|
||||
context
|
||||
|> Map.update!(:bytes_in, &(&1 + IO.iodata_length(chunk)))
|
||||
|
||||
{result, context}
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(:zstd) do
|
||||
def compress_chunk(chunk, %__MODULE__{method: :zstd} = context) do
|
||||
result = :zstd.compress(chunk, context.lib_context)
|
||||
|
||||
context =
|
||||
context
|
||||
|> Map.update!(:bytes_in, &(&1 + IO.iodata_length(chunk)))
|
||||
|
||||
{result, context}
|
||||
end
|
||||
end
|
||||
|
||||
def compress_chunk(chunk, %__MODULE__{method: :gzip, lib_context: nil} = context) do
|
||||
result = :zlib.gzip(chunk)
|
||||
|
||||
context =
|
||||
context
|
||||
|> Map.update!(:bytes_in, &(&1 + IO.iodata_length(chunk)))
|
||||
|> Map.put(:lib_context, :done)
|
||||
|
||||
{result, context}
|
||||
end
|
||||
|
||||
def compress_chunk(chunk, %__MODULE__{method: :identity} = context) do
|
||||
{chunk, context}
|
||||
end
|
||||
|
||||
def close(%__MODULE__{} = context) do
|
||||
chunk = close_context(context)
|
||||
|
||||
if context.method == :identity do
|
||||
{chunk, %{}}
|
||||
else
|
||||
{chunk,
|
||||
%{
|
||||
resp_compression_method: to_string(context.method),
|
||||
resp_uncompressed_body_bytes: context.bytes_in
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp close_context(%__MODULE__{method: :deflate, lib_context: lib_context}) do
|
||||
last = :zlib.deflate(lib_context, [], :finish)
|
||||
:ok = :zlib.deflateEnd(lib_context)
|
||||
:zlib.close(lib_context)
|
||||
last
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(:zstd) do
|
||||
defp close_context(%__MODULE__{method: :zstd, lib_context: lib_context}) do
|
||||
:zstd.close(lib_context)
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp close_context(_context), do: []
|
||||
end
|
||||
78
phoenix/deps/bandit/lib/bandit/delegating_handler.ex
Normal file
78
phoenix/deps/bandit/lib/bandit/delegating_handler.ex
Normal file
@@ -0,0 +1,78 @@
|
||||
defmodule Bandit.DelegatingHandler do
|
||||
@moduledoc false
|
||||
# Delegates all implementation of the ThousandIsland.Handler behaviour
|
||||
# to an implementation specified in state. Allows for clean separation
|
||||
# between protocol implementations & friction free protocol selection &
|
||||
# upgrades.
|
||||
|
||||
use ThousandIsland.Handler
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_connection(socket, %{handler_module: handler_module} = state) do
|
||||
handler_module.handle_connection(socket, state)
|
||||
|> handle_bandit_continuation(socket)
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_data(data, socket, %{handler_module: handler_module} = state) do
|
||||
handler_module.handle_data(data, socket, state)
|
||||
|> handle_bandit_continuation(socket)
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_shutdown(socket, %{handler_module: handler_module} = state) do
|
||||
handler_module.handle_shutdown(socket, state)
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_close(socket, %{handler_module: handler_module} = state) do
|
||||
handler_module.handle_close(socket, state)
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_timeout(socket, %{handler_module: handler_module} = state) do
|
||||
handler_module.handle_timeout(socket, state)
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_error(error, socket, %{handler_module: handler_module} = state) do
|
||||
handler_module.handle_error(error, socket, state)
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_call(msg, from, {_socket, %{handler_module: handler_module}} = state) do
|
||||
handler_module.handle_call(msg, from, state)
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_cast(msg, {_socket, %{handler_module: handler_module}} = state) do
|
||||
handler_module.handle_cast(msg, state)
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info(msg, {_socket, %{handler_module: handler_module}} = state) do
|
||||
handler_module.handle_info(msg, state)
|
||||
end
|
||||
|
||||
defp handle_bandit_continuation(continuation, socket) do
|
||||
case continuation do
|
||||
{:switch, next_handler, state} ->
|
||||
handle_connection(socket, %{state | handler_module: next_handler})
|
||||
|
||||
{:switch, next_handler, data, state} ->
|
||||
case handle_connection(socket, %{state | handler_module: next_handler}) do
|
||||
{:continue, state} ->
|
||||
handle_data(data, socket, state)
|
||||
|
||||
{:continue, state, _timeout} ->
|
||||
handle_data(data, socket, state)
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
end
|
||||
113
phoenix/deps/bandit/lib/bandit/extractor.ex
Normal file
113
phoenix/deps/bandit/lib/bandit/extractor.ex
Normal file
@@ -0,0 +1,113 @@
|
||||
defmodule Bandit.Extractor do
|
||||
@moduledoc false
|
||||
# A state machine for efficiently extracting full frames from received packets
|
||||
|
||||
@type deserialize_result :: any()
|
||||
|
||||
@callback header_and_payload_length(binary(), max_frame_size :: integer()) ::
|
||||
{:ok, {header_length :: integer(), payload_length :: integer()}}
|
||||
| {:error, term()}
|
||||
| :more
|
||||
|
||||
@callback deserialize(binary(), primitive_ops_module :: module()) :: deserialize_result()
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
header: binary(),
|
||||
payload: iodata(),
|
||||
payload_length: non_neg_integer(),
|
||||
required_length: non_neg_integer(),
|
||||
mode: :header_parsing | :payload_parsing,
|
||||
max_frame_size: non_neg_integer(),
|
||||
frame_parser: atom(),
|
||||
primitive_ops_module: module()
|
||||
}
|
||||
|
||||
defstruct header: <<>>,
|
||||
payload: [],
|
||||
payload_length: 0,
|
||||
required_length: 0,
|
||||
mode: :header_parsing,
|
||||
max_frame_size: 0,
|
||||
frame_parser: nil,
|
||||
primitive_ops_module: nil
|
||||
|
||||
@spec new(module(), module(), Keyword.t()) :: t()
|
||||
def new(frame_parser, primitive_ops_module, opts) do
|
||||
max_frame_size = Keyword.get(opts, :max_frame_size, 0)
|
||||
|
||||
%__MODULE__{
|
||||
max_frame_size: max_frame_size,
|
||||
frame_parser: frame_parser,
|
||||
primitive_ops_module: primitive_ops_module
|
||||
}
|
||||
end
|
||||
|
||||
@spec push_data(t(), binary()) :: t()
|
||||
def push_data(%__MODULE__{} = state, data) do
|
||||
case state do
|
||||
%{mode: :header_parsing} ->
|
||||
%{state | header: state.header <> data}
|
||||
|
||||
%{mode: :payload_parsing, payload: payload, payload_length: length} ->
|
||||
%{state | payload: [payload, data], payload_length: length + byte_size(data)}
|
||||
end
|
||||
end
|
||||
|
||||
@spec pop_frame(t()) :: {t(), :more | deserialize_result()}
|
||||
def pop_frame(state)
|
||||
|
||||
def pop_frame(%__MODULE__{mode: :header_parsing} = state) do
|
||||
case state.frame_parser.header_and_payload_length(state.header, state.max_frame_size) do
|
||||
{:ok, {header_length, required_length}} ->
|
||||
state
|
||||
|> transition_to_payload_parsing(header_length, required_length)
|
||||
|> pop_frame()
|
||||
|
||||
{:error, message} ->
|
||||
{state, {:error, message}}
|
||||
|
||||
:more ->
|
||||
{state, :more}
|
||||
end
|
||||
end
|
||||
|
||||
def pop_frame(
|
||||
%__MODULE__{
|
||||
mode: :payload_parsing,
|
||||
payload_length: payload_length,
|
||||
required_length: required_length
|
||||
} = state
|
||||
) do
|
||||
if payload_length >= required_length do
|
||||
<<payload::binary-size(required_length), rest::binary>> =
|
||||
IO.iodata_to_binary(state.payload)
|
||||
|
||||
frame = state.frame_parser.deserialize(state.header <> payload, state.primitive_ops_module)
|
||||
state = transition_to_header_parsing(state, rest)
|
||||
|
||||
{state, frame}
|
||||
else
|
||||
{state, :more}
|
||||
end
|
||||
end
|
||||
|
||||
defp transition_to_payload_parsing(state, header_length, required_length) do
|
||||
payload_length = byte_size(state.header) - header_length
|
||||
|
||||
state
|
||||
|> Map.put(:header, binary_part(state.header, 0, header_length))
|
||||
|> Map.put(:payload, binary_part(state.header, header_length, payload_length))
|
||||
|> Map.put(:payload_length, payload_length)
|
||||
|> Map.put(:required_length, required_length)
|
||||
|> Map.put(:mode, :payload_parsing)
|
||||
end
|
||||
|
||||
defp transition_to_header_parsing(state, rest) do
|
||||
state
|
||||
|> Map.put(:header, rest)
|
||||
|> Map.put(:payload, [])
|
||||
|> Map.put(:payload_length, 0)
|
||||
|> Map.put(:required_length, 0)
|
||||
|> Map.put(:mode, :header_parsing)
|
||||
end
|
||||
end
|
||||
123
phoenix/deps/bandit/lib/bandit/headers.ex
Normal file
123
phoenix/deps/bandit/lib/bandit/headers.ex
Normal file
@@ -0,0 +1,123 @@
|
||||
defmodule Bandit.Headers do
|
||||
@moduledoc false
|
||||
# Conveniences for dealing with headers.
|
||||
|
||||
@spec is_port_number(integer()) :: Macro.t()
|
||||
defguardp is_port_number(port) when Bitwise.band(port, 0xFFFF) === port
|
||||
|
||||
@spec get_header(Plug.Conn.headers(), header :: binary()) :: binary() | nil
|
||||
def get_header(headers, header) do
|
||||
case List.keyfind(headers, header, 0) do
|
||||
{_, value} -> value
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
# Covers IPv6 addresses, like `[::1]:4000` as defined in RFC3986.
|
||||
@spec parse_hostlike_header!(host_header :: binary()) ::
|
||||
{Plug.Conn.host(), nil | Plug.Conn.port_number()}
|
||||
def parse_hostlike_header!("[" <> _ = host_header) do
|
||||
host_header
|
||||
|> :binary.split("]:")
|
||||
|> case do
|
||||
[host, port] ->
|
||||
case parse_integer(port) do
|
||||
{port, ""} when is_port_number(port) -> {host <> "]", port}
|
||||
_ -> raise Bandit.HTTPError, "Header contains invalid port"
|
||||
end
|
||||
|
||||
[host] ->
|
||||
{host, nil}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_hostlike_header!(host_header) do
|
||||
host_header
|
||||
|> :binary.split(":")
|
||||
|> case do
|
||||
[host, port] ->
|
||||
case parse_integer(port) do
|
||||
{port, ""} when is_port_number(port) -> {host, port}
|
||||
_ -> raise Bandit.HTTPError, "Header contains invalid port"
|
||||
end
|
||||
|
||||
[host] ->
|
||||
{host, nil}
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_content_length(Plug.Conn.headers()) ::
|
||||
{:ok, nil | non_neg_integer()} | {:error, String.t()}
|
||||
def get_content_length(headers) do
|
||||
case get_header(headers, "content-length") do
|
||||
nil -> {:ok, nil}
|
||||
value -> parse_content_length(value)
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_content_length(binary()) :: {:ok, non_neg_integer()} | {:error, String.t()}
|
||||
defp parse_content_length(value) do
|
||||
case parse_integer(value) do
|
||||
{length, ""} ->
|
||||
{:ok, length}
|
||||
|
||||
{length, _rest} ->
|
||||
if value |> Plug.Conn.Utils.list() |> Enum.all?(&(&1 == to_string(length))),
|
||||
do: {:ok, length},
|
||||
else: {:error, "invalid content-length header (RFC9112§6.3.5)"}
|
||||
|
||||
:error ->
|
||||
{:error, "invalid content-length header (RFC9112§6.3.5)"}
|
||||
end
|
||||
end
|
||||
|
||||
# Parses non-negative integers from strings. Return the valid portion of an
|
||||
# integer and the remaining string as a tuple like `{123, ""}` or `:error`.
|
||||
@spec parse_integer(String.t()) :: {non_neg_integer(), rest :: String.t()} | :error
|
||||
defp parse_integer(<<digit::8, rest::binary>>) when digit >= ?0 and digit <= ?9 do
|
||||
parse_integer(rest, digit - ?0)
|
||||
end
|
||||
|
||||
defp parse_integer(_), do: :error
|
||||
|
||||
@spec parse_integer(String.t(), non_neg_integer()) :: {non_neg_integer(), String.t()}
|
||||
defp parse_integer(<<digit::8, rest::binary>>, total) when digit >= ?0 and digit <= ?9 do
|
||||
parse_integer(rest, total * 10 + digit - ?0)
|
||||
end
|
||||
|
||||
defp parse_integer(rest, total), do: {total, rest}
|
||||
|
||||
@spec add_content_length(
|
||||
headers :: Plug.Conn.headers(),
|
||||
length :: non_neg_integer(),
|
||||
status :: Plug.Conn.int_status(),
|
||||
method :: Plug.Conn.method()
|
||||
) ::
|
||||
Plug.Conn.headers()
|
||||
|
||||
# Per RFC9110§8.6, we use the following logic:
|
||||
#
|
||||
# * If the response is 1xx or 204, content-length is NEVER sent
|
||||
# * If the response is 304 or the method is HEAD AND the body length is zero, respect any
|
||||
# content-length header the plug may have set on the assumption that it knows what it would
|
||||
# have sent
|
||||
# * For all other responses, use the length of the provided response body as the content-length,
|
||||
# overwriting any content-length the plug may have set
|
||||
def add_content_length(headers, _length, status, _method)
|
||||
when status in 100..199 or status == 204 do
|
||||
drop_content_length(headers)
|
||||
end
|
||||
|
||||
def add_content_length(headers, 0, status, method) when status == 304 or method == "HEAD" do
|
||||
headers
|
||||
end
|
||||
|
||||
def add_content_length(headers, length, _status, _method) do
|
||||
[{"content-length", to_string(length)} | drop_content_length(headers)]
|
||||
end
|
||||
|
||||
@spec drop_content_length(Plug.Conn.headers()) :: Plug.Conn.headers()
|
||||
defp drop_content_length(headers) do
|
||||
Enum.reject(headers, &(elem(&1, 0) == "content-length"))
|
||||
end
|
||||
end
|
||||
30
phoenix/deps/bandit/lib/bandit/http1/README.md
Normal file
30
phoenix/deps/bandit/lib/bandit/http1/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# HTTP/1 Handler
|
||||
|
||||
Included in this folder is a complete `ThousandIsland.Handler` based implementation of HTTP/1.x as
|
||||
defined in [RFC 9112](https://datatracker.ietf.org/doc/rfc9112).
|
||||
|
||||
## Process model
|
||||
|
||||
Within a Bandit server, an HTTP/1 connection is modeled as a single process.
|
||||
This process is tied to the lifecycle of the underlying TCP connection; in the
|
||||
case of an HTTP client which makes use of HTTP's keep-alive feature to make
|
||||
multiple requests on the same connection, all of these requests will be serviced
|
||||
by this same process.
|
||||
|
||||
The execution model to handle a given request is quite straightforward: the
|
||||
underlying [Thousand Island](https://github.com/mtrudel/thousand_island) library
|
||||
will call `Bandit.HTTP1.Handler.handle_data/3`, which will then construct a
|
||||
`Bandit.HTTP1.Socket` struct that conforms to the `Bandit.HTTPTransport`
|
||||
protocol. It will then call `Bandit.Pipeline.run/3`, which will go through the
|
||||
process of reading the request (by calling functions on the
|
||||
`Bandit.HTTPTransport` protocol), and constructing a `Plug.Conn` structure to
|
||||
represent the request and subsequently pass it to the configured `Plug` module.
|
||||
|
||||
# Testing
|
||||
|
||||
All of this is exhaustively tested. Tests are located in `request_test.exs`, and
|
||||
are broadly either concerned with testing network-facing aspects of the
|
||||
implementation (ie: how well Bandit satisfies the relevant RFCs) or the Plug-facing
|
||||
aspects of the implementation.
|
||||
|
||||
Unfortunately, there is no HTTP/1 equivalent to the external h2spec test suite.
|
||||
96
phoenix/deps/bandit/lib/bandit/http1/handler.ex
Normal file
96
phoenix/deps/bandit/lib/bandit/http1/handler.ex
Normal file
@@ -0,0 +1,96 @@
|
||||
defmodule Bandit.HTTP1.Handler do
|
||||
@moduledoc false
|
||||
# An HTTP 1.0 & 1.1 Thousand Island Handler
|
||||
|
||||
use ThousandIsland.Handler
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_data(data, socket, state) do
|
||||
transport = %Bandit.HTTP1.Socket{socket: socket, buffer: data, opts: state.opts}
|
||||
connection_span = ThousandIsland.Socket.telemetry_span(socket)
|
||||
conn_data = Bandit.SocketHelpers.conn_data(socket)
|
||||
|
||||
case Bandit.Pipeline.run(transport, state.plug, connection_span, conn_data, state.opts) do
|
||||
{:ok, transport} -> maybe_keepalive(transport, state)
|
||||
{:error, _reason} -> {:close, state}
|
||||
{:upgrade, _transport, :websocket, opts} -> do_websocket_upgrade(opts, state)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_keepalive(transport, state) do
|
||||
requests_processed = Map.get(state, :requests_processed, 0) + 1
|
||||
request_limit = Keyword.get(state.opts.http_1, :max_requests, 0)
|
||||
under_limit = request_limit == 0 || requests_processed < request_limit
|
||||
|
||||
if under_limit && transport.keepalive do
|
||||
if Keyword.get(state.opts.http_1, :clear_process_dict, true), do: clear_process_dict()
|
||||
gc_every_n_requests = Keyword.get(state.opts.http_1, :gc_every_n_keepalive_requests, 5)
|
||||
if rem(requests_processed, gc_every_n_requests) == 0, do: :erlang.garbage_collect()
|
||||
|
||||
state = Map.put(state, :requests_processed, requests_processed)
|
||||
|
||||
# We have bytes that we've read but haven't yet processed, tail call handle_data to start
|
||||
# reading the next request
|
||||
if Bandit.SocketHelpers.iodata_empty?(transport.buffer) do
|
||||
{:continue, state}
|
||||
else
|
||||
handle_data(transport.buffer, transport.socket, state)
|
||||
end
|
||||
else
|
||||
{:close, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp clear_process_dict do
|
||||
Process.get_keys()
|
||||
|> Enum.each(
|
||||
&if &1 not in ~w[$ancestors $initial_call $process_label]a, do: Process.delete(&1)
|
||||
)
|
||||
end
|
||||
|
||||
defp do_websocket_upgrade(upgrade_opts, state) do
|
||||
:erlang.garbage_collect()
|
||||
{:switch, Bandit.WebSocket.Handler, Map.put(state, :upgrade_opts, upgrade_opts)}
|
||||
end
|
||||
|
||||
def handle_info({:plug_conn, :sent}, {socket, state}),
|
||||
do: {:noreply, {socket, state}, socket.read_timeout}
|
||||
|
||||
def handle_info({:EXIT, _pid, :normal}, {socket, state}),
|
||||
do: {:noreply, {socket, state}, socket.read_timeout}
|
||||
|
||||
def handle_info(msg, {socket, state}) do
|
||||
if Keyword.get(state.opts.http_1, :log_unknown_messages, false), do: log_no_handle_info(msg)
|
||||
{:noreply, {socket, state}, socket.read_timeout}
|
||||
end
|
||||
|
||||
def handle_info(msg, state) do
|
||||
log_no_handle_info(msg)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp log_no_handle_info(msg) do
|
||||
# Copied verbatim from lib/elixir/lib/gen_server.ex
|
||||
proc =
|
||||
case Process.info(self(), :registered_name) do
|
||||
{_, []} -> self()
|
||||
{_, name} -> name
|
||||
end
|
||||
|
||||
:logger.error(
|
||||
%{
|
||||
label: {GenServer, :no_handle_info},
|
||||
report: %{
|
||||
module: __MODULE__,
|
||||
message: msg,
|
||||
name: proc
|
||||
}
|
||||
},
|
||||
%{
|
||||
domain: [:otp, :elixir],
|
||||
error_logger: %{tag: :error_msg},
|
||||
report_cb: &GenServer.format_report/1
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
503
phoenix/deps/bandit/lib/bandit/http1/socket.ex
Normal file
503
phoenix/deps/bandit/lib/bandit/http1/socket.ex
Normal file
@@ -0,0 +1,503 @@
|
||||
defmodule Bandit.HTTP1.Socket do
|
||||
@moduledoc false
|
||||
# This module implements the lower level parts of HTTP/1 (roughly, the aspects of the protocol
|
||||
# described in RFC 9112 as opposed to RFC 9110). It is similar in spirit to
|
||||
# `Bandit.HTTP2.Stream` for HTTP/2, and indeed both implement the `Bandit.HTTPTransport`
|
||||
# behaviour. An instance of this struct is maintained as the state of a `Bandit.HTTP1.Handler`
|
||||
# process, and it moves an HTTP/1 request through its lifecycle by calling functions defined on
|
||||
# this module. This state is also tracked within the `Bandit.Adapter` instance that backs
|
||||
# Bandit's Plug API.
|
||||
|
||||
defstruct socket: nil,
|
||||
buffer: <<>>,
|
||||
read_state: :unread,
|
||||
write_state: :unsent,
|
||||
unread_content_length: nil,
|
||||
body_encoding: nil,
|
||||
version: :"HTTP/1.0",
|
||||
send_buffer: nil,
|
||||
request_connection_header: nil,
|
||||
keepalive: nil,
|
||||
opts: %{}
|
||||
|
||||
@typedoc "An HTTP/1 read state"
|
||||
@type read_state :: :unread | :headers_read | :read
|
||||
|
||||
@typedoc "An HTTP/1 write state"
|
||||
@type write_state :: :unsent | :writing | :chunking | :chunk_streaming | :sent
|
||||
|
||||
@typedoc "The information necessary to communicate to/from a socket"
|
||||
@type t :: %__MODULE__{
|
||||
socket: ThousandIsland.Socket.t(),
|
||||
buffer: iodata(),
|
||||
read_state: read_state(),
|
||||
write_state: write_state(),
|
||||
unread_content_length: non_neg_integer() | :chunked | nil,
|
||||
body_encoding: nil | binary(),
|
||||
version: nil | :"HTTP/1.1" | :"HTTP/1.0",
|
||||
send_buffer: iolist(),
|
||||
request_connection_header: binary(),
|
||||
keepalive: boolean(),
|
||||
opts: %{
|
||||
required(:http_1) => Bandit.http_1_options()
|
||||
}
|
||||
}
|
||||
|
||||
defimpl Bandit.HTTPTransport do
|
||||
def peer_data(%@for{} = socket), do: Bandit.SocketHelpers.peer_data(socket.socket)
|
||||
|
||||
def sock_data(%@for{} = socket), do: Bandit.SocketHelpers.sock_data(socket.socket)
|
||||
|
||||
def ssl_data(%@for{} = socket), do: Bandit.SocketHelpers.ssl_data(socket.socket)
|
||||
|
||||
def version(%@for{} = socket), do: socket.version
|
||||
|
||||
def read_headers(%@for{read_state: :unread} = socket) do
|
||||
{method, request_target, socket} = do_read_request_line!(socket)
|
||||
{headers, socket} = do_read_headers!(socket)
|
||||
content_length = get_content_length!(headers)
|
||||
body_encoding = Bandit.Headers.get_header(headers, "transfer-encoding")
|
||||
request_connection_header = safe_downcase(Bandit.Headers.get_header(headers, "connection"))
|
||||
socket = %{socket | request_connection_header: request_connection_header}
|
||||
|
||||
case {content_length, body_encoding} do
|
||||
{nil, nil} ->
|
||||
# No body, so just go straight to 'read'
|
||||
{:ok, method, request_target, headers, %{socket | read_state: :read}}
|
||||
|
||||
{content_length, nil} ->
|
||||
socket = %{socket | read_state: :headers_read, unread_content_length: content_length}
|
||||
{:ok, method, request_target, headers, socket}
|
||||
|
||||
{nil, body_encoding} ->
|
||||
socket = %{socket | read_state: :headers_read, body_encoding: body_encoding}
|
||||
{:ok, method, request_target, headers, socket}
|
||||
|
||||
{_content_length, _body_encoding} ->
|
||||
request_error!(
|
||||
"Request cannot contain both 'content-length' and 'transfer-encoding' (RFC9112§6.3.3)"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_read_request_line!(socket, request_target \\ nil) do
|
||||
packet_size = Keyword.get(socket.opts.http_1, :max_request_line_length, 10_000)
|
||||
|
||||
case :erlang.decode_packet(:http_bin, socket.buffer, packet_size: packet_size) do
|
||||
{:more, _len} ->
|
||||
chunk = read_available_for_header!(socket.socket)
|
||||
do_read_request_line!(%{socket | buffer: socket.buffer <> chunk}, request_target)
|
||||
|
||||
{:ok, {:http_request, method, request_target, version}, rest} ->
|
||||
version = get_version!(version)
|
||||
# decode_packet is inconsistent about atom/string method returns
|
||||
method = to_string(method)
|
||||
request_target = resolve_request_target!(request_target, method)
|
||||
socket = %{socket | buffer: rest, version: version}
|
||||
{method, request_target, socket}
|
||||
|
||||
{:ok, {:http_error, reason}, _rest} ->
|
||||
request_error!("Request line HTTP error: #{inspect(reason)}")
|
||||
|
||||
{:error, :invalid} ->
|
||||
request_error!("Request URI is too long", :request_uri_too_long)
|
||||
|
||||
{:error, reason} ->
|
||||
request_error!("Request line unknown error: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp get_version!({1, 1}), do: :"HTTP/1.1"
|
||||
defp get_version!({1, 0}), do: :"HTTP/1.0"
|
||||
defp get_version!(other), do: request_error!("Invalid HTTP version: #{inspect(other)}")
|
||||
|
||||
# Unwrap different request_targets returned by :erlang.decode_packet/3
|
||||
defp resolve_request_target!({:abs_path, path}, _), do: {nil, nil, nil, path}
|
||||
|
||||
defp resolve_request_target!({:absoluteURI, scheme, host, :undefined, path}, _),
|
||||
do: {to_string(scheme), host, nil, path}
|
||||
|
||||
defp resolve_request_target!({:absoluteURI, scheme, host, port, path}, _),
|
||||
do: {to_string(scheme), host, port, path}
|
||||
|
||||
defp resolve_request_target!(:*, "OPTIONS"), do: {nil, nil, nil, :*}
|
||||
|
||||
defp resolve_request_target!({:scheme, scheme, port}, "CONNECT"),
|
||||
do: {nil, scheme, port, nil}
|
||||
|
||||
defp resolve_request_target!(_request_target, _method),
|
||||
do: request_error!("Unsupported request target (RFC9112§3.2)")
|
||||
|
||||
defp do_read_headers!(socket, headers \\ []) do
|
||||
packet_size = Keyword.get(socket.opts.http_1, :max_header_length, 10_000)
|
||||
|
||||
case :erlang.decode_packet(:httph_bin, socket.buffer, packet_size: packet_size) do
|
||||
{:more, _len} ->
|
||||
chunk = read_available_for_header!(socket.socket)
|
||||
socket = %{socket | buffer: socket.buffer <> chunk}
|
||||
do_read_headers!(socket, headers)
|
||||
|
||||
{:ok, {:http_header, _, header, _, value}, rest} ->
|
||||
socket = %{socket | buffer: rest}
|
||||
headers = [{header |> to_string() |> String.downcase(:ascii), value} | headers]
|
||||
|
||||
if length(headers) <= Keyword.get(socket.opts.http_1, :max_header_count, 50) do
|
||||
do_read_headers!(socket, headers)
|
||||
else
|
||||
request_error!("Too many headers", :request_header_fields_too_large)
|
||||
end
|
||||
|
||||
{:ok, :http_eoh, rest} ->
|
||||
socket = %{socket | read_state: :headers_read, buffer: rest}
|
||||
{Enum.reverse(headers), socket}
|
||||
|
||||
{:ok, {:http_error, reason}, _rest} ->
|
||||
request_error!("Header read HTTP error: #{inspect(reason)}")
|
||||
|
||||
{:error, :invalid} ->
|
||||
request_error!("Header too long", :request_header_fields_too_large)
|
||||
|
||||
{:error, reason} ->
|
||||
request_error!("Header read unknown error: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp get_content_length!(headers) do
|
||||
case Bandit.Headers.get_content_length(headers) do
|
||||
{:ok, content_length} -> content_length
|
||||
{:error, reason} -> request_error!("Content length unknown error: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
def read_data(
|
||||
%@for{read_state: :headers_read, unread_content_length: unread_content_length} = socket,
|
||||
opts
|
||||
)
|
||||
when is_number(unread_content_length) do
|
||||
{to_return, buffer, remaining_unread_content_length} =
|
||||
do_read_content_length_data!(socket.socket, socket.buffer, unread_content_length, opts)
|
||||
|
||||
socket = %{socket | buffer: buffer, unread_content_length: remaining_unread_content_length}
|
||||
|
||||
if remaining_unread_content_length == 0 do
|
||||
{:ok, to_return, %{socket | read_state: :read}}
|
||||
else
|
||||
{:more, to_return, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def read_data(%@for{read_state: :headers_read, body_encoding: "chunked"} = socket, opts) do
|
||||
read_size = Keyword.get(opts, :read_length, 1_000_000)
|
||||
read_timeout = Keyword.get(opts, :read_timeout)
|
||||
|
||||
{body, buffer} =
|
||||
do_read_chunked_data!(socket.socket, socket.buffer, <<>>, read_size, read_timeout)
|
||||
|
||||
body = IO.iodata_to_binary(body)
|
||||
|
||||
{:ok, body, %{socket | read_state: :read, buffer: buffer}}
|
||||
end
|
||||
|
||||
def read_data(%@for{read_state: :headers_read, body_encoding: body_encoding}, _opts)
|
||||
when not is_nil(body_encoding) do
|
||||
request_error!("Unsupported transfer-encoding")
|
||||
end
|
||||
|
||||
def read_data(%@for{} = socket, _opts), do: {:ok, <<>>, socket}
|
||||
|
||||
@dialyzer {:no_improper_lists, do_read_content_length_data!: 4}
|
||||
defp do_read_content_length_data!(socket, buffer, unread_content_length, opts) do
|
||||
max_to_return = min(unread_content_length, Keyword.get(opts, :length, 8_000_000))
|
||||
|
||||
cond do
|
||||
max_to_return == 0 ->
|
||||
# We have already satisfied our content length
|
||||
{<<>>, buffer, unread_content_length}
|
||||
|
||||
byte_size(buffer) >= max_to_return ->
|
||||
# We can satisfy the read request entirely from our buffer
|
||||
<<to_return::binary-size(max_to_return), rest::binary>> = buffer
|
||||
{to_return, rest, unread_content_length - max_to_return}
|
||||
|
||||
byte_size(buffer) < max_to_return ->
|
||||
# We need to read off the wire
|
||||
read_size = Keyword.get(opts, :read_length, 1_000_000)
|
||||
read_timeout = Keyword.get(opts, :read_timeout)
|
||||
|
||||
to_return =
|
||||
read!(socket, max_to_return - byte_size(buffer), [buffer], read_size, read_timeout)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
# We may have read more than we need to return
|
||||
if byte_size(to_return) >= max_to_return do
|
||||
<<to_return::binary-size(max_to_return), rest::binary>> = to_return
|
||||
{to_return, rest, unread_content_length - max_to_return}
|
||||
else
|
||||
{to_return, <<>>, unread_content_length - byte_size(to_return)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@dialyzer {:no_improper_lists, do_read_chunked_data!: 5}
|
||||
defp do_read_chunked_data!(socket, buffer, body, read_size, read_timeout) do
|
||||
case :binary.split(buffer, "\r\n") do
|
||||
["0", "\r\n" <> rest] ->
|
||||
# We should be reading (and ignoring) trailers here
|
||||
{IO.iodata_to_binary(body), rest}
|
||||
|
||||
[chunk_size, rest] ->
|
||||
chunk_size = String.to_integer(chunk_size, 16)
|
||||
|
||||
case rest do
|
||||
<<next_chunk::binary-size(chunk_size), ?\r, ?\n, rest::binary>> ->
|
||||
do_read_chunked_data!(socket, rest, [body, next_chunk], read_size, read_timeout)
|
||||
|
||||
_ ->
|
||||
to_read = chunk_size - byte_size(rest)
|
||||
|
||||
if to_read > 0 do
|
||||
iolist = read!(socket, to_read, [], read_size, read_timeout)
|
||||
buffer = IO.iodata_to_binary([buffer | iolist])
|
||||
do_read_chunked_data!(socket, buffer, body, read_size, read_timeout)
|
||||
else
|
||||
chunk = read_available!(socket, read_timeout)
|
||||
buffer = buffer <> chunk
|
||||
do_read_chunked_data!(socket, buffer, body, read_size, read_timeout)
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
chunk = read_available!(socket, read_timeout)
|
||||
buffer = buffer <> chunk
|
||||
do_read_chunked_data!(socket, buffer, body, read_size, read_timeout)
|
||||
end
|
||||
end
|
||||
|
||||
##################
|
||||
# Internal Reading
|
||||
##################
|
||||
|
||||
@compile {:inline, read_available_for_header!: 1}
|
||||
@spec read_available_for_header!(ThousandIsland.Socket.t()) :: binary()
|
||||
defp read_available_for_header!(socket) do
|
||||
case ThousandIsland.Socket.recv(socket, 0) do
|
||||
{:ok, chunk} -> chunk
|
||||
{:error, reason} -> socket_error!(reason)
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, read_available!: 2}
|
||||
@spec read_available!(ThousandIsland.Socket.t(), timeout()) :: binary()
|
||||
defp read_available!(socket, read_timeout) do
|
||||
case ThousandIsland.Socket.recv(socket, 0, read_timeout) do
|
||||
{:ok, chunk} -> chunk
|
||||
{:error, :timeout} -> <<>>
|
||||
{:error, reason} -> socket_error!(reason)
|
||||
end
|
||||
end
|
||||
|
||||
@dialyzer {:no_improper_lists, read!: 5}
|
||||
@spec read!(
|
||||
ThousandIsland.Socket.t(),
|
||||
non_neg_integer(),
|
||||
iolist(),
|
||||
non_neg_integer(),
|
||||
timeout()
|
||||
) ::
|
||||
iolist()
|
||||
defp read!(socket, to_read, already_read, read_size, read_timeout) do
|
||||
case ThousandIsland.Socket.recv(socket, min(to_read, read_size), read_timeout) do
|
||||
{:ok, chunk} ->
|
||||
remaining_bytes = to_read - byte_size(chunk)
|
||||
|
||||
if remaining_bytes > 0 do
|
||||
read!(socket, remaining_bytes, [already_read | chunk], read_size, read_timeout)
|
||||
else
|
||||
[already_read | chunk]
|
||||
end
|
||||
|
||||
{:error, :timeout} ->
|
||||
handle_timeout_with_disconnect_check!(socket)
|
||||
|
||||
{:error, reason} ->
|
||||
socket_error!(reason)
|
||||
end
|
||||
end
|
||||
|
||||
# After a timeout, check if the peer is still connected. If not, this is
|
||||
# likely a client disconnect that manifested as a timeout.
|
||||
# We raise TransportError for disconnects and HTTPError for genuine timeouts.
|
||||
# Use a non-blocking recv (timeout: 0) to detect closed connections.
|
||||
@spec handle_timeout_with_disconnect_check!(ThousandIsland.Socket.t()) :: no_return()
|
||||
defp handle_timeout_with_disconnect_check!(socket) do
|
||||
case ThousandIsland.Socket.recv(socket, 0, 0) do
|
||||
{:error, :timeout} ->
|
||||
# Socket is still open but no data - genuine timeout
|
||||
request_error!("Body read timeout", :request_timeout)
|
||||
|
||||
{:error, reason} ->
|
||||
# Socket error (e.g., :closed) - client disconnected
|
||||
socket_error!(reason)
|
||||
|
||||
{:ok, _data} ->
|
||||
# Unexpected: data arrived just after timeout. Treat as timeout
|
||||
# since we already committed to the timeout path.
|
||||
request_error!("Body read timeout", :request_timeout)
|
||||
end
|
||||
end
|
||||
|
||||
def send_headers(%@for{write_state: :unsent} = socket, status, headers, body_disposition) do
|
||||
resp_line = "#{socket.version} #{status} #{Plug.Conn.Status.reason_phrase(status)}\r\n"
|
||||
|
||||
{headers, socket} = handle_keepalive(status, headers, socket)
|
||||
|
||||
has_content_length = Bandit.Headers.get_header(headers, "content-length") != nil
|
||||
|
||||
case body_disposition do
|
||||
:raw ->
|
||||
# This is an optimization for the common case of sending a non-encoded body (or file),
|
||||
# and coalesces the header and body send calls into a single ThousandIsland.Socket.send/2
|
||||
# call. This makes a _substantial_ difference in practice
|
||||
%{socket | write_state: :writing, send_buffer: [resp_line | encode_headers(headers)]}
|
||||
|
||||
:chunk_encoded when not has_content_length ->
|
||||
headers = [{"transfer-encoding", "chunked"} | headers]
|
||||
send!(socket.socket, [resp_line | encode_headers(headers)])
|
||||
%{socket | write_state: :chunking}
|
||||
|
||||
:chunk_encoded when has_content_length ->
|
||||
send!(socket.socket, [resp_line | encode_headers(headers)])
|
||||
%{socket | write_state: :chunk_streaming}
|
||||
|
||||
:no_body ->
|
||||
send!(socket.socket, [resp_line | encode_headers(headers)])
|
||||
%{socket | write_state: :sent}
|
||||
|
||||
:inform ->
|
||||
send!(socket.socket, [resp_line | encode_headers(headers)])
|
||||
%{socket | write_state: :unsent}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_keepalive(status, headers, socket) do
|
||||
response_connection_header = safe_downcase(Bandit.Headers.get_header(headers, "connection"))
|
||||
|
||||
# Per RFC9112§9.3
|
||||
cond do
|
||||
status in 100..199 ->
|
||||
{headers, socket}
|
||||
|
||||
socket.request_connection_header == "close" || response_connection_header == "close" ->
|
||||
{headers, %{socket | keepalive: false}}
|
||||
|
||||
socket.version == :"HTTP/1.1" ->
|
||||
{headers, %{socket | keepalive: true}}
|
||||
|
||||
socket.version == :"HTTP/1.0" && socket.request_connection_header == "keep-alive" ->
|
||||
{[{"connection", "keep-alive"} | headers], %{socket | keepalive: true}}
|
||||
|
||||
true ->
|
||||
{[{"connection", "close"} | headers], %{socket | keepalive: false}}
|
||||
end
|
||||
end
|
||||
|
||||
defp safe_downcase(str) when is_binary(str), do: String.downcase(str, :ascii)
|
||||
defp safe_downcase(str), do: str
|
||||
|
||||
defp encode_headers(headers) do
|
||||
headers
|
||||
|> Enum.map(fn {k, v} -> [k, ": ", v, "\r\n"] end)
|
||||
|> then(&[&1 | ["\r\n"]])
|
||||
end
|
||||
|
||||
def send_data(%@for{write_state: :writing} = socket, data, end_request) do
|
||||
send!(socket.socket, [socket.send_buffer | data])
|
||||
write_state = if end_request, do: :sent, else: :writing
|
||||
%{socket | write_state: write_state, send_buffer: []}
|
||||
end
|
||||
|
||||
def send_data(%@for{write_state: :chunking} = socket, data, end_request) do
|
||||
byte_size = data |> IO.iodata_length()
|
||||
send!(socket.socket, [Integer.to_string(byte_size, 16), "\r\n", data, "\r\n"])
|
||||
write_state = if end_request, do: :sent, else: :chunking
|
||||
%{socket | write_state: write_state}
|
||||
end
|
||||
|
||||
def send_data(%@for{write_state: :chunk_streaming} = socket, data, end_request) do
|
||||
send!(socket.socket, data)
|
||||
write_state = if end_request, do: :sent, else: :chunk_streaming
|
||||
%{socket | write_state: write_state}
|
||||
end
|
||||
|
||||
def sendfile(%@for{write_state: :writing} = socket, path, offset, length) do
|
||||
send!(socket.socket, socket.send_buffer)
|
||||
|
||||
case ThousandIsland.Socket.sendfile(socket.socket, path, offset, length) do
|
||||
{:ok, _bytes_written} -> %{socket | write_state: :sent}
|
||||
{:error, reason} -> socket_error!(reason)
|
||||
end
|
||||
end
|
||||
|
||||
@spec send!(ThousandIsland.Socket.t(), iolist()) :: :ok | no_return()
|
||||
defp send!(socket, payload) do
|
||||
case ThousandIsland.Socket.send(socket, payload) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
# Prevent error handlers from possibly trying to send again
|
||||
send(self(), {:plug_conn, :sent})
|
||||
socket_error!(reason)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_completed(%@for{read_state: :read} = socket), do: socket
|
||||
def ensure_completed(%@for{keepalive: false} = socket), do: socket
|
||||
|
||||
def ensure_completed(%@for{} = socket) do
|
||||
case read_data(socket, []) do
|
||||
{:ok, _data, socket} -> socket
|
||||
{:more, _data, _socket} -> request_error!("Unable to read remaining data in request body")
|
||||
end
|
||||
rescue
|
||||
e in [Bandit.HTTPError] ->
|
||||
# If we got a timeout during ensure_completed (draining the body),
|
||||
# check if the client actually disconnected.
|
||||
if e.plug_status == :request_timeout do
|
||||
handle_timeout_with_disconnect_check!(socket.socket)
|
||||
else
|
||||
reraise e, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
|
||||
def supported_upgrade?(%@for{} = _socket, protocol), do: protocol == :websocket
|
||||
|
||||
def send_on_error(%@for{}, %Bandit.TransportError{}), do: :ok
|
||||
|
||||
def send_on_error(%@for{} = socket, error) do
|
||||
receive do
|
||||
{:plug_conn, :sent} -> %{socket | write_state: :sent}
|
||||
after
|
||||
0 ->
|
||||
status = error |> Plug.Exception.status() |> Plug.Conn.Status.code()
|
||||
|
||||
try do
|
||||
send_headers(socket, status, [{"connection", "close"}], :no_body)
|
||||
rescue
|
||||
_e in [Bandit.TransportError, Bandit.HTTPError] -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec request_error!(term()) :: no_return()
|
||||
@spec request_error!(term(), Plug.Conn.status()) :: no_return()
|
||||
defp request_error!(reason, plug_status \\ :bad_request) do
|
||||
raise Bandit.HTTPError, message: to_string(reason), plug_status: plug_status
|
||||
end
|
||||
|
||||
@spec socket_error!(term()) :: no_return()
|
||||
defp socket_error!(reason) do
|
||||
raise Bandit.TransportError, message: "Unrecoverable error: #{reason}", error: reason
|
||||
end
|
||||
end
|
||||
end
|
||||
108
phoenix/deps/bandit/lib/bandit/http2/README.md
Normal file
108
phoenix/deps/bandit/lib/bandit/http2/README.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# HTTP/2 Handler
|
||||
|
||||
Included in this folder is a complete `ThousandIsland.Handler` based implementation of HTTP/2 as
|
||||
defined in [RFC 9110](https://datatracker.ietf.org/doc/rfc9110) & [RFC
|
||||
9113](https://datatracker.ietf.org/doc/rfc9113)
|
||||
|
||||
## Process model
|
||||
|
||||
Within a Bandit server, an HTTP/2 connection is modeled as a set of processes:
|
||||
|
||||
* 1 process per connection, a `Bandit.HTTP2.Handler` module implementing the
|
||||
`ThousandIsland.Handler` behaviour, and;
|
||||
* 1 process per stream (i.e.: per HTTP request) within the connection, implemented as
|
||||
a `Bandit.HTTP2.StreamProcess` process
|
||||
|
||||
Each of these processes model the majority of their state via a
|
||||
`Bandit.HTTP2.Connection` & `Bandit.HTTP2.Stream` struct, respectively.
|
||||
|
||||
The lifetimes of these processes correspond to their role; a connection process lives for as long
|
||||
as a client is connected, and a stream process lives only as long as is required to process
|
||||
a single stream request within a connection.
|
||||
|
||||
Connection processes are the 'root' of each connection's process group, and are supervised by
|
||||
Thousand Island in the same manner that `ThousandIsland.Handler` processes are usually supervised
|
||||
(see the [project README](https://github.com/mtrudel/thousand_island) for details).
|
||||
|
||||
Stream processes are not supervised by design. The connection process starts new
|
||||
stream processes as required, via a standard `start_link`
|
||||
call, and manages the termination of the resultant linked stream processes by
|
||||
handling `{:EXIT,...}` messages as described in the Elixir documentation. Each
|
||||
stream process stays alive long enough to fully model an HTTP/2 stream,
|
||||
beginning its life in the `:init` state and ending it in the `:closed` state (or
|
||||
else by a stream or connection error being raised). This approach is aligned
|
||||
with the realities of the HTTP/2 model, insofar as if a connection process
|
||||
terminates there is no reason to keep its constituent stream processes around,
|
||||
and if a stream process dies the connection should be able to handle this
|
||||
without itself terminating. It also means that our process model is very
|
||||
lightweight - there is no extra supervision overhead present because no such
|
||||
supervision is required for the system to function in the desired way.
|
||||
|
||||
## Reading client data
|
||||
|
||||
The overall structure of the implementation is managed by the `Bandit.HTTP2.Handler` module, and
|
||||
looks like the following:
|
||||
|
||||
1. Bytes are asynchronously received from ThousandIsland via the
|
||||
`Bandit.HTTP2.Handler.handle_data/3` function
|
||||
2. Frames are parsed from these bytes by calling the `Bandit.HTTP2.Frame.deserialize/2`
|
||||
function. If successful, the parsed frame(s) are returned. We retain any unparsed bytes in
|
||||
a buffer in order to attempt parsing them upon receipt of subsequent data from the client
|
||||
3. Parsed frames are passed into the `Bandit.HTTP2.Connection` module along with a struct of
|
||||
same module. Frames are processed via the `Bandit.HTTP2.Connection.handle_frame/3` function.
|
||||
Connection-level frames are handled within the `Bandit.HTTP2.Connection`
|
||||
struct, and stream-level frames are passed along to the corresponding stream
|
||||
process, which is wholly responsible for managing all aspects of a stream's
|
||||
state (which is tracked via the `Bandit.HTTP2.Stream` struct). The one
|
||||
exception to this is the handling of frames sent to streams which have
|
||||
already been closed (and whose corresponding processes have thus terminated).
|
||||
Any such frames are discarded without effect.
|
||||
4. This process is repeated every time we receive data from the client until the
|
||||
`Bandit.HTTP2.Connection` module indicates that the connection should be closed, either
|
||||
normally or due to error. Note that frame deserialization may end up returning a connection
|
||||
error if the parsed frames fail specific criteria (generally, the frame parsing modules are
|
||||
responsible for identifying errors as described in [section
|
||||
6](https://datatracker.ietf.org/doc/html/rfc9113#section-6) of RFC 9113). In these cases, the
|
||||
failure is passed through to the connection module for processing in order to coordinate an
|
||||
orderly shutdown or client notification as appropriate
|
||||
|
||||
## Processing requests
|
||||
|
||||
The state of a particular stream are contained within a `Bandit.HTTP2.Stream`
|
||||
struct, maintained within a `Bandit.HTTP2.StreamProcess` process. As part of the
|
||||
stream's lifecycle, the server's configured Plug is called, with an instance of
|
||||
the `Bandit.Adapter` struct being used to interface with the Plug. There
|
||||
is a separation of concerns between the aspect of HTTP semantics managed by
|
||||
`Bandit.Adapter` (roughly, those concerns laid out in
|
||||
[RFC9110](https://datatracker.ietf.org/doc/html/rfc9110)) and the more
|
||||
transport-specific HTTP/2 concerns managed by `Bandit.HTTP2.Stream` (roughly the
|
||||
concerns specified in [RFC9113](https://datatracker.ietf.org/doc/html/rfc9113)).
|
||||
|
||||
# Testing
|
||||
|
||||
All of this is exhaustively tested. Tests are broken up primarily into `protocol_test.exs`, which
|
||||
is concerned with aspects of the implementation relating to protocol conformance and
|
||||
client-facing concerns, while `plug_test.exs` is concerned with aspects of the implementation
|
||||
having to do with the Plug API and application-facing concerns. There are also more
|
||||
unit-style tests covering frame serialization and deserialization.
|
||||
|
||||
In addition, the `h2spec` conformance suite is run via a `System` wrapper & executes the entirety
|
||||
of the suite (in strict mode) against a running Bandit server.
|
||||
|
||||
## Limitations and Assumptions
|
||||
|
||||
Some limitations and assumptions of this implementation:
|
||||
|
||||
* This handler assumes that the HTTP/2 connection preface has already been consumed from the
|
||||
client. The `Bandit.InitialHandler` module uses this preface to discriminate between various
|
||||
HTTP versions when determining which handler to use
|
||||
* Priority frames are parsed and validated, but do not induce any action on the part of the
|
||||
server. There is no priority assigned to respective streams in terms of processing; all streams
|
||||
are run in parallel as soon as they arrive
|
||||
* While flow control is completely implemented here, the specific values used for upload flow
|
||||
control (that is, the end that we control) are fixed. Specifically, we attempt to maintain
|
||||
fairly large windows in order to not restrict client uploads (we 'slow-start' window changes
|
||||
upon receipt of first byte, mostly to retain parity between connection and stream window
|
||||
management since connection windows cannot be changed via settings). The majority of flow
|
||||
control logic has been encapsulated in the `Bandit.HTTP2.FlowControl` module should future
|
||||
refinement be required
|
||||
473
phoenix/deps/bandit/lib/bandit/http2/connection.ex
Normal file
473
phoenix/deps/bandit/lib/bandit/http2/connection.ex
Normal file
@@ -0,0 +1,473 @@
|
||||
defmodule Bandit.HTTP2.Connection do
|
||||
@moduledoc false
|
||||
# Represents the state of an HTTP/2 connection, in a process-free manner. An instance of this
|
||||
# struct is maintained as the state of a `Bandit.HTTP2.Handler` process, and it moves an HTTP/2
|
||||
# connection through its lifecycle by calling functions defined on this module
|
||||
|
||||
require Logger
|
||||
|
||||
defstruct local_settings: %Bandit.HTTP2.Settings{},
|
||||
remote_settings: %Bandit.HTTP2.Settings{},
|
||||
fragment_frame: nil,
|
||||
send_hpack_state: HPAX.new(4096),
|
||||
recv_hpack_state: HPAX.new(4096),
|
||||
send_window_size: 65_535,
|
||||
recv_window_size: 65_535,
|
||||
streams: %Bandit.HTTP2.StreamCollection{},
|
||||
pending_sends: [],
|
||||
conn_data: nil,
|
||||
telemetry_span: nil,
|
||||
plug: nil,
|
||||
opts: %{},
|
||||
reset_stream_timestamps: []
|
||||
|
||||
@typedoc "Encapsulates the state of an HTTP/2 connection"
|
||||
@type t :: %__MODULE__{
|
||||
local_settings: Bandit.HTTP2.Settings.t(),
|
||||
remote_settings: Bandit.HTTP2.Settings.t(),
|
||||
fragment_frame: Bandit.HTTP2.Frame.Headers.t() | nil,
|
||||
send_hpack_state: term(),
|
||||
recv_hpack_state: term(),
|
||||
send_window_size: non_neg_integer(),
|
||||
recv_window_size: non_neg_integer(),
|
||||
streams: Bandit.HTTP2.StreamCollection.t(),
|
||||
pending_sends: [{Bandit.HTTP2.Stream.stream_id(), iodata(), boolean(), fun()}],
|
||||
conn_data: Bandit.Pipeline.conn_data(),
|
||||
telemetry_span: ThousandIsland.Telemetry.t(),
|
||||
plug: Bandit.Pipeline.plug_def(),
|
||||
opts: %{
|
||||
required(:http) => Bandit.http_options(),
|
||||
required(:http_2) => Bandit.http_2_options()
|
||||
},
|
||||
reset_stream_timestamps: [integer()]
|
||||
}
|
||||
|
||||
@spec init(ThousandIsland.Socket.t(), Bandit.Pipeline.plug_def(), map()) :: t()
|
||||
def init(socket, plug, opts) do
|
||||
connection = %__MODULE__{
|
||||
local_settings:
|
||||
struct!(Bandit.HTTP2.Settings, Keyword.get(opts.http_2, :default_local_settings, [])),
|
||||
conn_data: Bandit.SocketHelpers.conn_data(socket),
|
||||
telemetry_span: ThousandIsland.Socket.telemetry_span(socket),
|
||||
plug: plug,
|
||||
opts: opts
|
||||
}
|
||||
|
||||
# Send SETTINGS frame per RFC9113§3.4
|
||||
%Bandit.HTTP2.Frame.Settings{ack: false, settings: Map.from_struct(connection.local_settings)}
|
||||
|> send_frame(socket, connection)
|
||||
|
||||
connection
|
||||
end
|
||||
|
||||
#
|
||||
# Receiving while expecting CONTINUATION frames is a special case (RFC9113§6.10); handle it first
|
||||
#
|
||||
|
||||
@spec handle_frame(Bandit.HTTP2.Frame.frame(), ThousandIsland.Socket.t(), t()) :: t()
|
||||
def handle_frame(
|
||||
%Bandit.HTTP2.Frame.Continuation{end_headers: true, stream_id: stream_id} = frame,
|
||||
socket,
|
||||
%__MODULE__{fragment_frame: %Bandit.HTTP2.Frame.Headers{stream_id: stream_id}} =
|
||||
connection
|
||||
) do
|
||||
header_block = connection.fragment_frame.fragment <> frame.fragment
|
||||
header_frame = %{connection.fragment_frame | end_headers: true, fragment: header_block}
|
||||
handle_frame(header_frame, socket, %{connection | fragment_frame: nil})
|
||||
end
|
||||
|
||||
def handle_frame(
|
||||
%Bandit.HTTP2.Frame.Continuation{end_headers: false, stream_id: stream_id} = frame,
|
||||
_socket,
|
||||
%__MODULE__{fragment_frame: %Bandit.HTTP2.Frame.Headers{stream_id: stream_id}} =
|
||||
connection
|
||||
) do
|
||||
fragment = connection.fragment_frame.fragment <> frame.fragment
|
||||
check_oversize_fragment!(fragment, connection)
|
||||
fragment_frame = %{connection.fragment_frame | fragment: fragment}
|
||||
%{connection | fragment_frame: fragment_frame}
|
||||
end
|
||||
|
||||
def handle_frame(_frame, _socket, %__MODULE__{fragment_frame: %Bandit.HTTP2.Frame.Headers{}}) do
|
||||
connection_error!("Expected CONTINUATION frame (RFC9113§6.10)")
|
||||
end
|
||||
|
||||
#
|
||||
# Connection-level receiving
|
||||
#
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Settings{ack: true}, _socket, connection), do: connection
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Settings{ack: false} = frame, socket, connection) do
|
||||
%Bandit.HTTP2.Frame.Settings{ack: true} |> send_frame(socket, connection)
|
||||
|
||||
# Merge whatever new settings were sent with our existing remote settings
|
||||
remote_settings = struct(connection.remote_settings, frame.settings)
|
||||
|
||||
send_hpack_state = HPAX.resize(connection.send_hpack_state, remote_settings.header_table_size)
|
||||
delta = remote_settings.initial_window_size - connection.remote_settings.initial_window_size
|
||||
|
||||
Bandit.HTTP2.StreamCollection.get_pids(connection.streams)
|
||||
|> Enum.each(&Bandit.HTTP2.Stream.deliver_send_window_update(&1, delta))
|
||||
|
||||
do_pending_sends(socket, %{
|
||||
connection
|
||||
| remote_settings: remote_settings,
|
||||
send_hpack_state: send_hpack_state
|
||||
})
|
||||
end
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Ping{ack: true}, _socket, connection), do: connection
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Ping{ack: false} = frame, socket, connection) do
|
||||
%Bandit.HTTP2.Frame.Ping{ack: true, payload: frame.payload} |> send_frame(socket, connection)
|
||||
connection
|
||||
end
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Goaway{}, _socket, connection), do: connection
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.WindowUpdate{stream_id: 0} = frame, socket, connection) do
|
||||
case Bandit.HTTP2.FlowControl.update_send_window(
|
||||
connection.send_window_size,
|
||||
frame.size_increment
|
||||
) do
|
||||
{:ok, new_window} -> do_pending_sends(socket, %{connection | send_window_size: new_window})
|
||||
{:error, error} -> connection_error!(error, Bandit.HTTP2.Errors.flow_control_error())
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Stream-level receiving
|
||||
#
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.WindowUpdate{} = frame, _socket, connection) do
|
||||
streams =
|
||||
with_stream(connection, frame.stream_id, fn stream ->
|
||||
Bandit.HTTP2.Stream.deliver_send_window_update(stream, frame.size_increment)
|
||||
end)
|
||||
|
||||
%{connection | streams: streams}
|
||||
end
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Headers{end_headers: true} = frame, _socket, connection) do
|
||||
check_oversize_fragment!(frame.fragment, connection)
|
||||
|
||||
case HPAX.decode(frame.fragment, connection.recv_hpack_state) do
|
||||
{:ok, headers, recv_hpack_state} ->
|
||||
streams =
|
||||
with_stream(connection, frame.stream_id, fn stream ->
|
||||
Bandit.HTTP2.Stream.deliver_headers(stream, headers, frame.end_stream)
|
||||
end)
|
||||
|
||||
%{connection | recv_hpack_state: recv_hpack_state, streams: streams}
|
||||
|
||||
_ ->
|
||||
connection_error!("Header decode error", Bandit.HTTP2.Errors.compression_error())
|
||||
end
|
||||
end
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Headers{end_headers: false} = frame, _socket, connection) do
|
||||
check_oversize_fragment!(frame.fragment, connection)
|
||||
%{connection | fragment_frame: frame}
|
||||
end
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Continuation{}, _socket, _connection) do
|
||||
connection_error!("Received unexpected CONTINUATION frame (RFC9113§6.10)")
|
||||
end
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Data{} = frame, socket, connection) do
|
||||
streams =
|
||||
with_stream(connection, frame.stream_id, fn stream ->
|
||||
Bandit.HTTP2.Stream.deliver_data(stream, frame.data, frame.end_stream)
|
||||
end)
|
||||
|
||||
{recv_window_size, window_increment} =
|
||||
Bandit.HTTP2.FlowControl.compute_recv_window(
|
||||
connection.recv_window_size,
|
||||
byte_size(frame.data)
|
||||
)
|
||||
|
||||
if window_increment > 0 do
|
||||
%Bandit.HTTP2.Frame.WindowUpdate{stream_id: 0, size_increment: window_increment}
|
||||
|> send_frame(socket, connection)
|
||||
end
|
||||
|
||||
%{connection | recv_window_size: recv_window_size, streams: streams}
|
||||
end
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Priority{}, _socket, connection), do: connection
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.RstStream{} = frame, _socket, connection) do
|
||||
streams =
|
||||
with_stream(connection, frame.stream_id, fn stream ->
|
||||
Bandit.HTTP2.Stream.deliver_rst_stream(stream, frame.error_code)
|
||||
end)
|
||||
|
||||
%{connection | streams: streams}
|
||||
|> check_reset_stream_rate_limit!()
|
||||
end
|
||||
|
||||
# Catch-all handler for unknown frame types
|
||||
|
||||
def handle_frame(%Bandit.HTTP2.Frame.Unknown{} = frame, _socket, connection) do
|
||||
Logger.warning("Unknown frame (#{inspect(Map.from_struct(frame))})",
|
||||
domain: [:bandit],
|
||||
plug: connection.plug
|
||||
)
|
||||
|
||||
connection
|
||||
end
|
||||
|
||||
defp with_stream(connection, stream_id, fun) do
|
||||
case Bandit.HTTP2.StreamCollection.get_pid(connection.streams, stream_id) do
|
||||
pid when is_pid(pid) or pid == :closed ->
|
||||
fun.(pid)
|
||||
connection.streams
|
||||
|
||||
:new ->
|
||||
new_stream!(connection, stream_id)
|
||||
|
||||
sendfile_chunk_size =
|
||||
Keyword.get(connection.opts.http_2, :sendfile_chunk_size, 1_048_576)
|
||||
|
||||
stream =
|
||||
Bandit.HTTP2.Stream.init(
|
||||
self(),
|
||||
stream_id,
|
||||
connection.remote_settings.initial_window_size,
|
||||
sendfile_chunk_size
|
||||
)
|
||||
|
||||
case Bandit.HTTP2.StreamProcess.start_link(
|
||||
stream,
|
||||
connection.plug,
|
||||
connection.telemetry_span,
|
||||
connection.conn_data,
|
||||
connection.opts
|
||||
) do
|
||||
{:ok, pid} ->
|
||||
streams = Bandit.HTTP2.StreamCollection.insert(connection.streams, stream_id, pid)
|
||||
with_stream(%{connection | streams: streams}, stream_id, fun)
|
||||
|
||||
_ ->
|
||||
raise "Unable to start stream process"
|
||||
end
|
||||
|
||||
:invalid ->
|
||||
connection_error!("Received invalid stream identifier")
|
||||
end
|
||||
end
|
||||
|
||||
defp new_stream!(connection, stream_id) do
|
||||
max_requests = Keyword.get(connection.opts.http_2, :max_requests, 0)
|
||||
|
||||
if max_requests != 0 and
|
||||
max_requests <= Bandit.HTTP2.StreamCollection.stream_count(connection.streams) do
|
||||
connection_error!("Connection count exceeded", Bandit.HTTP2.Errors.refused_stream())
|
||||
end
|
||||
|
||||
if connection.local_settings.max_concurrent_streams <=
|
||||
Bandit.HTTP2.StreamCollection.open_stream_count(connection.streams) do
|
||||
stream_error!(
|
||||
"Concurrent stream count exceeded",
|
||||
stream_id,
|
||||
Bandit.HTTP2.Errors.refused_stream()
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_oversize_fragment!(fragment, connection) do
|
||||
if byte_size(fragment) > Keyword.get(connection.opts.http_2, :max_header_block_size, 50_000),
|
||||
do: connection_error!("Received overlong headers")
|
||||
end
|
||||
|
||||
@spec check_reset_stream_rate_limit!(t()) :: t()
|
||||
defp check_reset_stream_rate_limit!(connection) do
|
||||
case Keyword.get(connection.opts.http_2, :max_reset_stream_rate, {500, 10_000}) do
|
||||
nil ->
|
||||
connection
|
||||
|
||||
{intensity, period} ->
|
||||
now = :erlang.monotonic_time(:millisecond)
|
||||
threshold = now - period
|
||||
resets = connection.reset_stream_timestamps
|
||||
recent_timestamps = can_reset(intensity - 1, threshold, resets, [], intensity, period)
|
||||
%{connection | reset_stream_timestamps: [now | recent_timestamps]}
|
||||
end
|
||||
end
|
||||
|
||||
defp can_reset(_, _, [], acc, _, _),
|
||||
do: :lists.reverse(acc)
|
||||
|
||||
defp can_reset(_, threshold, [restart | _], acc, _, _) when restart < threshold,
|
||||
do: :lists.reverse(acc)
|
||||
|
||||
defp can_reset(0, _, [_ | _], _acc, intensity, period),
|
||||
do:
|
||||
connection_error!(
|
||||
"Stream resets rate exceeded #{intensity} resets in #{period}ms",
|
||||
Bandit.HTTP2.Errors.enhance_your_calm()
|
||||
)
|
||||
|
||||
defp can_reset(n, threshold, [restart | restarts], acc, intensity, period),
|
||||
do: can_reset(n - 1, threshold, restarts, [restart | acc], intensity, period)
|
||||
|
||||
# Shared logic to send any pending frames upon adjustment of our send window
|
||||
defp do_pending_sends(socket, connection) do
|
||||
connection.pending_sends
|
||||
|> Enum.reverse()
|
||||
|> Enum.reduce(connection, fn pending_send, connection ->
|
||||
connection = connection |> Map.update!(:pending_sends, &List.delete(&1, pending_send))
|
||||
{stream_id, rest, end_stream, on_unblock} = pending_send
|
||||
send_data(stream_id, rest, end_stream, on_unblock, socket, connection)
|
||||
end)
|
||||
end
|
||||
|
||||
#
|
||||
# Sending logic
|
||||
#
|
||||
# All callers of functions below will be from stream processes
|
||||
#
|
||||
|
||||
#
|
||||
# Stream-level sending
|
||||
#
|
||||
|
||||
@spec send_headers(
|
||||
Bandit.HTTP2.Stream.stream_id(),
|
||||
Plug.Conn.headers(),
|
||||
boolean(),
|
||||
ThousandIsland.Socket.t(),
|
||||
t()
|
||||
) :: t()
|
||||
def send_headers(stream_id, headers, end_stream, socket, connection) do
|
||||
with enc_headers <- Enum.map(headers, fn {key, value} -> {:store, key, value} end),
|
||||
{block, send_hpack_state} <- HPAX.encode(enc_headers, connection.send_hpack_state) do
|
||||
%Bandit.HTTP2.Frame.Headers{
|
||||
stream_id: stream_id,
|
||||
end_stream: end_stream,
|
||||
fragment: block
|
||||
}
|
||||
|> send_frame(socket, connection)
|
||||
|
||||
%{connection | send_hpack_state: send_hpack_state}
|
||||
end
|
||||
end
|
||||
|
||||
@spec send_data(
|
||||
Bandit.HTTP2.Stream.stream_id(),
|
||||
iodata(),
|
||||
boolean(),
|
||||
fun(),
|
||||
ThousandIsland.Socket.t(),
|
||||
t()
|
||||
) :: t()
|
||||
def send_data(stream_id, data, end_stream, on_unblock, socket, connection) do
|
||||
with connection_window_size <- connection.send_window_size,
|
||||
max_bytes_to_send <- max(connection_window_size, 0),
|
||||
{data_to_send, bytes_to_send, rest} <- split_data(data, max_bytes_to_send),
|
||||
connection <- %{connection | send_window_size: connection_window_size - bytes_to_send},
|
||||
end_stream_to_send <- end_stream && byte_size(rest) == 0 do
|
||||
if end_stream_to_send || bytes_to_send > 0 do
|
||||
%Bandit.HTTP2.Frame.Data{
|
||||
stream_id: stream_id,
|
||||
end_stream: end_stream_to_send,
|
||||
data: data_to_send
|
||||
}
|
||||
|> send_frame(socket, connection)
|
||||
end
|
||||
|
||||
if byte_size(rest) == 0 do
|
||||
on_unblock.()
|
||||
connection
|
||||
else
|
||||
pending_sends = [{stream_id, rest, end_stream, on_unblock} | connection.pending_sends]
|
||||
%{connection | pending_sends: pending_sends}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp split_data(data, desired_length) do
|
||||
data_length = IO.iodata_length(data)
|
||||
|
||||
if data_length <= desired_length do
|
||||
{data, data_length, <<>>}
|
||||
else
|
||||
<<to_send::binary-size(desired_length), rest::binary>> = IO.iodata_to_binary(data)
|
||||
{to_send, desired_length, rest}
|
||||
end
|
||||
end
|
||||
|
||||
@spec send_recv_window_update(
|
||||
Bandit.HTTP2.Stream.stream_id(),
|
||||
non_neg_integer(),
|
||||
ThousandIsland.Socket.t(),
|
||||
t()
|
||||
) :: term()
|
||||
def send_recv_window_update(stream_id, size_increment, socket, connection) do
|
||||
%Bandit.HTTP2.Frame.WindowUpdate{stream_id: stream_id, size_increment: size_increment}
|
||||
|> send_frame(socket, connection)
|
||||
end
|
||||
|
||||
@spec send_rst_stream(
|
||||
Bandit.HTTP2.Stream.stream_id(),
|
||||
Bandit.HTTP2.Errors.error_code(),
|
||||
ThousandIsland.Socket.t(),
|
||||
t()
|
||||
) :: term()
|
||||
def send_rst_stream(stream_id, error_code, socket, connection) do
|
||||
%Bandit.HTTP2.Frame.RstStream{stream_id: stream_id, error_code: error_code}
|
||||
|> send_frame(socket, connection)
|
||||
end
|
||||
|
||||
@spec stream_terminated(pid(), t()) :: t()
|
||||
def stream_terminated(pid, connection) do
|
||||
%{connection | streams: Bandit.HTTP2.StreamCollection.delete(connection.streams, pid)}
|
||||
end
|
||||
|
||||
#
|
||||
# Helper functions
|
||||
#
|
||||
|
||||
@spec close_connection(Bandit.HTTP2.Errors.error_code(), term(), ThousandIsland.Socket.t(), t()) ::
|
||||
{:close, t()} | {:error, term(), t()}
|
||||
def close_connection(error_code, reason, socket, connection) do
|
||||
last_stream_id = Bandit.HTTP2.StreamCollection.last_stream_id(connection.streams)
|
||||
|
||||
%Bandit.HTTP2.Frame.Goaway{last_stream_id: last_stream_id, error_code: error_code}
|
||||
|> send_frame(socket, connection)
|
||||
|
||||
if error_code == Bandit.HTTP2.Errors.no_error(),
|
||||
do: {:close, connection},
|
||||
else: {:error, reason, connection}
|
||||
end
|
||||
|
||||
@spec connection_error!(term()) :: no_return()
|
||||
@spec connection_error!(term(), Bandit.HTTP2.Errors.error_code()) :: no_return()
|
||||
defp connection_error!(message, error_code \\ Bandit.HTTP2.Errors.protocol_error()) do
|
||||
raise Bandit.HTTP2.Errors.ConnectionError, message: message, error_code: error_code
|
||||
end
|
||||
|
||||
@spec stream_error!(
|
||||
String.t(),
|
||||
Bandit.HTTP2.Stream.stream_id(),
|
||||
Bandit.HTTP2.Errors.error_code()
|
||||
) ::
|
||||
no_return()
|
||||
defp stream_error!(message, stream_id, error_code) do
|
||||
raise Bandit.HTTP2.Errors.StreamError,
|
||||
message: message,
|
||||
error_code: error_code,
|
||||
stream_id: stream_id
|
||||
end
|
||||
|
||||
defp send_frame(frame, socket, connection) do
|
||||
_ =
|
||||
ThousandIsland.Socket.send(
|
||||
socket,
|
||||
Bandit.HTTP2.Frame.serialize(frame, connection.remote_settings.max_frame_size)
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
59
phoenix/deps/bandit/lib/bandit/http2/errors.ex
Normal file
59
phoenix/deps/bandit/lib/bandit/http2/errors.ex
Normal file
@@ -0,0 +1,59 @@
|
||||
defmodule Bandit.HTTP2.Errors do
|
||||
@moduledoc false
|
||||
# Errors as defined in RFC9113§7
|
||||
|
||||
@typedoc "An error code as defined for GOAWAY and RST_STREAM errors"
|
||||
@type error_code() ::
|
||||
(no_error :: 0x0)
|
||||
| (protocol_error :: 0x1)
|
||||
| (internal_error :: 0x2)
|
||||
| (flow_control_error :: 0x3)
|
||||
| (settings_timeout :: 0x4)
|
||||
| (stream_closed :: 0x5)
|
||||
| (frame_size_error :: 0x6)
|
||||
| (refused_stream :: 0x7)
|
||||
| (cancel :: 0x8)
|
||||
| (compression_error :: 0x9)
|
||||
| (connect_error :: 0xA)
|
||||
| (enhance_your_calm :: 0xB)
|
||||
| (inadequate_security :: 0xC)
|
||||
| (http_1_1_requires :: 0xD)
|
||||
|
||||
error_codes = %{
|
||||
no_error: 0x0,
|
||||
protocol_error: 0x1,
|
||||
internal_error: 0x2,
|
||||
flow_control_error: 0x3,
|
||||
settings_timeout: 0x4,
|
||||
stream_closed: 0x5,
|
||||
frame_size_error: 0x6,
|
||||
refused_stream: 0x7,
|
||||
cancel: 0x8,
|
||||
compression_error: 0x9,
|
||||
connect_error: 0xA,
|
||||
enhance_your_calm: 0xB,
|
||||
inadequate_security: 0xC,
|
||||
http_1_1_requires: 0xD
|
||||
}
|
||||
|
||||
@spec to_reason(integer()) :: atom()
|
||||
|
||||
for {name, value} <- error_codes do
|
||||
@spec unquote(name)() :: unquote(Macro.var(name, Elixir)) :: unquote(value)
|
||||
def unquote(name)(), do: unquote(value)
|
||||
|
||||
def to_reason(unquote(value)), do: unquote(name)
|
||||
end
|
||||
|
||||
def to_reason(_), do: :unknown
|
||||
|
||||
# Represents a stream error as defined in RFC9113§5.4.2
|
||||
defmodule StreamError do
|
||||
defexception [:message, :error_code, :stream_id]
|
||||
end
|
||||
|
||||
# Represents a stream error as defined in RFC9113§5.4.3
|
||||
defmodule ConnectionError do
|
||||
defexception [:message, :error_code]
|
||||
end
|
||||
end
|
||||
43
phoenix/deps/bandit/lib/bandit/http2/flow_control.ex
Normal file
43
phoenix/deps/bandit/lib/bandit/http2/flow_control.ex
Normal file
@@ -0,0 +1,43 @@
|
||||
defmodule Bandit.HTTP2.FlowControl do
|
||||
@moduledoc false
|
||||
# Helpers for working with flow control window calculations
|
||||
|
||||
import Bitwise
|
||||
|
||||
@max_window_increment (1 <<< 31) - 1
|
||||
@max_window_size (1 <<< 31) - 1
|
||||
@min_window_size 1 <<< 30
|
||||
|
||||
@spec compute_recv_window(non_neg_integer(), non_neg_integer()) ::
|
||||
{non_neg_integer(), non_neg_integer()}
|
||||
def compute_recv_window(recv_window_size, data_size) do
|
||||
# This is what our window size will be after receiving data_size bytes
|
||||
recv_window_size = recv_window_size - data_size
|
||||
|
||||
if recv_window_size > @min_window_size do
|
||||
# We have room to go before we need to update our window
|
||||
{recv_window_size, 0}
|
||||
else
|
||||
# We want our new window to be as large as possible, but are limited by both the maximum size
|
||||
# of the window (2^31-1) and the maximum size of the increment we can send to the client, both
|
||||
# per RFC9113§6.9. Be careful about handling cases where we have a negative window due to
|
||||
# misbehaving clients or network races
|
||||
new_recv_window_size = min(recv_window_size + @max_window_increment, @max_window_size)
|
||||
|
||||
# Finally, determine what increment to send to the client
|
||||
increment = new_recv_window_size - recv_window_size
|
||||
|
||||
{new_recv_window_size, increment}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_send_window(non_neg_integer(), non_neg_integer()) ::
|
||||
{:ok, non_neg_integer()} | {:error, String.t()}
|
||||
def update_send_window(current_send_window, increment) do
|
||||
if current_send_window + increment > @max_window_size do
|
||||
{:error, "Invalid WINDOW_UPDATE increment RFC9113§6.9.1"}
|
||||
else
|
||||
{:ok, current_send_window + increment}
|
||||
end
|
||||
end
|
||||
end
|
||||
106
phoenix/deps/bandit/lib/bandit/http2/frame.ex
Normal file
106
phoenix/deps/bandit/lib/bandit/http2/frame.ex
Normal file
@@ -0,0 +1,106 @@
|
||||
defmodule Bandit.HTTP2.Frame do
|
||||
@moduledoc false
|
||||
|
||||
@typedoc "Indicates a frame type"
|
||||
@type frame_type :: non_neg_integer()
|
||||
|
||||
@typedoc "The flags passed along with a frame"
|
||||
@type flags :: byte()
|
||||
|
||||
@typedoc "A valid HTTP/2 frame"
|
||||
@type frame ::
|
||||
Bandit.HTTP2.Frame.Data.t()
|
||||
| Bandit.HTTP2.Frame.Headers.t()
|
||||
| Bandit.HTTP2.Frame.Priority.t()
|
||||
| Bandit.HTTP2.Frame.RstStream.t()
|
||||
| Bandit.HTTP2.Frame.Settings.t()
|
||||
| Bandit.HTTP2.Frame.Ping.t()
|
||||
| Bandit.HTTP2.Frame.Goaway.t()
|
||||
| Bandit.HTTP2.Frame.WindowUpdate.t()
|
||||
| Bandit.HTTP2.Frame.Continuation.t()
|
||||
| Bandit.HTTP2.Frame.Unknown.t()
|
||||
|
||||
@spec deserialize(binary(), non_neg_integer()) ::
|
||||
{{:ok, frame()}, iodata()}
|
||||
| {{:more, iodata()}, <<>>}
|
||||
| {{:error, Bandit.HTTP2.Errors.error_code(), binary()}, iodata()}
|
||||
| nil
|
||||
def deserialize(
|
||||
<<length::24, type::8, flags::8, _reserved::1, stream_id::31,
|
||||
payload::binary-size(length), rest::binary>>,
|
||||
max_frame_size
|
||||
)
|
||||
when length <= max_frame_size do
|
||||
type
|
||||
|> case do
|
||||
0x0 -> Bandit.HTTP2.Frame.Data.deserialize(flags, stream_id, payload)
|
||||
0x1 -> Bandit.HTTP2.Frame.Headers.deserialize(flags, stream_id, payload)
|
||||
0x2 -> Bandit.HTTP2.Frame.Priority.deserialize(flags, stream_id, payload)
|
||||
0x3 -> Bandit.HTTP2.Frame.RstStream.deserialize(flags, stream_id, payload)
|
||||
0x4 -> Bandit.HTTP2.Frame.Settings.deserialize(flags, stream_id, payload)
|
||||
0x5 -> Bandit.HTTP2.Frame.PushPromise.deserialize(flags, stream_id, payload)
|
||||
0x6 -> Bandit.HTTP2.Frame.Ping.deserialize(flags, stream_id, payload)
|
||||
0x7 -> Bandit.HTTP2.Frame.Goaway.deserialize(flags, stream_id, payload)
|
||||
0x8 -> Bandit.HTTP2.Frame.WindowUpdate.deserialize(flags, stream_id, payload)
|
||||
0x9 -> Bandit.HTTP2.Frame.Continuation.deserialize(flags, stream_id, payload)
|
||||
unknown -> Bandit.HTTP2.Frame.Unknown.deserialize(unknown, flags, stream_id, payload)
|
||||
end
|
||||
|> then(&{&1, rest})
|
||||
end
|
||||
|
||||
# This is a little more aggressive than necessary. RFC9113§4.2 says we only need
|
||||
# to treat frame size violations as connection level errors if the frame in
|
||||
# question would affect the connection as a whole, so we could be more surgical
|
||||
# here and send stream level errors in some cases. However, we are well within
|
||||
# our rights to consider such errors as connection errors
|
||||
def deserialize(
|
||||
<<length::24, _type::8, _flags::8, _reserved::1, _stream_id::31,
|
||||
_payload::binary-size(length), rest::binary>>,
|
||||
max_frame_size
|
||||
)
|
||||
when length > max_frame_size do
|
||||
{{:error, Bandit.HTTP2.Errors.frame_size_error(), "Payload size too large (RFC9113§4.2)"},
|
||||
rest}
|
||||
end
|
||||
|
||||
# nil is used to indicate for Stream.unfold/2 that the frame deserialization is finished
|
||||
def deserialize(<<>>, _max_frame_size) do
|
||||
nil
|
||||
end
|
||||
|
||||
def deserialize(msg, _max_frame_size) do
|
||||
{{:more, msg}, <<>>}
|
||||
end
|
||||
|
||||
defmodule Flags do
|
||||
@moduledoc false
|
||||
|
||||
import Bitwise
|
||||
|
||||
defguard set?(flags, bit) when band(flags, bsl(1, bit)) != 0
|
||||
defguard clear?(flags, bit) when band(flags, bsl(1, bit)) == 0
|
||||
|
||||
@spec set([0..255]) :: 0..255
|
||||
def set([]), do: 0x0
|
||||
def set([bit | rest]), do: bor(bsl(1, bit), set(rest))
|
||||
end
|
||||
|
||||
defprotocol Serializable do
|
||||
@moduledoc false
|
||||
|
||||
@spec serialize(any(), non_neg_integer()) :: [
|
||||
{Bandit.HTTP2.Frame.frame_type(), Bandit.HTTP2.Frame.flags(),
|
||||
Bandit.HTTP2.Stream.stream_id(), iodata()}
|
||||
]
|
||||
def serialize(frame, max_frame_size)
|
||||
end
|
||||
|
||||
@spec serialize(frame(), non_neg_integer()) :: iolist()
|
||||
def serialize(frame, max_frame_size) do
|
||||
frame
|
||||
|> Serializable.serialize(max_frame_size)
|
||||
|> Enum.map(fn {type, flags, stream_id, payload} ->
|
||||
[<<IO.iodata_length(payload)::24, type::8, flags::8, 0::1, stream_id::31>>, payload]
|
||||
end)
|
||||
end
|
||||
end
|
||||
57
phoenix/deps/bandit/lib/bandit/http2/frame/continuation.ex
Normal file
57
phoenix/deps/bandit/lib/bandit/http2/frame/continuation.ex
Normal file
@@ -0,0 +1,57 @@
|
||||
defmodule Bandit.HTTP2.Frame.Continuation do
|
||||
@moduledoc false
|
||||
|
||||
import Bandit.HTTP2.Frame.Flags
|
||||
|
||||
defstruct stream_id: nil,
|
||||
end_headers: false,
|
||||
fragment: nil
|
||||
|
||||
@typedoc "An HTTP/2 CONTINUATION frame"
|
||||
@type t :: %__MODULE__{
|
||||
stream_id: Bandit.HTTP2.Stream.stream_id(),
|
||||
end_headers: boolean(),
|
||||
fragment: iodata()
|
||||
}
|
||||
|
||||
@end_headers_bit 2
|
||||
|
||||
@spec deserialize(Bandit.HTTP2.Frame.flags(), Bandit.HTTP2.Stream.stream_id(), iodata()) ::
|
||||
{:ok, t()} | {:error, Bandit.HTTP2.Errors.error_code(), binary()}
|
||||
def deserialize(_flags, 0, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(),
|
||||
"CONTINUATION frame with zero stream_id (RFC9113§6.10)"}
|
||||
end
|
||||
|
||||
def deserialize(flags, stream_id, <<fragment::binary>>) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
stream_id: stream_id,
|
||||
end_headers: set?(flags, @end_headers_bit),
|
||||
fragment: fragment
|
||||
}}
|
||||
end
|
||||
|
||||
defimpl Bandit.HTTP2.Frame.Serializable do
|
||||
@end_headers_bit 2
|
||||
|
||||
def serialize(%Bandit.HTTP2.Frame.Continuation{} = frame, max_frame_size) do
|
||||
fragment_length = IO.iodata_length(frame.fragment)
|
||||
|
||||
if fragment_length <= max_frame_size do
|
||||
[{0x9, set([@end_headers_bit]), frame.stream_id, frame.fragment}]
|
||||
else
|
||||
<<this_frame::binary-size(max_frame_size), rest::binary>> =
|
||||
IO.iodata_to_binary(frame.fragment)
|
||||
|
||||
[
|
||||
{0x9, 0x00, frame.stream_id, this_frame}
|
||||
| Bandit.HTTP2.Frame.Serializable.serialize(
|
||||
%Bandit.HTTP2.Frame.Continuation{stream_id: frame.stream_id, fragment: rest},
|
||||
max_frame_size
|
||||
)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
78
phoenix/deps/bandit/lib/bandit/http2/frame/data.ex
Normal file
78
phoenix/deps/bandit/lib/bandit/http2/frame/data.ex
Normal file
@@ -0,0 +1,78 @@
|
||||
defmodule Bandit.HTTP2.Frame.Data do
|
||||
@moduledoc false
|
||||
|
||||
import Bandit.HTTP2.Frame.Flags
|
||||
|
||||
defstruct stream_id: nil,
|
||||
end_stream: false,
|
||||
data: nil
|
||||
|
||||
@typedoc "An HTTP/2 DATA frame"
|
||||
@type t :: %__MODULE__{
|
||||
stream_id: Bandit.HTTP2.Stream.stream_id(),
|
||||
end_stream: boolean(),
|
||||
data: iodata()
|
||||
}
|
||||
|
||||
@end_stream_bit 0
|
||||
@padding_bit 3
|
||||
|
||||
@spec deserialize(Bandit.HTTP2.Frame.flags(), Bandit.HTTP2.Stream.stream_id(), iodata()) ::
|
||||
{:ok, t()} | {:error, Bandit.HTTP2.Errors.error_code(), binary()}
|
||||
def deserialize(_flags, 0, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(), "DATA frame with zero stream_id (RFC9113§6.1)"}
|
||||
end
|
||||
|
||||
def deserialize(flags, stream_id, <<padding_length::8, rest::binary>>)
|
||||
when set?(flags, @padding_bit) and byte_size(rest) >= padding_length do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
stream_id: stream_id,
|
||||
end_stream: set?(flags, @end_stream_bit),
|
||||
data: binary_part(rest, 0, byte_size(rest) - padding_length)
|
||||
}}
|
||||
end
|
||||
|
||||
def deserialize(flags, stream_id, <<data::binary>>) when clear?(flags, @padding_bit) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
stream_id: stream_id,
|
||||
end_stream: set?(flags, @end_stream_bit),
|
||||
data: data
|
||||
}}
|
||||
end
|
||||
|
||||
def deserialize(flags, _stream_id, <<_padding_length::8, _rest::binary>>)
|
||||
when set?(flags, @padding_bit) do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(),
|
||||
"DATA frame with invalid padding length (RFC9113§6.1)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.HTTP2.Frame.Serializable do
|
||||
@end_stream_bit 0
|
||||
|
||||
def serialize(%Bandit.HTTP2.Frame.Data{} = frame, max_frame_size) do
|
||||
data_length = IO.iodata_length(frame.data)
|
||||
|
||||
if data_length <= max_frame_size do
|
||||
flags = if frame.end_stream, do: [@end_stream_bit], else: []
|
||||
[{0x0, set(flags), frame.stream_id, frame.data}]
|
||||
else
|
||||
<<this_frame::binary-size(max_frame_size), rest::binary>> =
|
||||
IO.iodata_to_binary(frame.data)
|
||||
|
||||
[
|
||||
{0x0, 0x00, frame.stream_id, this_frame}
|
||||
| Bandit.HTTP2.Frame.Serializable.serialize(
|
||||
%Bandit.HTTP2.Frame.Data{
|
||||
stream_id: frame.stream_id,
|
||||
end_stream: frame.end_stream,
|
||||
data: rest
|
||||
},
|
||||
max_frame_size
|
||||
)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
42
phoenix/deps/bandit/lib/bandit/http2/frame/goaway.ex
Normal file
42
phoenix/deps/bandit/lib/bandit/http2/frame/goaway.ex
Normal file
@@ -0,0 +1,42 @@
|
||||
defmodule Bandit.HTTP2.Frame.Goaway do
|
||||
@moduledoc false
|
||||
|
||||
defstruct last_stream_id: 0, error_code: 0, debug_data: <<>>
|
||||
|
||||
@typedoc "An HTTP/2 GOAWAY frame"
|
||||
@type t :: %__MODULE__{
|
||||
last_stream_id: Bandit.HTTP2.Stream.stream_id(),
|
||||
error_code: Bandit.HTTP2.Errors.error_code(),
|
||||
debug_data: iodata()
|
||||
}
|
||||
|
||||
@spec deserialize(Bandit.HTTP2.Frame.flags(), Bandit.HTTP2.Stream.stream_id(), iodata()) ::
|
||||
{:ok, t()} | {:error, Bandit.HTTP2.Errors.error_code(), binary()}
|
||||
def deserialize(
|
||||
_flags,
|
||||
0,
|
||||
<<_reserved::1, last_stream_id::31, error_code::32, debug_data::binary>>
|
||||
) do
|
||||
{:ok,
|
||||
%__MODULE__{last_stream_id: last_stream_id, error_code: error_code, debug_data: debug_data}}
|
||||
end
|
||||
|
||||
def deserialize(_flags, stream_id, _payload) when stream_id != 0 do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(),
|
||||
"Invalid stream ID in GOAWAY frame (RFC9113§6.8)"}
|
||||
end
|
||||
|
||||
def deserialize(_flags, _stream_id, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.frame_size_error(),
|
||||
"GOAWAY frame with invalid payload size (RFC9113§6.8)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.HTTP2.Frame.Serializable do
|
||||
def serialize(%Bandit.HTTP2.Frame.Goaway{} = frame, _max_frame_size) do
|
||||
[
|
||||
{0x7, 0x0, 0,
|
||||
<<0x0::1, frame.last_stream_id::31, frame.error_code::32, frame.debug_data::binary>>}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
140
phoenix/deps/bandit/lib/bandit/http2/frame/headers.ex
Normal file
140
phoenix/deps/bandit/lib/bandit/http2/frame/headers.ex
Normal file
@@ -0,0 +1,140 @@
|
||||
defmodule Bandit.HTTP2.Frame.Headers do
|
||||
@moduledoc false
|
||||
|
||||
import Bandit.HTTP2.Frame.Flags
|
||||
|
||||
defstruct stream_id: nil,
|
||||
end_stream: false,
|
||||
end_headers: false,
|
||||
exclusive_dependency: false,
|
||||
stream_dependency: nil,
|
||||
weight: nil,
|
||||
fragment: nil
|
||||
|
||||
@typedoc "An HTTP/2 HEADERS frame"
|
||||
@type t :: %__MODULE__{
|
||||
stream_id: Bandit.HTTP2.Stream.stream_id(),
|
||||
end_stream: boolean(),
|
||||
end_headers: boolean(),
|
||||
exclusive_dependency: boolean(),
|
||||
stream_dependency: Bandit.HTTP2.Stream.stream_id() | nil,
|
||||
weight: non_neg_integer() | nil,
|
||||
fragment: iodata()
|
||||
}
|
||||
|
||||
@end_stream_bit 0
|
||||
@end_headers_bit 2
|
||||
@padding_bit 3
|
||||
@priority_bit 5
|
||||
|
||||
@spec deserialize(Bandit.HTTP2.Frame.flags(), Bandit.HTTP2.Stream.stream_id(), iodata()) ::
|
||||
{:ok, t()} | {:error, Bandit.HTTP2.Errors.error_code(), binary()}
|
||||
def deserialize(_flags, 0, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(),
|
||||
"HEADERS frame with zero stream_id (RFC9113§6.2)"}
|
||||
end
|
||||
|
||||
# Padding and priority
|
||||
def deserialize(
|
||||
flags,
|
||||
stream_id,
|
||||
<<padding_length::8, exclusive_dependency::1, stream_dependency::31, weight::8,
|
||||
rest::binary>>
|
||||
)
|
||||
when set?(flags, @padding_bit) and set?(flags, @priority_bit) and
|
||||
byte_size(rest) >= padding_length do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
stream_id: stream_id,
|
||||
end_stream: set?(flags, @end_stream_bit),
|
||||
end_headers: set?(flags, @end_headers_bit),
|
||||
exclusive_dependency: exclusive_dependency == 0x01,
|
||||
stream_dependency: stream_dependency,
|
||||
weight: weight,
|
||||
fragment: binary_part(rest, 0, byte_size(rest) - padding_length)
|
||||
}}
|
||||
end
|
||||
|
||||
# Padding but not priority
|
||||
def deserialize(flags, stream_id, <<padding_length::8, rest::binary>>)
|
||||
when set?(flags, @padding_bit) and clear?(flags, @priority_bit) and
|
||||
byte_size(rest) >= padding_length do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
stream_id: stream_id,
|
||||
end_stream: set?(flags, @end_stream_bit),
|
||||
end_headers: set?(flags, @end_headers_bit),
|
||||
fragment: binary_part(rest, 0, byte_size(rest) - padding_length)
|
||||
}}
|
||||
end
|
||||
|
||||
# Any other case where padding is set
|
||||
def deserialize(flags, _stream_id, <<_padding_length::8, _rest::binary>>)
|
||||
when set?(flags, @padding_bit) do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(),
|
||||
"HEADERS frame with invalid padding length (RFC9113§6.2)"}
|
||||
end
|
||||
|
||||
def deserialize(
|
||||
flags,
|
||||
stream_id,
|
||||
<<exclusive_dependency::1, stream_dependency::31, weight::8, fragment::binary>>
|
||||
)
|
||||
when set?(flags, @priority_bit) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
stream_id: stream_id,
|
||||
end_stream: set?(flags, @end_stream_bit),
|
||||
end_headers: set?(flags, @end_headers_bit),
|
||||
exclusive_dependency: exclusive_dependency == 0x01,
|
||||
stream_dependency: stream_dependency,
|
||||
weight: weight,
|
||||
fragment: fragment
|
||||
}}
|
||||
end
|
||||
|
||||
def deserialize(flags, stream_id, <<fragment::binary>>)
|
||||
when clear?(flags, @priority_bit) and clear?(flags, @padding_bit) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
stream_id: stream_id,
|
||||
end_stream: set?(flags, @end_stream_bit),
|
||||
end_headers: set?(flags, @end_headers_bit),
|
||||
fragment: fragment
|
||||
}}
|
||||
end
|
||||
|
||||
defimpl Bandit.HTTP2.Frame.Serializable do
|
||||
@end_stream_bit 0
|
||||
@end_headers_bit 2
|
||||
|
||||
def serialize(
|
||||
%Bandit.HTTP2.Frame.Headers{
|
||||
exclusive_dependency: false,
|
||||
stream_dependency: nil,
|
||||
weight: nil
|
||||
} =
|
||||
frame,
|
||||
max_frame_size
|
||||
) do
|
||||
flags = if frame.end_stream, do: [@end_stream_bit], else: []
|
||||
|
||||
fragment_length = IO.iodata_length(frame.fragment)
|
||||
|
||||
if fragment_length <= max_frame_size do
|
||||
[{0x1, set([@end_headers_bit | flags]), frame.stream_id, frame.fragment}]
|
||||
else
|
||||
<<this_frame::binary-size(max_frame_size), rest::binary>> =
|
||||
IO.iodata_to_binary(frame.fragment)
|
||||
|
||||
[
|
||||
{0x1, set(flags), frame.stream_id, this_frame}
|
||||
| Bandit.HTTP2.Frame.Serializable.serialize(
|
||||
%Bandit.HTTP2.Frame.Continuation{stream_id: frame.stream_id, fragment: rest},
|
||||
max_frame_size
|
||||
)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
45
phoenix/deps/bandit/lib/bandit/http2/frame/ping.ex
Normal file
45
phoenix/deps/bandit/lib/bandit/http2/frame/ping.ex
Normal file
@@ -0,0 +1,45 @@
|
||||
defmodule Bandit.HTTP2.Frame.Ping do
|
||||
@moduledoc false
|
||||
|
||||
import Bandit.HTTP2.Frame.Flags
|
||||
|
||||
defstruct ack: false, payload: nil
|
||||
|
||||
@typedoc "An HTTP/2 PING frame"
|
||||
@type t :: %__MODULE__{
|
||||
ack: boolean(),
|
||||
payload: iodata()
|
||||
}
|
||||
|
||||
@ack_bit 0
|
||||
|
||||
@spec deserialize(Bandit.HTTP2.Frame.flags(), Bandit.HTTP2.Stream.stream_id(), iodata()) ::
|
||||
{:ok, t()} | {:error, Bandit.HTTP2.Errors.error_code(), binary()}
|
||||
def deserialize(flags, 0, <<payload::binary-size(8)>>) when set?(flags, @ack_bit) do
|
||||
{:ok, %__MODULE__{ack: true, payload: payload}}
|
||||
end
|
||||
|
||||
def deserialize(flags, 0, <<payload::binary-size(8)>>) when clear?(flags, @ack_bit) do
|
||||
{:ok, %__MODULE__{ack: false, payload: payload}}
|
||||
end
|
||||
|
||||
def deserialize(_flags, stream_id, _payload) when stream_id != 0 do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(),
|
||||
"Invalid stream ID in PING frame (RFC9113§6.7)"}
|
||||
end
|
||||
|
||||
def deserialize(_flags, _stream_id, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.frame_size_error(),
|
||||
"PING frame with invalid payload size (RFC9113§6.7)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.HTTP2.Frame.Serializable do
|
||||
@ack_bit 0
|
||||
|
||||
def serialize(%Bandit.HTTP2.Frame.Ping{ack: true} = frame, _max_frame_size),
|
||||
do: [{0x6, set([@ack_bit]), 0, frame.payload}]
|
||||
|
||||
def serialize(%Bandit.HTTP2.Frame.Ping{ack: false} = frame, _max_frame_size),
|
||||
do: [{0x6, 0x0, 0, frame.payload}]
|
||||
end
|
||||
end
|
||||
35
phoenix/deps/bandit/lib/bandit/http2/frame/priority.ex
Normal file
35
phoenix/deps/bandit/lib/bandit/http2/frame/priority.ex
Normal file
@@ -0,0 +1,35 @@
|
||||
defmodule Bandit.HTTP2.Frame.Priority do
|
||||
@moduledoc false
|
||||
|
||||
defstruct stream_id: nil, dependent_stream_id: nil, weight: nil
|
||||
|
||||
@typedoc "An HTTP/2 PRIORITY frame"
|
||||
@type t :: %__MODULE__{
|
||||
stream_id: Bandit.HTTP2.Stream.stream_id(),
|
||||
dependent_stream_id: Bandit.HTTP2.Stream.stream_id(),
|
||||
weight: non_neg_integer()
|
||||
}
|
||||
|
||||
@spec deserialize(Bandit.HTTP2.Frame.flags(), Bandit.HTTP2.Stream.stream_id(), iodata()) ::
|
||||
{:ok, t()} | {:error, Bandit.HTTP2.Errors.error_code(), binary()}
|
||||
def deserialize(_flags, 0, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(),
|
||||
"PRIORITY frame with zero stream_id (RFC9113§6.3)"}
|
||||
end
|
||||
|
||||
def deserialize(_flags, stream_id, <<_reserved::1, dependent_stream_id::31, weight::8>>) do
|
||||
{:ok,
|
||||
%__MODULE__{stream_id: stream_id, dependent_stream_id: dependent_stream_id, weight: weight}}
|
||||
end
|
||||
|
||||
def deserialize(_flags, _stream_id, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.frame_size_error(),
|
||||
"Invalid payload size in PRIORITY frame (RFC9113§6.3)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.HTTP2.Frame.Serializable do
|
||||
def serialize(%Bandit.HTTP2.Frame.Priority{} = frame, _max_frame_size) do
|
||||
[{0x2, 0x0, frame.stream_id, <<0::1, frame.dependent_stream_id::31, frame.weight::8>>}]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule Bandit.HTTP2.Frame.PushPromise do
|
||||
@moduledoc false
|
||||
|
||||
@spec deserialize(Bandit.HTTP2.Frame.flags(), Bandit.HTTP2.Stream.stream_id(), iodata()) ::
|
||||
{:error, Bandit.HTTP2.Errors.error_code(), binary()}
|
||||
def deserialize(_flags, _stream, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(), "PUSH_PROMISE frame received (RFC9113§8.4)"}
|
||||
end
|
||||
end
|
||||
33
phoenix/deps/bandit/lib/bandit/http2/frame/rst_stream.ex
Normal file
33
phoenix/deps/bandit/lib/bandit/http2/frame/rst_stream.ex
Normal file
@@ -0,0 +1,33 @@
|
||||
defmodule Bandit.HTTP2.Frame.RstStream do
|
||||
@moduledoc false
|
||||
|
||||
defstruct stream_id: nil, error_code: nil
|
||||
|
||||
@typedoc "An HTTP/2 RST_STREAM frame"
|
||||
@type t :: %__MODULE__{
|
||||
stream_id: Bandit.HTTP2.Stream.stream_id(),
|
||||
error_code: Bandit.HTTP2.Errors.error_code()
|
||||
}
|
||||
|
||||
@spec deserialize(Bandit.HTTP2.Frame.flags(), Bandit.HTTP2.Stream.stream_id(), iodata()) ::
|
||||
{:ok, t()} | {:error, Bandit.HTTP2.Errors.error_code(), binary()}
|
||||
def deserialize(_flags, 0, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(),
|
||||
"RST_STREAM frame with zero stream_id (RFC9113§6.4)"}
|
||||
end
|
||||
|
||||
def deserialize(_flags, stream_id, <<error_code::32>>) do
|
||||
{:ok, %__MODULE__{stream_id: stream_id, error_code: error_code}}
|
||||
end
|
||||
|
||||
def deserialize(_flags, _stream_id, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.frame_size_error(),
|
||||
"Invalid payload size in RST_STREAM frame (RFC9113§6.4)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.HTTP2.Frame.Serializable do
|
||||
def serialize(%Bandit.HTTP2.Frame.RstStream{} = frame, _max_frame_size) do
|
||||
[{0x3, 0x0, frame.stream_id, <<frame.error_code::32>>}]
|
||||
end
|
||||
end
|
||||
end
|
||||
117
phoenix/deps/bandit/lib/bandit/http2/frame/settings.ex
Normal file
117
phoenix/deps/bandit/lib/bandit/http2/frame/settings.ex
Normal file
@@ -0,0 +1,117 @@
|
||||
defmodule Bandit.HTTP2.Frame.Settings do
|
||||
@moduledoc false
|
||||
|
||||
import Bandit.HTTP2.Frame.Flags
|
||||
import Bitwise
|
||||
|
||||
@max_window_size (1 <<< 31) - 1
|
||||
@min_frame_size 1 <<< 14
|
||||
@max_frame_size (1 <<< 24) - 1
|
||||
|
||||
defstruct ack: false, settings: nil
|
||||
|
||||
@typedoc "An HTTP/2 SETTINGS frame"
|
||||
@type t :: %__MODULE__{ack: true, settings: nil} | %__MODULE__{ack: false, settings: map()}
|
||||
|
||||
@ack_bit 0
|
||||
|
||||
@spec deserialize(Bandit.HTTP2.Frame.flags(), Bandit.HTTP2.Stream.stream_id(), iodata()) ::
|
||||
{:ok, t()} | {:error, Bandit.HTTP2.Errors.error_code(), binary()}
|
||||
def deserialize(flags, 0, payload) when clear?(flags, @ack_bit) do
|
||||
payload
|
||||
|> Stream.unfold(fn
|
||||
<<>> -> nil
|
||||
<<setting::16, value::32, rest::binary>> -> {{:ok, {setting, value}}, rest}
|
||||
<<rest::binary>> -> {{:error, rest}, <<>>}
|
||||
end)
|
||||
|> Enum.reduce_while({:ok, %{}}, fn
|
||||
{:ok, {0x01, value}}, {:ok, acc} ->
|
||||
{:cont, {:ok, Map.put(acc, :header_table_size, value)}}
|
||||
|
||||
{:ok, {0x02, val}}, {:ok, acc} when val in [0x00, 0x01] ->
|
||||
{:cont, {:ok, acc}}
|
||||
|
||||
{:ok, {0x02, _value}}, {:ok, _acc} ->
|
||||
{:halt,
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(), "Invalid enable_push value (RFC9113§6.5)"}}
|
||||
|
||||
{:ok, {0x03, value}}, {:ok, acc} ->
|
||||
{:cont, {:ok, Map.put(acc, :max_concurrent_streams, value)}}
|
||||
|
||||
{:ok, {0x04, value}}, {:ok, _acc} when value > @max_window_size ->
|
||||
{:halt,
|
||||
{:error, Bandit.HTTP2.Errors.flow_control_error(), "Invalid window_size (RFC9113§6.5)"}}
|
||||
|
||||
{:ok, {0x04, value}}, {:ok, acc} ->
|
||||
{:cont, {:ok, Map.put(acc, :initial_window_size, value)}}
|
||||
|
||||
{:ok, {0x05, value}}, {:ok, _acc} when value < @min_frame_size ->
|
||||
{:halt,
|
||||
{:error, Bandit.HTTP2.Errors.frame_size_error(), "Invalid max_frame_size (RFC9113§6.5)"}}
|
||||
|
||||
{:ok, {0x05, value}}, {:ok, _acc} when value > @max_frame_size ->
|
||||
{:halt,
|
||||
{:error, Bandit.HTTP2.Errors.frame_size_error(), "Invalid max_frame_size (RFC9113§6.5)"}}
|
||||
|
||||
{:ok, {0x05, value}}, {:ok, acc} ->
|
||||
{:cont, {:ok, Map.put(acc, :max_frame_size, value)}}
|
||||
|
||||
{:ok, {0x06, value}}, {:ok, acc} ->
|
||||
{:cont, {:ok, Map.put(acc, :max_header_list_size, value)}}
|
||||
|
||||
{:ok, {_setting, _value}}, {:ok, acc} ->
|
||||
{:cont, {:ok, acc}}
|
||||
|
||||
{:error, _rest}, _acc ->
|
||||
{:halt,
|
||||
{:error, Bandit.HTTP2.Errors.frame_size_error(), "Invalid SETTINGS size (RFC9113§6.5)"}}
|
||||
end)
|
||||
|> case do
|
||||
{:ok, settings} -> {:ok, %__MODULE__{ack: false, settings: settings}}
|
||||
{:error, error_code, reason} -> {:error, error_code, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def deserialize(flags, 0, <<>>) when set?(flags, @ack_bit) do
|
||||
{:ok, %__MODULE__{ack: true}}
|
||||
end
|
||||
|
||||
def deserialize(flags, 0, _payload) when set?(flags, @ack_bit) do
|
||||
{:error, Bandit.HTTP2.Errors.frame_size_error(),
|
||||
"SETTINGS ack frame with non-empty payload (RFC9113§6.5)"}
|
||||
end
|
||||
|
||||
def deserialize(_flags, _stream_id, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.protocol_error(), "Invalid SETTINGS frame (RFC9113§6.5)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.HTTP2.Frame.Serializable do
|
||||
@ack_bit 0
|
||||
|
||||
def serialize(%Bandit.HTTP2.Frame.Settings{ack: true}, _max_frame_size),
|
||||
do: [{0x4, set([@ack_bit]), 0, <<>>}]
|
||||
|
||||
def serialize(%Bandit.HTTP2.Frame.Settings{ack: false} = frame, _max_frame_size) do
|
||||
# Encode default settings values as empty binaries so that we do not send
|
||||
# them. This means we can't restore settings back to default values if we
|
||||
# change them, but since we don't ever change our settings this is fine
|
||||
payload =
|
||||
frame.settings
|
||||
|> Enum.uniq_by(fn {setting, _} -> setting end)
|
||||
|> Enum.map(fn
|
||||
{:header_table_size, 4_096} -> <<>>
|
||||
{:header_table_size, value} -> <<0x01::16, value::32>>
|
||||
{:max_concurrent_streams, :infinity} -> <<>>
|
||||
{:max_concurrent_streams, value} -> <<0x03::16, value::32>>
|
||||
{:initial_window_size, 65_535} -> <<>>
|
||||
{:initial_window_size, value} -> <<0x04::16, value::32>>
|
||||
{:max_frame_size, 16_384} -> <<>>
|
||||
{:max_frame_size, value} -> <<0x05::16, value::32>>
|
||||
{:max_header_list_size, :infinity} -> <<>>
|
||||
{:max_header_list_size, value} -> <<0x06::16, value::32>>
|
||||
end)
|
||||
|
||||
[{0x4, 0x0, 0, payload}]
|
||||
end
|
||||
end
|
||||
end
|
||||
27
phoenix/deps/bandit/lib/bandit/http2/frame/unknown.ex
Normal file
27
phoenix/deps/bandit/lib/bandit/http2/frame/unknown.ex
Normal file
@@ -0,0 +1,27 @@
|
||||
defmodule Bandit.HTTP2.Frame.Unknown do
|
||||
@moduledoc false
|
||||
|
||||
defstruct type: nil,
|
||||
flags: nil,
|
||||
stream_id: nil,
|
||||
payload: nil
|
||||
|
||||
@typedoc "An HTTP/2 frame of unknown type"
|
||||
@type t :: %__MODULE__{
|
||||
type: Bandit.HTTP2.Frame.frame_type(),
|
||||
flags: Bandit.HTTP2.Frame.flags(),
|
||||
stream_id: Bandit.HTTP2.Stream.stream_id(),
|
||||
payload: iodata()
|
||||
}
|
||||
|
||||
# Note this is arity 4
|
||||
@spec deserialize(
|
||||
Bandit.HTTP2.Frame.frame_type(),
|
||||
Bandit.HTTP2.Frame.flags(),
|
||||
Bandit.HTTP2.Stream.stream_id(),
|
||||
iodata()
|
||||
) :: {:ok, t()}
|
||||
def deserialize(type, flags, stream_id, payload) do
|
||||
{:ok, %__MODULE__{type: type, flags: flags, stream_id: stream_id, payload: payload}}
|
||||
end
|
||||
end
|
||||
33
phoenix/deps/bandit/lib/bandit/http2/frame/window_update.ex
Normal file
33
phoenix/deps/bandit/lib/bandit/http2/frame/window_update.ex
Normal file
@@ -0,0 +1,33 @@
|
||||
defmodule Bandit.HTTP2.Frame.WindowUpdate do
|
||||
@moduledoc false
|
||||
|
||||
defstruct stream_id: nil,
|
||||
size_increment: nil
|
||||
|
||||
@typedoc "An HTTP/2 WINDOW_UPDATE frame"
|
||||
@type t :: %__MODULE__{
|
||||
stream_id: Bandit.HTTP2.Stream.stream_id(),
|
||||
size_increment: non_neg_integer()
|
||||
}
|
||||
|
||||
@spec deserialize(Bandit.HTTP2.Frame.flags(), Bandit.HTTP2.Stream.stream_id(), iodata()) ::
|
||||
{:ok, t()} | {:error, Bandit.HTTP2.Errors.error_code(), binary()}
|
||||
def deserialize(_flags, _stream_id, <<_reserved::1, 0::31>>) do
|
||||
{:error, Bandit.HTTP2.Errors.flow_control_error(),
|
||||
"Invalid WINDOW_UPDATE size increment (RFC9113§6.9)"}
|
||||
end
|
||||
|
||||
def deserialize(_flags, stream_id, <<_reserved::1, size_increment::31>>) do
|
||||
{:ok, %__MODULE__{stream_id: stream_id, size_increment: size_increment}}
|
||||
end
|
||||
|
||||
def deserialize(_flags, _stream_id, _payload) do
|
||||
{:error, Bandit.HTTP2.Errors.frame_size_error(), "Invalid WINDOW_UPDATE frame (RFC9113§6.9)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.HTTP2.Frame.Serializable do
|
||||
def serialize(%Bandit.HTTP2.Frame.WindowUpdate{} = frame, _max_frame_size) do
|
||||
[{0x8, 0, frame.stream_id, <<0::1, frame.size_increment::31>>}]
|
||||
end
|
||||
end
|
||||
end
|
||||
186
phoenix/deps/bandit/lib/bandit/http2/handler.ex
Normal file
186
phoenix/deps/bandit/lib/bandit/http2/handler.ex
Normal file
@@ -0,0 +1,186 @@
|
||||
defmodule Bandit.HTTP2.Handler do
|
||||
@moduledoc false
|
||||
# An HTTP/2 handler, this module comprises the primary interface between Thousand Island and an
|
||||
# HTTP connection. It is responsible for:
|
||||
#
|
||||
# * All socket-level sending and receiving from the client
|
||||
# * Coordinating the parsing of frames & attendant error handling
|
||||
# * Tracking connection state as represented by a `Bandit.HTTP2.Connection` struct
|
||||
|
||||
use ThousandIsland.Handler
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_connection(socket, state) do
|
||||
connection = Bandit.HTTP2.Connection.init(socket, state.plug, state.opts)
|
||||
{:continue, Map.merge(state, %{buffer: <<>>, connection: connection})}
|
||||
rescue
|
||||
error -> rescue_connection_error(error, __STACKTRACE__, socket, state)
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_data(data, socket, state) do
|
||||
(state.buffer <> data)
|
||||
|> Stream.unfold(
|
||||
&Bandit.HTTP2.Frame.deserialize(&1, state.connection.local_settings.max_frame_size)
|
||||
)
|
||||
|> Enum.reduce_while(state, fn
|
||||
{:ok, frame}, state ->
|
||||
connection = Bandit.HTTP2.Connection.handle_frame(frame, socket, state.connection)
|
||||
{:cont, %{state | connection: connection, buffer: <<>>}}
|
||||
|
||||
{:more, rest}, state ->
|
||||
{:halt, %{state | buffer: rest}}
|
||||
|
||||
{:error, error_code, message}, _state ->
|
||||
# We encountered an error while deserializing the frame. Let the connection figure out
|
||||
# how to respond to it
|
||||
raise Bandit.HTTP2.Errors.ConnectionError, message: message, error_code: error_code
|
||||
end)
|
||||
|> then(&{:continue, &1})
|
||||
rescue
|
||||
error in Bandit.HTTP2.Errors.StreamError -> rescue_stream_error(error, socket, state)
|
||||
error -> rescue_connection_error(error, __STACKTRACE__, socket, state)
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_shutdown(socket, state) do
|
||||
Bandit.HTTP2.Connection.close_connection(
|
||||
Bandit.HTTP2.Errors.no_error(),
|
||||
"Server shutdown",
|
||||
socket,
|
||||
state.connection
|
||||
)
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_timeout(socket, state) do
|
||||
Bandit.HTTP2.Connection.close_connection(
|
||||
Bandit.HTTP2.Errors.no_error(),
|
||||
"Client timeout",
|
||||
socket,
|
||||
state.connection
|
||||
)
|
||||
end
|
||||
|
||||
def handle_call({:peer_data, _stream_id}, _from, {socket, state}) do
|
||||
{:reply, Bandit.SocketHelpers.peer_data(socket), {socket, state}, socket.read_timeout}
|
||||
end
|
||||
|
||||
def handle_call({:sock_data, _stream_id}, _from, {socket, state}) do
|
||||
{:reply, Bandit.SocketHelpers.sock_data(socket), {socket, state}, socket.read_timeout}
|
||||
end
|
||||
|
||||
def handle_call({:ssl_data, _stream_id}, _from, {socket, state}) do
|
||||
{:reply, Bandit.SocketHelpers.ssl_data(socket), {socket, state}, socket.read_timeout}
|
||||
end
|
||||
|
||||
def handle_call({{:send_data, data, end_stream}, stream_id}, from, {socket, state}) do
|
||||
# In 'normal' cases where there is sufficient space in the send windows for this message to be
|
||||
# sent, Connection will call `unblock` synchronously in the `Connection.send_data` call below.
|
||||
# In cases where there is not enough space in the connection window, Connection will call
|
||||
# `unblock` at some point in the future once space opens up in the window. This
|
||||
# keeps this code simple in that we can blindly send noreply here and let Connection handle
|
||||
# the separate cases. This ensures that we have backpressure all the way back to the
|
||||
# stream's handler process in the event of window overruns.
|
||||
#
|
||||
# Note that the above only applies to the connection-level send window; stream-level windows
|
||||
# are managed internally by the stream and are not considered here at all. If the stream has
|
||||
# managed to send this message, it is because there was enough room in the stream's send
|
||||
# window to do so.
|
||||
unblock = fn -> GenServer.reply(from, :ok) end
|
||||
|
||||
connection =
|
||||
Bandit.HTTP2.Connection.send_data(
|
||||
stream_id,
|
||||
data,
|
||||
end_stream,
|
||||
unblock,
|
||||
socket,
|
||||
state.connection
|
||||
)
|
||||
|
||||
{:noreply, {socket, %{state | connection: connection}}, socket.read_timeout}
|
||||
rescue
|
||||
error -> rescue_error_handle_info(error, __STACKTRACE__, socket, state)
|
||||
end
|
||||
|
||||
def handle_info({{:send_headers, headers, end_stream}, stream_id}, {socket, state}) do
|
||||
connection =
|
||||
Bandit.HTTP2.Connection.send_headers(
|
||||
stream_id,
|
||||
headers,
|
||||
end_stream,
|
||||
socket,
|
||||
state.connection
|
||||
)
|
||||
|
||||
{:noreply, {socket, %{state | connection: connection}}, socket.read_timeout}
|
||||
rescue
|
||||
error -> rescue_error_handle_info(error, __STACKTRACE__, socket, state)
|
||||
end
|
||||
|
||||
def handle_info({{:send_recv_window_update, size_increment}, stream_id}, {socket, state}) do
|
||||
Bandit.HTTP2.Connection.send_recv_window_update(
|
||||
stream_id,
|
||||
size_increment,
|
||||
socket,
|
||||
state.connection
|
||||
)
|
||||
|
||||
{:noreply, {socket, state}, socket.read_timeout}
|
||||
rescue
|
||||
error -> rescue_error_handle_info(error, __STACKTRACE__, socket, state)
|
||||
end
|
||||
|
||||
def handle_info({{:send_rst_stream, error_code}, stream_id}, {socket, state}) do
|
||||
Bandit.HTTP2.Connection.send_rst_stream(stream_id, error_code, socket, state.connection)
|
||||
{:noreply, {socket, state}, socket.read_timeout}
|
||||
rescue
|
||||
error -> rescue_error_handle_info(error, __STACKTRACE__, socket, state)
|
||||
end
|
||||
|
||||
def handle_info({{:close_connection, error_code, msg}, _stream_id}, {socket, state}) do
|
||||
_ = Bandit.HTTP2.Connection.close_connection(error_code, msg, socket, state.connection)
|
||||
{:stop, :normal, {socket, state}}
|
||||
end
|
||||
|
||||
def handle_info({:EXIT, pid, _reason}, {socket, state}) do
|
||||
connection = Bandit.HTTP2.Connection.stream_terminated(pid, state.connection)
|
||||
{:noreply, {socket, %{state | connection: connection}}, socket.read_timeout}
|
||||
end
|
||||
|
||||
defp rescue_stream_error(error, socket, state) do
|
||||
Bandit.HTTP2.Connection.send_rst_stream(
|
||||
error.stream_id,
|
||||
error.error_code,
|
||||
socket,
|
||||
state.connection
|
||||
)
|
||||
|
||||
{:continue, state}
|
||||
end
|
||||
|
||||
defp rescue_connection_error(error, stacktrace, socket, state) do
|
||||
do_rescue_error(error, stacktrace, socket, state)
|
||||
{:close, state}
|
||||
end
|
||||
|
||||
defp rescue_error_handle_info(error, stacktrace, socket, state) do
|
||||
do_rescue_error(error, stacktrace, socket, state)
|
||||
{:stop, :normal}
|
||||
end
|
||||
|
||||
defp do_rescue_error(error, stacktrace, socket, state) do
|
||||
_ =
|
||||
if state[:connection] do
|
||||
Bandit.HTTP2.Connection.close_connection(
|
||||
error.error_code,
|
||||
error.message,
|
||||
socket,
|
||||
state[:connection]
|
||||
)
|
||||
end
|
||||
|
||||
Bandit.Logger.maybe_log_protocol_error(error, stacktrace, state.opts, plug: state.plug)
|
||||
end
|
||||
end
|
||||
20
phoenix/deps/bandit/lib/bandit/http2/settings.ex
Normal file
20
phoenix/deps/bandit/lib/bandit/http2/settings.ex
Normal file
@@ -0,0 +1,20 @@
|
||||
defmodule Bandit.HTTP2.Settings do
|
||||
@moduledoc """
|
||||
Settings as defined in RFC9113§6.5.2
|
||||
"""
|
||||
|
||||
defstruct header_table_size: 4_096,
|
||||
max_concurrent_streams: :infinity,
|
||||
initial_window_size: 65_535,
|
||||
max_frame_size: 16_384,
|
||||
max_header_list_size: :infinity
|
||||
|
||||
@typedoc "A collection of settings as defined in RFC9113§6.5"
|
||||
@type t :: %__MODULE__{
|
||||
header_table_size: non_neg_integer(),
|
||||
max_concurrent_streams: non_neg_integer() | :infinity,
|
||||
initial_window_size: non_neg_integer(),
|
||||
max_frame_size: non_neg_integer(),
|
||||
max_header_list_size: non_neg_integer() | :infinity
|
||||
}
|
||||
end
|
||||
634
phoenix/deps/bandit/lib/bandit/http2/stream.ex
Normal file
634
phoenix/deps/bandit/lib/bandit/http2/stream.ex
Normal file
@@ -0,0 +1,634 @@
|
||||
defmodule Bandit.HTTP2.Stream do
|
||||
@moduledoc false
|
||||
# This module implements an HTTP/2 stream as described in RFC 9113, without concern for the higher-level
|
||||
# HTTP semantics described in RFC 9110. It is similar in spirit to `Bandit.HTTP1.Socket` for
|
||||
# HTTP/1, and indeed both implement the `Bandit.HTTPTransport` behaviour. An instance of this
|
||||
# struct is maintained as the state of a `Bandit.HTTP2.StreamProcess` process, and it moves an
|
||||
# HTTP/2 stream through its lifecycle by calling functions defined on this module. This state is
|
||||
# also tracked within the `Bandit.Adapter` instance that backs Bandit's Plug API.
|
||||
#
|
||||
# A note about naming:
|
||||
#
|
||||
# This module has several intended callers, and due to its nature as a coordinator, needs to be
|
||||
# careful about how it uses terms like 'read', 'send', 'receive', etc. To that end, there are
|
||||
# some conventions in place:
|
||||
#
|
||||
# * Functions on this module which are intended to be called internally by the containing
|
||||
# `Bandit.HTTP2.Connection` to pass information received from the client (such as headers or
|
||||
# request data) to this stream. These functions are named `deliver_*`, and are intended to be
|
||||
# called by the connection process. As such, they take a `stream_handle()` argument, which
|
||||
# corresponds either to a pid (in the case of an active stream), or the value `:closed` (in the
|
||||
# case of a stream which has already completed processing)
|
||||
#
|
||||
# * Functions on this module which are intended to be called by the higher-level implementation
|
||||
# that is processing this stream are implemented via the `Bandit.HTTPTransport` protocol
|
||||
#
|
||||
# * In order for this stream to receive information from the containing connection process, we
|
||||
# use carefully crafted `receive` calls (we do this in a manner that is safe to do within a
|
||||
# GenServer). This work is handled internally by a number of functions named `do_recv_*`, which
|
||||
# generally present a blocking interface in order to align with the expectations of the
|
||||
# `Plug.Conn.Adapter` behaviour.
|
||||
#
|
||||
# This module also uses exceptions by convention rather than error tuples since many
|
||||
# of these functions are called within `Plug.Conn.Adapter` calls, which makes it
|
||||
# difficult to properly unwind many error conditions back to a place where we can properly shut
|
||||
# down the stream by sending a RstStream frame to the client and terminating our process. The
|
||||
# pattern here is to raise exceptions, and have the `Bandit.HTTP2.StreamProcess`'s `terminate/2`
|
||||
# callback take care of calling back into us via the `reset_stream/2` and `close_connection/2`
|
||||
# functions here, with the luxury of a nicely unwound stack and a process that is guaranteed to
|
||||
# be terminated as soon as these functions are called
|
||||
|
||||
require Logger
|
||||
|
||||
defstruct connection_pid: nil,
|
||||
stream_id: nil,
|
||||
state: :idle,
|
||||
recv_window_size: 65_535,
|
||||
send_window_size: nil,
|
||||
sendfile_chunk_size: nil,
|
||||
bytes_remaining: nil,
|
||||
read_timeout: 15_000
|
||||
|
||||
@typedoc "An HTTP/2 stream identifier"
|
||||
@type stream_id :: non_neg_integer()
|
||||
|
||||
@typedoc "A handle to a stream, suitable for passing to the `deliver_*` functions on this module"
|
||||
@type stream_handle :: pid() | :closed
|
||||
|
||||
@typedoc "An HTTP/2 stream state"
|
||||
@type state :: :idle | :open | :local_closed | :remote_closed | :closed
|
||||
|
||||
@typedoc "The information necessary to communicate to/from a stream"
|
||||
@type t :: %__MODULE__{
|
||||
connection_pid: pid(),
|
||||
stream_id: non_neg_integer(),
|
||||
state: state(),
|
||||
recv_window_size: non_neg_integer(),
|
||||
send_window_size: non_neg_integer(),
|
||||
sendfile_chunk_size: pos_integer(),
|
||||
bytes_remaining: non_neg_integer() | nil,
|
||||
read_timeout: timeout()
|
||||
}
|
||||
|
||||
def init(connection_pid, stream_id, initial_send_window_size, sendfile_chunk_size) do
|
||||
%__MODULE__{
|
||||
connection_pid: connection_pid,
|
||||
stream_id: stream_id,
|
||||
send_window_size: initial_send_window_size,
|
||||
sendfile_chunk_size: sendfile_chunk_size
|
||||
}
|
||||
end
|
||||
|
||||
# Collection API - Delivery
|
||||
#
|
||||
# These functions are intended to be called by the connection process which contains this
|
||||
# stream. All of these start with `deliver_`
|
||||
|
||||
@spec deliver_headers(stream_handle(), Plug.Conn.headers(), boolean()) :: term()
|
||||
def deliver_headers(:closed, _headers, _end_stream), do: :ok
|
||||
|
||||
def deliver_headers(pid, headers, end_stream),
|
||||
do: send(pid, {:bandit, {:headers, headers, end_stream}})
|
||||
|
||||
@spec deliver_data(stream_handle(), iodata(), boolean()) :: term()
|
||||
def deliver_data(:closed, _data, _end_stream), do: :ok
|
||||
def deliver_data(pid, data, end_stream), do: send(pid, {:bandit, {:data, data, end_stream}})
|
||||
|
||||
@spec deliver_send_window_update(stream_handle(), non_neg_integer()) :: term()
|
||||
def deliver_send_window_update(:closed, _delta), do: :ok
|
||||
|
||||
def deliver_send_window_update(pid, delta),
|
||||
do: send(pid, {:bandit, {:send_window_update, delta}})
|
||||
|
||||
@spec deliver_rst_stream(stream_handle(), Bandit.HTTP2.Errors.error_code()) :: term()
|
||||
def deliver_rst_stream(:closed, _error_code), do: :ok
|
||||
def deliver_rst_stream(pid, error_code), do: send(pid, {:bandit, {:rst_stream, error_code}})
|
||||
|
||||
defimpl Bandit.HTTPTransport do
|
||||
def peer_data(%@for{} = stream), do: call(stream, :peer_data, :infinity)
|
||||
|
||||
def sock_data(%@for{} = stream), do: call(stream, :sock_data, :infinity)
|
||||
|
||||
def ssl_data(%@for{} = stream), do: call(stream, :ssl_data, :infinity)
|
||||
|
||||
def version(%@for{}), do: :"HTTP/2"
|
||||
|
||||
def read_headers(%@for{state: :idle} = stream) do
|
||||
case do_recv(stream, stream.read_timeout) do
|
||||
{:headers, headers, stream} ->
|
||||
method = Bandit.Headers.get_header(headers, ":method")
|
||||
request_target = build_request_target!(headers, stream)
|
||||
{pseudo_headers, headers} = split_headers!(headers, stream)
|
||||
pseudo_headers_all_request!(pseudo_headers, stream)
|
||||
exactly_one_instance_of!(pseudo_headers, ":scheme", stream)
|
||||
exactly_one_instance_of!(pseudo_headers, ":method", stream)
|
||||
exactly_one_instance_of!(pseudo_headers, ":path", stream)
|
||||
headers_all_lowercase!(headers, stream)
|
||||
no_connection_headers!(headers, stream)
|
||||
valid_te_header!(headers, stream)
|
||||
content_length = get_content_length!(headers, stream)
|
||||
headers = combine_cookie_crumbs(headers)
|
||||
stream = %{stream | bytes_remaining: content_length}
|
||||
{:ok, method, request_target, headers, stream}
|
||||
|
||||
:timeout ->
|
||||
stream_error!("Timed out waiting for HEADER", stream)
|
||||
|
||||
%@for{} = stream ->
|
||||
read_headers(stream)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_request_target!(headers, stream) do
|
||||
scheme = Bandit.Headers.get_header(headers, ":scheme")
|
||||
{host, port} = get_host_and_port!(headers)
|
||||
path = get_path!(headers, stream)
|
||||
{scheme, host, port, path}
|
||||
end
|
||||
|
||||
defp get_host_and_port!(headers) do
|
||||
case Bandit.Headers.get_header(headers, ":authority") do
|
||||
authority when not is_nil(authority) -> Bandit.Headers.parse_hostlike_header!(authority)
|
||||
nil -> {nil, nil}
|
||||
end
|
||||
end
|
||||
|
||||
# RFC9113§8.3.1 - path should be non-empty and absolute
|
||||
defp get_path!(headers, stream) do
|
||||
headers
|
||||
|> Bandit.Headers.get_header(":path")
|
||||
|> case do
|
||||
nil -> stream_error!("Received empty :path", stream)
|
||||
"*" -> :*
|
||||
"/" <> _ = path -> split_path!(path, stream)
|
||||
_ -> stream_error!("Path does not start with /", stream)
|
||||
end
|
||||
end
|
||||
|
||||
# RFC9113§8.3.1 - path should match the path-absolute production from RFC3986
|
||||
defp split_path!(path, stream) do
|
||||
if path |> String.split("/") |> Enum.all?(&(&1 not in [".", ".."])),
|
||||
do: path,
|
||||
else: stream_error!("Path contains dot segment", stream)
|
||||
end
|
||||
|
||||
# RFC9113§8.3 - pseudo headers must appear first
|
||||
defp split_headers!(headers, stream) do
|
||||
{pseudo_headers, headers} =
|
||||
Enum.split_while(headers, fn {key, _value} -> String.starts_with?(key, ":") end)
|
||||
|
||||
if Enum.any?(headers, fn {key, _value} -> String.starts_with?(key, ":") end),
|
||||
do: stream_error!("Received pseudo headers after regular one", stream),
|
||||
else: {pseudo_headers, headers}
|
||||
end
|
||||
|
||||
# RFC9113§8.3.1 - only request pseudo headers may appear
|
||||
defp pseudo_headers_all_request!(headers, stream) do
|
||||
if Enum.any?(headers, fn {key, _value} ->
|
||||
key not in ~w[:method :scheme :authority :path]
|
||||
end),
|
||||
do: stream_error!("Received invalid pseudo header", stream)
|
||||
end
|
||||
|
||||
# RFC9113§8.3.1 - method, scheme, path pseudo headers must appear exactly once
|
||||
defp exactly_one_instance_of!(headers, header, stream) do
|
||||
if Enum.count(headers, fn {key, _value} -> key == header end) != 1,
|
||||
do: stream_error!("Expected 1 #{header} headers", stream)
|
||||
end
|
||||
|
||||
# RFC9113§8.2 - all headers name fields must be lowercsae
|
||||
defp headers_all_lowercase!(headers, stream) do
|
||||
if !Enum.all?(headers, fn {key, _value} -> lowercase?(key) end),
|
||||
do: stream_error!("Received uppercase header", stream)
|
||||
end
|
||||
|
||||
defp lowercase?(<<char, _rest::bits>>) when char >= ?A and char <= ?Z, do: false
|
||||
defp lowercase?(<<_char, rest::bits>>), do: lowercase?(rest)
|
||||
defp lowercase?(<<>>), do: true
|
||||
|
||||
# RFC9113§8.2.2 - no hop-by-hop headers
|
||||
# Note that we do not filter out the TE header here, since it is allowed in
|
||||
# specific cases by RFC9113§8.2.2. We check those cases in a separate filter
|
||||
defp no_connection_headers!(headers, stream) do
|
||||
connection_headers =
|
||||
~w[connection keep-alive proxy-authenticate proxy-authorization trailers transfer-encoding upgrade]
|
||||
|
||||
if Enum.any?(headers, fn {key, _value} -> key in connection_headers end),
|
||||
do: stream_error!("Received connection-specific header", stream)
|
||||
end
|
||||
|
||||
# RFC9113§8.2.2 - TE header may be present if it contains exactly 'trailers'
|
||||
defp valid_te_header!(headers, stream) do
|
||||
if Bandit.Headers.get_header(headers, "te") not in [nil, "trailers"],
|
||||
do: stream_error!("Received invalid TE header", stream)
|
||||
end
|
||||
|
||||
defp get_content_length!(headers, stream) do
|
||||
case Bandit.Headers.get_content_length(headers) do
|
||||
{:ok, content_length} -> content_length
|
||||
{:error, reason} -> stream_error!(reason, stream)
|
||||
end
|
||||
end
|
||||
|
||||
# RFC9113§8.2.3 - cookie headers may be split during transmission
|
||||
defp combine_cookie_crumbs(headers) do
|
||||
{crumbs, other_headers} =
|
||||
headers |> Enum.split_with(fn {header, _} -> header == "cookie" end)
|
||||
|
||||
case Enum.map_join(crumbs, "; ", fn {"cookie", crumb} -> crumb end) do
|
||||
"" -> other_headers
|
||||
combined_cookie -> [{"cookie", combined_cookie} | other_headers]
|
||||
end
|
||||
end
|
||||
|
||||
def read_data(%@for{} = stream, opts) do
|
||||
max_bytes = Keyword.get(opts, :length, 8_000_000)
|
||||
timeout = Keyword.get(opts, :read_timeout, 15_000)
|
||||
do_read_data(stream, max_bytes, timeout, [])
|
||||
end
|
||||
|
||||
defp do_read_data(%@for{state: state} = stream, max_bytes, timeout, acc)
|
||||
when state in [:open, :local_closed] do
|
||||
case do_recv(stream, timeout) do
|
||||
{:headers, trailers, stream} ->
|
||||
no_pseudo_headers!(trailers, stream)
|
||||
Logger.warning("Ignoring trailers #{inspect(trailers)}", domain: [:bandit])
|
||||
do_read_data(stream, max_bytes, timeout, acc)
|
||||
|
||||
{:data, data, stream} ->
|
||||
acc = [data | acc]
|
||||
max_bytes = max_bytes - byte_size(data)
|
||||
|
||||
if max_bytes >= 0 do
|
||||
do_read_data(stream, max_bytes, timeout, acc)
|
||||
else
|
||||
{:more, Enum.reverse(acc), stream}
|
||||
end
|
||||
|
||||
:timeout ->
|
||||
{:more, Enum.reverse(acc), stream}
|
||||
|
||||
%@for{} = stream ->
|
||||
do_read_data(stream, max_bytes, timeout, acc)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_read_data(%@for{state: :remote_closed} = stream, _max_bytes, _timeout, acc) do
|
||||
{:ok, Enum.reverse(acc), stream}
|
||||
end
|
||||
|
||||
defp no_pseudo_headers!(headers, stream) do
|
||||
if Enum.any?(headers, fn {key, _value} -> String.starts_with?(key, ":") end),
|
||||
do: stream_error!("Received trailers with pseudo headers", stream)
|
||||
end
|
||||
|
||||
defp do_recv(%@for{state: :idle} = stream, timeout) do
|
||||
receive do
|
||||
{:bandit, {:headers, headers, end_stream}} ->
|
||||
{:headers, headers, stream |> do_recv_headers() |> do_recv_end_stream(end_stream)}
|
||||
|
||||
{:bandit, {:data, _data, _end_stream}} ->
|
||||
connection_error!("Received DATA in idle state")
|
||||
|
||||
{:bandit, {:send_window_update, _delta}} ->
|
||||
connection_error!("Received WINDOW_UPDATE in idle state")
|
||||
|
||||
{:bandit, {:rst_stream, _error_code}} ->
|
||||
connection_error!("Received RST_STREAM in idle state")
|
||||
after
|
||||
timeout -> :timeout
|
||||
end
|
||||
end
|
||||
|
||||
defp do_recv(%@for{state: state} = stream, timeout)
|
||||
when state in [:open, :local_closed] do
|
||||
receive do
|
||||
{:bandit, {:headers, headers, end_stream}} ->
|
||||
{:headers, headers, stream |> do_recv_headers() |> do_recv_end_stream(end_stream)}
|
||||
|
||||
{:bandit, {:data, data, end_stream}} ->
|
||||
{:data, data,
|
||||
stream |> do_recv_data(data, end_stream) |> do_recv_end_stream(end_stream)}
|
||||
|
||||
{:bandit, {:send_window_update, delta}} ->
|
||||
do_recv_send_window_update(stream, delta)
|
||||
|
||||
{:bandit, {:rst_stream, error_code}} ->
|
||||
do_recv_rst_stream!(stream, error_code)
|
||||
after
|
||||
timeout -> :timeout
|
||||
end
|
||||
end
|
||||
|
||||
defp do_recv(%@for{state: :remote_closed} = stream, timeout) do
|
||||
receive do
|
||||
{:bandit, {:headers, _headers, _end_stream}} ->
|
||||
do_stream_closed_error!("Received HEADERS in remote_closed state", stream)
|
||||
|
||||
{:bandit, {:data, _data, _end_stream}} ->
|
||||
do_stream_closed_error!("Received DATA in remote_closed state", stream)
|
||||
|
||||
{:bandit, {:send_window_update, delta}} ->
|
||||
do_recv_send_window_update(stream, delta)
|
||||
|
||||
{:bandit, {:rst_stream, error_code}} ->
|
||||
do_recv_rst_stream!(stream, error_code)
|
||||
after
|
||||
timeout -> :timeout
|
||||
end
|
||||
end
|
||||
|
||||
defp do_recv(%@for{state: :closed} = stream, timeout) do
|
||||
receive do
|
||||
{:bandit, {:headers, _headers, _end_stream}} -> stream
|
||||
{:bandit, {:data, _data, _end_stream}} -> stream
|
||||
{:bandit, {:send_window_update, _delta}} -> stream
|
||||
{:bandit, {:rst_stream, _error_code}} -> stream
|
||||
after
|
||||
timeout -> :timeout
|
||||
end
|
||||
end
|
||||
|
||||
defp do_recv_headers(%@for{state: :idle} = stream), do: %{stream | state: :open}
|
||||
defp do_recv_headers(stream), do: stream
|
||||
|
||||
defp do_recv_data(stream, data, end_stream) do
|
||||
{new_window, increment} =
|
||||
Bandit.HTTP2.FlowControl.compute_recv_window(stream.recv_window_size, byte_size(data))
|
||||
|
||||
if increment > 0 && !end_stream, do: do_send(stream, {:send_recv_window_update, increment})
|
||||
|
||||
bytes_remaining =
|
||||
case stream.bytes_remaining do
|
||||
nil -> nil
|
||||
bytes_remaining -> bytes_remaining - byte_size(data)
|
||||
end
|
||||
|
||||
%{stream | recv_window_size: new_window, bytes_remaining: bytes_remaining}
|
||||
end
|
||||
|
||||
defp do_recv_end_stream(stream, false), do: stream
|
||||
|
||||
defp do_recv_end_stream(stream, true) do
|
||||
next_state =
|
||||
case stream.state do
|
||||
:open -> :remote_closed
|
||||
:local_closed -> :closed
|
||||
end
|
||||
|
||||
if stream.bytes_remaining not in [nil, 0],
|
||||
do: stream_error!("Received END_STREAM with byte still pending", stream)
|
||||
|
||||
%{stream | state: next_state}
|
||||
end
|
||||
|
||||
defp do_recv_send_window_update(stream, delta) do
|
||||
case Bandit.HTTP2.FlowControl.update_send_window(stream.send_window_size, delta) do
|
||||
{:ok, new_window} ->
|
||||
%{stream | send_window_size: new_window}
|
||||
|
||||
{:error, reason} ->
|
||||
stream_error!(reason, stream, Bandit.HTTP2.Errors.flow_control_error())
|
||||
end
|
||||
end
|
||||
|
||||
@spec do_recv_rst_stream!(term(), term()) :: no_return()
|
||||
defp do_recv_rst_stream!(_stream, error_code) do
|
||||
case Bandit.HTTP2.Errors.to_reason(error_code) do
|
||||
reason when reason in [:no_error, :cancel] ->
|
||||
raise(Bandit.TransportError, message: "Client reset stream normally", error: :closed)
|
||||
|
||||
reason ->
|
||||
raise(Bandit.TransportError,
|
||||
message: "Received RST_STREAM from client: #{reason} (#{error_code})",
|
||||
error: reason
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@spec do_stream_closed_error!(String.t(), Bandit.HTTP2.Stream.t()) :: no_return()
|
||||
defp do_stream_closed_error!(msg, stream),
|
||||
do: stream_error!(msg, stream, Bandit.HTTP2.Errors.stream_closed())
|
||||
|
||||
# Stream API - Sending
|
||||
|
||||
def send_headers(%@for{state: state} = stream, status, headers, body_disposition)
|
||||
when state in [:open, :remote_closed] do
|
||||
# We need to map body_disposition into the state model of HTTP/2. This turns out to be really
|
||||
# easy, since HTTP/2 only has one way to send data. The only bit we need from the disposition
|
||||
# is whether there will be any data forthcoming (ie: whether or not to end the stream). That
|
||||
# will possibly walk us to a different state per RFC9113§5.1, as determined by the tail call
|
||||
# to set_state_on_send_end_stream/2
|
||||
end_stream = body_disposition == :no_body
|
||||
headers = [{":status", to_string(status)} | split_cookies(headers)]
|
||||
do_send(stream, {:send_headers, headers, end_stream})
|
||||
set_state_on_send_end_stream(stream, end_stream)
|
||||
end
|
||||
|
||||
# RFC9113§8.2.3 - cookie headers may be split during transmission
|
||||
defp split_cookies(headers) do
|
||||
headers
|
||||
|> Enum.flat_map(fn
|
||||
{"cookie", cookie} ->
|
||||
cookie |> String.split("; ") |> Enum.map(fn crumb -> {"cookie", crumb} end)
|
||||
|
||||
{header, value} ->
|
||||
[{header, value}]
|
||||
end)
|
||||
end
|
||||
|
||||
def send_data(%@for{state: state} = stream, data, end_stream)
|
||||
when state in [:open, :remote_closed] do
|
||||
stream =
|
||||
receive do
|
||||
{:bandit, {:send_window_update, delta}} -> do_recv_send_window_update(stream, delta)
|
||||
{:bandit, {:rst_stream, error_code}} -> do_recv_rst_stream!(stream, error_code)
|
||||
after
|
||||
0 -> stream
|
||||
end
|
||||
|
||||
max_bytes_to_send = max(stream.send_window_size, 0)
|
||||
{data_to_send, bytes_to_send, rest} = split_data(data, max_bytes_to_send)
|
||||
|
||||
stream =
|
||||
if end_stream || bytes_to_send > 0 do
|
||||
end_stream_to_send = end_stream && byte_size(rest) == 0
|
||||
call(stream, {:send_data, data_to_send, end_stream_to_send}, :infinity)
|
||||
%{stream | send_window_size: stream.send_window_size - bytes_to_send}
|
||||
else
|
||||
stream
|
||||
end
|
||||
|
||||
if byte_size(rest) == 0 do
|
||||
set_state_on_send_end_stream(stream, end_stream)
|
||||
else
|
||||
receive do
|
||||
{:bandit, {:send_window_update, delta}} ->
|
||||
stream
|
||||
|> do_recv_send_window_update(delta)
|
||||
|> send_data(rest, end_stream)
|
||||
after
|
||||
stream.read_timeout ->
|
||||
stream_error!(
|
||||
"Timeout waiting for space in the send_window",
|
||||
stream,
|
||||
Bandit.HTTP2.Errors.flow_control_error()
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sendfile(%@for{} = stream, path, offset, length) do
|
||||
case :file.open(path, [:raw, :binary]) do
|
||||
{:ok, fd} ->
|
||||
try do
|
||||
if length == 0 do
|
||||
send_data(stream, "", true)
|
||||
else
|
||||
sendfile_loop(stream, fd, offset, length, 0)
|
||||
end
|
||||
after
|
||||
:file.close(fd)
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
raise "Error opening file for sendfile: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp sendfile_loop(stream, _fd, _offset, length, sent) when sent >= length do
|
||||
stream
|
||||
end
|
||||
|
||||
defp sendfile_loop(stream, fd, offset, length, sent) do
|
||||
read_size = min(length - sent, sendfile_chunk_size(stream))
|
||||
|
||||
case :file.pread(fd, offset + sent, read_size) do
|
||||
{:ok, data} ->
|
||||
now_sent = byte_size(data)
|
||||
end_stream = sent + now_sent >= length
|
||||
stream = send_data(stream, data, end_stream)
|
||||
|
||||
if end_stream do
|
||||
stream
|
||||
else
|
||||
sendfile_loop(stream, fd, offset, length, sent + now_sent)
|
||||
end
|
||||
|
||||
:eof ->
|
||||
raise "Error reading file for sendfile: :eof"
|
||||
|
||||
{:error, reason} ->
|
||||
raise "Error reading file for sendfile: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp sendfile_chunk_size(%@for{sendfile_chunk_size: sendfile_chunk_size}) do
|
||||
max(sendfile_chunk_size, 1)
|
||||
end
|
||||
|
||||
defp split_data(data, desired_length) do
|
||||
data_length = IO.iodata_length(data)
|
||||
|
||||
if data_length <= desired_length do
|
||||
{data, data_length, <<>>}
|
||||
else
|
||||
<<to_send::binary-size(desired_length), rest::binary>> = IO.iodata_to_binary(data)
|
||||
{to_send, desired_length, rest}
|
||||
end
|
||||
end
|
||||
|
||||
defp set_state_on_send_end_stream(stream, false), do: stream
|
||||
|
||||
defp set_state_on_send_end_stream(%@for{state: :open} = stream, true),
|
||||
do: %{stream | state: :local_closed}
|
||||
|
||||
defp set_state_on_send_end_stream(%@for{state: :remote_closed} = stream, true),
|
||||
do: %{stream | state: :closed}
|
||||
|
||||
# Closing off the stream upon completion or error
|
||||
|
||||
def ensure_completed(%@for{state: :closed} = stream), do: stream
|
||||
|
||||
def ensure_completed(%@for{state: :local_closed} = stream) do
|
||||
receive do
|
||||
{:bandit, {:headers, _headers, true}} ->
|
||||
do_recv_end_stream(stream, true)
|
||||
|
||||
{:bandit, {:data, data, true}} ->
|
||||
do_recv_data(stream, data, true) |> do_recv_end_stream(true)
|
||||
after
|
||||
# RFC9113§8.1 - hint the client to stop sending data
|
||||
0 -> do_send(stream, {:send_rst_stream, Bandit.HTTP2.Errors.no_error()})
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_completed(%@for{state: state} = stream) do
|
||||
stream_error!(
|
||||
"Terminating stream in #{state} state",
|
||||
stream,
|
||||
Bandit.HTTP2.Errors.internal_error()
|
||||
)
|
||||
end
|
||||
|
||||
def supported_upgrade?(%@for{} = _stream, _protocol), do: false
|
||||
|
||||
def send_on_error(%@for{} = stream, %Bandit.HTTP2.Errors.StreamError{} = error) do
|
||||
do_send(stream, {:send_rst_stream, error.error_code})
|
||||
%{stream | state: :closed}
|
||||
end
|
||||
|
||||
def send_on_error(%@for{} = stream, %Bandit.HTTP2.Errors.ConnectionError{} = error) do
|
||||
do_send(stream, {:close_connection, error.error_code, error.message})
|
||||
stream
|
||||
end
|
||||
|
||||
def send_on_error(%@for{state: state} = stream, error) when state in [:idle, :open] do
|
||||
stream = maybe_send_error(%{stream | state: :open}, error)
|
||||
%{stream | state: :local_closed}
|
||||
end
|
||||
|
||||
def send_on_error(%@for{state: :remote_closed} = stream, error) do
|
||||
stream = maybe_send_error(%{stream | state: :open}, error)
|
||||
%{stream | state: :closed}
|
||||
end
|
||||
|
||||
def send_on_error(%@for{} = stream, _error), do: stream
|
||||
|
||||
defp maybe_send_error(stream, error) do
|
||||
receive do
|
||||
{:plug_conn, :sent} -> stream
|
||||
after
|
||||
0 ->
|
||||
status = error |> Plug.Exception.status() |> Plug.Conn.Status.code()
|
||||
send_headers(stream, status, [], :no_body)
|
||||
end
|
||||
end
|
||||
|
||||
# Helpers
|
||||
|
||||
defp do_send(stream, msg), do: send(stream.connection_pid, {msg, stream.stream_id})
|
||||
|
||||
defp call(stream, msg, timeout),
|
||||
do: GenServer.call(stream.connection_pid, {msg, stream.stream_id}, timeout)
|
||||
|
||||
@spec stream_error!(String.t(), Bandit.HTTP2.Stream.t()) :: no_return()
|
||||
@spec stream_error!(
|
||||
String.t(),
|
||||
Bandit.HTTP2.Stream.t(),
|
||||
Bandit.HTTP2.Errors.error_code()
|
||||
) :: no_return()
|
||||
defp stream_error!(message, stream, error_code \\ Bandit.HTTP2.Errors.protocol_error()),
|
||||
do:
|
||||
raise(Bandit.HTTP2.Errors.StreamError,
|
||||
message: message,
|
||||
error_code: error_code,
|
||||
stream_id: stream.stream_id
|
||||
)
|
||||
|
||||
@spec connection_error!(term()) :: no_return()
|
||||
@spec connection_error!(term(), Bandit.HTTP2.Errors.error_code()) :: no_return()
|
||||
defp connection_error!(message, error_code \\ Bandit.HTTP2.Errors.protocol_error()),
|
||||
do: raise(Bandit.HTTP2.Errors.ConnectionError, message: message, error_code: error_code)
|
||||
end
|
||||
end
|
||||
78
phoenix/deps/bandit/lib/bandit/http2/stream_collection.ex
Normal file
78
phoenix/deps/bandit/lib/bandit/http2/stream_collection.ex
Normal file
@@ -0,0 +1,78 @@
|
||||
defmodule Bandit.HTTP2.StreamCollection do
|
||||
@moduledoc false
|
||||
# Represents a collection of stream IDs and what process IDs are running them. An instance of
|
||||
# this struct is contained within each `Bandit.HTTP2.Connection` struct and is responsible for
|
||||
# encapsulating the data about the streams which are currently active within the connection.
|
||||
#
|
||||
# This collection has a number of useful properties:
|
||||
#
|
||||
# * Process IDs are accessible by stream id
|
||||
# * Process IDs are deletable by themselves (ie: deletion is via PID)
|
||||
# * The collection is able to determine if a stream not currently contained in this collection
|
||||
# represents a previously seen stream (in which case it is considered to be in a 'closed'
|
||||
# state), or if it is a stream ID of a stream that has yet to be created
|
||||
|
||||
require Integer
|
||||
|
||||
defstruct last_stream_id: 0,
|
||||
stream_count: 0,
|
||||
id_to_pid: %{},
|
||||
pid_to_id: %{}
|
||||
|
||||
@typedoc "A map from stream id to pid"
|
||||
@type t :: %__MODULE__{
|
||||
last_stream_id: Bandit.HTTP2.Stream.stream_id(),
|
||||
stream_count: non_neg_integer(),
|
||||
id_to_pid: %{Bandit.HTTP2.Stream.stream_id() => pid()},
|
||||
pid_to_id: %{pid() => Bandit.HTTP2.Stream.stream_id()}
|
||||
}
|
||||
|
||||
@spec get_pids(t()) :: [pid()]
|
||||
def get_pids(collection), do: Map.values(collection.id_to_pid)
|
||||
|
||||
@spec get_pid(t(), Bandit.HTTP2.Stream.stream_id()) :: pid() | :new | :closed | :invalid
|
||||
def get_pid(_collection, stream_id) when Integer.is_even(stream_id), do: :invalid
|
||||
def get_pid(collection, stream_id) when stream_id > collection.last_stream_id, do: :new
|
||||
|
||||
def get_pid(collection, stream_id) do
|
||||
case Map.get(collection.id_to_pid, stream_id) do
|
||||
pid when is_pid(pid) -> pid
|
||||
nil -> :closed
|
||||
end
|
||||
end
|
||||
|
||||
@spec insert(t(), Bandit.HTTP2.Stream.stream_id(), pid()) :: t()
|
||||
def insert(collection, stream_id, pid) do
|
||||
%__MODULE__{
|
||||
last_stream_id: stream_id,
|
||||
stream_count: collection.stream_count + 1,
|
||||
id_to_pid: Map.put(collection.id_to_pid, stream_id, pid),
|
||||
pid_to_id: Map.put(collection.pid_to_id, pid, stream_id)
|
||||
}
|
||||
end
|
||||
|
||||
# Dialyzer insists on the atom() here even though it doesn't make sense
|
||||
@spec delete(t(), pid()) :: t() | atom()
|
||||
def delete(collection, pid) do
|
||||
case Map.pop(collection.pid_to_id, pid) do
|
||||
{nil, _} ->
|
||||
collection
|
||||
|
||||
{stream_id, new_pid_to_id} ->
|
||||
%{
|
||||
collection
|
||||
| id_to_pid: Map.delete(collection.id_to_pid, stream_id),
|
||||
pid_to_id: new_pid_to_id
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@spec stream_count(t()) :: non_neg_integer()
|
||||
def stream_count(collection), do: collection.stream_count
|
||||
|
||||
@spec open_stream_count(t()) :: non_neg_integer()
|
||||
def open_stream_count(collection), do: collection.pid_to_id |> map_size()
|
||||
|
||||
@spec last_stream_id(t()) :: Bandit.HTTP2.Stream.stream_id()
|
||||
def last_stream_id(collection), do: collection.last_stream_id
|
||||
end
|
||||
31
phoenix/deps/bandit/lib/bandit/http2/stream_process.ex
Normal file
31
phoenix/deps/bandit/lib/bandit/http2/stream_process.ex
Normal file
@@ -0,0 +1,31 @@
|
||||
defmodule Bandit.HTTP2.StreamProcess do
|
||||
@moduledoc false
|
||||
# This process runs the lifecycle of an HTTP/2 stream, which is modeled by a
|
||||
# `Bandit.HTTP2.Stream` struct that this process maintains in its state
|
||||
#
|
||||
# As part of this lifecycle, the execution of a Plug to handle this stream's request
|
||||
# takes place here; the entirety of the Plug lifecycle takes place in a single
|
||||
# `c:handle_continue/2` call.
|
||||
|
||||
use GenServer, restart: :temporary
|
||||
|
||||
@spec start_link(
|
||||
Bandit.HTTP2.Stream.t(),
|
||||
Bandit.Pipeline.plug_def(),
|
||||
Bandit.Telemetry.t(),
|
||||
Bandit.Pipeline.conn_data(),
|
||||
keyword()
|
||||
) :: GenServer.on_start()
|
||||
def start_link(stream, plug, connection_span, conn_data, opts) do
|
||||
GenServer.start_link(__MODULE__, {stream, plug, connection_span, conn_data, opts})
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def init(state), do: {:ok, state, {:continue, :start_stream}}
|
||||
|
||||
@impl GenServer
|
||||
def handle_continue(:start_stream, {stream, plug, connection_span, conn_data, opts} = state) do
|
||||
_ = Bandit.Pipeline.run(stream, plug, connection_span, conn_data, opts)
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
end
|
||||
6
phoenix/deps/bandit/lib/bandit/http_error.ex
Normal file
6
phoenix/deps/bandit/lib/bandit/http_error.ex
Normal file
@@ -0,0 +1,6 @@
|
||||
defmodule Bandit.HTTPError do
|
||||
# Represents an error suitable for return as an HTTP status. Note that these may be surfaced
|
||||
# from anywhere that such a message is well defined, including within HTTP/1 transport concerns
|
||||
# and also within shared HTTP semantics (ie: within Bandit.Adapter or Bandit.Pipeline)
|
||||
defexception message: nil, plug_status: :bad_request
|
||||
end
|
||||
47
phoenix/deps/bandit/lib/bandit/http_transport.ex
Normal file
47
phoenix/deps/bandit/lib/bandit/http_transport.ex
Normal file
@@ -0,0 +1,47 @@
|
||||
defprotocol Bandit.HTTPTransport do
|
||||
@moduledoc false
|
||||
# A protocol implemented by the lower level transports (HTTP/1 and HTTP/2) to encapsulate the
|
||||
# low-level mechanics needed to complete an HTTP request/response cycle. Implementations of this
|
||||
# protocol should be broadly concerned with the protocol-specific aspects of a connection, and
|
||||
# can rely on higher-level code taking care of shared HTTP semantics
|
||||
|
||||
@typedoc "How the response body is to be delivered"
|
||||
@type body_disposition :: :raw | :chunk_encoded | :no_body | :inform
|
||||
|
||||
@spec peer_data(t()) :: Plug.Conn.Adapter.peer_data()
|
||||
def peer_data(transport)
|
||||
|
||||
@spec sock_data(t()) :: Plug.Conn.Adapter.sock_data()
|
||||
def sock_data(transport)
|
||||
|
||||
@spec ssl_data(t()) :: Plug.Conn.Adapter.ssl_data()
|
||||
def ssl_data(transport)
|
||||
|
||||
@spec version(t()) :: Plug.Conn.Adapter.http_protocol()
|
||||
def version(transport)
|
||||
|
||||
@spec read_headers(t()) ::
|
||||
{:ok, Plug.Conn.method(), Bandit.Pipeline.request_target(), Plug.Conn.headers(), t()}
|
||||
def read_headers(transport)
|
||||
|
||||
@spec read_data(t(), opts :: keyword()) :: {:ok, iodata(), t()} | {:more, iodata(), t()}
|
||||
def read_data(transport, opts)
|
||||
|
||||
@spec send_headers(t(), Plug.Conn.status(), Plug.Conn.headers(), body_disposition()) :: t()
|
||||
def send_headers(transport, status, heeaders, disposition)
|
||||
|
||||
@spec send_data(t(), data :: iodata(), end_request :: boolean()) :: t()
|
||||
def send_data(transport, data, end_request)
|
||||
|
||||
@spec sendfile(t(), Path.t(), offset :: integer(), length :: integer() | :all) :: t()
|
||||
def sendfile(transport, path, offset, length)
|
||||
|
||||
@spec ensure_completed(t()) :: t()
|
||||
def ensure_completed(transport)
|
||||
|
||||
@spec supported_upgrade?(t(), atom()) :: boolean()
|
||||
def supported_upgrade?(transport, protocol)
|
||||
|
||||
@spec send_on_error(t(), struct()) :: t()
|
||||
def send_on_error(transport, error)
|
||||
end
|
||||
86
phoenix/deps/bandit/lib/bandit/initial_handler.ex
Normal file
86
phoenix/deps/bandit/lib/bandit/initial_handler.ex
Normal file
@@ -0,0 +1,86 @@
|
||||
defmodule Bandit.InitialHandler do
|
||||
@moduledoc false
|
||||
# The initial protocol implementation used for all connections. Switches to a
|
||||
# specific protocol implementation based on configuration, ALPN negotiation, and
|
||||
# line heuristics.
|
||||
|
||||
use ThousandIsland.Handler
|
||||
|
||||
require Logger
|
||||
|
||||
@type on_switch_handler ::
|
||||
{:switch, bandit_http_handler(), data :: term(), state :: term()}
|
||||
| {:switch, bandit_http_handler(), state :: term()}
|
||||
|
||||
@type bandit_http_handler :: Bandit.HTTP1.Handler | Bandit.HTTP2.Handler
|
||||
|
||||
# Attempts to guess the protocol in use, returning the applicable next handler and any
|
||||
# data consumed in the course of guessing which must be processed by the actual protocol handler
|
||||
@impl ThousandIsland.Handler
|
||||
@spec handle_connection(ThousandIsland.Socket.t(), state :: term()) ::
|
||||
ThousandIsland.Handler.handler_result() | on_switch_handler()
|
||||
def handle_connection(socket, state) do
|
||||
case {state.http_1_enabled, state.http_2_enabled, alpn_protocol(socket), sniff_wire(socket)} do
|
||||
{_, _, _, :likely_tls} ->
|
||||
Logger.warning("Connection that looks like TLS received on a clear channel",
|
||||
domain: [:bandit],
|
||||
plug: state.plug
|
||||
)
|
||||
|
||||
{:close, state}
|
||||
|
||||
{_, true, Bandit.HTTP2.Handler, Bandit.HTTP2.Handler} ->
|
||||
{:switch, Bandit.HTTP2.Handler, state}
|
||||
|
||||
{true, _, Bandit.HTTP1.Handler, {:no_match, data}} ->
|
||||
{:switch, Bandit.HTTP1.Handler, data, state}
|
||||
|
||||
{_, true, :no_match, Bandit.HTTP2.Handler} ->
|
||||
{:switch, Bandit.HTTP2.Handler, state}
|
||||
|
||||
{true, _, :no_match, {:no_match, data}} ->
|
||||
{:switch, Bandit.HTTP1.Handler, data, state}
|
||||
|
||||
_other ->
|
||||
{:close, state}
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the protocol as negotiated via ALPN, if applicable
|
||||
@spec alpn_protocol(ThousandIsland.Socket.t()) ::
|
||||
Bandit.HTTP2.Handler | Bandit.HTTP1.Handler | :no_match
|
||||
defp alpn_protocol(socket) do
|
||||
case ThousandIsland.Socket.negotiated_protocol(socket) do
|
||||
{:ok, "h2"} -> Bandit.HTTP2.Handler
|
||||
{:ok, "http/1.1"} -> Bandit.HTTP1.Handler
|
||||
_ -> :no_match
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the protocol as suggested by received data, if possible.
|
||||
# We do this in two phases so that we don't hang on *really* short HTTP/1
|
||||
# requests that are less than 24 bytes
|
||||
@spec sniff_wire(ThousandIsland.Socket.t()) ::
|
||||
Bandit.HTTP2.Handler
|
||||
| :likely_tls
|
||||
| {:no_match, binary()}
|
||||
| {:error, :closed | :timeout | :inet.posix()}
|
||||
defp sniff_wire(socket) do
|
||||
case ThousandIsland.Socket.recv(socket, 3) do
|
||||
{:ok, "PRI" = buffer} -> sniff_wire_for_http2(socket, buffer)
|
||||
{:ok, <<22::8, 3::8, minor::8>>} when minor in [1, 3] -> :likely_tls
|
||||
{:ok, data} -> {:no_match, data}
|
||||
{:error, :timeout} -> {:no_match, <<>>}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp sniff_wire_for_http2(socket, buffer) do
|
||||
case ThousandIsland.Socket.recv(socket, 21) do
|
||||
{:ok, " * HTTP/2.0\r\n\r\nSM\r\n\r\n"} -> Bandit.HTTP2.Handler
|
||||
{:ok, data} -> {:no_match, buffer <> data}
|
||||
{:error, :timeout} -> {:no_match, buffer}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
46
phoenix/deps/bandit/lib/bandit/logger.ex
Normal file
46
phoenix/deps/bandit/lib/bandit/logger.ex
Normal file
@@ -0,0 +1,46 @@
|
||||
defmodule Bandit.Logger do
|
||||
@moduledoc false
|
||||
|
||||
require Logger
|
||||
|
||||
def maybe_log_protocol_error(error, stacktrace, opts, metadata) do
|
||||
logging_verbosity =
|
||||
case error do
|
||||
%Bandit.TransportError{error: :closed} ->
|
||||
Keyword.get(opts.http, :log_client_closures, false)
|
||||
|
||||
_error ->
|
||||
Keyword.get(opts.http, :log_protocol_errors, :short)
|
||||
end
|
||||
|
||||
case logging_verbosity do
|
||||
:short ->
|
||||
logger_metadata = logger_metadata_for(:error, error, stacktrace, metadata)
|
||||
Logger.error(Exception.format_banner(:error, error, stacktrace), logger_metadata)
|
||||
|
||||
:verbose ->
|
||||
logger_metadata = logger_metadata_for(:error, error, stacktrace, metadata)
|
||||
Logger.error(Exception.format(:error, error, stacktrace), logger_metadata)
|
||||
|
||||
false ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def logger_metadata_for(kind, reason, stacktrace, metadata) do
|
||||
crash_reason = crash_reason(kind, reason, stacktrace)
|
||||
|
||||
case reason do
|
||||
%Bandit.HTTP2.Errors.StreamError{stream_id: stream_id} when is_integer(stream_id) ->
|
||||
[stream_id: stream_id, domain: [:bandit], crash_reason: crash_reason]
|
||||
|> Keyword.merge(metadata)
|
||||
|
||||
_ ->
|
||||
[domain: [:bandit], crash_reason: crash_reason]
|
||||
|> Keyword.merge(metadata)
|
||||
end
|
||||
end
|
||||
|
||||
defp crash_reason(:throw, reason, stacktrace), do: {{:nocatch, reason}, stacktrace}
|
||||
defp crash_reason(_, reason, stacktrace), do: {reason, stacktrace}
|
||||
end
|
||||
116
phoenix/deps/bandit/lib/bandit/phoenix_adapter.ex
Normal file
116
phoenix/deps/bandit/lib/bandit/phoenix_adapter.ex
Normal file
@@ -0,0 +1,116 @@
|
||||
defmodule Bandit.PhoenixAdapter do
|
||||
@moduledoc """
|
||||
A Bandit adapter for Phoenix.
|
||||
|
||||
This adapter provides out-of-the-box support for all aspects of Phoenix 1.7 and later. Earlier
|
||||
versions of Phoenix will work with this adapter, but without support for WebSockets.
|
||||
|
||||
To use this adapter, your project will need to include Bandit as a dependency:
|
||||
|
||||
```elixir
|
||||
{:bandit, "~> 1.0"}
|
||||
```
|
||||
|
||||
Once Bandit is included as a dependency of your Phoenix project, add the following `adapter:`
|
||||
line to your endpoint configuration in `config/config.exs`, as in the following example:
|
||||
|
||||
```
|
||||
# config/config.exs
|
||||
|
||||
config :your_app, YourAppWeb.Endpoint,
|
||||
adapter: Bandit.PhoenixAdapter, # <---- ADD THIS LINE
|
||||
url: [host: "localhost"],
|
||||
render_errors: ...
|
||||
```
|
||||
|
||||
That's it! **After restarting Phoenix you should see the startup message indicate that it is being
|
||||
served by Bandit**, and everything should 'just work'. Note that if you have set any exotic
|
||||
configuration options within your endpoint, you may need to update that configuration to work
|
||||
with Bandit; see below for details.
|
||||
|
||||
## Endpoint configuration
|
||||
|
||||
This adapter supports the standard Phoenix structure for endpoint configuration. Top-level keys for
|
||||
`:http` and `:https` are supported, and configuration values within each of those are interpreted
|
||||
as raw Bandit configuration as specified by `t:Bandit.options/0`. Bandit's configuration supports
|
||||
all values used in a standard out-of-the-box Phoenix application, so if you haven't made any
|
||||
substantial changes to your endpoint configuration things should 'just work' for you.
|
||||
|
||||
In the event that you *have* made advanced changes to your endpoint configuration, you may need
|
||||
to update this config to work with Bandit. Consult Bandit's documentation at
|
||||
`t:Bandit.options/0` for details.
|
||||
|
||||
It can be difficult to know exactly *where* to put the options that you may need to set from the
|
||||
ones available at `t:Bandit.options/0`. The general idea is that anything inside the `http:` or
|
||||
`https:` keyword lists in your configuration are passed directly to `Bandit.start_link/1`, so an
|
||||
example may look like so:
|
||||
|
||||
```elixir
|
||||
# config/{dev,prod,etc}.exs
|
||||
|
||||
config :your_app, YourAppWeb.Endpoint,
|
||||
http: [
|
||||
ip: {127, 0, 0, 1},
|
||||
port: 4000,
|
||||
thousand_island_options: [num_acceptors: 123],
|
||||
http_options: [log_protocol_errors: false],
|
||||
http_1_options: [max_requests: 1],
|
||||
websocket_options: [compress: false]
|
||||
],
|
||||
```
|
||||
|
||||
Note that, unlike the `adapter: Bandit.PhoenixAdapter` configuration change outlined previously,
|
||||
configuration of specific `http:` and `https:` values is done on a per-environment basis in
|
||||
Phoenix, so these changes will typically be in your `config/dev.exs`, `config/prod.exs` and
|
||||
similar files.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns the Bandit server process for the provided scheme within the given Phoenix Endpoint
|
||||
"""
|
||||
@spec bandit_pid(module()) ::
|
||||
{:ok, Supervisor.child() | :restarting | :undefined} | {:error, :no_server_found}
|
||||
def bandit_pid(endpoint, scheme \\ :http) do
|
||||
endpoint
|
||||
|> Supervisor.which_children()
|
||||
|> Enum.find(fn {id, _, _, _} -> id == {endpoint, scheme} end)
|
||||
|> case do
|
||||
{_, pid, _, _} -> {:ok, pid}
|
||||
nil -> {:error, :no_server_found}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the bound address and port of the Bandit server process for the provided
|
||||
scheme within the given Phoenix Endpoint
|
||||
"""
|
||||
def server_info(endpoint, scheme) do
|
||||
case bandit_pid(endpoint, scheme) do
|
||||
{:ok, pid} -> ThousandIsland.listener_info(pid)
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def child_specs(endpoint, config) do
|
||||
otp_app = Keyword.fetch!(config, :otp_app)
|
||||
|
||||
plug = resolve_plug(config[:code_reloader], endpoint)
|
||||
|
||||
for scheme <- [:http, :https], opts = config[scheme] do
|
||||
([plug: plug, display_plug: endpoint, scheme: scheme, otp_app: otp_app] ++ opts)
|
||||
|> Bandit.child_spec()
|
||||
|> Supervisor.child_spec(id: {endpoint, scheme})
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_plug(code_reload?, endpoint) do
|
||||
if code_reload? &&
|
||||
Code.ensure_loaded?(Phoenix.Endpoint.SyncCodeReloadPlug) &&
|
||||
function_exported?(Phoenix.Endpoint.SyncCodeReloadPlug, :call, 2) do
|
||||
{Phoenix.Endpoint.SyncCodeReloadPlug, {endpoint, []}}
|
||||
else
|
||||
endpoint
|
||||
end
|
||||
end
|
||||
end
|
||||
248
phoenix/deps/bandit/lib/bandit/pipeline.ex
Normal file
248
phoenix/deps/bandit/lib/bandit/pipeline.ex
Normal file
@@ -0,0 +1,248 @@
|
||||
defmodule Bandit.Pipeline do
|
||||
@moduledoc false
|
||||
# Provides a common pipeline for HTTP/1.1 and h2 adapters, factoring together shared
|
||||
# functionality relating to `Plug.Conn` management
|
||||
|
||||
@type plug_def :: {function() | module(), Plug.opts()}
|
||||
@type conn_data :: {boolean(), :inet.ip_address()}
|
||||
@type request_target ::
|
||||
{scheme(), nil | Plug.Conn.host(), nil | Plug.Conn.port_number(), path()}
|
||||
@type scheme :: String.t() | nil
|
||||
@type path :: String.t() | :*
|
||||
|
||||
require Logger
|
||||
|
||||
@spec run(
|
||||
Bandit.HTTPTransport.t(),
|
||||
plug_def(),
|
||||
ThousandIsland.Telemetry.t() | Bandit.Telemetry.t(),
|
||||
conn_data(),
|
||||
map()
|
||||
) ::
|
||||
{:ok, Bandit.HTTPTransport.t()}
|
||||
| {:upgrade, Bandit.HTTPTransport.t(), :websocket, tuple()}
|
||||
| {:error, term()}
|
||||
def run(transport, plug, connection_span, conn_data, opts) do
|
||||
measurements = %{monotonic_time: Bandit.Telemetry.monotonic_time()}
|
||||
|
||||
metadata = %{
|
||||
connection_telemetry_span_context: connection_span.telemetry_span_context,
|
||||
plug: plug
|
||||
}
|
||||
|
||||
try do
|
||||
{:ok, method, request_target, headers, transport} =
|
||||
Bandit.HTTPTransport.read_headers(transport)
|
||||
|
||||
conn = build_conn!(transport, method, request_target, headers, conn_data, opts)
|
||||
span = Bandit.Telemetry.start_span(:request, measurements, Map.put(metadata, :conn, conn))
|
||||
|
||||
try do
|
||||
conn
|
||||
|> call_plug!(plug)
|
||||
|> maybe_upgrade!()
|
||||
|> case do
|
||||
{:no_upgrade, conn} ->
|
||||
%Plug.Conn{adapter: {_mod, adapter}} = conn = commit_response!(conn)
|
||||
Bandit.Telemetry.stop_span(span, adapter.metrics, %{conn: conn})
|
||||
{:ok, adapter.transport}
|
||||
|
||||
{:upgrade, %Plug.Conn{adapter: {_mod, adapter}} = conn, protocol, opts} ->
|
||||
conn = Plug.Conn.put_status(conn, 101)
|
||||
Bandit.Telemetry.stop_span(span, adapter.metrics, %{conn: conn})
|
||||
{:upgrade, adapter.transport, protocol, opts}
|
||||
end
|
||||
catch
|
||||
kind, value ->
|
||||
handle_error(kind, value, __STACKTRACE__, transport, span, opts, plug: plug, conn: conn)
|
||||
end
|
||||
rescue
|
||||
exception ->
|
||||
span = Bandit.Telemetry.start_span(:request, measurements, metadata)
|
||||
handle_error(:error, exception, __STACKTRACE__, transport, span, opts, plug: plug)
|
||||
end
|
||||
end
|
||||
|
||||
@spec build_conn!(
|
||||
Bandit.HTTPTransport.t(),
|
||||
Plug.Conn.method(),
|
||||
request_target(),
|
||||
Plug.Conn.headers(),
|
||||
conn_data(),
|
||||
map()
|
||||
) :: Plug.Conn.t()
|
||||
defp build_conn!(transport, method, request_target, headers, {secure?, peer_address}, opts) do
|
||||
adapter = Bandit.Adapter.init(self(), transport, method, headers, opts)
|
||||
scheme = determine_scheme(secure?, request_target)
|
||||
version = Bandit.HTTPTransport.version(transport)
|
||||
{host, port} = determine_host_and_port!(scheme, version, request_target, headers)
|
||||
{path, query} = determine_path_and_query(request_target)
|
||||
uri = %URI{scheme: scheme, host: host, port: port, path: path, query: query}
|
||||
Plug.Conn.Adapter.conn({Bandit.Adapter, adapter}, method, uri, peer_address, headers)
|
||||
end
|
||||
|
||||
@spec determine_scheme(boolean(), request_target()) :: String.t() | nil
|
||||
defp determine_scheme(secure?, {scheme, _, _, _}) do
|
||||
case {secure?, scheme} do
|
||||
{true, nil} -> "https"
|
||||
{false, nil} -> "http"
|
||||
{_, scheme} -> scheme
|
||||
end
|
||||
end
|
||||
|
||||
@spec determine_host_and_port!(binary(), atom(), request_target(), Plug.Conn.headers()) ::
|
||||
{Plug.Conn.host(), Plug.Conn.port_number()}
|
||||
defp determine_host_and_port!(scheme, version, {_, nil, nil, _}, headers) do
|
||||
case {Bandit.Headers.get_header(headers, "host"), version} do
|
||||
{nil, :"HTTP/1.0"} ->
|
||||
{"", URI.default_port(scheme)}
|
||||
|
||||
{nil, _} ->
|
||||
request_error!("Unable to obtain host and port: No host header")
|
||||
|
||||
{host_header, _} ->
|
||||
{host, port} = Bandit.Headers.parse_hostlike_header!(host_header)
|
||||
{host, port || URI.default_port(scheme)}
|
||||
end
|
||||
end
|
||||
|
||||
defp determine_host_and_port!(scheme, _version, {_, host, port, _}, _headers),
|
||||
do: {to_string(host), port || URI.default_port(scheme)}
|
||||
|
||||
@spec determine_path_and_query(request_target()) :: {String.t(), nil | String.t()}
|
||||
defp determine_path_and_query({_, _, _, :*}), do: {"*", nil}
|
||||
defp determine_path_and_query({_, _, _, path}), do: split_path(path)
|
||||
|
||||
@spec split_path(String.t()) :: {String.t(), nil | String.t()}
|
||||
defp split_path(path) do
|
||||
path
|
||||
|> to_string()
|
||||
|> :binary.split("#")
|
||||
|> hd()
|
||||
|> :binary.split("?")
|
||||
|> case do
|
||||
[path, query] -> {path, query}
|
||||
[path] -> {path, nil}
|
||||
end
|
||||
end
|
||||
|
||||
@spec call_plug!(Plug.Conn.t(), plug_def()) :: Plug.Conn.t() | no_return()
|
||||
defp call_plug!(%Plug.Conn{} = conn, {plug, plug_opts}) when is_atom(plug) do
|
||||
case plug.call(conn, plug_opts) do
|
||||
%Plug.Conn{} = conn -> conn
|
||||
other -> raise("Expected #{plug}.call/2 to return %Plug.Conn{} but got: #{inspect(other)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp call_plug!(%Plug.Conn{} = conn, {plug_fn, plug_opts}) when is_function(plug_fn) do
|
||||
case plug_fn.(conn, plug_opts) do
|
||||
%Plug.Conn{} = conn -> conn
|
||||
other -> raise("Expected Plug function to return %Plug.Conn{} but got: #{inspect(other)}")
|
||||
end
|
||||
end
|
||||
|
||||
@spec maybe_upgrade!(Plug.Conn.t()) ::
|
||||
{:no_upgrade, Plug.Conn.t()} | {:upgrade, Plug.Conn.t(), :websocket, tuple()}
|
||||
defp maybe_upgrade!(
|
||||
%Plug.Conn{
|
||||
state: :upgraded,
|
||||
adapter:
|
||||
{_,
|
||||
%{upgrade: {:websocket, {websock, websock_opts, connection_opts}, websocket_opts}}}
|
||||
} = conn
|
||||
) do
|
||||
# We can safely unset the state, since we match on :upgraded above
|
||||
case Bandit.WebSocket.Handshake.handshake(
|
||||
%{conn | state: :unset},
|
||||
connection_opts,
|
||||
websocket_opts
|
||||
) do
|
||||
{:ok, conn, connection_opts} ->
|
||||
{:upgrade, conn, :websocket, {websock, websock_opts, connection_opts}}
|
||||
|
||||
{:error, reason} ->
|
||||
request_error!(reason)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_upgrade!(conn), do: {:no_upgrade, conn}
|
||||
|
||||
@spec commit_response!(Plug.Conn.t()) :: Plug.Conn.t() | no_return()
|
||||
defp commit_response!(conn) do
|
||||
case conn do
|
||||
%Plug.Conn{state: :unset} ->
|
||||
raise(Plug.Conn.NotSentError)
|
||||
|
||||
%Plug.Conn{state: :set} ->
|
||||
Plug.Conn.send_resp(conn)
|
||||
|
||||
%Plug.Conn{state: :chunked, adapter: {mod, adapter}} ->
|
||||
adapter =
|
||||
case mod.chunk(adapter, "") do
|
||||
{:ok, _, adapter} -> adapter
|
||||
_ -> adapter
|
||||
end
|
||||
|
||||
%{conn | adapter: {mod, adapter}}
|
||||
|
||||
%Plug.Conn{} ->
|
||||
conn
|
||||
end
|
||||
|> then(fn %Plug.Conn{adapter: {mod, adapter}} = conn ->
|
||||
transport = Bandit.HTTPTransport.ensure_completed(adapter.transport)
|
||||
%{conn | adapter: {mod, %{adapter | transport: transport}}}
|
||||
end)
|
||||
end
|
||||
|
||||
@spec request_error!(term()) :: no_return()
|
||||
@spec request_error!(term(), Plug.Conn.status()) :: no_return()
|
||||
defp request_error!(reason, plug_status \\ :bad_request) do
|
||||
raise Bandit.HTTPError, message: reason, plug_status: plug_status
|
||||
end
|
||||
|
||||
@spec handle_error(
|
||||
:error | :throw | :exit,
|
||||
Exception.t() | term(),
|
||||
Exception.stacktrace(),
|
||||
Bandit.HTTPTransport.t(),
|
||||
Bandit.Telemetry.t(),
|
||||
map(),
|
||||
keyword()
|
||||
) :: {:ok, Bandit.HTTPTransport.t()} | {:error, term()}
|
||||
defp handle_error(:error, %Plug.Conn.WrapperError{} = error, _, transport, span, opts, metadata) do
|
||||
# Unwrap the inner error and handle it
|
||||
handle_error(error.kind, error.reason, error.stack, transport, span, opts, metadata)
|
||||
end
|
||||
|
||||
defp handle_error(:error, %type{} = error, stacktrace, transport, span, opts, metadata)
|
||||
when type in [
|
||||
Bandit.HTTPError,
|
||||
Bandit.TransportError,
|
||||
Bandit.HTTP2.Errors.StreamError,
|
||||
Bandit.HTTP2.Errors.ConnectionError
|
||||
] do
|
||||
Bandit.Telemetry.stop_span(span, %{}, Enum.into(metadata, %{error: error.message}))
|
||||
|
||||
Bandit.Logger.maybe_log_protocol_error(error, stacktrace, opts, metadata)
|
||||
|
||||
# We want to do this at the end of the function, since the HTTP2 stack may kill this process
|
||||
# in the course of handling a ConnectionError
|
||||
Bandit.HTTPTransport.send_on_error(transport, error)
|
||||
{:error, error}
|
||||
end
|
||||
|
||||
defp handle_error(kind, reason, stacktrace, transport, span, opts, metadata) do
|
||||
reason = Exception.normalize(kind, reason, stacktrace)
|
||||
|
||||
Bandit.Telemetry.span_exception(span, kind, reason, stacktrace)
|
||||
status = reason |> Plug.Exception.status() |> Plug.Conn.Status.code()
|
||||
|
||||
if status in Keyword.get(opts.http, :log_exceptions_with_status_codes, 500..599) do
|
||||
logger_metadata = Bandit.Logger.logger_metadata_for(kind, reason, stacktrace, metadata)
|
||||
Logger.error(Exception.format(kind, reason, stacktrace), logger_metadata)
|
||||
end
|
||||
|
||||
Bandit.HTTPTransport.send_on_error(transport, reason)
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
34
phoenix/deps/bandit/lib/bandit/primitive_ops/websocket.ex
Normal file
34
phoenix/deps/bandit/lib/bandit/primitive_ops/websocket.ex
Normal file
@@ -0,0 +1,34 @@
|
||||
defmodule Bandit.PrimitiveOps.WebSocket do
|
||||
@moduledoc """
|
||||
WebSocket primitive operations behaviour and default implementation
|
||||
"""
|
||||
|
||||
@doc """
|
||||
WebSocket masking according to [RFC6455§5.3](https://www.rfc-editor.org/rfc/rfc6455#section-5.3)
|
||||
"""
|
||||
@callback ws_mask(payload :: binary(), mask :: integer()) :: binary()
|
||||
|
||||
@behaviour __MODULE__
|
||||
|
||||
# Note that masking is an involution, so we don't need a separate unmask function
|
||||
@impl true
|
||||
def ws_mask(payload, mask)
|
||||
when is_binary(payload) and is_integer(mask) and mask >= 0x00000000 and mask <= 0xFFFFFFFF do
|
||||
ws_mask(<<>>, payload, mask)
|
||||
end
|
||||
|
||||
defp ws_mask(acc, <<h::32, rest::binary>>, mask) do
|
||||
ws_mask(<<acc::binary, (<<Bitwise.bxor(h, mask)::32>>)>>, rest, mask)
|
||||
end
|
||||
|
||||
for size <- [24, 16, 8] do
|
||||
defp ws_mask(acc, <<h::unquote(size)>>, mask) do
|
||||
<<mask::unquote(size), _::binary>> = <<mask::32>>
|
||||
<<acc::binary, (<<Bitwise.bxor(h, mask)::unquote(size)>>)>>
|
||||
end
|
||||
end
|
||||
|
||||
defp ws_mask(acc, <<>>, _mask) do
|
||||
acc
|
||||
end
|
||||
end
|
||||
74
phoenix/deps/bandit/lib/bandit/socket_helpers.ex
Normal file
74
phoenix/deps/bandit/lib/bandit/socket_helpers.ex
Normal file
@@ -0,0 +1,74 @@
|
||||
defmodule Bandit.SocketHelpers do
|
||||
@moduledoc false
|
||||
|
||||
def iodata_empty?(""), do: true
|
||||
def iodata_empty?([]), do: true
|
||||
def iodata_empty?([head | tail]), do: iodata_empty?(head) and iodata_empty?(tail)
|
||||
def iodata_empty?(_), do: false
|
||||
|
||||
@spec conn_data(ThousandIsland.Socket.t()) :: Bandit.Pipeline.conn_data()
|
||||
def conn_data(socket) do
|
||||
secure? = ThousandIsland.Socket.secure?(socket)
|
||||
|
||||
{peer_address, _port} =
|
||||
case ThousandIsland.Socket.peername(socket) do
|
||||
{:ok, peername} -> map_address(peername)
|
||||
{:error, reason} -> transport_error!("Unable to obtain conn_data", reason)
|
||||
end
|
||||
|
||||
{secure?, peer_address}
|
||||
end
|
||||
|
||||
@spec peer_data(ThousandIsland.Socket.t()) :: Plug.Conn.Adapter.peer_data()
|
||||
def peer_data(socket) do
|
||||
with {:ok, peername} <- ThousandIsland.Socket.peername(socket),
|
||||
{address, port} <- map_address(peername),
|
||||
{:ok, ssl_cert} <- peercert(socket) do
|
||||
%{address: address, port: port, ssl_cert: ssl_cert}
|
||||
else
|
||||
{:error, reason} -> transport_error!("Unable to obtain peer_data", reason)
|
||||
end
|
||||
end
|
||||
|
||||
@spec sock_data(ThousandIsland.Socket.t()) :: Plug.Conn.Adapter.sock_data()
|
||||
def sock_data(socket) do
|
||||
with {:ok, sockname} <- ThousandIsland.Socket.sockname(socket),
|
||||
{address, port} <- map_address(sockname) do
|
||||
%{address: address, port: port}
|
||||
else
|
||||
{:error, reason} -> transport_error!("Unable to obtain sock_data", reason)
|
||||
end
|
||||
end
|
||||
|
||||
@spec ssl_data(ThousandIsland.Socket.t()) :: Plug.Conn.Adapter.ssl_data()
|
||||
def ssl_data(socket) do
|
||||
case ThousandIsland.Socket.connection_information(socket) do
|
||||
{:ok, connection_information} -> connection_information
|
||||
{:error, :not_secure} -> nil
|
||||
{:error, reason} -> transport_error!("Unable to obtain ssl_data", reason)
|
||||
end
|
||||
end
|
||||
|
||||
defp map_address(address) do
|
||||
case address do
|
||||
{:local, path} -> {{:local, path}, 0}
|
||||
{:unspec, <<>>} -> {:unspec, 0}
|
||||
{:undefined, term} -> {{:undefined, term}, 0}
|
||||
{ip, port} -> {ip, port}
|
||||
end
|
||||
end
|
||||
|
||||
defp peercert(socket) do
|
||||
case ThousandIsland.Socket.peercert(socket) do
|
||||
{:ok, cert} -> {:ok, cert}
|
||||
{:error, :no_peercert} -> {:ok, nil}
|
||||
{:error, :not_secure} -> {:ok, nil}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@spec transport_error!(term(), term()) :: no_return()
|
||||
defp transport_error!(message, error) do
|
||||
raise Bandit.TransportError, message: message, error: error
|
||||
end
|
||||
end
|
||||
250
phoenix/deps/bandit/lib/bandit/telemetry.ex
Normal file
250
phoenix/deps/bandit/lib/bandit/telemetry.ex
Normal file
@@ -0,0 +1,250 @@
|
||||
defmodule Bandit.Telemetry do
|
||||
@moduledoc """
|
||||
The following telemetry spans are emitted by bandit
|
||||
|
||||
## `[:bandit, :request, *]`
|
||||
|
||||
Represents Bandit handling a specific client HTTP request
|
||||
|
||||
This span is started by the following event:
|
||||
|
||||
* `[:bandit, :request, :start]`
|
||||
|
||||
Represents the start of the span
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `connection_telemetry_span_context`: The span context of the Thousand Island `:connection`
|
||||
span which contains this request
|
||||
* `conn`: The `Plug.Conn` representing this connection. Not present in cases where `error`
|
||||
is also set and the nature of error is such that Bandit was unable to successfully build
|
||||
the conn
|
||||
* `plug`: The Plug which is being used to serve this request. Specified as `{plug_module, plug_opts}`
|
||||
|
||||
This span is ended by the following event:
|
||||
|
||||
* `[:bandit, :request, :stop]`
|
||||
|
||||
Represents the end of the span
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
* `duration`: The span duration, in `:native` units
|
||||
* `req_header_end_time`: The time that header reading completed, in `:native` units
|
||||
* `req_body_start_time`: The time that request body reading started, in `:native` units.
|
||||
* `req_body_end_time`: The time that request body reading completed, in `:native` units
|
||||
* `req_body_bytes`: The length of the request body, in octets
|
||||
* `resp_start_time`: The time that the response started, in `:native` units
|
||||
* `resp_end_time`: The time that the response completed, in `:native` units
|
||||
* `resp_body_bytes`: The length of the response body, in octets. If the response is
|
||||
compressed, this is the size of the compressed payload as sent on the wire
|
||||
* `resp_uncompressed_body_bytes`: The length of the original, uncompressed body. Only
|
||||
included for responses which are compressed
|
||||
* `resp_compression_method`: The method of compression, as sent in the `Content-Encoding`
|
||||
header of the response. Only included for responses which are compressed
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `connection_telemetry_span_context`: The span context of the Thousand Island `:connection`
|
||||
span which contains this request
|
||||
* `conn`: The `Plug.Conn` representing this connection. Not present in cases where `error`
|
||||
is also set and the nature of error is such that Bandit was unable to successfully build
|
||||
the conn
|
||||
* `plug`: The Plug which is being used to serve this request. Specified as `{plug_module, plug_opts}`
|
||||
* `error`: The error that caused the span to end, if it ended in error
|
||||
|
||||
The following events may be emitted within this span:
|
||||
|
||||
* `[:bandit, :request, :exception]`
|
||||
|
||||
The request for this span ended unexpectedly
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `connection_telemetry_span_context`: The span context of the Thousand Island `:connection`
|
||||
span which contains this request
|
||||
* `conn`: The `Plug.Conn` representing this connection. Not present in cases where `error`
|
||||
is also set and the nature of error is such that Bandit was unable to successfully build
|
||||
the conn
|
||||
* `plug`: The Plug which is being used to serve this request. Specified as `{plug_module, plug_opts}`
|
||||
* `kind`: The kind of unexpected condition, typically `:exit`
|
||||
* `exception`: The exception which caused this unexpected termination. May be an exception
|
||||
or an arbitrary value when the event was an uncaught throw or an exit
|
||||
* `stacktrace`: The stacktrace of the location which caused this unexpected termination
|
||||
|
||||
## `[:bandit, :websocket, *]`
|
||||
|
||||
Represents Bandit handling a WebSocket connection
|
||||
|
||||
This span is started by the following event:
|
||||
|
||||
* `[:bandit, :websocket, :start]`
|
||||
|
||||
Represents the start of the span
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
* `compress`: Details about the compression configuration for this connection
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `connection_telemetry_span_context`: The span context of the Thousand Island `:connection`
|
||||
span which contains this request
|
||||
* `websock`: The WebSock which is being used to serve this request. Specified as `websock_module`
|
||||
|
||||
This span is ended by the following event:
|
||||
|
||||
* `[:bandit, :websocket, :stop]`
|
||||
|
||||
Represents the end of the span
|
||||
|
||||
This event contains the following measurements:
|
||||
|
||||
* `monotonic_time`: The time of this event, in `:native` units
|
||||
* `duration`: The span duration, in `:native` units
|
||||
* `recv_text_frame_count`: The number of text frames received
|
||||
* `recv_text_frame_bytes`: The total number of bytes received in the payload of text frames
|
||||
* `recv_binary_frame_count`: The number of binary frames received
|
||||
* `recv_binary_frame_bytes`: The total number of bytes received in the payload of binary frames
|
||||
* `recv_ping_frame_count`: The number of ping frames received
|
||||
* `recv_ping_frame_bytes`: The total number of bytes received in the payload of ping frames
|
||||
* `recv_pong_frame_count`: The number of pong frames received
|
||||
* `recv_pong_frame_bytes`: The total number of bytes received in the payload of pong frames
|
||||
* `recv_connection_close_frame_count`: The number of connection close frames received
|
||||
* `recv_connection_close_frame_bytes`: The total number of bytes received in the payload of connection close frames
|
||||
* `recv_continuation_frame_count`: The number of continuation frames received
|
||||
* `recv_continuation_frame_bytes`: The total number of bytes received in the payload of continuation frames
|
||||
* `send_text_frame_count`: The number of text frames sent
|
||||
* `send_text_frame_bytes`: The total number of bytes sent in the payload of text frames
|
||||
* `send_binary_frame_count`: The number of binary frames sent
|
||||
* `send_binary_frame_bytes`: The total number of bytes sent in the payload of binary frames
|
||||
* `send_ping_frame_count`: The number of ping frames sent
|
||||
* `send_ping_frame_bytes`: The total number of bytes sent in the payload of ping frames
|
||||
* `send_pong_frame_count`: The number of pong frames sent
|
||||
* `send_pong_frame_bytes`: The total number of bytes sent in the payload of pong frames
|
||||
* `send_connection_close_frame_count`: The number of connection close frames sent
|
||||
* `send_connection_close_frame_bytes`: The total number of bytes sent in the payload of connection close frames
|
||||
* `send_continuation_frame_count`: The number of continuation frames sent
|
||||
* `send_continuation_frame_bytes`: The total number of bytes sent in the payload of continuation frames
|
||||
|
||||
This event contains the following metadata:
|
||||
|
||||
* `telemetry_span_context`: A unique identifier for this span
|
||||
* `origin_telemetry_span_context`: The span context of the Bandit `:request` span from which
|
||||
this connection originated
|
||||
* `connection_telemetry_span_context`: The span context of the Thousand Island `:connection`
|
||||
span which contains this request
|
||||
* `websock`: The WebSock which is being used to serve this request. Specified as `websock_module`
|
||||
* `error`: The error that caused the span to end, if it ended in error
|
||||
"""
|
||||
|
||||
defstruct span_name: nil, telemetry_span_context: nil, start_time: nil, start_metadata: nil
|
||||
|
||||
@typep span_name :: atom()
|
||||
@opaque t :: %__MODULE__{
|
||||
span_name: span_name(),
|
||||
telemetry_span_context: reference(),
|
||||
start_time: integer(),
|
||||
start_metadata: :telemetry.event_metadata()
|
||||
}
|
||||
|
||||
@app_name :bandit
|
||||
|
||||
@doc false
|
||||
@spec start_span(span_name(), :telemetry.event_measurements(), :telemetry.event_metadata()) ::
|
||||
t()
|
||||
def start_span(span_name, measurements \\ %{}, metadata \\ %{}) do
|
||||
measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0)
|
||||
telemetry_span_context = make_ref()
|
||||
metadata = Map.put(metadata, :telemetry_span_context, telemetry_span_context)
|
||||
event([span_name, :start], measurements, metadata)
|
||||
|
||||
%__MODULE__{
|
||||
span_name: span_name,
|
||||
telemetry_span_context: telemetry_span_context,
|
||||
start_time: measurements[:monotonic_time],
|
||||
start_metadata: metadata
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec stop_span(t(), :telemetry.event_measurements(), :telemetry.event_metadata()) :: :ok
|
||||
def stop_span(span, measurements \\ %{}, metadata \\ %{}) do
|
||||
measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0)
|
||||
|
||||
measurements =
|
||||
Map.put(measurements, :duration, measurements[:monotonic_time] - span.start_time)
|
||||
|
||||
metadata = Map.merge(span.start_metadata, metadata)
|
||||
|
||||
untimed_span_event(span, :stop, measurements, metadata)
|
||||
end
|
||||
|
||||
@spec span_exception(t(), Exception.kind(), Exception.t() | term(), Exception.stacktrace()) ::
|
||||
:ok
|
||||
def span_exception(span, kind, reason, stacktrace) do
|
||||
# Using :exit for backwards-compatibility with Bandit =< 1.5.7
|
||||
kind = if kind == :error, do: :exit, else: kind
|
||||
|
||||
metadata =
|
||||
Map.merge(span.start_metadata, %{
|
||||
kind: kind,
|
||||
exception: reason,
|
||||
stacktrace: stacktrace
|
||||
})
|
||||
|
||||
span_event(span, :exception, %{}, metadata)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec span_event(t(), span_name(), :telemetry.event_measurements(), :telemetry.event_metadata()) ::
|
||||
:ok
|
||||
def span_event(span, name, measurements \\ %{}, metadata \\ %{}) do
|
||||
measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0)
|
||||
untimed_span_event(span, name, measurements, metadata)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec untimed_span_event(
|
||||
t(),
|
||||
span_name(),
|
||||
:telemetry.event_measurements(),
|
||||
:telemetry.event_metadata()
|
||||
) :: :ok
|
||||
def untimed_span_event(span, name, measurements \\ %{}, metadata \\ %{}) do
|
||||
metadata = Map.put(metadata, :telemetry_span_context, span.telemetry_span_context)
|
||||
event([span.span_name, name], measurements, metadata)
|
||||
end
|
||||
|
||||
@spec monotonic_time :: integer()
|
||||
defdelegate monotonic_time, to: System
|
||||
|
||||
@spec event(
|
||||
:telemetry.event_name(),
|
||||
:telemetry.event_measurements(),
|
||||
:telemetry.event_metadata()
|
||||
) :: :ok
|
||||
defp event(suffix, measurements, metadata) do
|
||||
:telemetry.execute([@app_name | suffix], measurements, metadata)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec telemetry_span_context(t()) :: reference()
|
||||
def telemetry_span_context(span) do
|
||||
span.telemetry_span_context
|
||||
end
|
||||
end
|
||||
150
phoenix/deps/bandit/lib/bandit/trace.ex
Normal file
150
phoenix/deps/bandit/lib/bandit/trace.ex
Normal file
@@ -0,0 +1,150 @@
|
||||
defmodule Bandit.Trace do
|
||||
@moduledoc """
|
||||
**THIS MODULE IS EXPERIMENTAL AND SUBJECT TO CHANGE**
|
||||
|
||||
Helper functions to provide visibility into runtime errors within a running Bandit instance
|
||||
|
||||
Can be used within an IEx session attached to a running Bandit instance, as follows:
|
||||
|
||||
```
|
||||
iex> Bandit.Trace.start_tracing()
|
||||
... # Wait for traces to show up whenever exceptions are raised
|
||||
iex> Bandit.Trace.stop_tracing()
|
||||
```
|
||||
|
||||
It can also be started within your application by adding `Bandit.Trace` to your process tree.
|
||||
|
||||
`Bandit.Trace` will emit a trace on every exception that Bandit sees (both those emitted from
|
||||
within your Plug as well as internal ones due to protocol violations and the like). These traces
|
||||
consist of a complete dump of all telemetry events that occur in the offending request's parent
|
||||
connection.
|
||||
|
||||
Tracing imposes a modest but non-zero load; it *should* be safe to run in most production
|
||||
environments, but it is not intended to run on an ongoing basis.
|
||||
|
||||
By default, `Bandit.Trace` maintains a FIFO log of the last 10000 telemetry events that Bandit
|
||||
has emitted. Events which correlate to the parent connection which have been evicted from this
|
||||
queue will not be included in this output.
|
||||
|
||||
**WARNING** The emitted logs contains a *complete* copy of your request's Plug data, as well as *all* data
|
||||
sent and received on all requests which are contained in the output. It is therefore of the utmost
|
||||
importance that you carefully redact the output before sharing it publicly.
|
||||
"""
|
||||
|
||||
defstruct queue: nil, size: 0, max_size: 10_000, trace_on_exception: true
|
||||
|
||||
use GenServer
|
||||
|
||||
@events [
|
||||
[:bandit, :request, :start],
|
||||
[:bandit, :request, :stop],
|
||||
[:bandit, :request, :exception],
|
||||
[:bandit, :websocket, :start],
|
||||
[:bandit, :websocket, :stop],
|
||||
[:thousand_island, :connection, :start],
|
||||
[:thousand_island, :connection, :stop],
|
||||
[:thousand_island, :connection, :ready],
|
||||
[:thousand_island, :connection, :async_recv],
|
||||
[:thousand_island, :connection, :recv],
|
||||
[:thousand_island, :connection, :recv_error],
|
||||
[:thousand_island, :connection, :send],
|
||||
[:thousand_island, :connection, :send_error],
|
||||
[:thousand_island, :connection, :sendfile],
|
||||
[:thousand_island, :connection, :sendfile_error],
|
||||
[:thousand_island, :connection, :socket_shutdown]
|
||||
]
|
||||
|
||||
@doc """
|
||||
Start tracing of all Bandit requests
|
||||
|
||||
See module documentation for intended usage. Accepts the following options:
|
||||
|
||||
* `max_size`: The size of the telemetry event queue to maintain. By default, `Bandit.Trace` maintains a
|
||||
queue of the last 10000 telemetry events
|
||||
* `trace_on_exception`: Whether or not to emit traces when an error is raised within
|
||||
Bandit. Defaults to `true`
|
||||
"""
|
||||
def start_tracing(opts \\ []), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
|
||||
@doc """
|
||||
Stop any active trace session
|
||||
"""
|
||||
def stop_tracing, do: GenServer.stop(__MODULE__)
|
||||
|
||||
def handle_event(event, measurements, metadata, pid),
|
||||
do: GenServer.cast(pid, {:event, {event, measurements, metadata, :os.perf_counter()}})
|
||||
|
||||
@doc """
|
||||
Return the complete queue of telemetry events that `Bandit.Trace` is currently tracking
|
||||
"""
|
||||
def get_events, do: GenServer.call(__MODULE__, :get_events)
|
||||
|
||||
@impl GenServer
|
||||
def init(opts) do
|
||||
_ = :telemetry.attach_many(self(), @events, &__MODULE__.handle_event/4, self())
|
||||
{:ok, struct!(%__MODULE__{queue: :queue.new()}, opts)}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def terminate(_, _), do: :telemetry.detach(self())
|
||||
|
||||
@impl GenServer
|
||||
def handle_cast({:event, event}, state) do
|
||||
state
|
||||
|> maybe_pop()
|
||||
|> push(event)
|
||||
|> tap(&maybe_trace(&1, event))
|
||||
|> then(&{:noreply, &1})
|
||||
end
|
||||
|
||||
defp maybe_pop(%{size: size, max_size: max_size} = state) when size >= max_size,
|
||||
do: maybe_pop(%{state | queue: :queue.drop(state.queue), size: size - 1})
|
||||
|
||||
defp maybe_pop(state), do: state
|
||||
|
||||
defp push(state, event),
|
||||
do: %{state | queue: :queue.in(event, state.queue), size: state.size + 1}
|
||||
|
||||
defp maybe_trace(
|
||||
%{trace_on_exception: true} = state,
|
||||
{[:bandit, :request, :exception], _, metadata, _}
|
||||
) do
|
||||
connection_span_context = Map.get(metadata, :connection_telemetry_span_context)
|
||||
|
||||
IO.puts("======================================")
|
||||
IO.puts("Starting telemetry trace for exception")
|
||||
IO.puts("======================================")
|
||||
|
||||
:queue.to_list(state.queue)
|
||||
|> Enum.filter(fn {_, _, metadata, _} ->
|
||||
Map.get(metadata, :telemetry_span_context) == connection_span_context ||
|
||||
Map.get(metadata, :connection_telemetry_span_context) == connection_span_context
|
||||
end)
|
||||
|> format_list()
|
||||
|> inspect(limit: :infinity, pretty: true, printable_limit: :infinity)
|
||||
|> IO.puts()
|
||||
|
||||
IO.puts("=======================================")
|
||||
IO.puts("Completed telemetry trace for exception")
|
||||
IO.puts("=======================================")
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp maybe_trace(_state, _event), do: :ok
|
||||
|
||||
@impl GenServer
|
||||
def handle_call(:get_events, _from, state),
|
||||
do: {:reply, :queue.to_list(state.queue) |> format_list(), state}
|
||||
|
||||
defp format_list([]), do: :ok
|
||||
|
||||
defp format_list([{_, _, _, start_time} | _] = events),
|
||||
do: Enum.map(events, &format_tuple(&1, start_time))
|
||||
|
||||
defp format_tuple({event, measurements, metadata, time}, start_time) do
|
||||
time = :erlang.convert_time_unit(time - start_time, :perf_counter, :microsecond)
|
||||
%{telemetry_span_context: span_id} = metadata
|
||||
{time, span_id, event, measurements, metadata}
|
||||
end
|
||||
end
|
||||
6
phoenix/deps/bandit/lib/bandit/transport_error.ex
Normal file
6
phoenix/deps/bandit/lib/bandit/transport_error.ex
Normal file
@@ -0,0 +1,6 @@
|
||||
defmodule Bandit.TransportError do
|
||||
# Represents an error coming from the underlying transport which cannot be signalled back to the
|
||||
# client by conventional means within the request. Examples include TCP socket closures and
|
||||
# errors in the case of HTTP/1, and stream resets in HTTP/2
|
||||
defexception message: nil, error: nil
|
||||
end
|
||||
58
phoenix/deps/bandit/lib/bandit/websocket/README.md
Normal file
58
phoenix/deps/bandit/lib/bandit/websocket/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# WebSocket Handler
|
||||
|
||||
Included in this folder is a complete `ThousandIsland.Handler` based implementation of WebSockets
|
||||
as defined in [RFC 6455](https://datatracker.ietf.org/doc/rfc6455).
|
||||
|
||||
## Upgrade mechanism
|
||||
|
||||
A good overview of this process is contained in this [ElixirConf EU
|
||||
talk](https://www.youtube.com/watch?v=usKLrYl4zlY).
|
||||
|
||||
Upgrading an HTTP connection to a WebSocket connection is coordinated by code
|
||||
contained within several libraries, including Bandit,
|
||||
[WebSockAdapter](https://github.com/phoenixframework/websock_adapter), and
|
||||
[Plug](https://github.com/elixir-plug/plug).
|
||||
|
||||
The HTTP request containing the upgrade request is first passed to the user's
|
||||
application as a standard Plug call. After inspecting the request and deeming it
|
||||
a suitable upgrade candidate (via whatever policy the application dictates), the
|
||||
user indicates a desire to upgrade the connection to a WebSocket by calling
|
||||
`WebSockAdapter.upgrade/4`, which checks that the request is a valid WebSocket
|
||||
upgrade request, and then calls `Plug.Conn.upgrade_adapter/3` to signal to
|
||||
Bandit that the connection should be upgraded at the conclusion of the request.
|
||||
At the conclusion of the `Plug.call/2` callback, `Bandit.Pipeline` will then
|
||||
attempt to upgrade the underlying connection. As part of this upgrade process,
|
||||
`Bandit.DelegatingHandler` will switch the Handler for the connection to be
|
||||
`Bandit.WebSocket.Handler`. This will cause any future communication after the
|
||||
upgrade process to be handled directly by Bandit's WebSocket stack.
|
||||
|
||||
## Process model
|
||||
|
||||
Within a Bandit server, a WebSocket connection is modeled as a single process.
|
||||
This process is directly tied to the lifecycle of the underlying WebSocket
|
||||
connection; when upgrading from HTTP/1, the existing HTTP/1 handler process
|
||||
'magically' becomes a WebSocket process by changing which Handler the
|
||||
`Bandit.DelegatingHandler` delegates to.
|
||||
|
||||
The execution model to handle a given request is quite straightforward: at
|
||||
upgrade time, the `Bandit.DelegatingHandler` will call `handle_connection/2` to
|
||||
allow the WebSocket handler to initialize any startup state. Connection state is
|
||||
modeled by the `Bandit.WebSocket.Connection` struct and module.
|
||||
|
||||
All data subsequently received by the underlying [Thousand
|
||||
Island](https://github.com/mtrudel/thousand_island) library will result in
|
||||
a call to `Bandit.WebSocket.Handler.handle_data/3`, which will then attempt to
|
||||
parse the data into one or more WebSocket frames. Once a frame has been
|
||||
constructed, it is them passed through to the configured `WebSock` handler by
|
||||
way of the underlying `Bandit.WebSocket.Connection`.
|
||||
|
||||
# Testing
|
||||
|
||||
All of this is exhaustively tested. Tests are broken up primarily into `protocol_test.exs`, which
|
||||
is concerned with aspects of the implementation relating to protocol conformance and
|
||||
client-facing concerns, while `sock_test.exs` is concerned with aspects of the implementation
|
||||
having to do with the WebSock API and application-facing concerns. There are also more
|
||||
unit-style tests covering frame serialization and deserialization.
|
||||
|
||||
In addition, the `autobahn` conformance suite is run via a `System` wrapper & executes the entirety
|
||||
of the suite against a running Bandit server.
|
||||
315
phoenix/deps/bandit/lib/bandit/websocket/connection.ex
Normal file
315
phoenix/deps/bandit/lib/bandit/websocket/connection.ex
Normal file
@@ -0,0 +1,315 @@
|
||||
defmodule Bandit.WebSocket.Connection do
|
||||
@moduledoc false
|
||||
# Implementation of a WebSocket lifecycle, implemented using a Socket protocol for communication
|
||||
|
||||
alias Bandit.WebSocket.{Frame, PerMessageDeflate, Socket}
|
||||
|
||||
defstruct websock: nil,
|
||||
websock_state: nil,
|
||||
state: :open,
|
||||
compress: nil,
|
||||
opts: [],
|
||||
fragment_frame: nil,
|
||||
span: nil,
|
||||
metrics: %{}
|
||||
|
||||
@typedoc "Connection state"
|
||||
@type state :: :open | :closing | :closed
|
||||
|
||||
@typedoc "Encapsulates the state of a WebSocket connection"
|
||||
@type t :: %__MODULE__{
|
||||
websock: WebSock.impl(),
|
||||
websock_state: WebSock.state(),
|
||||
state: state(),
|
||||
compress: PerMessageDeflate.t() | nil,
|
||||
opts: keyword(),
|
||||
fragment_frame: Frame.Text.t() | Frame.Binary.t() | nil,
|
||||
span: Bandit.Telemetry.t(),
|
||||
metrics: map()
|
||||
}
|
||||
|
||||
def init(websock, websock_state, connection_opts, socket) do
|
||||
compress = Keyword.get(connection_opts, :compress)
|
||||
|
||||
connection_telemetry_span_context =
|
||||
ThousandIsland.Socket.telemetry_span(socket).telemetry_span_context
|
||||
|
||||
span =
|
||||
Bandit.Telemetry.start_span(:websocket, %{compress: compress}, %{
|
||||
connection_telemetry_span_context: connection_telemetry_span_context,
|
||||
websock: websock
|
||||
})
|
||||
|
||||
instance = %__MODULE__{
|
||||
websock: websock,
|
||||
websock_state: websock_state,
|
||||
compress: compress,
|
||||
opts: connection_opts,
|
||||
span: span
|
||||
}
|
||||
|
||||
websock.init(websock_state) |> handle_continutation(socket, instance)
|
||||
end
|
||||
|
||||
def handle_frame(frame, socket, %{fragment_frame: nil} = connection) do
|
||||
connection = do_recv_metrics(frame, connection)
|
||||
|
||||
case frame do
|
||||
%Frame.Continuation{} ->
|
||||
do_error(1002, "Received unexpected continuation frame (RFC6455§5.4)", socket, connection)
|
||||
|
||||
%Frame.Text{fin: true, compressed: true} = frame ->
|
||||
do_inflate(frame, socket, connection)
|
||||
|
||||
%Frame.Text{fin: true} = frame ->
|
||||
if !Keyword.get(connection.opts, :validate_text_frames, true) || String.valid?(frame.data) do
|
||||
connection.websock.handle_in({frame.data, opcode: :text}, connection.websock_state)
|
||||
|> handle_continutation(socket, connection)
|
||||
else
|
||||
do_error(1007, "Received non UTF-8 text frame (RFC6455§8.1)", socket, connection)
|
||||
end
|
||||
|
||||
%Frame.Text{fin: false} = frame ->
|
||||
{:continue, %{connection | fragment_frame: frame}}
|
||||
|
||||
%Frame.Binary{fin: true, compressed: true} = frame ->
|
||||
do_inflate(frame, socket, connection)
|
||||
|
||||
%Frame.Binary{fin: true} = frame ->
|
||||
connection.websock.handle_in({frame.data, opcode: :binary}, connection.websock_state)
|
||||
|> handle_continutation(socket, connection)
|
||||
|
||||
%Frame.Binary{fin: false} = frame ->
|
||||
{:continue, %{connection | fragment_frame: frame}}
|
||||
|
||||
frame ->
|
||||
handle_control_frame(frame, socket, connection)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_frame(frame, socket, %{fragment_frame: fragment_frame} = connection)
|
||||
when not is_nil(fragment_frame) do
|
||||
connection = do_recv_metrics(frame, connection)
|
||||
|
||||
case frame do
|
||||
%Frame.Continuation{fin: true} = frame ->
|
||||
data = IO.iodata_to_binary([connection.fragment_frame.data | frame.data])
|
||||
frame = %{connection.fragment_frame | fin: true, data: data}
|
||||
handle_frame(frame, socket, %{connection | fragment_frame: nil})
|
||||
|
||||
%Frame.Continuation{fin: false} = frame ->
|
||||
data = [connection.fragment_frame.data | frame.data]
|
||||
frame = %{connection.fragment_frame | fin: true, data: data}
|
||||
{:continue, %{connection | fragment_frame: frame}}
|
||||
|
||||
%Frame.Text{} ->
|
||||
do_error(1002, "Received unexpected text frame (RFC6455§5.4)", socket, connection)
|
||||
|
||||
%Frame.Binary{} ->
|
||||
do_error(1002, "Received unexpected binary frame (RFC6455§5.4)", socket, connection)
|
||||
|
||||
frame ->
|
||||
handle_control_frame(frame, socket, connection)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_control_frame(frame, socket, connection) do
|
||||
case frame do
|
||||
%Frame.ConnectionClose{} = frame ->
|
||||
# This is a bit of a subtle case, see RFC6455§7.4.1-2
|
||||
reply_code =
|
||||
case frame.code do
|
||||
code when code in 1000..1003 or code in 1007..1011 or code > 2999 -> 1000
|
||||
_code -> 1002
|
||||
end
|
||||
|
||||
{:continue, connection} = do_stop(reply_code, :remote, socket, connection)
|
||||
{:close, %{connection | state: :closed, compress: nil}}
|
||||
|
||||
%Frame.Ping{} = frame ->
|
||||
connection =
|
||||
Socket.send_frame(socket, {:pong, frame.data}, false)
|
||||
|> do_send_metrics(connection)
|
||||
|
||||
if function_exported?(connection.websock, :handle_control, 2) do
|
||||
connection.websock.handle_control({frame.data, opcode: :ping}, connection.websock_state)
|
||||
|> handle_continutation(socket, connection)
|
||||
else
|
||||
{:continue, connection}
|
||||
end
|
||||
|
||||
%Frame.Pong{} = frame ->
|
||||
if function_exported?(connection.websock, :handle_control, 2) do
|
||||
connection.websock.handle_control({frame.data, opcode: :pong}, connection.websock_state)
|
||||
|> handle_continutation(socket, connection)
|
||||
else
|
||||
{:continue, connection}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_recv_metrics(frame, connection) do
|
||||
metrics =
|
||||
Bandit.WebSocket.Frame.recv_metrics(frame)
|
||||
|> Enum.reduce(connection.metrics, fn {key, value}, metrics ->
|
||||
Map.update(metrics, key, value, &(&1 + value))
|
||||
end)
|
||||
|
||||
%{connection | metrics: metrics}
|
||||
end
|
||||
|
||||
defp do_send_metrics(metrics, connection) do
|
||||
metrics =
|
||||
metrics
|
||||
|> Enum.reduce(connection.metrics, fn {key, value}, metrics ->
|
||||
Map.update(metrics, key, value, &(&1 + value))
|
||||
end)
|
||||
|
||||
%{connection | metrics: metrics}
|
||||
end
|
||||
|
||||
def handle_close(socket, connection), do: do_error(1006, :closed, socket, connection)
|
||||
|
||||
# Some uncertainty if this should be 1000 or 1001 @ https://github.com/mtrudel/bandit/issues/89
|
||||
def handle_shutdown(socket, connection), do: do_stop(1000, :shutdown, socket, connection)
|
||||
|
||||
def handle_error({:deserializing, :max_frame_size_exceeded = reason}, socket, connection),
|
||||
do: do_error(1009, reason, socket, connection)
|
||||
|
||||
def handle_error({:deserializing, reason}, socket, connection),
|
||||
do: do_error(1002, reason, socket, connection)
|
||||
|
||||
def handle_error(reason, socket, connection), do: do_error(1011, reason, socket, connection)
|
||||
|
||||
def handle_timeout(socket, connection), do: do_error(1002, :timeout, socket, connection)
|
||||
|
||||
def handle_info(msg, socket, connection) do
|
||||
connection.websock.handle_info(msg, connection.websock_state)
|
||||
|> handle_continutation(socket, connection)
|
||||
end
|
||||
|
||||
defp handle_continutation(continutation, socket, connection) do
|
||||
case continutation do
|
||||
{:ok, websock_state} ->
|
||||
{:continue, %{connection | websock_state: websock_state}}
|
||||
|
||||
{:reply, _status, msg, websock_state} ->
|
||||
do_deflate(msg, socket, %{connection | websock_state: websock_state})
|
||||
|
||||
{:push, msg, websock_state} ->
|
||||
do_deflate(msg, socket, %{connection | websock_state: websock_state})
|
||||
|
||||
{:stop, :normal, websock_state} ->
|
||||
do_stop(1000, :normal, socket, %{connection | websock_state: websock_state})
|
||||
|
||||
{:stop, :normal, code, websock_state} ->
|
||||
do_stop(code, :normal, socket, %{connection | websock_state: websock_state})
|
||||
|
||||
{:stop, :normal, code, msg, websock_state} ->
|
||||
case do_deflate(msg, socket, %{connection | websock_state: websock_state}) do
|
||||
{:continue, connection} -> do_stop(code, :normal, socket, connection)
|
||||
other -> other
|
||||
end
|
||||
|
||||
{:stop, {:shutdown, :disconnected}, websock_state} ->
|
||||
do_stop(1000, :normal, socket, %{connection | websock_state: websock_state})
|
||||
|
||||
{:stop, {:shutdown, :restart}, websock_state} ->
|
||||
do_stop(1012, :normal, socket, %{connection | websock_state: websock_state})
|
||||
|
||||
{:stop, reason, websock_state} ->
|
||||
do_error(1011, reason, socket, %{connection | websock_state: websock_state})
|
||||
|
||||
{:stop, reason, code, websock_state} ->
|
||||
do_error(code, reason, socket, %{connection | websock_state: websock_state})
|
||||
|
||||
{:stop, reason, code, msg, websock_state} ->
|
||||
case do_deflate(msg, socket, %{connection | websock_state: websock_state}) do
|
||||
{:continue, connection} -> do_error(code, reason, socket, connection)
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_stop(code, reason, socket, connection) do
|
||||
if connection.state == :open do
|
||||
if function_exported?(connection.websock, :terminate, 2) do
|
||||
connection.websock.terminate(reason, connection.websock_state)
|
||||
end
|
||||
|
||||
_ = Socket.close(socket, code)
|
||||
if connection.compress, do: PerMessageDeflate.close(connection.compress)
|
||||
Bandit.Telemetry.stop_span(connection.span, connection.metrics)
|
||||
end
|
||||
|
||||
{:continue, %{connection | state: :closing, compress: nil}}
|
||||
end
|
||||
|
||||
defp do_error(code, reason, socket, connection) do
|
||||
if connection.state == :open do
|
||||
if function_exported?(connection.websock, :terminate, 2) do
|
||||
connection.websock.terminate(maybe_wrap_reason(reason), connection.websock_state)
|
||||
end
|
||||
|
||||
_ = Socket.close(socket, code)
|
||||
if connection.compress, do: PerMessageDeflate.close(connection.compress)
|
||||
Bandit.Telemetry.stop_span(connection.span, connection.metrics, %{error: reason})
|
||||
end
|
||||
|
||||
{:error, reason, %{connection | state: :closed, compress: nil}}
|
||||
end
|
||||
|
||||
defp maybe_wrap_reason(:timeout), do: :timeout
|
||||
defp maybe_wrap_reason(reason), do: {:error, reason}
|
||||
|
||||
defp do_deflate(msgs, socket, connection) when is_list(msgs) do
|
||||
Enum.reduce(msgs, {:continue, connection}, fn
|
||||
msg, {:continue, connection} -> do_deflate(msg, socket, connection)
|
||||
_msg, other -> other
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_deflate({opcode, data} = msg, socket, connection) when opcode in [:text, :binary] do
|
||||
case PerMessageDeflate.deflate(data, connection.compress) do
|
||||
{:ok, data, compress} ->
|
||||
connection =
|
||||
Socket.send_frame(socket, {opcode, data}, true)
|
||||
|> do_send_metrics(connection)
|
||||
|
||||
{:continue, %{connection | compress: compress}}
|
||||
|
||||
{:error, :no_compress} ->
|
||||
connection =
|
||||
Socket.send_frame(socket, msg, false)
|
||||
|> do_send_metrics(connection)
|
||||
|
||||
{:continue, connection}
|
||||
|
||||
{:error, reason} ->
|
||||
do_error(1007, "Deflation error: #{inspect(reason)}", socket, connection)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_deflate({opcode, _data} = msg, socket, connection) when opcode in [:ping, :pong] do
|
||||
connection =
|
||||
Socket.send_frame(socket, msg, false)
|
||||
|> do_send_metrics(connection)
|
||||
|
||||
{:continue, connection}
|
||||
end
|
||||
|
||||
defp do_inflate(frame, socket, connection) do
|
||||
case PerMessageDeflate.inflate(frame.data, connection.compress) do
|
||||
{:ok, data, compress} ->
|
||||
frame = %{frame | data: data, compressed: false}
|
||||
connection = %{connection | compress: compress}
|
||||
handle_frame(frame, socket, connection)
|
||||
|
||||
{:error, :no_compress} ->
|
||||
do_error(1002, "Received unexpected compressed frame (RFC6455§5.2)", socket, connection)
|
||||
|
||||
{:error, _reason} ->
|
||||
do_error(1007, "Inflation error", socket, connection)
|
||||
end
|
||||
end
|
||||
end
|
||||
205
phoenix/deps/bandit/lib/bandit/websocket/frame.ex
Normal file
205
phoenix/deps/bandit/lib/bandit/websocket/frame.ex
Normal file
@@ -0,0 +1,205 @@
|
||||
defmodule Bandit.WebSocket.Frame do
|
||||
@moduledoc false
|
||||
|
||||
alias Bandit.WebSocket.Frame
|
||||
|
||||
@behaviour Bandit.Extractor
|
||||
|
||||
@typedoc "Indicates an opcode"
|
||||
@type opcode ::
|
||||
(binary :: 0x2)
|
||||
| (connection_close :: 0x8)
|
||||
| (continuation :: 0x0)
|
||||
| (ping :: 0x9)
|
||||
| (pong :: 0xA)
|
||||
| (text :: 0x1)
|
||||
|
||||
@typedoc "A valid WebSocket frame"
|
||||
@type frame ::
|
||||
Frame.Continuation.t()
|
||||
| Frame.Text.t()
|
||||
| Frame.Binary.t()
|
||||
| Frame.ConnectionClose.t()
|
||||
| Frame.Ping.t()
|
||||
| Frame.Pong.t()
|
||||
|
||||
@impl Bandit.Extractor
|
||||
@spec header_and_payload_length(binary(), non_neg_integer()) ::
|
||||
{:ok, {header_length :: integer(), payload_length :: integer()}}
|
||||
| {:error, :max_frame_size_exceeded | :client_frame_without_mask}
|
||||
| :more
|
||||
def header_and_payload_length(
|
||||
<<_fin::1, _compressed::1, _rsv::2, _opcode::4, 1::1, 127::7, length::64, _mask::32,
|
||||
_rest::binary>>,
|
||||
max_frame_size
|
||||
) do
|
||||
validate_max_frame_size(14, length, max_frame_size)
|
||||
end
|
||||
|
||||
def header_and_payload_length(
|
||||
<<_fin::1, _compressed::1, _rsv::2, _opcode::4, 1::1, 126::7, length::16, _mask::32,
|
||||
_rest::binary>>,
|
||||
max_frame_size
|
||||
) do
|
||||
validate_max_frame_size(8, length, max_frame_size)
|
||||
end
|
||||
|
||||
def header_and_payload_length(
|
||||
<<_fin::1, _compressed::1, _rsv::2, _opcode::4, 1::1, length::7, _mask::32,
|
||||
_rest::binary>>,
|
||||
max_frame_size
|
||||
)
|
||||
when length <= 125 do
|
||||
validate_max_frame_size(6, length, max_frame_size)
|
||||
end
|
||||
|
||||
def header_and_payload_length(
|
||||
<<_fin::1, _compressed::1, _rsv::2, _opcode::4, 0::1, _rest::binary>>,
|
||||
_max_frame_size
|
||||
) do
|
||||
{:error, :client_frame_without_mask}
|
||||
end
|
||||
|
||||
def header_and_payload_length(_msg, _max_frame_size) do
|
||||
:more
|
||||
end
|
||||
|
||||
defp validate_max_frame_size(header_length, payload_length, max_frame_size) do
|
||||
if max_frame_size != 0 and header_length + payload_length > max_frame_size do
|
||||
{:error, :max_frame_size_exceeded}
|
||||
else
|
||||
{:ok, {header_length, payload_length}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Bandit.Extractor
|
||||
@spec deserialize(binary(), module()) :: {:ok, frame()} | {:error, term()}
|
||||
def deserialize(
|
||||
<<fin::1, compressed::1, rsv::2, opcode::4, 1::1, 127::7, length::64, mask::32,
|
||||
payload::binary-size(length)>>,
|
||||
primitive_ops_module
|
||||
) do
|
||||
to_frame(fin, compressed, rsv, opcode, mask, payload, primitive_ops_module)
|
||||
end
|
||||
|
||||
def deserialize(
|
||||
<<fin::1, compressed::1, rsv::2, opcode::4, 1::1, 126::7, length::16, mask::32,
|
||||
payload::binary-size(length)>>,
|
||||
primitive_ops_module
|
||||
) do
|
||||
to_frame(fin, compressed, rsv, opcode, mask, payload, primitive_ops_module)
|
||||
end
|
||||
|
||||
def deserialize(
|
||||
<<fin::1, compressed::1, rsv::2, opcode::4, 1::1, length::7, mask::32,
|
||||
payload::binary-size(length)>>,
|
||||
primitive_ops_module
|
||||
) do
|
||||
to_frame(fin, compressed, rsv, opcode, mask, payload, primitive_ops_module)
|
||||
end
|
||||
|
||||
def deserialize(_msg, _primitive_ops_module) do
|
||||
{:error, :deserialization_failed}
|
||||
end
|
||||
|
||||
def recv_metrics(%frame_type{} = frame) do
|
||||
case frame_type do
|
||||
Frame.Continuation ->
|
||||
[
|
||||
recv_continuation_frame_count: 1,
|
||||
recv_continuation_frame_bytes: IO.iodata_length(frame.data)
|
||||
]
|
||||
|
||||
Frame.Text ->
|
||||
[recv_text_frame_count: 1, recv_text_frame_bytes: IO.iodata_length(frame.data)]
|
||||
|
||||
Frame.Binary ->
|
||||
[recv_binary_frame_count: 1, recv_binary_frame_bytes: IO.iodata_length(frame.data)]
|
||||
|
||||
Frame.ConnectionClose ->
|
||||
[
|
||||
recv_connection_close_frame_count: 1,
|
||||
recv_connection_close_frame_bytes: IO.iodata_length(frame.reason)
|
||||
]
|
||||
|
||||
Frame.Ping ->
|
||||
[recv_ping_frame_count: 1, recv_ping_frame_bytes: IO.iodata_length(frame.data)]
|
||||
|
||||
Frame.Pong ->
|
||||
[recv_pong_frame_count: 1, recv_pong_frame_bytes: IO.iodata_length(frame.data)]
|
||||
end
|
||||
end
|
||||
|
||||
def send_metrics(%frame_type{} = frame) do
|
||||
case frame_type do
|
||||
Frame.Continuation ->
|
||||
[
|
||||
send_continuation_frame_count: 1,
|
||||
send_continuation_frame_bytes: IO.iodata_length(frame.data)
|
||||
]
|
||||
|
||||
Frame.Text ->
|
||||
[send_text_frame_count: 1, send_text_frame_bytes: IO.iodata_length(frame.data)]
|
||||
|
||||
Frame.Binary ->
|
||||
[send_binary_frame_count: 1, send_binary_frame_bytes: IO.iodata_length(frame.data)]
|
||||
|
||||
Frame.ConnectionClose ->
|
||||
[
|
||||
send_connection_close_frame_count: 1,
|
||||
send_connection_close_frame_bytes: IO.iodata_length(frame.reason)
|
||||
]
|
||||
|
||||
Frame.Ping ->
|
||||
[send_ping_frame_count: 1, send_ping_frame_bytes: IO.iodata_length(frame.data)]
|
||||
|
||||
Frame.Pong ->
|
||||
[send_pong_frame_count: 1, send_pong_frame_bytes: IO.iodata_length(frame.data)]
|
||||
end
|
||||
end
|
||||
|
||||
defp to_frame(_fin, _compressed, rsv, _opcode, _mask, _payload, _primitive_ops_module)
|
||||
when rsv != 0x0 do
|
||||
{:error, "Received unsupported RSV flags #{rsv}"}
|
||||
end
|
||||
|
||||
defp to_frame(fin, compressed, 0x0, opcode, mask, payload, primitive_ops_module) do
|
||||
fin = fin == 0x1
|
||||
compressed = compressed == 0x1
|
||||
unmasked_payload = primitive_ops_module.ws_mask(payload, mask)
|
||||
|
||||
opcode
|
||||
|> case do
|
||||
0x0 -> Frame.Continuation.deserialize(fin, compressed, unmasked_payload)
|
||||
0x1 -> Frame.Text.deserialize(fin, compressed, unmasked_payload)
|
||||
0x2 -> Frame.Binary.deserialize(fin, compressed, unmasked_payload)
|
||||
0x8 -> Frame.ConnectionClose.deserialize(fin, compressed, unmasked_payload)
|
||||
0x9 -> Frame.Ping.deserialize(fin, compressed, unmasked_payload)
|
||||
0xA -> Frame.Pong.deserialize(fin, compressed, unmasked_payload)
|
||||
unknown -> {:error, "unknown opcode #{unknown}"}
|
||||
end
|
||||
end
|
||||
|
||||
defprotocol Serializable do
|
||||
@moduledoc false
|
||||
|
||||
@spec serialize(any()) :: [{Frame.opcode(), boolean(), boolean(), iodata()}]
|
||||
def serialize(frame)
|
||||
end
|
||||
|
||||
@spec serialize(frame()) :: iolist()
|
||||
def serialize(frame) do
|
||||
frame
|
||||
|> Serializable.serialize()
|
||||
|> Enum.map(fn {opcode, fin, compressed, payload} ->
|
||||
fin = if fin, do: 0x1, else: 0x0
|
||||
compressed = if compressed, do: 0x1, else: 0x0
|
||||
mask_and_length = payload |> IO.iodata_length() |> mask_and_length()
|
||||
[<<fin::1, compressed::1, 0x0::2, opcode::4>>, mask_and_length, payload]
|
||||
end)
|
||||
end
|
||||
|
||||
defp mask_and_length(length) when length <= 125, do: <<0::1, length::7>>
|
||||
defp mask_and_length(length) when length <= 65_535, do: <<0::1, 126::7, length::16>>
|
||||
defp mask_and_length(length), do: <<0::1, 127::7, length::64>>
|
||||
end
|
||||
20
phoenix/deps/bandit/lib/bandit/websocket/frame/binary.ex
Normal file
20
phoenix/deps/bandit/lib/bandit/websocket/frame/binary.ex
Normal file
@@ -0,0 +1,20 @@
|
||||
defmodule Bandit.WebSocket.Frame.Binary do
|
||||
@moduledoc false
|
||||
|
||||
defstruct fin: nil, compressed: false, data: <<>>
|
||||
|
||||
@typedoc "A WebSocket binary frame"
|
||||
@type t :: %__MODULE__{fin: boolean(), compressed: boolean(), data: iodata()}
|
||||
|
||||
@spec deserialize(boolean(), boolean(), iodata()) :: {:ok, t()}
|
||||
def deserialize(fin, compressed, payload) do
|
||||
{:ok, %__MODULE__{fin: fin, compressed: compressed, data: payload}}
|
||||
end
|
||||
|
||||
defimpl Bandit.WebSocket.Frame.Serializable do
|
||||
alias Bandit.WebSocket.Frame
|
||||
|
||||
@spec serialize(@for.t()) :: [{Frame.opcode(), boolean(), boolean(), iodata()}]
|
||||
def serialize(%@for{} = frame), do: [{0x2, frame.fin, frame.compressed, frame.data}]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
defmodule Bandit.WebSocket.Frame.ConnectionClose do
|
||||
@moduledoc false
|
||||
|
||||
defstruct code: nil, reason: <<>>
|
||||
|
||||
@typedoc "A WebSocket status code, or none at all"
|
||||
@type status_code :: non_neg_integer() | nil
|
||||
|
||||
@typedoc "A WebSocket connection close frame"
|
||||
@type t :: %__MODULE__{code: status_code(), reason: binary()}
|
||||
|
||||
@spec deserialize(boolean(), boolean(), iodata()) :: {:ok, t()} | {:error, term()}
|
||||
def deserialize(true, false, <<>>) do
|
||||
{:ok, %__MODULE__{}}
|
||||
end
|
||||
|
||||
def deserialize(true, false, <<code::16>>) do
|
||||
{:ok, %__MODULE__{code: code}}
|
||||
end
|
||||
|
||||
def deserialize(true, false, <<code::16, reason::binary>>) when byte_size(reason) <= 123 do
|
||||
if String.valid?(reason) do
|
||||
{:ok, %__MODULE__{code: code, reason: reason}}
|
||||
else
|
||||
{:error, "Received non UTF-8 connection close frame (RFC6455§5.5.1)"}
|
||||
end
|
||||
end
|
||||
|
||||
def deserialize(true, false, _payload) do
|
||||
{:error, "Invalid connection close payload (RFC6455§5.5)"}
|
||||
end
|
||||
|
||||
def deserialize(false, false, _payload) do
|
||||
{:error, "Cannot have a fragmented connection close frame (RFC6455§5.5)"}
|
||||
end
|
||||
|
||||
def deserialize(true, true, _payload) do
|
||||
{:error, "Cannot have a compressed connection close frame (RFC7692§6.1)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.WebSocket.Frame.Serializable do
|
||||
alias Bandit.WebSocket.Frame
|
||||
|
||||
@spec serialize(@for.t()) :: [{Frame.opcode(), boolean(), boolean(), iodata()}]
|
||||
def serialize(%@for{code: nil}), do: [{0x8, true, false, <<>>}]
|
||||
def serialize(%@for{reason: nil} = frame), do: [{0x8, true, false, <<frame.code::16>>}]
|
||||
def serialize(%@for{} = frame), do: [{0x8, true, false, [<<frame.code::16>>, frame.reason]}]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
defmodule Bandit.WebSocket.Frame.Continuation do
|
||||
@moduledoc false
|
||||
|
||||
defstruct fin: nil, data: <<>>
|
||||
|
||||
@typedoc "A WebSocket continuation frame"
|
||||
@type t :: %__MODULE__{fin: boolean(), data: iodata()}
|
||||
|
||||
@spec deserialize(boolean(), boolean(), iodata()) :: {:ok, t()} | {:error, term()}
|
||||
def deserialize(fin, false, payload) do
|
||||
{:ok, %__MODULE__{fin: fin, data: payload}}
|
||||
end
|
||||
|
||||
def deserialize(_fin, true, _payload) do
|
||||
{:error, "Cannot have a compressed continuation frame (RFC7692§6.1)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.WebSocket.Frame.Serializable do
|
||||
alias Bandit.WebSocket.Frame
|
||||
|
||||
@spec serialize(@for.t()) :: [{Frame.opcode(), boolean(), boolean(), iodata()}]
|
||||
def serialize(%@for{} = frame), do: [{0x0, frame.fin, false, frame.data}]
|
||||
end
|
||||
end
|
||||
32
phoenix/deps/bandit/lib/bandit/websocket/frame/ping.ex
Normal file
32
phoenix/deps/bandit/lib/bandit/websocket/frame/ping.ex
Normal file
@@ -0,0 +1,32 @@
|
||||
defmodule Bandit.WebSocket.Frame.Ping do
|
||||
@moduledoc false
|
||||
|
||||
defstruct data: <<>>
|
||||
|
||||
@typedoc "A WebSocket ping frame"
|
||||
@type t :: %__MODULE__{data: iodata()}
|
||||
|
||||
@spec deserialize(boolean(), boolean(), iodata()) :: {:ok, t()} | {:error, term()}
|
||||
def deserialize(true, false, <<data::binary>>) when byte_size(data) <= 125 do
|
||||
{:ok, %__MODULE__{data: data}}
|
||||
end
|
||||
|
||||
def deserialize(true, false, _payload) do
|
||||
{:error, "Invalid ping payload (RFC6455§5.5.2)"}
|
||||
end
|
||||
|
||||
def deserialize(false, false, _payload) do
|
||||
{:error, "Cannot have a fragmented ping frame (RFC6455§5.5.2)"}
|
||||
end
|
||||
|
||||
def deserialize(true, true, _payload) do
|
||||
{:error, "Cannot have a compressed ping frame (RFC7692§6.1)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.WebSocket.Frame.Serializable do
|
||||
alias Bandit.WebSocket.Frame
|
||||
|
||||
@spec serialize(@for.t()) :: [{Frame.opcode(), boolean(), boolean(), iodata()}]
|
||||
def serialize(%@for{} = frame), do: [{0x9, true, false, frame.data}]
|
||||
end
|
||||
end
|
||||
32
phoenix/deps/bandit/lib/bandit/websocket/frame/pong.ex
Normal file
32
phoenix/deps/bandit/lib/bandit/websocket/frame/pong.ex
Normal file
@@ -0,0 +1,32 @@
|
||||
defmodule Bandit.WebSocket.Frame.Pong do
|
||||
@moduledoc false
|
||||
|
||||
defstruct data: <<>>
|
||||
|
||||
@typedoc "A WebSocket pong frame"
|
||||
@type t :: %__MODULE__{data: iodata()}
|
||||
|
||||
@spec deserialize(boolean(), boolean(), iodata()) :: {:ok, t()} | {:error, term()}
|
||||
def deserialize(true, false, <<data::binary>>) when byte_size(data) <= 125 do
|
||||
{:ok, %__MODULE__{data: data}}
|
||||
end
|
||||
|
||||
def deserialize(true, false, _payload) do
|
||||
{:error, "Invalid pong payload (RFC6455§5.5.3)"}
|
||||
end
|
||||
|
||||
def deserialize(false, false, _payload) do
|
||||
{:error, "Cannot have a fragmented pong frame (RFC6455§5.5.3)"}
|
||||
end
|
||||
|
||||
def deserialize(true, true, _payload) do
|
||||
{:error, "Cannot have a compressed pong frame (RFC7692§6.1)"}
|
||||
end
|
||||
|
||||
defimpl Bandit.WebSocket.Frame.Serializable do
|
||||
alias Bandit.WebSocket.Frame
|
||||
|
||||
@spec serialize(@for.t()) :: [{Frame.opcode(), boolean(), boolean(), iodata()}]
|
||||
def serialize(%@for{} = frame), do: [{0xA, true, false, frame.data}]
|
||||
end
|
||||
end
|
||||
20
phoenix/deps/bandit/lib/bandit/websocket/frame/text.ex
Normal file
20
phoenix/deps/bandit/lib/bandit/websocket/frame/text.ex
Normal file
@@ -0,0 +1,20 @@
|
||||
defmodule Bandit.WebSocket.Frame.Text do
|
||||
@moduledoc false
|
||||
|
||||
defstruct fin: nil, compressed: false, data: <<>>
|
||||
|
||||
@typedoc "A WebSocket text frame"
|
||||
@type t :: %__MODULE__{fin: boolean(), compressed: boolean(), data: iodata()}
|
||||
|
||||
@spec deserialize(boolean(), boolean(), iodata()) :: {:ok, t()}
|
||||
def deserialize(fin, compressed, payload) do
|
||||
{:ok, %__MODULE__{fin: fin, compressed: compressed, data: payload}}
|
||||
end
|
||||
|
||||
defimpl Bandit.WebSocket.Frame.Serializable do
|
||||
alias Bandit.WebSocket.Frame
|
||||
|
||||
@spec serialize(@for.t()) :: [{Frame.opcode(), boolean(), boolean(), iodata()}]
|
||||
def serialize(%@for{} = frame), do: [{0x1, frame.fin, frame.compressed, frame.data}]
|
||||
end
|
||||
end
|
||||
97
phoenix/deps/bandit/lib/bandit/websocket/handler.ex
Normal file
97
phoenix/deps/bandit/lib/bandit/websocket/handler.ex
Normal file
@@ -0,0 +1,97 @@
|
||||
defmodule Bandit.WebSocket.Handler do
|
||||
@moduledoc false
|
||||
# A WebSocket handler conforming to RFC6455, structured as a ThousandIsland.Handler
|
||||
|
||||
use ThousandIsland.Handler
|
||||
|
||||
alias Bandit.Extractor
|
||||
alias Bandit.WebSocket.{Connection, Frame}
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_connection(socket, state) do
|
||||
{websock, websock_opts, connection_opts} = state.upgrade_opts
|
||||
|
||||
connection_opts
|
||||
|> Keyword.take([:fullsweep_after, :max_heap_size])
|
||||
|> Enum.each(fn {key, value} -> :erlang.process_flag(key, value) end)
|
||||
|
||||
connection_opts = Keyword.merge(state.opts.websocket, connection_opts)
|
||||
|
||||
primitive_ops_module =
|
||||
Keyword.get(state.opts.websocket, :primitive_ops_module, Bandit.PrimitiveOps.WebSocket)
|
||||
|
||||
state =
|
||||
state
|
||||
|> Map.take([:handler_module])
|
||||
|> Map.put(:extractor, Extractor.new(Frame, primitive_ops_module, connection_opts))
|
||||
|
||||
case Connection.init(websock, websock_opts, connection_opts, socket) do
|
||||
{:continue, connection} ->
|
||||
case Keyword.get(connection_opts, :timeout) do
|
||||
nil -> {:continue, Map.put(state, :connection, connection)}
|
||||
timeout -> {:continue, Map.put(state, :connection, connection), {:persistent, timeout}}
|
||||
end
|
||||
|
||||
{:error, reason, connection} ->
|
||||
{:error, reason, Map.put(state, :connection, connection)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_data(data, socket, state) do
|
||||
state.extractor
|
||||
|> Extractor.push_data(data)
|
||||
|> pop_frame(socket, state)
|
||||
end
|
||||
|
||||
defp pop_frame(extractor, socket, state) do
|
||||
case Extractor.pop_frame(extractor) do
|
||||
{extractor, {:ok, frame}} ->
|
||||
case Connection.handle_frame(frame, socket, state.connection) do
|
||||
{:continue, connection} ->
|
||||
pop_frame(extractor, socket, %{state | extractor: extractor, connection: connection})
|
||||
|
||||
{:close, connection} ->
|
||||
{:close, %{state | extractor: extractor, connection: connection}}
|
||||
|
||||
{:error, reason, connection} ->
|
||||
{:error, reason, %{state | extractor: extractor, connection: connection}}
|
||||
end
|
||||
|
||||
{extractor, {:error, reason}} ->
|
||||
{:error, {:deserializing, reason}, %{state | extractor: extractor}}
|
||||
|
||||
{extractor, :more} ->
|
||||
{:continue, %{state | extractor: extractor}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_close(socket, %{connection: connection}),
|
||||
do: Connection.handle_close(socket, connection)
|
||||
|
||||
def handle_close(_socket, _state), do: :ok
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_shutdown(socket, state), do: Connection.handle_shutdown(socket, state.connection)
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_error(reason, socket, state),
|
||||
do: Connection.handle_error(reason, socket, state.connection)
|
||||
|
||||
@impl ThousandIsland.Handler
|
||||
def handle_timeout(socket, state), do: Connection.handle_timeout(socket, state.connection)
|
||||
|
||||
def handle_info({:plug_conn, :sent}, {socket, state}),
|
||||
do: {:noreply, {socket, state}, socket.read_timeout}
|
||||
|
||||
def handle_info(msg, {socket, state}) do
|
||||
case Connection.handle_info(msg, socket, state.connection) do
|
||||
{:continue, connection_state} ->
|
||||
{:noreply, {socket, %{state | connection: connection_state}}, socket.read_timeout}
|
||||
|
||||
{:error, reason, connection_state} ->
|
||||
{:stop, reason, {socket, %{state | connection: connection_state}}}
|
||||
end
|
||||
end
|
||||
end
|
||||
105
phoenix/deps/bandit/lib/bandit/websocket/handshake.ex
Normal file
105
phoenix/deps/bandit/lib/bandit/websocket/handshake.ex
Normal file
@@ -0,0 +1,105 @@
|
||||
defmodule Bandit.WebSocket.Handshake do
|
||||
@moduledoc false
|
||||
# Functions to support WebSocket handshaking as described in RFC6455§4.2 & RFC7692
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
@type extensions :: [{String.t(), [{String.t(), String.t() | true}]}]
|
||||
|
||||
@spec handshake(Plug.Conn.t(), keyword(), keyword()) ::
|
||||
{:ok, Plug.Conn.t(), Keyword.t()} | {:error, String.t()}
|
||||
def handshake(%Plug.Conn{} = conn, opts, websocket_opts) do
|
||||
with :ok <- Bandit.WebSocket.UpgradeValidation.validate_upgrade(conn) do
|
||||
do_handshake(conn, opts, websocket_opts)
|
||||
end
|
||||
end
|
||||
|
||||
@spec do_handshake(Plug.Conn.t(), keyword(), keyword()) :: {:ok, Plug.Conn.t(), keyword()}
|
||||
defp do_handshake(conn, opts, websocket_opts) do
|
||||
requested_extensions = requested_extensions(conn)
|
||||
|
||||
{negotiated_params, returned_data} =
|
||||
if Keyword.get(opts, :compress) && Keyword.get(websocket_opts, :compress, true) do
|
||||
Bandit.WebSocket.PerMessageDeflate.negotiate(requested_extensions, websocket_opts)
|
||||
else
|
||||
{nil, []}
|
||||
end
|
||||
|
||||
conn = send_handshake(conn, returned_data)
|
||||
{:ok, conn, Keyword.put(opts, :compress, negotiated_params)}
|
||||
end
|
||||
|
||||
@spec requested_extensions(Plug.Conn.t()) :: extensions()
|
||||
defp requested_extensions(%Plug.Conn{} = conn) do
|
||||
conn
|
||||
|> get_req_header("sec-websocket-extensions")
|
||||
|> Enum.flat_map(&Plug.Conn.Utils.list/1)
|
||||
|> Enum.map(fn extension ->
|
||||
[name | params] =
|
||||
extension
|
||||
|> String.split(";", trim: true)
|
||||
|> Enum.map(&String.trim/1)
|
||||
|
||||
params = split_params(params)
|
||||
|
||||
{name, params}
|
||||
end)
|
||||
end
|
||||
|
||||
@spec split_params([String.t()]) :: [{String.t(), String.t() | true}]
|
||||
defp split_params(params) do
|
||||
params
|
||||
|> Enum.map(fn param ->
|
||||
param
|
||||
|> String.split("=", trim: true)
|
||||
|> Enum.map(&String.trim/1)
|
||||
|> case do
|
||||
[key, value] -> {key, value}
|
||||
[key] -> {key, true}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec send_handshake(Plug.Conn.t(), extensions()) :: Plug.Conn.t()
|
||||
defp send_handshake(%Plug.Conn{} = conn, extensions) do
|
||||
# Taken from RFC6455§4.2.2/5. Note that we can take for granted the existence of the
|
||||
# sec-websocket-key header in the request, since we check for it in the handshake? call above
|
||||
[client_key] = get_req_header(conn, "sec-websocket-key")
|
||||
concatenated_key = client_key <> "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
hashed_key = :crypto.hash(:sha, concatenated_key)
|
||||
server_key = Base.encode64(hashed_key)
|
||||
|
||||
headers =
|
||||
[
|
||||
{:upgrade, "websocket"},
|
||||
{:connection, "Upgrade"},
|
||||
{:"sec-websocket-accept", server_key}
|
||||
] ++
|
||||
websocket_extension_header(extensions) ++
|
||||
conn.resp_headers
|
||||
|
||||
inform(conn, 101, headers)
|
||||
end
|
||||
|
||||
@spec websocket_extension_header(extensions()) :: keyword()
|
||||
defp websocket_extension_header([]), do: []
|
||||
|
||||
defp websocket_extension_header(extensions) do
|
||||
extensions =
|
||||
extensions
|
||||
|> Enum.map_join(",", fn {extension, params} ->
|
||||
params =
|
||||
params
|
||||
|> Enum.flat_map(fn
|
||||
{_param, false} -> []
|
||||
{param, true} -> [to_string(param)]
|
||||
{param, value} -> [to_string(param) <> "=" <> to_string(value)]
|
||||
end)
|
||||
|
||||
[to_string(extension) | params]
|
||||
|> Enum.join(";")
|
||||
end)
|
||||
|
||||
[{:"sec-websocket-extensions", extensions}]
|
||||
end
|
||||
end
|
||||
151
phoenix/deps/bandit/lib/bandit/websocket/permessage_deflate.ex
Normal file
151
phoenix/deps/bandit/lib/bandit/websocket/permessage_deflate.ex
Normal file
@@ -0,0 +1,151 @@
|
||||
defmodule Bandit.WebSocket.PerMessageDeflate do
|
||||
@moduledoc false
|
||||
# Support for per-message deflate extension, per RFC7692§7
|
||||
|
||||
@typedoc "Encapsulates the state of a WebSocket permessage-deflate context"
|
||||
@type t :: %__MODULE__{
|
||||
server_no_context_takeover: boolean(),
|
||||
client_no_context_takeover: boolean(),
|
||||
server_max_window_bits: 8..15,
|
||||
client_max_window_bits: 8..15,
|
||||
inflate_context: :zlib.zstream(),
|
||||
deflate_context: :zlib.zstream()
|
||||
}
|
||||
|
||||
defstruct server_no_context_takeover: false,
|
||||
client_no_context_takeover: false,
|
||||
server_max_window_bits: 15,
|
||||
client_max_window_bits: 15,
|
||||
inflate_context: nil,
|
||||
deflate_context: nil
|
||||
|
||||
@valid_params ~w[server_no_context_takeover client_no_context_takeover server_max_window_bits client_max_window_bits]
|
||||
|
||||
def negotiate(requested_extensions, opts) do
|
||||
:proplists.get_all_values("permessage-deflate", requested_extensions)
|
||||
|> Enum.find_value(&do_negotiate/1)
|
||||
|> case do
|
||||
nil -> {nil, []}
|
||||
params -> {init(params, opts), "permessage-deflate": params}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_negotiate(params) do
|
||||
with params <- normalize_params(params),
|
||||
true <- validate_params(params) do
|
||||
resolve_params(params)
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_params(params) do
|
||||
params
|
||||
|> Enum.map(fn
|
||||
{"server_max_window_bits", true} -> {"server_max_window_bits", true}
|
||||
{"server_max_window_bits", value} -> {"server_max_window_bits", parse(value)}
|
||||
{"client_max_window_bits", true} -> {"client_max_window_bits", 15}
|
||||
{"client_max_window_bits", value} -> {"client_max_window_bits", parse(value)}
|
||||
value -> value
|
||||
end)
|
||||
end
|
||||
|
||||
defp parse(value) do
|
||||
case Integer.parse(value) do
|
||||
{int_value, ""} -> int_value
|
||||
:error -> value
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_params(params) do
|
||||
no_invalid_params = params |> :proplists.split(@valid_params) |> elem(1) == []
|
||||
no_repeat_params = params |> :proplists.get_keys() |> length() == length(params)
|
||||
|
||||
no_invalid_values =
|
||||
:proplists.get_value("server_no_context_takeover", params) in [:undefined, true] &&
|
||||
:proplists.get_value("client_no_context_takeover", params) in [:undefined, true] &&
|
||||
:proplists.get_value("server_max_window_bits", params, 15) in 8..15 &&
|
||||
:proplists.get_value("client_max_window_bits", params, 15) in 8..15
|
||||
|
||||
no_invalid_params && no_repeat_params && no_invalid_values
|
||||
end
|
||||
|
||||
# This is where we finally determine which parameters to accept. Note that we don't convert to
|
||||
# atoms until this stage to avoid potential atom exhaustion
|
||||
defp resolve_params(params) do
|
||||
@valid_params
|
||||
|> Enum.flat_map(fn param_name ->
|
||||
case :proplists.get_value(param_name, params) do
|
||||
:undefined -> []
|
||||
param -> [{String.to_existing_atom(param_name), param}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp init(params, opts) do
|
||||
instance = struct(__MODULE__, params)
|
||||
inflate_context = :zlib.open()
|
||||
:ok = :zlib.inflateInit(inflate_context, fix_bits(-instance.client_max_window_bits))
|
||||
deflate_context = :zlib.open()
|
||||
deflate_opts = Keyword.get(opts, :deflate_options, [])
|
||||
|
||||
:ok =
|
||||
:zlib.deflateInit(
|
||||
deflate_context,
|
||||
Keyword.get(deflate_opts, :level, :default),
|
||||
:deflated,
|
||||
fix_bits(-instance.server_max_window_bits),
|
||||
Keyword.get(deflate_opts, :mem_level, 8),
|
||||
Keyword.get(deflate_opts, :strategy, :default)
|
||||
)
|
||||
|
||||
%{instance | inflate_context: inflate_context, deflate_context: deflate_context}
|
||||
end
|
||||
|
||||
# https://www.erlang.org/doc/man/zlib.html#deflateInit-6
|
||||
defp fix_bits(-8), do: -9
|
||||
defp fix_bits(other), do: other
|
||||
|
||||
# Note that we pass back the context to the caller even though it is unmodified locally
|
||||
|
||||
def inflate(data, %__MODULE__{} = context) do
|
||||
inflated_data =
|
||||
context.inflate_context
|
||||
|> :zlib.inflate(<<data::binary, 0x00, 0x00, 0xFF, 0xFF>>)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
if context.client_no_context_takeover, do: :zlib.inflateReset(context.inflate_context)
|
||||
{:ok, inflated_data, context}
|
||||
rescue
|
||||
e -> {:error, "Error encountered #{inspect(e)}"}
|
||||
end
|
||||
|
||||
def inflate(_data, nil), do: {:error, :no_compress}
|
||||
|
||||
def deflate(data, %__MODULE__{} = context) do
|
||||
deflated_data =
|
||||
context.deflate_context
|
||||
|> :zlib.deflate(data, :sync)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
deflated_size = byte_size(deflated_data) - 4
|
||||
|
||||
deflated_data =
|
||||
case deflated_data do
|
||||
<<deflated_data::binary-size(deflated_size), 0x00, 0x00, 0xFF, 0xFF>> -> deflated_data
|
||||
deflated -> deflated
|
||||
end
|
||||
|
||||
if context.server_no_context_takeover, do: :zlib.deflateReset(context.deflate_context)
|
||||
{:ok, deflated_data, context}
|
||||
rescue
|
||||
e -> {:error, "Error encountered #{inspect(e)}"}
|
||||
end
|
||||
|
||||
def deflate(_data, nil), do: {:error, :no_compress}
|
||||
|
||||
def close(%__MODULE__{} = context) do
|
||||
:zlib.close(context.inflate_context)
|
||||
:zlib.close(context.deflate_context)
|
||||
end
|
||||
end
|
||||
75
phoenix/deps/bandit/lib/bandit/websocket/socket.ex
Normal file
75
phoenix/deps/bandit/lib/bandit/websocket/socket.ex
Normal file
@@ -0,0 +1,75 @@
|
||||
defprotocol Bandit.WebSocket.Socket do
|
||||
@moduledoc false
|
||||
#
|
||||
# A protocol defining the low-level functionality of a WebSocket
|
||||
#
|
||||
|
||||
@type t :: term()
|
||||
@type frame_type :: :text | :binary | :ping | :pong
|
||||
@type send_frame_stats :: [
|
||||
send_binary_frame_bytes: non_neg_integer(),
|
||||
send_binary_frame_count: non_neg_integer(),
|
||||
send_ping_frame_bytes: non_neg_integer(),
|
||||
send_ping_frame_count: non_neg_integer(),
|
||||
send_pong_frame_bytes: non_neg_integer(),
|
||||
send_pong_frame_count: non_neg_integer(),
|
||||
send_text_frame_bytes: non_neg_integer(),
|
||||
send_text_frame_count: non_neg_integer()
|
||||
]
|
||||
|
||||
@spec send_frame(t(), {frame_type :: frame_type(), data :: iodata()}, boolean()) ::
|
||||
send_frame_stats()
|
||||
def send_frame(socket, data_and_frame_type, compressed)
|
||||
|
||||
@spec close(t(), code :: WebSock.close_detail()) :: :ok | {:error, :inet.posix()}
|
||||
def close(socket, code)
|
||||
end
|
||||
|
||||
defimpl Bandit.WebSocket.Socket, for: ThousandIsland.Socket do
|
||||
@moduledoc false
|
||||
#
|
||||
# An implementation of Bandit.WebSocket.Socket for use with ThousandIsland.Socket instances
|
||||
#
|
||||
|
||||
alias Bandit.WebSocket.Frame
|
||||
|
||||
@spec send_frame(@for.t(), {frame_type :: @protocol.frame_type(), data :: iodata()}, boolean()) ::
|
||||
@protocol.send_frame_stats()
|
||||
def send_frame(socket, {:text, data}, compressed) do
|
||||
_ = do_send_frame(socket, %Frame.Text{fin: true, data: data, compressed: compressed})
|
||||
[send_text_frame_count: 1, send_text_frame_bytes: IO.iodata_length(data)]
|
||||
end
|
||||
|
||||
def send_frame(socket, {:binary, data}, compressed) do
|
||||
_ = do_send_frame(socket, %Frame.Binary{fin: true, data: data, compressed: compressed})
|
||||
[send_binary_frame_count: 1, send_binary_frame_bytes: IO.iodata_length(data)]
|
||||
end
|
||||
|
||||
def send_frame(socket, {:ping, data}, false) do
|
||||
_ = do_send_frame(socket, %Frame.Ping{data: data})
|
||||
[send_ping_frame_count: 1, send_ping_frame_bytes: IO.iodata_length(data)]
|
||||
end
|
||||
|
||||
def send_frame(socket, {:pong, data}, false) do
|
||||
_ = do_send_frame(socket, %Frame.Pong{data: data})
|
||||
[send_pong_frame_count: 1, send_pong_frame_bytes: IO.iodata_length(data)]
|
||||
end
|
||||
|
||||
@spec close(@for.t(), non_neg_integer() | {non_neg_integer(), binary()}) ::
|
||||
:ok | {:error, :inet.posix()}
|
||||
def close(socket, {code, detail}) when is_integer(code) do
|
||||
_ = do_send_frame(socket, %Frame.ConnectionClose{code: code, reason: detail})
|
||||
@for.shutdown(socket, :write)
|
||||
end
|
||||
|
||||
def close(socket, code) when is_integer(code) do
|
||||
_ = do_send_frame(socket, %Frame.ConnectionClose{code: code})
|
||||
@for.shutdown(socket, :write)
|
||||
end
|
||||
|
||||
@spec do_send_frame(@for.t(), Frame.frame()) ::
|
||||
:ok | {:error, :closed | :timeout | :inet.posix()}
|
||||
defp do_send_frame(socket, frame) do
|
||||
@for.send(socket, Frame.serialize(frame))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
defmodule Bandit.WebSocket.UpgradeValidation do
|
||||
@moduledoc false
|
||||
# Provides validation of WebSocket upgrade requests as described in RFC6455§4.2
|
||||
|
||||
# Validates that the request satisfies the requirements to issue a WebSocket upgrade response.
|
||||
# Validations are performed based on the clauses laid out in RFC6455§4.2
|
||||
#
|
||||
# This function does not actually perform an upgrade or change the connection in any way
|
||||
#
|
||||
# Returns `:ok` if the connection satisfies the requirements for a WebSocket upgrade, and
|
||||
# `{:error, reason}` if not
|
||||
#
|
||||
@spec validate_upgrade(Plug.Conn.t()) :: :ok | {:error, String.t()}
|
||||
def validate_upgrade(conn) do
|
||||
case Plug.Conn.get_http_protocol(conn) do
|
||||
:"HTTP/1.1" -> validate_upgrade_http1(conn)
|
||||
other -> {:error, "HTTP version #{other} unsupported"}
|
||||
end
|
||||
end
|
||||
|
||||
# Validate the conn per RFC6455§4.2.1
|
||||
defp validate_upgrade_http1(conn) do
|
||||
with :ok <- assert_method(conn, "GET"),
|
||||
:ok <- assert_header_nonempty(conn, "host"),
|
||||
:ok <- assert_header_contains(conn, "connection", "upgrade"),
|
||||
:ok <- assert_header_contains(conn, "upgrade", "websocket"),
|
||||
:ok <- assert_header_nonempty(conn, "sec-websocket-key"),
|
||||
:ok <- assert_header_equals(conn, "sec-websocket-version", "13") do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_method(conn, verb) do
|
||||
case conn.method do
|
||||
^verb -> :ok
|
||||
other -> {:error, "HTTP method #{other} unsupported"}
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_header_nonempty(conn, header) do
|
||||
case Plug.Conn.get_req_header(conn, header) do
|
||||
[] -> {:error, "'#{header}' header is absent"}
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_header_equals(conn, header, expected) do
|
||||
case Plug.Conn.get_req_header(conn, header) |> Enum.map(&String.downcase(&1, :ascii)) do
|
||||
[^expected] -> :ok
|
||||
value -> {:error, "'#{header}' header must equal '#{expected}', got #{inspect(value)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_header_contains(conn, header, needle) do
|
||||
haystack = Plug.Conn.get_req_header(conn, header)
|
||||
|
||||
haystack
|
||||
|> Enum.flat_map(&Plug.Conn.Utils.list/1)
|
||||
|> Enum.any?(&(String.downcase(&1, :ascii) == needle))
|
||||
|> case do
|
||||
true -> :ok
|
||||
false -> {:error, "'#{header}' header must contain '#{needle}', got #{inspect(haystack)}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
95
phoenix/deps/bandit/mix.exs
Normal file
95
phoenix/deps/bandit/mix.exs
Normal file
@@ -0,0 +1,95 @@
|
||||
defmodule Bandit.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :bandit,
|
||||
version: "1.10.4",
|
||||
elixir: "~> 1.13",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
elixirc_paths: elixirc_path(Mix.env()),
|
||||
dialyzer: dialyzer(),
|
||||
name: "Bandit",
|
||||
description: "A pure-Elixir HTTP server built for Plug & WebSock apps",
|
||||
source_url: "https://github.com/mtrudel/bandit",
|
||||
package: [
|
||||
maintainers: ["Mat Trudel"],
|
||||
licenses: ["MIT"],
|
||||
links: %{
|
||||
"GitHub" => "https://github.com/mtrudel/bandit",
|
||||
"Changelog" => "https://hexdocs.pm/bandit/changelog.html"
|
||||
},
|
||||
files: ["lib", "mix.exs", "README*", "LICENSE*", "CHANGELOG*"]
|
||||
],
|
||||
docs: docs()
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[extra_applications: [:logger], mod: {Bandit.Application, []}]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:thousand_island, "~> 1.0"},
|
||||
{:plug, "~> 1.18"},
|
||||
{:websock, "~> 0.5"},
|
||||
{:hpax, "~> 1.0"},
|
||||
{:telemetry, "~> 0.4 or ~> 1.0"},
|
||||
{:req, "~> 0.3", only: [:dev, :test]},
|
||||
{:machete, ">= 0.0.0", only: [:dev, :test]},
|
||||
{:ex_doc, "~> 0.24", only: [:dev, :test], runtime: false},
|
||||
{:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false},
|
||||
{:credo, "~> 1.0", only: [:dev, :test], runtime: false},
|
||||
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false}
|
||||
]
|
||||
end
|
||||
|
||||
defp elixirc_path(:test), do: ["lib/", "test/support"]
|
||||
defp elixirc_path(_), do: ["lib/"]
|
||||
|
||||
defp dialyzer do
|
||||
[
|
||||
plt_core_path: "priv/plts",
|
||||
plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
|
||||
plt_add_deps: :apps_direct,
|
||||
plt_add_apps: [:ssl, :public_key],
|
||||
flags: [
|
||||
"-Werror_handling",
|
||||
"-Wextra_return",
|
||||
"-Wmissing_return",
|
||||
"-Wunknown",
|
||||
"-Wunmatched_returns",
|
||||
"-Wunderspecs"
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
defp docs do
|
||||
[
|
||||
extras: [
|
||||
"CHANGELOG.md": [title: "Changelog"],
|
||||
"README.md": [title: "README"],
|
||||
"lib/bandit/http1/README.md": [
|
||||
filename: "HTTP1_README.md",
|
||||
title: "HTTP/1 Implementation Notes"
|
||||
],
|
||||
"lib/bandit/http2/README.md": [
|
||||
filename: "HTTP2_README.md",
|
||||
title: "HTTP/2 Implementation Notes"
|
||||
],
|
||||
"lib/bandit/websocket/README.md": [
|
||||
filename: "WebSocket_README.md",
|
||||
title: "WebSocket Implementation Notes"
|
||||
]
|
||||
],
|
||||
groups_for_extras: [
|
||||
"Implementation Notes": Path.wildcard("lib/bandit/*/README.md")
|
||||
],
|
||||
skip_undefined_reference_warnings_on: Path.wildcard("**/*.md"),
|
||||
main: "Bandit",
|
||||
logo: "assets/ex_doc_logo.png"
|
||||
]
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user