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
|
||||
BIN
phoenix/deps/cc_precompiler/.hex
Normal file
BIN
phoenix/deps/cc_precompiler/.hex
Normal file
Binary file not shown.
76
phoenix/deps/cc_precompiler/CHANGELOG.md
Normal file
76
phoenix/deps/cc_precompiler/CHANGELOG.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Changelog
|
||||
|
||||
## v0.1.11 (2025-08-02)
|
||||
|
||||
- allow `{:elixir_make, "~> 0.7 or ~> 0.8 or ~> 0.9"}`
|
||||
|
||||
## v0.1.10 (2024-03-10)
|
||||
|
||||
- allow `{:elixir_make, "~> 0.7 or ~> 0.8"}`
|
||||
|
||||
## v0.1.9 (2023-11-16)
|
||||
|
||||
- added an `exclude_current_target` option
|
||||
- updated ex_doc
|
||||
|
||||
## v0.1.8 (2023-07-19)
|
||||
|
||||
### Changed
|
||||
|
||||
Fixed `CCPrecompiler.all_supported_targets(:fetch)`. It should fetch and merge default available compilers when `include_default_ones` is `true`.
|
||||
|
||||
## v0.1.7 (2022-03-13)
|
||||
|
||||
### Added
|
||||
- using `:include_default_ones` in `project.cc_precompiler.compilers`. Default (cross-)compiler will be included if it's `true`, otherwise only specified targets will be used.
|
||||
|
||||
Default value of `:include_default_ones` is `false` to avoid breaking changes.
|
||||
|
||||
If a custom target has the same name as a default one, then the custom one will override the default configuration for that target (e.g., the `x86_64-linux-gnu` entry below will override the default gcc configuration and use clang instead).
|
||||
|
||||
```elixir
|
||||
def project do
|
||||
[
|
||||
# ...
|
||||
cc_precompiler: [
|
||||
compilers: %{
|
||||
{:unix, :linux} => %{
|
||||
:include_default_ones => true,
|
||||
"my-custom-target" => {
|
||||
"my-custom-target-gcc",
|
||||
"my-custom-target-g++"
|
||||
},
|
||||
"x86_64-linux-gnu" => {
|
||||
"x86_64-linux-gnu-clang",
|
||||
"x86_64-linux-gnu-clang++"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
## v0.1.6 (2022-02-20)
|
||||
|
||||
### Added
|
||||
- allow missing CC or CXX when detecting available targets by setting `allow_missing_compiler` to `true`.
|
||||
|
||||
Adding this option because there is no need to require the presence of both CC and CXX for projects that only uses one of them.
|
||||
|
||||
```elixir
|
||||
def project do
|
||||
[
|
||||
# ...
|
||||
cc_precompiler: [
|
||||
# optional config key
|
||||
# true - the corresponding target will be available as long as we can detect either `CC` or `CXX`
|
||||
# false - both `CC` and `CXX` should be present on the system
|
||||
# defaults to `false`
|
||||
allow_missing_compiler: false,
|
||||
# ...
|
||||
],
|
||||
# ...
|
||||
]
|
||||
end
|
||||
```
|
||||
201
phoenix/deps/cc_precompiler/LICENSE
Normal file
201
phoenix/deps/cc_precompiler/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
342
phoenix/deps/cc_precompiler/PRECOMPILATION_GUIDE.md
Normal file
342
phoenix/deps/cc_precompiler/PRECOMPILATION_GUIDE.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# Precompilation guide
|
||||
|
||||
This guide has two sections, the first one is intended for precompiler module developers. It covers a minimal example of creating a precompiler module. The second section is intended for library developers who want their library to be able to use precompiled artefacts in a simple way.
|
||||
|
||||
## Library Developer
|
||||
|
||||
This guide assumes you have already added `elixir_make` to your library and you have written a `Makefile` that compiles the native code in your project. Once your native code compile and works as expected, you are now ready to precompile it.
|
||||
|
||||
A full demo project is available on [cocoa-xu/cc_precompiler_example](https://github.com/cocoa-xu/cc_precompiler_example).
|
||||
|
||||
### Setup mix.exs
|
||||
|
||||
To use a precompiler module such as the `CCPrecompiler` example above, we first add the precompiler (`:cc_precompiler` here) and `:elixir_make` to `deps`.
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
# ...
|
||||
{:elixir_make, "~> 0.6", runtime: false},
|
||||
{:cc_precompiler, "~> 0.1", runtime: false, github: "cocoa-xu/cc_precompiler"}
|
||||
# ...
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Then add `:elixir_make` to the `compilers` list, and set `CCPrecompile` as the value for `make_precompiler`.
|
||||
|
||||
```elixir
|
||||
@version "0.1.0"
|
||||
def project do
|
||||
[
|
||||
# ...
|
||||
compilers: [:elixir_make] ++ Mix.compilers(),
|
||||
# elixir_make specific config
|
||||
make_precompiler: {:nif, CCPrecompiler},
|
||||
make_precompiler_url: "https://github.com/cocoa-xu/cc_precompiler_example/releases/download/v#{@version}/@{artefact_filename}",
|
||||
make_precompiler_filename: "nif",
|
||||
make_precompiler_priv_paths: ["nif.*"],
|
||||
make_precompiler_unavailable_target: :compile,
|
||||
# ...
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Another required field is `make_precompiled_url`. It is a URL template to the artefact file.
|
||||
|
||||
`@{artefact_filename}` in the URL template string will be replaced by corresponding artefact filenames when fetching them. For example, `cc_precompiler_example-nif-2.16-x86_64-linux-gnu-0.1.0.tar.gz`.
|
||||
|
||||
Note that there is an optional config key for elixir_make, `make_precompiler_filename`. If the name (file extension does not count) of the shared library is different from your app's name, then `make_precompiler_filename` should be set. For example, if the app name is `"cc_precompiler_example"` while the name shared library is `"nif.so"` (or `"nif.dll"` on windows), then `make_precompiler_filename` should be set as `"nif"`.
|
||||
|
||||
Another optional config key is `make_precompiler_priv_paths`. For example, say the `priv` directory is organised as follows in Linux, macOS and Windows respectively,
|
||||
|
||||
Also, you can specify how to recover from unavailable targets using the `make_precompiler_unavailable_target` config key. Allowed values are `:compile` and `:ignore`. Defaults to `:compile`.
|
||||
|
||||
It is also possible to pass in a 2-arity function to `make_precompiler_unavailable_target`: the first argument is the triplet of the unavailable target, and the second argument is a list that contains all available targets given by the precompiler.
|
||||
|
||||
```
|
||||
# Linux
|
||||
.
|
||||
├── assets
|
||||
│ ├── model.onnx
|
||||
│ └── data.json
|
||||
├── lib
|
||||
│ ├── libpriv1.so
|
||||
│ ├── libpriv2.so
|
||||
│ └── libpriv3.so
|
||||
└── nif.so
|
||||
|
||||
# macOS
|
||||
.
|
||||
├── assets
|
||||
│ ├── model.onnx
|
||||
│ └── data.json
|
||||
├── lib
|
||||
│ ├── libpriv1.dylib
|
||||
│ ├── libpriv2.dylib
|
||||
│ └── libpriv3.dylib
|
||||
└── nif.so
|
||||
|
||||
# Windows
|
||||
.
|
||||
├── assets
|
||||
│ ├── model.onnx
|
||||
│ └── data.json
|
||||
├── lib
|
||||
│ ├── libpriv1.dll
|
||||
│ ├── libpriv2.dll
|
||||
│ └── libpriv3.dll
|
||||
└── nif.dll
|
||||
```
|
||||
|
||||
By default, everything in `priv` will be included in the precompiled tar file. However, files in `assets` can be very large or platform-independent, therefore, we would like to only include the `nif.so` (`nif.dll`) file and everything in the `lib` directory in the precompiled tar file to reduce the footprint. In this case, we can set `make_precompiler_priv_paths` to `["nif.so", "nif.dll", "lib"]`.
|
||||
|
||||
Of course, wildcards (`?`, `**`, `*`) are supported when specifying files. For example, `["nif.*", "lib/*.so", "lib/*.dll", "lib/*.dylib"]` will include `nif.so` (Linux/macOS) or `nif.dll` (Windows), and `.so` or `.dll` files in the `lib` directory.
|
||||
|
||||
Directory structures and symbolic links are preserved.
|
||||
|
||||
### (Optional) Test the NIF code locally
|
||||
|
||||
To test the NIF code locally, you can either set `force_build` to `true` or append `"-dev"` to your NIF library's version string.
|
||||
|
||||
```elixir
|
||||
@version "0.1.0-dev"
|
||||
|
||||
def project do
|
||||
[
|
||||
# either append `"-dev"` to your NIF library's version string
|
||||
version: @version,
|
||||
# or set force_build to true
|
||||
force_build: true,
|
||||
# ...
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Doing so will ask `elixir_make` to only compile for the current host instead of building for all available targets.
|
||||
|
||||
```shell
|
||||
$ mix compile
|
||||
cc -shared -std=c11 -O3 -fPIC -I"/usr/local/lib/erlang/erts-13.0.3/include" -undefined dynamic_lookup -flat_namespace -undefined suppress "/Users/cocoa/git/cc_precompiler_example/c_src/cc_precompiler_example.c" -o "/Users/cocoa/Git/cc_precompiler_example/_build/dev/lib/cc_precompiler_example/priv/nif.so"
|
||||
$ mix test
|
||||
make: Nothing to be done for `build'.
|
||||
Generated cc_precompiler_example app
|
||||
.
|
||||
|
||||
Finished in 0.00 seconds (0.00s async, 0.00s sync)
|
||||
1 test, 0 failures
|
||||
|
||||
Randomized with seed 102464
|
||||
```
|
||||
|
||||
### Precompile for available targets
|
||||
|
||||
It's possible to either setup a CI task to do the precompilation job or precompile on a local machine and upload the precompiled artefacts.
|
||||
|
||||
To precompile for all targets on a local machine:
|
||||
|
||||
```shell
|
||||
MIX_ENV=prod mix elixir_make.precompile
|
||||
```
|
||||
|
||||
Environment variable `ELIXIR_MAKE_CACHE_DIR` can be used to set the cache dir for the precompiled artefacts, for instance, to output precompiled artefacts in the cache directory of the current working directory, `export ELIXIR_MAKE_CACHE_DIR="$(pwd)/cache"`.
|
||||
|
||||
To setup a CI task such as GitHub Actions, the following workflow file can be used for reference:
|
||||
|
||||
```yml
|
||||
name: precompile
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MIX_ENV: "prod"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: "25.1"
|
||||
elixir-version: "1.14"
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential automake autoconf pkg-config bc m4 unzip zip \
|
||||
gcc g++ \
|
||||
gcc-i686-linux-gnu g++-i686-linux-gnu \
|
||||
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \
|
||||
gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf \
|
||||
gcc-riscv64-linux-gnu g++-riscv64-linux-gnu \
|
||||
gcc-powerpc64le-linux-gnu g++-powerpc64le-linux-gnu \
|
||||
gcc-s390x-linux-gnu g++-s390x-linux-gnu
|
||||
|
||||
- name: Get musl cross-compilers (Optional, use this if you have musl targets to compile)
|
||||
run: |
|
||||
for musl_arch in x86_64 aarch64 riscv64
|
||||
do
|
||||
wget "https://musl.cc/${musl_arch}-linux-musl-cross.tgz" -O "${musl_arch}-linux-musl-cross.tgz"
|
||||
tar -xf "${musl_arch}-linux-musl-cross.tgz"
|
||||
done
|
||||
|
||||
- name: Mix Test
|
||||
run: |
|
||||
# Optional, use this if you have musl targets to compile
|
||||
for musl_arch in x86_64 aarch64 riscv64
|
||||
do
|
||||
export PATH="$(pwd)/${musl_arch}-linux-musl-cross/bin:${PATH}"
|
||||
done
|
||||
|
||||
mix deps.get
|
||||
MIX_ENV=test mix test
|
||||
|
||||
- name: Create precompiled library
|
||||
run: |
|
||||
export ELIXIR_MAKE_CACHE_DIR=$(pwd)/cache
|
||||
mkdir -p "${ELIXIR_MAKE_CACHE_DIR}"
|
||||
mix elixir_make.precompile
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
cache/*.tar.gz
|
||||
|
||||
macos:
|
||||
runs-on: macos-11
|
||||
env:
|
||||
MIX_ENV: "prod"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install erlang and elixir
|
||||
run: |
|
||||
brew install erlang elixir
|
||||
mix local.hex --force
|
||||
mix local.rebar --force
|
||||
|
||||
- name: Mix Test
|
||||
run: |
|
||||
mix deps.get
|
||||
MIX_ENV=test mix test
|
||||
|
||||
- name: Create precompiled library
|
||||
run: |
|
||||
export ELIXIR_MAKE_CACHE_DIR=$(pwd)/cache
|
||||
mkdir -p "${ELIXIR_MAKE_CACHE_DIR}"
|
||||
mix elixir_make.precompile
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
cache/*.tar.gz
|
||||
```
|
||||
|
||||
### Generate checksum file
|
||||
After CI has finished, you can fetch the precompiled binaries from GitHub.
|
||||
|
||||
```shell
|
||||
$ MIX_ENV=prod mix elixir_make.checksum --all --ignore-unavailable
|
||||
```
|
||||
|
||||
Meanwhile, a checksum file will be generated. In this example, the checksum file will be named as `checksum.exs` in current working directory.
|
||||
|
||||
This checksum file is extremely important in the scenario where you need to release a Hex package using precompiled NIFs. It's **MANDATORY** to include this file in your Hex package (by updating the `files` field in the `mix.exs`). Otherwise your package **won't work**.
|
||||
|
||||
```elixir
|
||||
defp package do
|
||||
[
|
||||
files: [
|
||||
# ...
|
||||
"checksum.exs",
|
||||
# ...
|
||||
],
|
||||
# ...
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
However, there is no need to track the checksum file in your version control system (git or other).
|
||||
|
||||
### (Optional) Test fetched artefacts can work locally
|
||||
```shell
|
||||
# delete previously built binaries so that
|
||||
# elixir_make will try to restore the NIF library
|
||||
# from the downloaded tarball file
|
||||
$ rm -rf _build/prod/lib/cc_precompiler_example
|
||||
# set to prod env and test everything
|
||||
$ MIX_ENV=prod mix test
|
||||
==> castore
|
||||
Compiling 1 file (.ex)
|
||||
Generated castore app
|
||||
==> elixir_make
|
||||
Compiling 5 files (.ex)
|
||||
Generated elixir_make app
|
||||
==> cc_precompiler
|
||||
Compiling 1 file (.ex)
|
||||
Generated cc_precompiler app
|
||||
|
||||
20:47:42.262 [debug] Restore NIF for current node from: /Users/cocoa/Library/Caches/cc_precompiler_example-nif-2.16-aarch64-apple-darwin-0.1.0.tar.gz
|
||||
==> cc_precompiler_example
|
||||
Compiling 1 file (.ex)
|
||||
Generated cc_precompiler_example app
|
||||
.
|
||||
|
||||
Finished in 0.01 seconds (0.00s async, 0.01s sync)
|
||||
1 test, 0 failures
|
||||
|
||||
Randomized with seed 539590
|
||||
```
|
||||
|
||||
## Recommended flow
|
||||
To recap, the suggested flow is the following:
|
||||
|
||||
1. Choose an appropriate precompiler for your NIF library and set all necessary options in the `mix.exs`.
|
||||
2. (Optional) Test if your NIF library compiles locally.
|
||||
|
||||
```shell
|
||||
mix compile
|
||||
mix test
|
||||
```
|
||||
|
||||
3. (Optional) Test if your NIF library can precompile to all specified targets locally.
|
||||
```shell
|
||||
MIX_ENV=prod mix elixir_make.precompile
|
||||
```
|
||||
|
||||
4. Precompile your library on CI or locally.
|
||||
|
||||
```shell
|
||||
# locally
|
||||
MIX_ENV=prod mix elixir_make.precompile
|
||||
# CI
|
||||
# please see the docs above
|
||||
```
|
||||
|
||||
5. Fetch precompiled binaries from GitHub.
|
||||
|
||||
```shell
|
||||
# only fetch artefact for current host
|
||||
MIX_ENV=prod mix elixir_make.checksum --only-local --print
|
||||
# fetch all
|
||||
MIX_ENV=prod mix elixir_make.checksum --all --print
|
||||
# to fetch all available artefacts at the moment
|
||||
MIX_ENV=prod mix elixir_make.checksum --all --print --ignore-unavailable
|
||||
```
|
||||
|
||||
6. (Optional) Test if the downloaded artefacts works as expected.
|
||||
|
||||
```shell
|
||||
rm -rf _build/prod/lib/NIF_LIBRARY_NAME
|
||||
MIX_ENV=prod mix test
|
||||
```
|
||||
|
||||
6. Update Hex package to include the checksum file.
|
||||
7. Release the package to Hex.pm (make sure your release includes the correct files).
|
||||
342
phoenix/deps/cc_precompiler/README.md
Normal file
342
phoenix/deps/cc_precompiler/README.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# CC Precompiler
|
||||
|
||||
[](https://hex.pm/packages/cc_precompiler)
|
||||
|
||||
C/C++ Cross-compiler Precompiler is a library that supports [elixir_make](https://github.com/elixir-lang/elixir_make)'s precompilation feature. It's customisble and easy to extend.
|
||||
|
||||
The guide for how to `cc_precompiler` can be found in the `PRECOMPILATION_GUIED.md` file.
|
||||
|
||||
## Installation
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||
by adding `cc_precompiler` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:cc_precompiler, "~> 0.1.6"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||
be found at <https://hexdocs.pm/cc_precompiler>.
|
||||
|
||||
## Default Targets
|
||||
|
||||
By default, it will probe some well-known C/C++ crosss-compilers existing on your system:
|
||||
|
||||
#### Linux
|
||||
|
||||
| Target Triplet | Compiler Prefix, `prefix` | `CC` | `CXX` |
|
||||
|:------------------------|:--------------------------|:----------------|:----------------|
|
||||
| `x86_64-linux-gnu` | `x86_64-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` |
|
||||
| `i686-linux-gnu` | `i686-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` |
|
||||
| `aarch64-linux-gnu` | `aarch64-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` |
|
||||
| `armv7l-linux-gnuabihf` | `arm-linux-gnueabihf-` | `#{prefix}gcc` | `#{prefix}g++` |
|
||||
| `riscv64-linux-gnu` | `riscv64-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` |
|
||||
| `powerpc64le-linux-gnu` | `powerpc64le-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` |
|
||||
| `s390x-linux-gnu` | `s390x-linux-gnu-` | `#{prefix}gcc` | `#{prefix}g++` |
|
||||
|
||||
`cc_precompiler` will try to find `#{prefix}gcc` in `$PATH`, and if `#{prefix}gcc` can be found, then the correspondong target will be activiated. Otherwise, that target will be ignored.
|
||||
|
||||
#### macOS
|
||||
|
||||
| Target Triplet | Compiler Prefix, `prefix` | `CC` | `CXX` |
|
||||
|:------------------------|:--------------------------|:-------------------|:-------------------|
|
||||
| `x86_64-apple-darwin` | N/A | `gcc -arch x86_64` | `g++ -arch x86_64` |
|
||||
| `aarch64-apple-darwin` | N/A | `gcc -arch arm64` | `g++ -arch arm64` |
|
||||
|
||||
`cc_precompiler` will try to find `gcc` in `$PATH`, and if `gcc` can be found, then both `x86_64` and `arm64` target will be activiated. Otherwise, both targets will be ignored.
|
||||
|
||||
#### Note
|
||||
Triplet for current host will be always available, `:erlang.system_info(:system_architecture)`.
|
||||
|
||||
For macOS targets, the version part will be trimmed, e.g., `x86_64-apple-darwin21.6.0` will be `x86_64-apple-darwin`.
|
||||
|
||||
### Note
|
||||
#### Conditionally switch on/off compilation flags depending on the target
|
||||
During the compilation, `cc_precompiler` will set and update the environment variable `CC_PRECOMPILER_CURRENT_TARGET` to the current target's triplet.
|
||||
|
||||
The reason we might need this is that some 3rd party library may support some feature, like AVX, but they do not offer an auto-detection mechanism, and we have to manually switch on/off corresponding compilation flags.
|
||||
|
||||
An example with further explanation can be found on [cocoa-xu/nif_opt_flags](https://github.com/cocoa-xu/nif_opt_flags).
|
||||
|
||||
Last but not least, as the name suggests, this environment variable is set by `cc_precompiler`, thus if you switch to another precompiler, please check their manual for the equvilent.
|
||||
|
||||
### Customise Precompilation Targets
|
||||
|
||||
#### Quick Start
|
||||
|
||||
To add custom targets in addition to the default configuration, you can set `:include_default_ones` in `project.cc_precompiler.compilers`.
|
||||
|
||||
Default (cross-)compiler will be included if it's `true`, otherwise only specified targets will be used.
|
||||
|
||||
Default value of `:include_default_ones` is `false` to avoid breaking changes.
|
||||
|
||||
If a custom target has the same name as a default one, then the custom one will override the default configuration for that target (e.g., the `x86_64-linux-gnu` entry below will override the default gcc configuration and use clang instead).
|
||||
|
||||
```elixir
|
||||
def project do
|
||||
[
|
||||
# ...
|
||||
cc_precompiler: [
|
||||
compilers: %{
|
||||
{:unix, :linux} => %{
|
||||
:include_default_ones => true,
|
||||
"my-custom-target" => {
|
||||
"my-custom-target-gcc",
|
||||
"my-custom-target-g++"
|
||||
},
|
||||
"x86_64-linux-gnu" => {
|
||||
"x86_64-linux-gnu-clang",
|
||||
"x86_64-linux-gnu-clang++"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
#### Fully Customise Precompilation Targets
|
||||
|
||||
```elixir
|
||||
|
||||
def project do
|
||||
[
|
||||
# ...
|
||||
cc_precompiler: [
|
||||
# optional config key
|
||||
# false - target triplet for the current machine will be included in all available targets
|
||||
# true - only targets listed in `compilers` will be included in all available targets
|
||||
# defaults to `false`
|
||||
only_listed_targets: true,
|
||||
|
||||
# optional config key
|
||||
# this option is valid if and only if `only_listed_targets` is set to `true`
|
||||
# - when `exclude_current_target` is `true`, it excludes current target (i.e., the machine that builds these binaries)
|
||||
# from the list. This can be helpful when you're doing some complex cross-compilations,
|
||||
# e.g., you'd like to specify which CI job should build for the x86_64-linux-gnu target
|
||||
# this will force current target to be excluded from the list
|
||||
exclude_current_target: false,
|
||||
|
||||
# optional config key
|
||||
# clean up the priv directory between different targets
|
||||
#
|
||||
# for example, common assets for different targets can stay
|
||||
# in the `priv` directory (instead of copying/downloading them
|
||||
# multiple times)
|
||||
# but target specific assets or .o files should be cleaned
|
||||
# so that `make` can compile/generate these files for the next target
|
||||
#
|
||||
# the value for `cleanup` should be a string indicating the cleanup target
|
||||
# in the makefile.
|
||||
#
|
||||
# for example, cc_precompiler will call `make mycleanup` between each build
|
||||
# if the value for the key `cleanup` is set to `mycleanup`
|
||||
#
|
||||
# also, cc_precompiler will stop if `make mycleanup` exited with non-zero code
|
||||
#
|
||||
# the default value for this key is `nil`, and in such case, cc_precompiler
|
||||
# will not do anything between each build
|
||||
cleanup: "mycleanup",
|
||||
|
||||
# optional config key
|
||||
# true - the corresponding target will be available as long as we can detect either `CC` or `CXX`
|
||||
# false - both `CC` and `CXX` should be present on the system
|
||||
# defaults to `false`
|
||||
allow_missing_compiler: false,
|
||||
|
||||
# optional config that provides a map of available compilers
|
||||
# on different systems
|
||||
compilers: %{
|
||||
# key (`:os.type()`)
|
||||
# this allows us to provide different available targets
|
||||
# on different systems
|
||||
# value is a map that describes which compilers are available
|
||||
#
|
||||
# key == {:unix, :linux} => when compiling on Linux
|
||||
{:unix, :linux} => %{
|
||||
# key (target triplet) => `riscv64-linux-gnu`
|
||||
# value => `PREFIX`
|
||||
# - for strings, the string will be used as the prefix of
|
||||
# the C and C++ compiler respectively, i.e.,
|
||||
# CC=`#{prefix}gcc`
|
||||
# CXX=`#{prefix}g++`
|
||||
"riscv64-linux-gnu" => "riscv64-linux-gnu-",
|
||||
# key (target triplet) => `armv7l-linux-gnueabihf`
|
||||
# value => `{CC, CXX}`
|
||||
# - for 2-tuples, the elements are the executable name of
|
||||
# the C and C++ compiler respectively
|
||||
"armv7l-linux-gnueabihf" => {
|
||||
"arm-linux-gnueabihf-gcc",
|
||||
"arm-linux-gnueabihf-g++"
|
||||
},
|
||||
# key (target triplet) => `armv7l-linux-gnueabihf`
|
||||
# value => `{CC_EXECUTABLE, CXX_EXECUTABLE, CC_TEMPLATE, CXX_TEMPLATE}`
|
||||
#
|
||||
# - for 4-tuples, the first two elements are the same as in
|
||||
# 2-tuple, the third and fourth elements are the template
|
||||
# string for CC and CPP/CXX. for example,
|
||||
#
|
||||
# the last entry below shows the example of using zig as the
|
||||
# crosscompiler for `aarch64-linux-musl`,
|
||||
# the "CC" will be
|
||||
# "zig cc -target aarch64-linux-musl",
|
||||
# and "CXX" and "CPP" will be
|
||||
# "zig c++ -target aarch64-linux-musl"
|
||||
"aarch64-linux-musl" => {
|
||||
"zig",
|
||||
"zig",
|
||||
"<% cc %> cc -target aarch64-linux-musl",
|
||||
"<% cxx %> c++ -target aarch64-linux-musl"
|
||||
}
|
||||
},
|
||||
# key == {:unix, :darwin} => when compiling on macOS
|
||||
{:unix, :darwin} => %{
|
||||
# key (target triplet) => `aarch64-apple-darwin`
|
||||
# value => `{CC, CXX}`
|
||||
"aarch64-apple-darwin" => {
|
||||
"gcc -arch arm64", "g++ -arch arm64"
|
||||
},
|
||||
# key (target triplet) => `aarch64-linux-musl`
|
||||
# value => `{CC_EXECUTABLE, CXX_EXECUTABLE, CC_TEMPLATE, CXX_TEMPLATE}`
|
||||
"aarch64-linux-musl" => {
|
||||
"zig",
|
||||
"zig",
|
||||
"<% cc %> cc -target aarch64-linux-musl",
|
||||
"<% cxx %> c++ -target aarch64-linux-musl"
|
||||
},
|
||||
# key (target triplet) => `my-custom-target`
|
||||
# - for 3-tuples, the first element should be `:script`
|
||||
# the second element is the path to the elixir script file
|
||||
# the third element is a 2-tuple,
|
||||
# the first one is the name of the module
|
||||
# the second one is custom args
|
||||
# the module need to impl the `compile/5` callback declared in
|
||||
# `CCPrecompiler.CompilationScript`
|
||||
"my-custom-target" => {
|
||||
:script, "custom.exs", {CustomCompile, []}
|
||||
},
|
||||
# key (target triplet) => `macos-universal`
|
||||
# on macOS, CCPrecompiler also provides a builtin module to create
|
||||
# universal binary for NIF libraries that only has a `nif.so` file
|
||||
"macos-universal" => {
|
||||
:script, "", {CCPrecompiler.UniversalBinary, []}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
`CCPrecompiler.CompilationScript` is defined as follows,
|
||||
|
||||
```elixir
|
||||
defmodule CCPrecompiler.CompilationScript do
|
||||
@callback compile(
|
||||
app :: atom(),
|
||||
version :: String.t(),
|
||||
nif_version :: String.t(),
|
||||
target :: String.t(),
|
||||
command_line_args :: [String.t()],
|
||||
custom_args :: [String.t()]
|
||||
) :: :ok | {:error, String.t()}
|
||||
end
|
||||
```
|
||||
|
||||
### Custom Compilation Script Examples
|
||||
#### Compile with `ccache`
|
||||
|
||||
```elixir
|
||||
defmodule CCPrecompiler.CCache do
|
||||
@moduledoc """
|
||||
Compile with ccache
|
||||
|
||||
## Example
|
||||
|
||||
"x86_64-linux-gnu" => {
|
||||
:script, "custom.exs", {CCPrecompiler.CCache, []}
|
||||
}
|
||||
|
||||
It's also possible to do this using a 4-tuple:
|
||||
|
||||
"x86_64-linux-musl" => {
|
||||
"gcc", "g++", "ccache <% cc %>", "ccache <% cxx %>"
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
@behaviour CCPrecompiler.CompilationScript
|
||||
|
||||
@impl CCPrecompiler.CompilationScript
|
||||
def compile(app, version, nif_version, target, args, _custom_args) do
|
||||
System.put_env("CC", "ccache gcc")
|
||||
System.put_env("CXX", "ccache g++")
|
||||
System.put_env("CPP", "ccache g++")
|
||||
|
||||
ElixirMake.Precompiler.mix_compile(args)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Build A Universal NIF Binary on macOS
|
||||
File can be found at `lib/complation_script/universal_binary.ex`.
|
||||
|
||||
```elixir
|
||||
defmodule CCPrecompiler.UniversalBinary do
|
||||
@moduledoc """
|
||||
Build a universal binary on macOS
|
||||
|
||||
## Example
|
||||
|
||||
"macos-universal" => {
|
||||
:script, "universal_binary.exs", {CCPrecompiler.UniversalBinary, []}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
@behaviour CCPrecompiler.CompilationScript
|
||||
|
||||
@impl CCPrecompiler.CompilationScript
|
||||
def compile(_app, _version, _nif_version, _target, args, _custom_args) do
|
||||
config = Mix.Project.config()
|
||||
app_priv = Path.join(Mix.Project.app_path(config), "priv")
|
||||
make_precompiler_filename = config[:make_precompiler_filename] || "nif"
|
||||
nif_file = "#{make_precompiler_filename}.so"
|
||||
|
||||
compiled_bin = Path.join(app_priv, nif_file)
|
||||
x86_64_bin = Path.join(app_priv, "#{make_precompiler_filename}_x86_64.so")
|
||||
aarch64_bin = Path.join(app_priv, "#{make_precompiler_filename}_aarch64.so")
|
||||
|
||||
File.rm(compiled_bin)
|
||||
|
||||
# first we compile `x86_64-apple-darwin`
|
||||
:ok = System.put_env("CC", "gcc -arch x86_64")
|
||||
System.put_env("CXX", "gcc -arch x86_64")
|
||||
System.put_env("CPP", "g++ -arch x86_64")
|
||||
ElixirMake.Compiler.compile(args)
|
||||
File.rename!(compiled_bin, x86_64_bin)
|
||||
|
||||
# then we compile `aarch64-apple-darwin`
|
||||
System.put_env("CC", "gcc -arch arm64")
|
||||
System.put_env("CXX", "gcc -arch arm64")
|
||||
System.put_env("CPP", "g++ -arch arm64")
|
||||
ElixirMake.Compiler.compile(args)
|
||||
File.rename!(compiled_bin, aarch64_bin)
|
||||
|
||||
{%IO.Stream{}, exit_status} = System.cmd("lipo", ["-create", "-output", compiled_bin, x86_64_bin, aarch64_bin])
|
||||
|
||||
File.rm!(x86_64_bin)
|
||||
File.rm!(aarch64_bin)
|
||||
|
||||
if exit_status == 0 do
|
||||
:ok
|
||||
else
|
||||
Mix.raise("Failed to create universal binary")
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
28
phoenix/deps/cc_precompiler/hex_metadata.config
Normal file
28
phoenix/deps/cc_precompiler/hex_metadata.config
Normal file
@@ -0,0 +1,28 @@
|
||||
{<<"links">>,
|
||||
[{<<"Changelog">>,
|
||||
<<"https://github.com/cocoa-xu/cc_precompiler/blob/v0.1.11/CHANGELOG.md">>},
|
||||
{<<"GitHub">>,<<"https://github.com/cocoa-xu/cc_precompiler">>},
|
||||
{<<"Precompilation Guide">>,
|
||||
<<"https://github.com/cocoa-xu/cc_precompiler/blob/v0.1.11/PRECOMPILATION_GUIDE.md">>},
|
||||
{<<"Readme">>,
|
||||
<<"https://github.com/cocoa-xu/cc_precompiler/blob/v0.1.11/README.md">>}]}.
|
||||
{<<"name">>,<<"cc_precompiler">>}.
|
||||
{<<"version">>,<<"0.1.11">>}.
|
||||
{<<"description">>,
|
||||
<<"NIF library Precompiler that uses C/C++ (cross-)compiler.">>}.
|
||||
{<<"elixir">>,<<"~> 1.11">>}.
|
||||
{<<"app">>,<<"cc_precompiler">>}.
|
||||
{<<"files">>,
|
||||
[<<"lib">>,<<"lib/cc_precompiler.ex">>,<<"lib/compilation_script">>,
|
||||
<<"lib/compilation_script/universal_binary.ex">>,
|
||||
<<"lib/compilation_script/compilation_script.ex">>,<<"mix.exs">>,
|
||||
<<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>,
|
||||
<<"PRECOMPILATION_GUIDE.md">>]}.
|
||||
{<<"licenses">>,[<<"Apache-2.0">>]}.
|
||||
{<<"requirements">>,
|
||||
[[{<<"name">>,<<"elixir_make">>},
|
||||
{<<"app">>,<<"elixir_make">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 0.7 or ~> 0.8 or ~> 0.9">>},
|
||||
{<<"repository">>,<<"hexpm">>}]]}.
|
||||
{<<"build_tools">>,[<<"mix">>]}.
|
||||
576
phoenix/deps/cc_precompiler/lib/cc_precompiler.ex
Normal file
576
phoenix/deps/cc_precompiler/lib/cc_precompiler.ex
Normal file
@@ -0,0 +1,576 @@
|
||||
defmodule CCPrecompiler do
|
||||
@moduledoc """
|
||||
Precompile with existing crosscompiler in the system.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
@behaviour ElixirMake.Precompiler
|
||||
|
||||
# The default configuration for this precompiler module on linux systems.
|
||||
# It will detect for the following targets
|
||||
# - x86_64-linux-gnu
|
||||
# - i686-linux-gnu
|
||||
# - aarch64-linux-gnu
|
||||
# - armv7l-linux-gnueabihf
|
||||
# - riscv64-linux-gnu
|
||||
# - powerpc64le-linux-gnu
|
||||
# - s390x-linux-gnu
|
||||
# by trying to find the corresponding executable, i.e.,
|
||||
# - x86_64-linux-gnu-gcc
|
||||
# - i686-linux-gnu-gcc
|
||||
# - aarch64-linux-gnu-gcc
|
||||
# - arm-linux-gnueabihf-gcc
|
||||
# - riscv64-linux-gnu-gcc
|
||||
# - powerpc64le-linux-gnu-gcc
|
||||
# - s390x-linux-gnu-gcc
|
||||
# (this module will only try to find the CC executable, a step further
|
||||
# will be trying to compile a simple C/C++ program using them)
|
||||
@default_compilers %{
|
||||
{:unix, :linux} => %{
|
||||
"x86_64-linux-gnu" => "x86_64-linux-gnu-",
|
||||
"i686-linux-gnu" => "i686-linux-gnu-",
|
||||
"aarch64-linux-gnu" => "aarch64-linux-gnu-",
|
||||
"armv7l-linux-gnueabihf" => "arm-linux-gnueabihf-",
|
||||
"riscv64-linux-gnu" => "riscv64-linux-gnu-",
|
||||
"powerpc64le-linux-gnu" => "powerpc64le-linux-gnu-",
|
||||
"s390x-linux-gnu" => "s390x-linux-gnu-"
|
||||
},
|
||||
{:unix, :darwin} => %{
|
||||
"x86_64-apple-darwin" => {
|
||||
"gcc",
|
||||
"g++",
|
||||
"<%= cc %> -arch x86_64",
|
||||
"<%= cxx %> -arch x86_64"
|
||||
},
|
||||
"aarch64-apple-darwin" => {
|
||||
"gcc",
|
||||
"g++",
|
||||
"<%= cc %> -arch arm64",
|
||||
"<%= cxx %> -arch arm64"
|
||||
}
|
||||
},
|
||||
{:win32, :nt} => %{
|
||||
"x86_64-windows-msvc" => {"cl", "cl"}
|
||||
}
|
||||
}
|
||||
defp default_compilers, do: @default_compilers
|
||||
defp user_config, do: Mix.Project.config()[:cc_precompiler] || default_compilers()
|
||||
defp compilers, do: Access.get(user_config(), :compilers, default_compilers())
|
||||
|
||||
defp compilers_current_os,
|
||||
do:
|
||||
{Access.get(compilers(), :os.type(), %{}), Access.get(default_compilers(), :os.type(), %{})}
|
||||
|
||||
defp compilers_current_os_with_override do
|
||||
{compiler_map1, compiler_map2} = compilers_current_os()
|
||||
|
||||
if Map.has_key?(compiler_map1, :include_default_ones) do
|
||||
include_default_ones = Map.get(compiler_map1, :include_default_ones, false)
|
||||
compiler_map1 = Map.delete(compiler_map1, :include_default_ones)
|
||||
|
||||
if include_default_ones == true do
|
||||
Map.merge(compiler_map1, compiler_map2, fn _, _, user_override -> user_override end)
|
||||
else
|
||||
compiler_map1
|
||||
end
|
||||
else
|
||||
compiler_map1
|
||||
end
|
||||
end
|
||||
|
||||
defp only_listed_targets, do: Access.get(user_config(), :only_listed_targets, false)
|
||||
defp exclude_current_target, do: Access.get(user_config(), :exclude_current_target, false)
|
||||
defp allow_missing_compiler, do: Access.get(user_config(), :allow_missing_compiler, false)
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def current_target do
|
||||
current_target_from_env = current_target_from_env()
|
||||
|
||||
if current_target_from_env do
|
||||
# overwrite current target triplet from environment variables
|
||||
{:ok, current_target_from_env}
|
||||
else
|
||||
current_target(:os.type())
|
||||
end
|
||||
end
|
||||
|
||||
defp current_target_from_env do
|
||||
arch = System.get_env("TARGET_ARCH")
|
||||
os = System.get_env("TARGET_OS")
|
||||
abi = System.get_env("TARGET_ABI")
|
||||
|
||||
if !Enum.all?([arch, os, abi], &Kernel.is_nil/1) do
|
||||
"#{arch}-#{os}-#{abi}"
|
||||
end
|
||||
end
|
||||
|
||||
def current_target({:win32, _}) do
|
||||
processor_architecture =
|
||||
String.downcase(String.trim(System.get_env("PROCESSOR_ARCHITECTURE")))
|
||||
|
||||
# https://docs.microsoft.com/en-gb/windows/win32/winprog64/wow64-implementation-details?redirectedfrom=MSDN
|
||||
partial_triplet =
|
||||
case processor_architecture do
|
||||
"amd64" ->
|
||||
"x86_64-windows-"
|
||||
|
||||
"ia64" ->
|
||||
"ia64-windows-"
|
||||
|
||||
"arm64" ->
|
||||
"aarch64-windows-"
|
||||
|
||||
"x86" ->
|
||||
"x86-windows-"
|
||||
end
|
||||
|
||||
{compiler, _} = :erlang.system_info(:c_compiler_used)
|
||||
|
||||
case compiler do
|
||||
:msc ->
|
||||
{:ok, partial_triplet <> "msvc"}
|
||||
|
||||
:gnuc ->
|
||||
{:ok, partial_triplet <> "gnu"}
|
||||
|
||||
other ->
|
||||
{:ok, partial_triplet <> Atom.to_string(other)}
|
||||
end
|
||||
end
|
||||
|
||||
def current_target({:unix, _}) do
|
||||
# get current target triplet from `:erlang.system_info/1`
|
||||
system_architecture = to_string(:erlang.system_info(:system_architecture))
|
||||
current = String.split(system_architecture, "-", trim: true)
|
||||
|
||||
case length(current) do
|
||||
4 ->
|
||||
{:ok, "#{Enum.at(current, 0)}-#{Enum.at(current, 2)}-#{Enum.at(current, 3)}"}
|
||||
|
||||
3 ->
|
||||
case :os.type() do
|
||||
{:unix, :darwin} ->
|
||||
# could be something like aarch64-apple-darwin21.0.0
|
||||
# but we don't really need the last 21.0.0 part
|
||||
if String.match?(Enum.at(current, 2), ~r/^darwin.*/) do
|
||||
{:ok, "#{Enum.at(current, 0)}-#{Enum.at(current, 1)}-darwin"}
|
||||
else
|
||||
{:ok, system_architecture}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:ok, system_architecture}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, "cannot decide current target"}
|
||||
end
|
||||
end
|
||||
|
||||
defp only_local do
|
||||
System.get_env("CC_PRECOMPILER_PRECOMPILE_ONLY_LOCAL") == "true"
|
||||
end
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def all_supported_targets(:compile) do
|
||||
# this callback is expected to return a list of string for
|
||||
# all supported targets by this precompiler. in this
|
||||
# implementation, we will try to find a few crosscompilers
|
||||
# available in the system.
|
||||
# Note that this implementation is mainly used for demonstration
|
||||
# purpose, therefore the hardcoded compiler names are used in
|
||||
# DEBIAN/Ubuntu Linux (as I only installed these ones at the
|
||||
# time of writing this example)
|
||||
available_targets = find_all_available_targets()
|
||||
|
||||
targets =
|
||||
case {only_local(), only_listed_targets(), current_target()} do
|
||||
{true, true, {:ok, current}} ->
|
||||
if Enum.member?(available_targets, current) do
|
||||
[current]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{true, _, {:error, err_msg}} ->
|
||||
Mix.raise(err_msg)
|
||||
|
||||
{true, false, {:ok, current}} ->
|
||||
Enum.uniq([current] ++ available_targets)
|
||||
|
||||
{false, true, _} ->
|
||||
available_targets
|
||||
|
||||
{false, false, {:ok, current}} ->
|
||||
Enum.uniq([current] ++ available_targets)
|
||||
end
|
||||
|
||||
if exclude_current_target() do
|
||||
case current_target() do
|
||||
{:ok, current} ->
|
||||
targets -- [current]
|
||||
|
||||
_ ->
|
||||
targets
|
||||
end
|
||||
else
|
||||
targets
|
||||
end
|
||||
end
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def all_supported_targets(:fetch) do
|
||||
Enum.map(compilers(), fn {os, compilers} ->
|
||||
Enum.map(Map.keys(compilers), fn key ->
|
||||
if key == :include_default_ones do
|
||||
Map.keys(default_compilers()[os])
|
||||
else
|
||||
key
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|> List.flatten()
|
||||
end
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def unavailable_target(_) do
|
||||
if only_listed_targets() do
|
||||
:ignore
|
||||
else
|
||||
:compile
|
||||
end
|
||||
end
|
||||
|
||||
defp find_all_available_targets do
|
||||
compilers = compilers_current_os_with_override()
|
||||
|
||||
compilers
|
||||
|> Map.keys()
|
||||
|> Enum.map(&find_available_compilers(&1, Map.get(compilers, &1)))
|
||||
|> Enum.reject(fn x -> x == nil end)
|
||||
end
|
||||
|
||||
defp find_available_compilers(triplet, prefix) when is_binary(prefix) do
|
||||
if ensure_executable(["#{prefix}gcc", "#{prefix}g++"]) do
|
||||
Logger.debug("Found compiler for #{triplet}")
|
||||
triplet
|
||||
else
|
||||
Logger.debug("Compiler not found for #{triplet}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp find_available_compilers(triplet, {cc, cxx}) when is_binary(cc) and is_binary(cxx) do
|
||||
if ensure_executable([cc, cxx]) do
|
||||
Logger.debug("Found compiler for #{triplet}")
|
||||
triplet
|
||||
else
|
||||
Logger.debug("Compiler not found for #{triplet}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp find_available_compilers(triplet, {:script, _, _}) do
|
||||
triplet
|
||||
end
|
||||
|
||||
defp find_available_compilers(triplet, {cc_executable, cxx_executable, _, _})
|
||||
when is_binary(cc_executable) and is_binary(cxx_executable) do
|
||||
if ensure_executable([cc_executable, cxx_executable]) do
|
||||
Logger.debug("Found compiler for #{triplet}")
|
||||
triplet
|
||||
else
|
||||
Logger.debug("Compiler not found for #{triplet}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp find_available_compilers(triplet, invalid) do
|
||||
Mix.raise(
|
||||
"Invalid configuration for #{triplet}, expecting a string, 2-tuple or 4-tuple. Got `#{inspect(invalid)}`"
|
||||
)
|
||||
end
|
||||
|
||||
defp ensure_executable(executable_list) when is_list(executable_list) do
|
||||
if allow_missing_compiler() do
|
||||
Enum.any?(executable_list, &System.find_executable/1)
|
||||
else
|
||||
Enum.all?(executable_list, &System.find_executable/1)
|
||||
end
|
||||
end
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def build_native(args) do
|
||||
# In this callback we just build the NIF library natively,
|
||||
# and because this precompiler module is designed for NIF
|
||||
# libraries that use C/C++ as the main language with Makefile,
|
||||
# we can just call `ElixirMake.Precompiler.mix_compile(args)`
|
||||
#
|
||||
# It's also possible to forward this call to:
|
||||
#
|
||||
# `precompile(args, elem(current_target(), 1))`
|
||||
#
|
||||
# This could be useful when the precompiler is using a universal
|
||||
# (cross-)compiler, say zig. in this way, the compiled binaries
|
||||
# (`mix compile`) will be consistent as the corresponding precompiled
|
||||
# one (with `mix elixir_make.precompile`)
|
||||
#
|
||||
# However, if you'd prefer to having the same behaviour for `mix compile`
|
||||
# then the following line is okay
|
||||
ElixirMake.Precompiler.mix_compile(args)
|
||||
end
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def precompile(args, target) do
|
||||
# in this callback we compile the NIF library for a given target
|
||||
config = Mix.Project.config()
|
||||
app = config[:app]
|
||||
version = config[:version]
|
||||
priv_paths = config[:make_precompiler_priv_paths] || ["."]
|
||||
|
||||
saved_cc = System.get_env("CC") || ""
|
||||
saved_cxx = System.get_env("CXX") || ""
|
||||
saved_cpp = System.get_env("CPP") || ""
|
||||
|
||||
Logger.debug("Current compiling target: #{target}")
|
||||
|
||||
cc_cxx = get_cc_and_cxx(target)
|
||||
|
||||
# remove files in the lists
|
||||
app_priv = Path.join(Mix.Project.app_path(config), "priv")
|
||||
|
||||
case priv_paths do
|
||||
["."] ->
|
||||
File.rm_rf!(app_priv)
|
||||
|
||||
_ ->
|
||||
for include <- priv_paths,
|
||||
file <- Path.wildcard(Path.join(app_priv, include)) do
|
||||
File.rm_rf(file)
|
||||
end
|
||||
end
|
||||
|
||||
File.mkdir_p!(app_priv)
|
||||
|
||||
case cc_cxx do
|
||||
{cc, cxx} ->
|
||||
System.put_env("CC", cc)
|
||||
System.put_env("CXX", cxx)
|
||||
System.put_env("CPP", cxx)
|
||||
|
||||
System.put_env("CC_PRECOMPILER_CURRENT_TARGET", target)
|
||||
ElixirMake.Precompiler.mix_compile(args)
|
||||
|
||||
{:script, module, custom_args} ->
|
||||
System.put_env("CC_PRECOMPILER_CURRENT_TARGET", target)
|
||||
|
||||
Kernel.apply(module, :compile, [
|
||||
app,
|
||||
version,
|
||||
"#{:erlang.system_info(:nif_version)}",
|
||||
target,
|
||||
args,
|
||||
custom_args
|
||||
])
|
||||
end
|
||||
|
||||
System.put_env("CC", saved_cc)
|
||||
System.put_env("CXX", saved_cxx)
|
||||
System.put_env("CPP", saved_cpp)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp get_cc_and_cxx(triplet) do
|
||||
case Access.get(compilers_current_os_with_override(), triplet, nil) do
|
||||
nil ->
|
||||
cc = System.get_env("CC")
|
||||
cxx = System.get_env("CXX")
|
||||
cpp = System.get_env("CPP")
|
||||
|
||||
case {cc, cxx, cpp} do
|
||||
{nil, _, _} ->
|
||||
{"gcc", "g++"}
|
||||
|
||||
{_, nil, nil} ->
|
||||
{"gcc", "g++"}
|
||||
|
||||
{_, _, nil} ->
|
||||
{cc, cxx}
|
||||
|
||||
{_, nil, _} ->
|
||||
{cc, cpp}
|
||||
|
||||
{_, _, _} ->
|
||||
{cc, cxx}
|
||||
end
|
||||
|
||||
{cc, cxx} ->
|
||||
{cc, cxx}
|
||||
|
||||
prefix when is_binary(prefix) ->
|
||||
{"#{prefix}gcc", "#{prefix}g++"}
|
||||
|
||||
{:script, script_path, {module, args}} ->
|
||||
case {script_path, module} do
|
||||
{"", CCPrecompiler.UniversalBinary} ->
|
||||
{:script, module, args}
|
||||
|
||||
_ ->
|
||||
Code.require_file(script_path)
|
||||
{:script, module, args}
|
||||
end
|
||||
|
||||
{cc, cxx, cc_args, cxx_args} ->
|
||||
{EEx.eval_string(cc_args, cc: cc), EEx.eval_string(cxx_args, cxx: cxx)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def post_precompile_target(target) do
|
||||
config = Mix.Project.config()
|
||||
cc_precompiler_config = config[:cc_precompiler]
|
||||
cleanup(config, cc_precompiler_config[:cleanup], target)
|
||||
end
|
||||
|
||||
defp cleanup(_, nil, _), do: :ok
|
||||
|
||||
defp cleanup(config, make_target, current_precompilation_target) when is_binary(make_target) do
|
||||
exec =
|
||||
System.get_env("MAKE") ||
|
||||
os_specific_executable(Keyword.get(config, :make_executable, :default))
|
||||
|
||||
makefile = Keyword.get(config, :make_makefile, :default)
|
||||
env = Keyword.get(config, :make_env, %{})
|
||||
env = if is_function(env), do: env.(), else: env
|
||||
env = default_env(config, env, current_precompilation_target)
|
||||
|
||||
# In OTP 19, Erlang's `open_port/2` ignores the current working
|
||||
# directory when expanding relative paths. This means that `:make_cwd`
|
||||
# must be an absolute path. This is a different behaviour from earlier
|
||||
# OTP versions and appears to be a bug. It is being tracked at
|
||||
# https://bugs.erlang.org/browse/ERL-175.
|
||||
cwd = Keyword.get(config, :make_cwd, ".") |> Path.expand(File.cwd!())
|
||||
|
||||
if String.contains?(cwd, " ") do
|
||||
IO.warn(
|
||||
"the absolute path to the makefile for this project contains spaces. Make might " <>
|
||||
"not work properly if spaces are present in the path. The absolute path is: " <>
|
||||
inspect(cwd)
|
||||
)
|
||||
end
|
||||
|
||||
base = exec |> Path.basename() |> Path.rootname()
|
||||
args = args_for_makefile(base, makefile) ++ [make_target]
|
||||
|
||||
case cmd(exec, args, cwd, env) do
|
||||
0 ->
|
||||
:ok
|
||||
|
||||
exit_status ->
|
||||
raise_cleanup_error(exec, exit_status)
|
||||
end
|
||||
end
|
||||
|
||||
defp raise_cleanup_error(exec, exit_status) do
|
||||
Mix.raise(~s{Could not complete cleanup work with "#{exec}" (exit status: #{exit_status}).\n})
|
||||
end
|
||||
|
||||
# Returns a map of default environment variables
|
||||
# Defaults may be overwritten.
|
||||
defp default_env(config, default_env, current_precompilation_target) do
|
||||
root_dir = :code.root_dir()
|
||||
erl_interface_dir = Path.join(root_dir, "usr")
|
||||
erts_dir = Path.join(root_dir, "erts-#{:erlang.system_info(:version)}")
|
||||
erts_include_dir = Path.join(erts_dir, "include")
|
||||
erl_ei_lib_dir = Path.join(erl_interface_dir, "lib")
|
||||
erl_ei_include_dir = Path.join(erl_interface_dir, "include")
|
||||
|
||||
Map.merge(
|
||||
%{
|
||||
# Don't use Mix.target/0 here for backwards compatibility
|
||||
"MIX_TARGET" => env("MIX_TARGET", "host"),
|
||||
"MIX_ENV" => to_string(Mix.env()),
|
||||
"MIX_BUILD_PATH" => Mix.Project.build_path(config),
|
||||
"MIX_APP_PATH" => Mix.Project.app_path(config),
|
||||
"MIX_COMPILE_PATH" => Mix.Project.compile_path(config),
|
||||
"MIX_CONSOLIDATION_PATH" => Mix.Project.consolidation_path(config),
|
||||
"MIX_DEPS_PATH" => Mix.Project.deps_path(config),
|
||||
"MIX_MANIFEST_PATH" => Mix.Project.manifest_path(config),
|
||||
|
||||
# Rebar naming
|
||||
"ERL_EI_LIBDIR" => env("ERL_EI_LIBDIR", erl_ei_lib_dir),
|
||||
"ERL_EI_INCLUDE_DIR" => env("ERL_EI_INCLUDE_DIR", erl_ei_include_dir),
|
||||
|
||||
# erlang.mk naming
|
||||
"ERTS_INCLUDE_DIR" => env("ERTS_INCLUDE_DIR", erts_include_dir),
|
||||
"ERL_INTERFACE_LIB_DIR" => env("ERL_INTERFACE_LIB_DIR", erl_ei_lib_dir),
|
||||
"ERL_INTERFACE_INCLUDE_DIR" => env("ERL_INTERFACE_INCLUDE_DIR", erl_ei_include_dir),
|
||||
|
||||
# Disable default erlang values
|
||||
"BINDIR" => nil,
|
||||
"ROOTDIR" => nil,
|
||||
"PROGNAME" => nil,
|
||||
"EMU" => nil,
|
||||
|
||||
# cc_precompiler
|
||||
"CC_PRECOMPILER_CURRENT_TARGET" => current_precompilation_target
|
||||
},
|
||||
default_env
|
||||
)
|
||||
end
|
||||
|
||||
defp os_specific_executable(exec) when is_binary(exec) do
|
||||
exec
|
||||
end
|
||||
|
||||
defp os_specific_executable(:default) do
|
||||
case :os.type() do
|
||||
{:win32, _} ->
|
||||
cond do
|
||||
System.find_executable("nmake") -> "nmake"
|
||||
System.find_executable("make") -> "make"
|
||||
true -> "nmake"
|
||||
end
|
||||
|
||||
{:unix, type} when type in [:freebsd, :openbsd, :netbsd] ->
|
||||
"gmake"
|
||||
|
||||
_ ->
|
||||
"make"
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a list of command-line args to pass to make (or nmake/gmake) in
|
||||
# order to specify the makefile to use.
|
||||
defp args_for_makefile("nmake", :default), do: ["/F", "Makefile.win"]
|
||||
defp args_for_makefile("nmake", makefile), do: ["/F", makefile]
|
||||
defp args_for_makefile(_, :default), do: []
|
||||
defp args_for_makefile(_, makefile), do: ["-f", makefile]
|
||||
|
||||
# Runs `exec [args]` in `cwd` and prints the stdout and stderr in real time,
|
||||
# as soon as `exec` prints them (using `IO.Stream`).
|
||||
defp cmd(exec, args, cwd, env) do
|
||||
opts = [
|
||||
into: IO.stream(:stdio, :line),
|
||||
stderr_to_stdout: true,
|
||||
cd: cwd,
|
||||
env: env
|
||||
]
|
||||
|
||||
{%IO.Stream{}, status} = System.cmd(find_executable(exec), args, opts)
|
||||
status
|
||||
end
|
||||
|
||||
defp find_executable(exec) do
|
||||
System.find_executable(exec) ||
|
||||
Mix.raise("""
|
||||
"#{exec}" not found in the path. If you have set the MAKE environment variable,
|
||||
please make sure it is correct.
|
||||
""")
|
||||
end
|
||||
|
||||
defp env(var, default) do
|
||||
System.get_env(var) || default
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
defmodule CCPrecompiler.CompilationScript do
|
||||
@callback compile(
|
||||
app :: atom(),
|
||||
version :: String.t(),
|
||||
nif_version :: String.t(),
|
||||
target :: String.t(),
|
||||
command_line_args :: [String.t()],
|
||||
custom_args :: [String.t()]
|
||||
) :: :ok | {:error, String.t()}
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
defmodule CCPrecompiler.UniversalBinary do
|
||||
@moduledoc """
|
||||
Build a universal binary on macOS
|
||||
|
||||
## Example
|
||||
|
||||
"macos-universal" => {
|
||||
:script, "", {CCPrecompiler.UniversalBinary, []}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
@behaviour CCPrecompiler.CompilationScript
|
||||
|
||||
@impl CCPrecompiler.CompilationScript
|
||||
def compile(_app, _version, _nif_version, _target, args, _custom_args) do
|
||||
config = Mix.Project.config()
|
||||
app_priv = Path.join(Mix.Project.app_path(config), "priv")
|
||||
make_precompiler_filename = config[:make_precompiler_filename] || "nif"
|
||||
nif_file = "#{make_precompiler_filename}.so"
|
||||
|
||||
compiled_bin = Path.join(app_priv, nif_file)
|
||||
x86_64_bin = Path.join(app_priv, "#{make_precompiler_filename}_x86_64.so")
|
||||
aarch64_bin = Path.join(app_priv, "#{make_precompiler_filename}_aarch64.so")
|
||||
|
||||
File.rm(compiled_bin)
|
||||
|
||||
# first we compile `x86_64-apple-darwin`
|
||||
:ok = System.put_env("CC", "gcc -arch x86_64")
|
||||
System.put_env("CXX", "gcc -arch x86_64")
|
||||
System.put_env("CPP", "g++ -arch x86_64")
|
||||
ElixirMake.Compiler.compile(args)
|
||||
File.rename!(compiled_bin, x86_64_bin)
|
||||
|
||||
# then we compile `aarch64-apple-darwin`
|
||||
System.put_env("CC", "gcc -arch arm64")
|
||||
System.put_env("CXX", "gcc -arch arm64")
|
||||
System.put_env("CPP", "g++ -arch arm64")
|
||||
ElixirMake.Compiler.compile(args)
|
||||
File.rename!(compiled_bin, aarch64_bin)
|
||||
|
||||
{_, exit_status} =
|
||||
System.cmd("lipo", ["-create", "-output", compiled_bin, x86_64_bin, aarch64_bin])
|
||||
|
||||
File.rm!(x86_64_bin)
|
||||
File.rm!(aarch64_bin)
|
||||
|
||||
if exit_status == 0 do
|
||||
:ok
|
||||
else
|
||||
Mix.raise("Failed to create universal binary")
|
||||
end
|
||||
end
|
||||
end
|
||||
63
phoenix/deps/cc_precompiler/mix.exs
Normal file
63
phoenix/deps/cc_precompiler/mix.exs
Normal file
@@ -0,0 +1,63 @@
|
||||
defmodule CCPrecompiler.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@app :cc_precompiler
|
||||
@version "0.1.11"
|
||||
@github_url "https://github.com/cocoa-xu/cc_precompiler"
|
||||
def project do
|
||||
[
|
||||
app: @app,
|
||||
version: @version,
|
||||
elixir: "~> 1.11",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
docs: docs(),
|
||||
description: "NIF library Precompiler that uses C/C++ (cross-)compiler.",
|
||||
package: package()
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger, :eex]
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:elixir_make, "~> 0.7 or ~> 0.8 or ~> 0.9", runtime: false},
|
||||
# docs
|
||||
{:ex_doc, ">= 0.0.0", only: :docs, runtime: false}
|
||||
]
|
||||
end
|
||||
|
||||
defp package do
|
||||
[
|
||||
name: Atom.to_string(@app),
|
||||
files: ~w(lib mix.exs README* LICENSE* *.md),
|
||||
licenses: ["Apache-2.0"],
|
||||
links: links()
|
||||
]
|
||||
end
|
||||
|
||||
defp docs do
|
||||
[
|
||||
main: "PRECOMPILATION_GUIDE",
|
||||
source_ref: "v#{@version}",
|
||||
source_url: @github_url,
|
||||
extras: [
|
||||
"PRECOMPILATION_GUIDE.md",
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
defp links do
|
||||
%{
|
||||
"GitHub" => @github_url,
|
||||
"Readme" => "#{@github_url}/blob/v#{@version}/README.md",
|
||||
"Precompilation Guide" => "#{@github_url}/blob/v#{@version}/PRECOMPILATION_GUIDE.md",
|
||||
"Changelog" => "#{@github_url}/blob/v#{@version}/CHANGELOG.md"
|
||||
}
|
||||
end
|
||||
end
|
||||
4
phoenix/deps/db_connection/.formatter.exs
Normal file
4
phoenix/deps/db_connection/.formatter.exs
Normal file
@@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test,examples,integration_test}/**/*.{ex,exs}"]
|
||||
]
|
||||
BIN
phoenix/deps/db_connection/.hex
Normal file
BIN
phoenix/deps/db_connection/.hex
Normal file
Binary file not shown.
112
phoenix/deps/db_connection/CHANGELOG.md
Normal file
112
phoenix/deps/db_connection/CHANGELOG.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Changelog
|
||||
|
||||
## v2.9.0 (2026-01-10)
|
||||
|
||||
* Enhancements
|
||||
* Whenever possible, have error messages that include a PID also include ancestors and one of process label, name, or initial call
|
||||
* Allow clients to retry when specified
|
||||
* Remove sensitive options before calling `after_connect`
|
||||
|
||||
## v2.8.1 (2025-06-24)
|
||||
|
||||
* Enhancements
|
||||
* Automatically set and read process labels in error reports
|
||||
|
||||
## v2.8.0 (2025-06-24)
|
||||
|
||||
* Enhancements
|
||||
* Allow `unallow_existing` as an opt to `ownership_allow/4`
|
||||
* Improve ETS performance by enabling descentralized counters
|
||||
* Increase default queue interval to 2000ms
|
||||
|
||||
## v2.7.0 (2024-07-02)
|
||||
|
||||
* Enhancements
|
||||
* Add API for retrieving pool metrics
|
||||
* Include a built-in listener that emits telemetry events
|
||||
|
||||
* Bug fixes
|
||||
* Discard EXIT messages from trapped exits
|
||||
|
||||
## v2.6.0 (2023-10-15)
|
||||
|
||||
* Enhancements
|
||||
* Call `disconnect` on terminate
|
||||
* Allow `handle_begin` callbacks to return query for logging purposes
|
||||
* Add `:connection_listeners_tag`
|
||||
* Add `DBConnection.available_connection_options/0`
|
||||
* Add `DBConnection.available_start_options/0`
|
||||
|
||||
## v2.5.0 (2023-04-10)
|
||||
|
||||
* Internal changes
|
||||
* No longer depend on `connection`
|
||||
|
||||
## v2.4.3 (2022-11-22)
|
||||
|
||||
* Bug fixes
|
||||
* Fix bug where `disconnect_all/2` interval would be disabled above 4294ms
|
||||
* Add `:idle_limit` to limit the amount of disconnections on a ping
|
||||
|
||||
## v2.4.2 (2022-03-03)
|
||||
|
||||
* Enhancements
|
||||
* Add `DBConnection.connection_module/1`
|
||||
|
||||
## v2.4.1 (2021-10-14)
|
||||
|
||||
* Enhancements
|
||||
* Add `DBConnection.disconnect_all/2`
|
||||
|
||||
## v2.4.0 (2021-04-02)
|
||||
|
||||
* Enhancements
|
||||
* Add telemetry events for connection errors
|
||||
* Use `:rand` default algorithm
|
||||
* Allow decentralized lookups on DBConnection.Ownership
|
||||
|
||||
## v2.3.1 (2020-11-25)
|
||||
|
||||
* Enhancements
|
||||
* Add `:connection_listeners` to `DBConnection.start_link/2`
|
||||
* Allow connection `~> 1.0`
|
||||
|
||||
## v2.3.0 (2020-10-14)
|
||||
|
||||
This release requires Elixir v1.7+.
|
||||
|
||||
* Bug fixes
|
||||
* Fix deprecation warnings related to the use of `System.stacktrace()`
|
||||
|
||||
## v2.2.2 (2020-04-22)
|
||||
|
||||
* Bug fixes
|
||||
* Make sure all idle connections in the pool are pinged on each idle interval
|
||||
|
||||
## v2.2.1 (2020-02-04)
|
||||
|
||||
* Enhancements
|
||||
* Remove warnings
|
||||
|
||||
## v2.2.0 (2019-12-11)
|
||||
|
||||
* Enhancements
|
||||
* Add `:idle_time` to `DBConnection.LogEntry`
|
||||
* Ping all stale connections on idle interval
|
||||
* Add `crash_reason` to relevant Logger error reports
|
||||
* Ping all stale connections on idle interval. One possible downside of this approach is that we may shut down all connections at once and if there is a request around this time, the response time will be higher. However, this is likely better than the current approach, where we ping only the first one, which means we can have a pool of stale connections. The current behaviour is the same as in v1.0
|
||||
|
||||
## v2.1.1 (2019-07-17)
|
||||
|
||||
* Enhancements
|
||||
* Reduce severity in client exits to info
|
||||
* Improve error message on redirect checkout
|
||||
|
||||
* Bug fixes
|
||||
* Make sure ownership timeout is respected on automatic checkouts
|
||||
|
||||
## v2.1.0 (2019-06-07)
|
||||
|
||||
* Enhancements
|
||||
* Require Elixir v1.6+
|
||||
* Include client stacktrace on check out timeouts
|
||||
100
phoenix/deps/db_connection/README.md
Normal file
100
phoenix/deps/db_connection/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# DBConnection
|
||||
|
||||
Database connection behaviour and database connection pool designed for
|
||||
handling transaction, prepare/execute, cursors and client process
|
||||
describe/encode/decode.
|
||||
|
||||
Examples of using the `DBConnection` behaviour are available in
|
||||
`./examples/db_agent/` and `./examples/tcp_connection/`.
|
||||
|
||||
There is also [a series of articles on building database adapters](http://blog.plataformatec.com.br/2018/11/building-a-new-mysql-adapter-for-ecto-part-i-hello-world/). It includes articles covering both DBConnection and Ecto integrations.
|
||||
|
||||
## Contributing
|
||||
|
||||
Run unit tests with:
|
||||
|
||||
$ mix test
|
||||
|
||||
To run the integration tests (for each available pool):
|
||||
|
||||
$ mix test.pools
|
||||
|
||||
To run all tests:
|
||||
|
||||
$ mix test.all
|
||||
|
||||
## Design
|
||||
|
||||
This library is made of four main modules:
|
||||
|
||||
* `DBConnection` - this is the code running on the client
|
||||
and the specification of the DBConnection API
|
||||
|
||||
* `DBConnection.Connection` - this is the process that
|
||||
establishes the database connection
|
||||
|
||||
* `DBConnection.ConnectionPool` - this is the connection
|
||||
pool. A client asks the connection pool for a connection.
|
||||
There is also an ownership pool, used mostly during tests,
|
||||
which we won't discuss here.
|
||||
|
||||
* `DBConnection.Holder` - the holder is responsible for
|
||||
keeping the connection and checkout state. It is modelled
|
||||
by using an ETS table.
|
||||
|
||||
Once a connection is created, it creates a holder and
|
||||
assigns the connection pool as the heir. Then the holder
|
||||
is promptly given away to the pool. The connection itself
|
||||
is mostly a dummy. It is there to handle connections and pings.
|
||||
The state itself (such as the socket) is all in the holder.
|
||||
|
||||
Once there is a checkout, the pool gives the holder to the
|
||||
client process and stores all relevant information in the
|
||||
holder table itself. If the client terminates without
|
||||
checking in, then the holder is given back to the pool via
|
||||
the heir mechanism. The pool will then discard the connection.
|
||||
|
||||
One important design detail in DBConnection is that it avoids
|
||||
copying data. Other database libraries would send a request
|
||||
to the connection process, perform the query in the connection
|
||||
process, and then send it back to the client. This means a lot of
|
||||
data copying in Elixir. DBConnection keeps the socket in the
|
||||
holder and works on it directly.
|
||||
|
||||
DBConnection also takes all of the care necessary to handle
|
||||
failures, and it shuts down the connection and the socket
|
||||
whenever the client does not check in the connection to avoid
|
||||
recycling sockets/connections in a corrupted state (such as a socket
|
||||
that is stuck inside a transaction).
|
||||
|
||||
### Deadlines
|
||||
|
||||
When a checkout happens, a deadline is started by the client
|
||||
to send a message to the pool after a time interval. If the
|
||||
deadline is reached and the connection is still checked out,
|
||||
the holder is deleted and the connection is terminated. If the
|
||||
client tries to use a terminated connection, an error will
|
||||
be raised (see `Holder.handle/4`).
|
||||
|
||||
### Pool
|
||||
|
||||
The queuing algorithm used by the pool is [CoDel](https://queue.acm.org/appendices/codel.html)
|
||||
which allows us to plan for overloads and reject requests
|
||||
without clogging the pool once checkouts do not read a certain
|
||||
target.
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2015 James Fish
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
33
phoenix/deps/db_connection/hex_metadata.config
Normal file
33
phoenix/deps/db_connection/hex_metadata.config
Normal file
@@ -0,0 +1,33 @@
|
||||
{<<"links">>,
|
||||
[{<<"GitHub">>,<<"https://github.com/elixir-ecto/db_connection">>}]}.
|
||||
{<<"name">>,<<"db_connection">>}.
|
||||
{<<"version">>,<<"2.9.0">>}.
|
||||
{<<"description">>,
|
||||
<<"Database connection behaviour for database transactions and connection pooling">>}.
|
||||
{<<"elixir">>,<<"~> 1.11">>}.
|
||||
{<<"files">>,
|
||||
[<<"lib">>,<<"lib/db_connection">>,<<"lib/db_connection/util.ex">>,
|
||||
<<"lib/db_connection/backoff.ex">>,<<"lib/db_connection/log_entry.ex">>,
|
||||
<<"lib/db_connection/ownership.ex">>,
|
||||
<<"lib/db_connection/connection_pool">>,
|
||||
<<"lib/db_connection/connection_pool/pool.ex">>,
|
||||
<<"lib/db_connection/task.ex">>,<<"lib/db_connection/connection_error.ex">>,
|
||||
<<"lib/db_connection/query.ex">>,<<"lib/db_connection/holder.ex">>,
|
||||
<<"lib/db_connection/ownership">>,
|
||||
<<"lib/db_connection/ownership/proxy.ex">>,
|
||||
<<"lib/db_connection/ownership/manager.ex">>,
|
||||
<<"lib/db_connection/watcher.ex">>,<<"lib/db_connection/pool.ex">>,
|
||||
<<"lib/db_connection/connection_pool.ex">>,
|
||||
<<"lib/db_connection/connection.ex">>,
|
||||
<<"lib/db_connection/telemetry_listener.ex">>,
|
||||
<<"lib/db_connection/app.ex">>,<<"lib/db_connection.ex">>,
|
||||
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>]}.
|
||||
{<<"app">>,<<"db_connection">>}.
|
||||
{<<"licenses">>,[<<"Apache-2.0">>]}.
|
||||
{<<"requirements">>,
|
||||
[[{<<"name">>,<<"telemetry">>},
|
||||
{<<"app">>,<<"telemetry">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 0.4 or ~> 1.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}]]}.
|
||||
{<<"build_tools">>,[<<"mix">>]}.
|
||||
1991
phoenix/deps/db_connection/lib/db_connection.ex
Normal file
1991
phoenix/deps/db_connection/lib/db_connection.ex
Normal file
File diff suppressed because it is too large
Load Diff
23
phoenix/deps/db_connection/lib/db_connection/app.ex
Normal file
23
phoenix/deps/db_connection/lib/db_connection/app.ex
Normal file
@@ -0,0 +1,23 @@
|
||||
defmodule DBConnection.App do
|
||||
@moduledoc false
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
{Task.Supervisor, name: DBConnection.Task},
|
||||
dynamic_supervisor(DBConnection.Ownership.Supervisor),
|
||||
dynamic_supervisor(DBConnection.ConnectionPool.Supervisor),
|
||||
DBConnection.Watcher
|
||||
]
|
||||
|
||||
Supervisor.start_link(children, strategy: :one_for_all, name: __MODULE__)
|
||||
end
|
||||
|
||||
defp dynamic_supervisor(name) do
|
||||
Supervisor.child_spec(
|
||||
{DynamicSupervisor, name: name, strategy: :one_for_one},
|
||||
id: name
|
||||
)
|
||||
end
|
||||
end
|
||||
111
phoenix/deps/db_connection/lib/db_connection/backoff.ex
Normal file
111
phoenix/deps/db_connection/lib/db_connection/backoff.ex
Normal file
@@ -0,0 +1,111 @@
|
||||
defmodule DBConnection.Backoff do
|
||||
# This module provides a functional abstraction over backoffs with different types. It exposes
|
||||
# a struct and a couple of functions to work with it.
|
||||
@moduledoc false
|
||||
@compile :nowarn_deprecated_function
|
||||
|
||||
alias DBConnection.Backoff
|
||||
|
||||
@default_type :rand_exp
|
||||
@min 1_000
|
||||
@max 30_000
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :stop | :rand | :exp | :rand_exp,
|
||||
min: non_neg_integer(),
|
||||
max: non_neg_integer(),
|
||||
state: term()
|
||||
}
|
||||
|
||||
defstruct [:type, :min, :max, :state]
|
||||
|
||||
@spec new(keyword) :: t | nil
|
||||
def new(opts) when is_list(opts) do
|
||||
case Keyword.get(opts, :backoff_type, @default_type) do
|
||||
:stop ->
|
||||
nil
|
||||
|
||||
type ->
|
||||
{min, max} = min_max(opts)
|
||||
new(type, min, max)
|
||||
end
|
||||
end
|
||||
|
||||
@spec backoff(t) :: {non_neg_integer, t}
|
||||
def backoff(backoff)
|
||||
|
||||
def backoff(%Backoff{type: :rand, min: min, max: max} = s) do
|
||||
{rand(min, max), s}
|
||||
end
|
||||
|
||||
def backoff(%Backoff{type: :exp, min: min, state: nil} = s) do
|
||||
{min, %{s | state: min}}
|
||||
end
|
||||
|
||||
def backoff(%Backoff{type: :exp, max: max, state: prev} = s) do
|
||||
next = min(Bitwise.<<<(prev, 1), max)
|
||||
{next, %{s | state: next}}
|
||||
end
|
||||
|
||||
def backoff(%Backoff{type: :rand_exp, max: max, state: state} = s) do
|
||||
{prev, lower} = state
|
||||
next_min = min(prev, lower)
|
||||
next_max = min(prev * 3, max)
|
||||
next = rand(next_min, next_max)
|
||||
{next, %{s | state: {next, lower}}}
|
||||
end
|
||||
|
||||
@spec reset(t) :: t
|
||||
def reset(backoff)
|
||||
|
||||
def reset(%Backoff{type: :rand} = s), do: s
|
||||
def reset(%Backoff{type: :exp} = s), do: %{s | state: nil}
|
||||
|
||||
def reset(%Backoff{type: :rand_exp, min: min, state: {_, lower}} = s) do
|
||||
%{s | state: {min, lower}}
|
||||
end
|
||||
|
||||
## Internal
|
||||
|
||||
defp min_max(opts) do
|
||||
case {opts[:backoff_min], opts[:backoff_max]} do
|
||||
{nil, nil} -> {@min, @max}
|
||||
{nil, max} -> {min(@min, max), max}
|
||||
{min, nil} -> {min, max(min, @max)}
|
||||
{min, max} -> {min, max}
|
||||
end
|
||||
end
|
||||
|
||||
defp new(_, min, _) when not (is_integer(min) and min >= 0) do
|
||||
raise ArgumentError, "minimum #{inspect(min)} not 0 or a positive integer"
|
||||
end
|
||||
|
||||
defp new(_, _, max) when not (is_integer(max) and max >= 0) do
|
||||
raise ArgumentError, "maximum #{inspect(max)} not 0 or a positive integer"
|
||||
end
|
||||
|
||||
defp new(_, min, max) when min > max do
|
||||
raise ArgumentError, "minimum #{min} is greater than maximum #{max}"
|
||||
end
|
||||
|
||||
defp new(:rand, min, max) do
|
||||
%Backoff{type: :rand, min: min, max: max, state: nil}
|
||||
end
|
||||
|
||||
defp new(:exp, min, max) do
|
||||
%Backoff{type: :exp, min: min, max: max, state: nil}
|
||||
end
|
||||
|
||||
defp new(:rand_exp, min, max) do
|
||||
lower = max(min, div(max, 3))
|
||||
%Backoff{type: :rand_exp, min: min, max: max, state: {min, lower}}
|
||||
end
|
||||
|
||||
defp new(type, _, _) do
|
||||
raise ArgumentError, "unknown type #{inspect(type)}"
|
||||
end
|
||||
|
||||
defp rand(min, max) do
|
||||
:rand.uniform(max - min + 1) + min - 1
|
||||
end
|
||||
end
|
||||
536
phoenix/deps/db_connection/lib/db_connection/connection.ex
Normal file
536
phoenix/deps/db_connection/lib/db_connection/connection.ex
Normal file
@@ -0,0 +1,536 @@
|
||||
defmodule DBConnection.Connection do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour :gen_statem
|
||||
|
||||
require Logger
|
||||
alias DBConnection.Backoff
|
||||
alias DBConnection.Holder
|
||||
alias DBConnection.Util
|
||||
|
||||
@timeout 15_000
|
||||
@sensitive_opts [:parameters, :hostname, :port, :username, :password, :database]
|
||||
|
||||
@doc false
|
||||
def start_link(mod, opts, pool, tag) do
|
||||
start_opts = Keyword.take(opts, [:debug, :spawn_opt])
|
||||
:gen_statem.start_link(__MODULE__, {mod, opts, pool, tag}, start_opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def child_spec(mod, opts, pool, tag, child_opts) do
|
||||
Supervisor.child_spec(
|
||||
%{id: __MODULE__, start: {__MODULE__, :start_link, [mod, opts, pool, tag]}},
|
||||
child_opts
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def disconnect({pid, ref}, err, state) do
|
||||
:gen_statem.cast(pid, {:disconnect, ref, err, state})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def stop({pid, ref}, err, state) do
|
||||
:gen_statem.cast(pid, {:stop, ref, err, state})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def ping({pid, ref}, state) do
|
||||
:gen_statem.cast(pid, {:ping, ref, state})
|
||||
end
|
||||
|
||||
## gen_statem API
|
||||
|
||||
@doc false
|
||||
@impl :gen_statem
|
||||
def callback_mode, do: :handle_event_function
|
||||
|
||||
@doc false
|
||||
@impl :gen_statem
|
||||
def init({mod, opts, pool, tag}) do
|
||||
pool_index = Keyword.get(opts, :pool_index)
|
||||
label = if pool_index, do: "db_conn_#{pool_index}", else: "db_conn"
|
||||
Util.set_label(label)
|
||||
|
||||
s = %{
|
||||
mod: mod,
|
||||
opts: opts,
|
||||
state: nil,
|
||||
client: :closed,
|
||||
pool: pool,
|
||||
tag: tag,
|
||||
timer: nil,
|
||||
backoff: Backoff.new(opts),
|
||||
connection_listeners: Keyword.get(opts, :connection_listeners, []),
|
||||
after_connect: Keyword.get(opts, :after_connect),
|
||||
after_connect_timeout: Keyword.get(opts, :after_connect_timeout, @timeout)
|
||||
}
|
||||
|
||||
{:ok, :no_state, s, {:next_event, :internal, {:connect, :init}}}
|
||||
end
|
||||
|
||||
@impl :gen_statem
|
||||
def handle_event(type, info, state, s)
|
||||
|
||||
def handle_event(:internal, {:connect, _info}, :no_state, s) do
|
||||
%{mod: mod, opts: opts, backoff: backoff, after_connect: after_connect} = s
|
||||
|
||||
try do
|
||||
apply(mod, :connect, [connect_opts(opts)])
|
||||
rescue
|
||||
e ->
|
||||
{e, stack} = maybe_sanitize_exception(e, __STACKTRACE__, opts)
|
||||
reraise e, stack
|
||||
else
|
||||
{:ok, state} when after_connect != nil ->
|
||||
ref = make_ref()
|
||||
:gen_statem.cast(self(), {:after_connect, ref})
|
||||
{:keep_state, %{s | state: state, client: {ref, :connect}}}
|
||||
|
||||
{:ok, state} ->
|
||||
backoff = backoff && Backoff.reset(backoff)
|
||||
ref = make_ref()
|
||||
:gen_statem.cast(self(), {:connected, ref})
|
||||
{:keep_state, %{s | state: state, client: {ref, :connect}, backoff: backoff}}
|
||||
|
||||
{:error, err} when is_nil(backoff) ->
|
||||
Logger.error(
|
||||
fn ->
|
||||
[
|
||||
inspect(mod),
|
||||
" (",
|
||||
Util.inspect_pid(self()),
|
||||
") failed to connect: " | Exception.format_banner(:error, err, [])
|
||||
]
|
||||
end,
|
||||
crash_reason: {err, []}
|
||||
)
|
||||
|
||||
raise err
|
||||
|
||||
{:error, err} ->
|
||||
Logger.error(
|
||||
fn ->
|
||||
[
|
||||
inspect(mod),
|
||||
?\s,
|
||||
?(,
|
||||
Util.inspect_pid(self()),
|
||||
") failed to connect: "
|
||||
| Exception.format_banner(:error, err, [])
|
||||
]
|
||||
end,
|
||||
crash_reason: {err, []}
|
||||
)
|
||||
|
||||
{timeout, backoff} = Backoff.backoff(backoff)
|
||||
{:keep_state, %{s | backoff: backoff}, {{:timeout, :backoff}, timeout, nil}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(:internal, {:disconnect, {log, err}}, :no_state, %{mod: mod} = s) do
|
||||
if log == :log do
|
||||
severity =
|
||||
case err do
|
||||
%DBConnection.ConnectionError{severity: severity} -> severity
|
||||
_ -> :error
|
||||
end
|
||||
|
||||
Logger.log(severity, fn ->
|
||||
[
|
||||
inspect(mod),
|
||||
?\s,
|
||||
?(,
|
||||
Util.inspect_pid(self()),
|
||||
") disconnected: " | Exception.format_banner(:error, err, [])
|
||||
]
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
%{state: state, client: client, timer: timer, backoff: backoff} = s
|
||||
demonitor(client)
|
||||
cancel_timer(timer)
|
||||
:ok = apply(mod, :disconnect, [err, state])
|
||||
s = %{s | state: nil, client: :closed, timer: nil}
|
||||
|
||||
notify_connection_listeners(:disconnected, s)
|
||||
|
||||
case client do
|
||||
_ when backoff == nil ->
|
||||
{:stop, {:shutdown, err}, s}
|
||||
|
||||
{_, :after_connect} ->
|
||||
{timeout, backoff} = Backoff.backoff(backoff)
|
||||
{:keep_state, %{s | backoff: backoff}, {{:timeout, :backoff}, timeout, nil}}
|
||||
|
||||
_ ->
|
||||
{:keep_state, s, {:next_event, :internal, {:connect, :disconnect}}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event({:timeout, :backoff}, _content, :no_state, s) do
|
||||
{:keep_state, s, {:next_event, :internal, {:connect, :backoff}}}
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:ping, ref, state}, :no_state, %{client: {ref, :pool}, mod: mod} = s) do
|
||||
case apply(mod, :ping, [state]) do
|
||||
{:ok, state} ->
|
||||
pool_update(state, s)
|
||||
|
||||
{:disconnect, err, state} ->
|
||||
{:keep_state, %{s | state: state}, {:next_event, :internal, {:disconnect, {:log, err}}}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:disconnect, ref, err, state}, :no_state, %{client: {ref, _}} = s) do
|
||||
{:keep_state, %{s | state: state}, {:next_event, :internal, {:disconnect, {:log, err}}}}
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:stop, ref, err, state}, :no_state, %{client: {ref, _}} = s) do
|
||||
{_, stack} = :erlang.process_info(self(), :current_stacktrace)
|
||||
|
||||
case err do
|
||||
ok when ok in [:normal, :shutdown] ->
|
||||
:ok
|
||||
|
||||
{:shutdown, _term} ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
reason =
|
||||
case err do
|
||||
%{__exception__: true} -> Exception.format_banner(:error, err, stack)
|
||||
_other -> "** #{inspect(err)}"
|
||||
end
|
||||
|
||||
format =
|
||||
~c"** State machine ~p terminating~n" ++
|
||||
~c"** Reason for termination ==~n" ++
|
||||
~c"~s~n"
|
||||
|
||||
:error_logger.format(format, [self(), reason])
|
||||
end
|
||||
|
||||
{:stop, {err, stack}, %{s | state: state}}
|
||||
end
|
||||
|
||||
def handle_event(:cast, {tag, _, _, _}, :no_state, s) when tag in [:disconnect, :stop] do
|
||||
handle_timeout(s)
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:after_connect, ref}, :no_state, %{client: {ref, :connect}} = s) do
|
||||
%{
|
||||
mod: mod,
|
||||
state: state,
|
||||
after_connect: after_connect,
|
||||
after_connect_timeout: timeout,
|
||||
opts: opts
|
||||
} = s
|
||||
|
||||
notify_connection_listeners(:connected, s)
|
||||
|
||||
case apply(mod, :checkout, [state]) do
|
||||
{:ok, state} ->
|
||||
opts = [timeout: timeout] ++ opts
|
||||
opts = Keyword.drop(opts, @sensitive_opts)
|
||||
{pid, ref} = DBConnection.Task.run_child(mod, state, after_connect, opts)
|
||||
timer = start_timer(pid, timeout)
|
||||
s = %{s | client: {ref, :after_connect}, timer: timer, state: state}
|
||||
{:keep_state, s}
|
||||
|
||||
{:disconnect, err, state} ->
|
||||
{:keep_state, %{s | state: state}, {:next_event, :internal, {:disconnect, {:log, err}}}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:after_connect, _}, :no_state, _s) do
|
||||
:keep_state_and_data
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:connected, ref}, :no_state, %{client: {ref, :connect}} = s) do
|
||||
%{mod: mod, state: state} = s
|
||||
|
||||
notify_connection_listeners(:connected, s)
|
||||
|
||||
case apply(mod, :checkout, [state]) do
|
||||
{:ok, state} ->
|
||||
pool_update(state, s)
|
||||
|
||||
{:disconnect, err, state} ->
|
||||
{:keep_state, %{s | state: state}, {:next_event, :internal, {:disconnect, {:log, err}}}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:connected, _}, :no_state, _s) do
|
||||
:keep_state_and_data
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
:info,
|
||||
{:DOWN, ref, _, pid, reason},
|
||||
:no_state,
|
||||
%{client: {ref, :after_connect}} = s
|
||||
) do
|
||||
message =
|
||||
"client #{Util.inspect_pid(pid)} exited: " <> Exception.format_exit(reason)
|
||||
|
||||
err = DBConnection.ConnectionError.exception(message)
|
||||
|
||||
{:keep_state, %{s | client: {nil, :after_connect}},
|
||||
{:next_event, :internal, {:disconnect, {down_log(reason), err}}}}
|
||||
end
|
||||
|
||||
def handle_event(:info, {:DOWN, mon, _, pid, reason}, :no_state, %{client: {ref, mon}} = s) do
|
||||
message =
|
||||
"client #{Util.inspect_pid(pid)} exited: " <> Exception.format_exit(reason)
|
||||
|
||||
err = DBConnection.ConnectionError.exception(message)
|
||||
|
||||
{:keep_state, %{s | client: {ref, nil}},
|
||||
{:next_event, :internal, {:disconnect, {down_log(reason), err}}}}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
:info,
|
||||
{:timeout, timer, {__MODULE__, pid, timeout}},
|
||||
:no_state,
|
||||
%{timer: timer} = s
|
||||
)
|
||||
when is_reference(timer) do
|
||||
message =
|
||||
"client #{Util.inspect_pid(pid)} timed out because it checked out " <>
|
||||
"the connection for longer than #{timeout}ms"
|
||||
|
||||
exc =
|
||||
case Process.info(pid, :current_stacktrace) do
|
||||
{:current_stacktrace, stacktrace} ->
|
||||
message <>
|
||||
"\n\n#{Util.inspect_pid(pid)} was at location:\n\n" <>
|
||||
Exception.format_stacktrace(stacktrace)
|
||||
|
||||
_ ->
|
||||
message
|
||||
end
|
||||
|> DBConnection.ConnectionError.exception()
|
||||
|
||||
{:keep_state, %{s | timer: nil}, {:next_event, :internal, {:disconnect, {:log, exc}}}}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
:info,
|
||||
{:"ETS-TRANSFER", holder, _pid, {msg, ref, extra}},
|
||||
:no_state,
|
||||
%{client: {ref, :after_connect}, timer: timer} = s
|
||||
) do
|
||||
{_, state} = Holder.delete(holder)
|
||||
cancel_timer(timer)
|
||||
s = %{s | timer: nil}
|
||||
|
||||
case msg do
|
||||
:checkin -> handle_checkin(state, s)
|
||||
:disconnect -> handle_event(:cast, {:disconnect, ref, extra, state}, :no_state, s)
|
||||
:stop -> handle_event(:cast, {:stop, ref, extra, state}, :no_state, s)
|
||||
end
|
||||
end
|
||||
|
||||
# We discard EXIT messages which may arrive if the process is trapping exits
|
||||
def handle_event(:info, {:EXIT, _, _}, :no_state, s) do
|
||||
handle_timeout(s)
|
||||
end
|
||||
|
||||
def handle_event(:info, msg, :no_state, %{mod: mod} = s) do
|
||||
Logger.info(fn ->
|
||||
[inspect(mod), ?\s, ?(, Util.inspect_pid(self()), ") missed message: " | inspect(msg)]
|
||||
end)
|
||||
|
||||
handle_timeout(s)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl :gen_statem
|
||||
# If client is :closed then the connection was previously disconnected
|
||||
# and cleanup is not required.
|
||||
def terminate(_, _, %{client: :closed}), do: :ok
|
||||
|
||||
def terminate(reason, _, s) do
|
||||
%{mod: mod, state: state} = s
|
||||
msg = "connection exited: " <> Exception.format_exit(reason)
|
||||
|
||||
msg
|
||||
|> DBConnection.ConnectionError.exception()
|
||||
|> mod.disconnect(state)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl :gen_statem
|
||||
def format_status(info, [_, :no_state, %{client: :closed, mod: mod}]) do
|
||||
case info do
|
||||
:normal -> [{:data, [{~c"Module", mod}]}]
|
||||
:terminate -> mod
|
||||
end
|
||||
end
|
||||
|
||||
def format_status(info, [pdict, :no_state, %{mod: mod, state: state}]) do
|
||||
case function_exported?(mod, :format_status, 2) do
|
||||
true when info == :normal ->
|
||||
normal_status(mod, pdict, state)
|
||||
|
||||
false when info == :normal ->
|
||||
normal_status_default(mod, state)
|
||||
|
||||
true when info == :terminate ->
|
||||
{mod, terminate_status(mod, pdict, state)}
|
||||
|
||||
false when info == :terminate ->
|
||||
{mod, state}
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp maybe_sanitize_exception(e, stack, opts) do
|
||||
if Keyword.get(opts, :show_sensitive_data_on_connection_error, false) do
|
||||
{e, stack}
|
||||
else
|
||||
message =
|
||||
"connect raised #{inspect(e.__struct__)} exception#{sanitized_message(e)}. " <>
|
||||
"The exception details are hidden, as they may contain sensitive data such as " <>
|
||||
"database credentials. You may set :show_sensitive_data_on_connection_error " <>
|
||||
"to true when starting your connection if you wish to see all of the details"
|
||||
|
||||
{RuntimeError.exception(message), cleanup_stacktrace(stack)}
|
||||
end
|
||||
end
|
||||
|
||||
defp sanitized_message(%KeyError{} = e), do: ": #{Exception.message(%{e | term: nil})}"
|
||||
defp sanitized_message(_), do: ""
|
||||
|
||||
defp connect_opts(opts) do
|
||||
case Keyword.get(opts, :configure) do
|
||||
{mod, fun, args} ->
|
||||
apply(mod, fun, [opts | args])
|
||||
|
||||
fun when is_function(fun, 1) ->
|
||||
fun.(opts)
|
||||
|
||||
nil ->
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
defp down_log(:normal), do: :nolog
|
||||
defp down_log(:shutdown), do: :nolog
|
||||
defp down_log({:shutdown, _}), do: :nolog
|
||||
defp down_log(_), do: :log
|
||||
|
||||
defp handle_timeout(s), do: {:keep_state, s}
|
||||
|
||||
defp demonitor({_, mon}) when is_reference(mon) do
|
||||
Process.demonitor(mon, [:flush])
|
||||
end
|
||||
|
||||
defp demonitor({mon, :after_connect}) when is_reference(mon) do
|
||||
Process.demonitor(mon, [:flush])
|
||||
end
|
||||
|
||||
defp demonitor({_, _}), do: true
|
||||
defp demonitor(nil), do: true
|
||||
|
||||
defp start_timer(_, :infinity), do: nil
|
||||
|
||||
defp start_timer(pid, timeout) do
|
||||
:erlang.start_timer(timeout, self(), {__MODULE__, pid, timeout})
|
||||
end
|
||||
|
||||
defp cancel_timer(nil), do: :ok
|
||||
|
||||
defp cancel_timer(timer) do
|
||||
case :erlang.cancel_timer(timer) do
|
||||
false -> flush_timer(timer)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp flush_timer(timer) do
|
||||
receive do
|
||||
{:timeout, ^timer, {__MODULE__, _, _}} ->
|
||||
:ok
|
||||
after
|
||||
0 ->
|
||||
raise ArgumentError, "timer #{inspect(timer)} does not exist"
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_checkin(state, s) do
|
||||
%{backoff: backoff, client: client} = s
|
||||
backoff = backoff && Backoff.reset(backoff)
|
||||
demonitor(client)
|
||||
pool_update(state, %{s | client: nil, backoff: backoff})
|
||||
end
|
||||
|
||||
defp pool_update(state, %{pool: pool, tag: tag, mod: mod} = s) do
|
||||
case Holder.update(pool, tag, mod, state) do
|
||||
{:ok, ref} ->
|
||||
{:keep_state, %{s | client: {ref, :pool}, state: state}, :hibernate}
|
||||
|
||||
:error ->
|
||||
{:stop, {:shutdown, :no_more_pool}, s}
|
||||
end
|
||||
end
|
||||
|
||||
defp normal_status(mod, pdict, state) do
|
||||
try do
|
||||
mod.format_status(:normal, [pdict, state])
|
||||
catch
|
||||
_, _ ->
|
||||
normal_status_default(mod, state)
|
||||
else
|
||||
status ->
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
defp normal_status_default(mod, state) do
|
||||
[{:data, [{~c"Module", mod}, {~c"State", state}]}]
|
||||
end
|
||||
|
||||
defp terminate_status(mod, pdict, state) do
|
||||
try do
|
||||
mod.format_status(:terminate, [pdict, state])
|
||||
catch
|
||||
_, _ ->
|
||||
state
|
||||
else
|
||||
status ->
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_stacktrace(stack) do
|
||||
case stack do
|
||||
[{_, _, arity, _} | _rest] = stacktrace when is_integer(arity) ->
|
||||
stacktrace
|
||||
|
||||
[{mod, fun, args, info} | rest] when is_list(args) ->
|
||||
[{mod, fun, length(args), info} | rest]
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_connection_listeners(action, %{} = state) do
|
||||
%{connection_listeners: connection_listeners} = state
|
||||
|
||||
{listeners, message} =
|
||||
case connection_listeners do
|
||||
listeners when is_list(listeners) ->
|
||||
{listeners, {action, self()}}
|
||||
|
||||
{listeners, tag} when is_list(listeners) ->
|
||||
{listeners, {action, self(), tag}}
|
||||
end
|
||||
|
||||
Enum.each(listeners, &send(&1, message))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
defmodule DBConnection.ConnectionError do
|
||||
@moduledoc """
|
||||
A generic connection error exception.
|
||||
|
||||
The raised exception might include the reason which would be useful
|
||||
to programmatically determine what was causing the error.
|
||||
"""
|
||||
|
||||
@typedoc since: "2.7.0"
|
||||
@type t() :: %__MODULE__{
|
||||
message: String.t(),
|
||||
reason: :error | :queue_timeout,
|
||||
severity: Logger.level()
|
||||
}
|
||||
|
||||
defexception [:message, severity: :error, reason: :error]
|
||||
|
||||
@doc false
|
||||
def exception(message, reason) when is_binary(message) and reason in [:error, :queue_timeout] do
|
||||
message
|
||||
|> exception()
|
||||
|> Map.replace!(:reason, reason)
|
||||
end
|
||||
end
|
||||
391
phoenix/deps/db_connection/lib/db_connection/connection_pool.ex
Normal file
391
phoenix/deps/db_connection/lib/db_connection/connection_pool.ex
Normal file
@@ -0,0 +1,391 @@
|
||||
defmodule DBConnection.ConnectionPool do
|
||||
@moduledoc """
|
||||
The default connection pool.
|
||||
|
||||
The queueing algorithm is based on [CoDel](https://queue.acm.org/appendices/codel.html).
|
||||
|
||||
You're not supposed to call any functions on this pool directly, but only pass this
|
||||
as the value of the `:pool` option in functions such as `DBConnection.start_link/2`.
|
||||
|
||||
`disconnect_all/3`, which by default will result in connections being
|
||||
reestablished, can be called periodically to recycle checked-in connections
|
||||
after a maximum lifetime is reached. `Ecto SQL` users may find it at
|
||||
https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.html#disconnect_all/3
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
alias DBConnection.Holder
|
||||
alias DBConnection.Util
|
||||
|
||||
@behaviour DBConnection.Pool
|
||||
|
||||
@queue_target 50
|
||||
@queue_interval 2000
|
||||
@idle_interval 1000
|
||||
@time_unit 1000
|
||||
|
||||
@doc false
|
||||
def start_link({mod, opts}) do
|
||||
GenServer.start_link(__MODULE__, {mod, opts}, start_opts(opts))
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
def checkout(pool, callers, opts) do
|
||||
Holder.checkout(pool, callers, opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
def disconnect_all(pool, interval, _opts) do
|
||||
GenServer.call(pool, {:disconnect_all, interval}, :infinity)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
def get_connection_metrics(pool) do
|
||||
GenServer.call(pool, :get_connection_metrics, :infinity)
|
||||
end
|
||||
|
||||
## GenServer api
|
||||
|
||||
@impl GenServer
|
||||
def init({mod, opts}) do
|
||||
DBConnection.register_as_pool(mod)
|
||||
|
||||
queue = :ets.new(__MODULE__.Queue, [:protected, :ordered_set, decentralized_counters: true])
|
||||
ts = {System.monotonic_time(), 0}
|
||||
{:ok, _} = DBConnection.ConnectionPool.Pool.start_supervised(queue, mod, opts)
|
||||
target = Keyword.get(opts, :queue_target, @queue_target)
|
||||
interval = Keyword.get(opts, :queue_interval, @queue_interval)
|
||||
idle_interval = Keyword.get(opts, :idle_interval, @idle_interval)
|
||||
idle_limit = Keyword.get_lazy(opts, :idle_limit, fn -> Keyword.get(opts, :pool_size, 1) end)
|
||||
now_in_native = System.monotonic_time()
|
||||
now_in_ms = System.convert_time_unit(now_in_native, :native, @time_unit)
|
||||
|
||||
codel = %{
|
||||
target: target,
|
||||
interval: interval,
|
||||
delay: 0,
|
||||
slow: false,
|
||||
next: now_in_ms,
|
||||
poll: nil,
|
||||
idle_interval: idle_interval,
|
||||
idle_limit: idle_limit,
|
||||
idle: nil
|
||||
}
|
||||
|
||||
codel = start_idle(now_in_native, start_poll(now_in_ms, now_in_ms, codel))
|
||||
{:ok, {:busy, queue, codel, ts}}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_call(:get_connection_metrics, _from, {status, queue, _, _} = state) do
|
||||
{ready_conn_count, checkout_queue_length} =
|
||||
case status do
|
||||
:busy ->
|
||||
{0, :ets.select_count(queue, [{{{:_, :_, :_}}, [], [true]}])}
|
||||
|
||||
:ready ->
|
||||
{:ets.select_count(queue, [{{{:_, :_}}, [], [true]}]), 0}
|
||||
end
|
||||
|
||||
metrics = %{
|
||||
source: {:pool, self()},
|
||||
ready_conn_count: ready_conn_count,
|
||||
checkout_queue_length: checkout_queue_length
|
||||
}
|
||||
|
||||
{:reply, [metrics], state}
|
||||
end
|
||||
|
||||
def handle_call({:disconnect_all, interval}, _from, {type, queue, codel, _ts}) do
|
||||
ts = {System.monotonic_time(), interval}
|
||||
{:reply, :ok, {type, queue, codel, ts}}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info(
|
||||
{:db_connection, from, {:checkout, _caller, now, queue?}},
|
||||
{:busy, queue, _, _} = busy
|
||||
) do
|
||||
case queue? do
|
||||
true ->
|
||||
:ets.insert(queue, {{now, System.unique_integer(), from}})
|
||||
{:noreply, busy}
|
||||
|
||||
false ->
|
||||
message = "connection not available and queuing is disabled"
|
||||
err = DBConnection.ConnectionError.exception(message)
|
||||
Holder.reply_error(from, err)
|
||||
{:noreply, busy}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:db_connection, from, {:checkout, _caller, _now, _queue?}} = checkout,
|
||||
{:ready, queue, _codel, _ts} = ready
|
||||
) do
|
||||
case :ets.first(queue) do
|
||||
{queued_in_native, holder} = key ->
|
||||
Holder.handle_checkout(holder, from, queue, queued_in_native) and :ets.delete(queue, key)
|
||||
{:noreply, ready}
|
||||
|
||||
:"$end_of_table" ->
|
||||
handle_info(checkout, put_elem(ready, 0, :busy))
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:"ETS-TRANSFER", holder, pid, queue}, {_, queue, _, _} = data) do
|
||||
message = "client #{Util.inspect_pid(pid)} exited"
|
||||
err = DBConnection.ConnectionError.exception(message: message, severity: :info)
|
||||
Holder.handle_disconnect(holder, err)
|
||||
{:noreply, data}
|
||||
end
|
||||
|
||||
def handle_info({:"ETS-TRANSFER", holder, _, {msg, queue, extra}}, {_, queue, _, ts} = data) do
|
||||
case msg do
|
||||
:checkin ->
|
||||
owner = self()
|
||||
|
||||
case :ets.info(holder, :owner) do
|
||||
^owner ->
|
||||
{time, interval} = ts
|
||||
|
||||
if Holder.maybe_disconnect(holder, time, interval) do
|
||||
{:noreply, data}
|
||||
else
|
||||
handle_checkin(holder, extra, data)
|
||||
end
|
||||
|
||||
:undefined ->
|
||||
{:noreply, data}
|
||||
end
|
||||
|
||||
:disconnect ->
|
||||
Holder.handle_disconnect(holder, extra)
|
||||
{:noreply, data}
|
||||
|
||||
:stop ->
|
||||
Holder.handle_stop(holder, extra)
|
||||
{:noreply, data}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:timeout, deadline, {queue, holder, pid, len}}, {_, queue, _, _} = data) do
|
||||
# Check that timeout refers to current holder (and not previous)
|
||||
if Holder.handle_deadline(holder, deadline) do
|
||||
message =
|
||||
"client #{Util.inspect_pid(pid)} timed out because " <>
|
||||
"it queued and checked out the connection for longer than #{len}ms"
|
||||
|
||||
exc =
|
||||
case Process.info(pid, :current_stacktrace) do
|
||||
{:current_stacktrace, stacktrace} ->
|
||||
message <>
|
||||
"\n\n#{Util.inspect_pid(pid)} was at location:\n\n" <>
|
||||
Exception.format_stacktrace(stacktrace)
|
||||
|
||||
_ ->
|
||||
message
|
||||
end
|
||||
|> DBConnection.ConnectionError.exception()
|
||||
|
||||
Holder.handle_disconnect(holder, exc)
|
||||
end
|
||||
|
||||
{:noreply, data}
|
||||
end
|
||||
|
||||
def handle_info({:timeout, poll, {time, last_sent}}, {_, _, %{poll: poll}, _} = data) do
|
||||
{status, queue, codel, ts} = data
|
||||
|
||||
# If no queue progress since last poll check queue
|
||||
case :ets.first(queue) do
|
||||
{sent, _, _} when sent <= last_sent and status == :busy ->
|
||||
delay = time - sent
|
||||
timeout(delay, time, queue, start_poll(time, sent, codel), ts)
|
||||
|
||||
{sent, _, _} ->
|
||||
{:noreply, {status, queue, start_poll(time, sent, codel), ts}}
|
||||
|
||||
_ ->
|
||||
{:noreply, {status, queue, start_poll(time, time, codel), ts}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:timeout, idle, past_in_native}, {_, _, %{idle: idle}, _} = data) do
|
||||
{status, queue, %{idle_limit: limit} = codel, ts} = data
|
||||
drop_idle(past_in_native, limit, status, queue, codel, ts)
|
||||
end
|
||||
|
||||
defp drop_idle(past_in_native, limit, status, queue, codel, ts) do
|
||||
with true <- status == :ready and limit > 0,
|
||||
{queued_in_native, holder} = key when queued_in_native <= past_in_native <-
|
||||
:ets.first(queue) do
|
||||
:ets.delete(queue, key)
|
||||
Holder.maybe_disconnect(holder, elem(ts, 0), 0) or Holder.handle_ping(holder)
|
||||
drop_idle(past_in_native, limit - 1, status, queue, codel, ts)
|
||||
else
|
||||
_ ->
|
||||
{:noreply, {status, queue, start_idle(System.monotonic_time(), codel), ts}}
|
||||
end
|
||||
end
|
||||
|
||||
defp timeout(delay, time, queue, codel, ts) do
|
||||
case codel do
|
||||
%{delay: min_delay, next: next, target: target, interval: interval}
|
||||
when time >= next and min_delay > target ->
|
||||
codel = %{codel | slow: true, delay: delay, next: time + interval}
|
||||
drop_slow(time, target * 2, queue)
|
||||
{:noreply, {:busy, queue, codel, ts}}
|
||||
|
||||
%{next: next, interval: interval} when time >= next ->
|
||||
codel = %{codel | slow: false, delay: delay, next: time + interval}
|
||||
{:noreply, {:busy, queue, codel, ts}}
|
||||
|
||||
_ ->
|
||||
{:noreply, {:busy, queue, codel, ts}}
|
||||
end
|
||||
end
|
||||
|
||||
defp drop_slow(time, timeout, queue) do
|
||||
min_sent = time - timeout
|
||||
match = {{:"$1", :_, :"$2"}}
|
||||
guards = [{:<, :"$1", min_sent}]
|
||||
select_slow = [{match, guards, [{{:"$1", :"$2"}}]}]
|
||||
|
||||
for {sent, from} <- :ets.select(queue, select_slow) do
|
||||
drop(time - sent, from)
|
||||
end
|
||||
|
||||
:ets.select_delete(queue, [{match, guards, [true]}])
|
||||
end
|
||||
|
||||
defp handle_checkin(holder, now_in_native, {:ready, queue, _, _} = data) do
|
||||
:ets.insert(queue, {{now_in_native, holder}})
|
||||
{:noreply, data}
|
||||
end
|
||||
|
||||
defp handle_checkin(holder, now_in_native, {:busy, queue, codel, ts}) do
|
||||
now_in_ms = System.convert_time_unit(now_in_native, :native, @time_unit)
|
||||
|
||||
case dequeue(now_in_ms, holder, queue, codel, ts) do
|
||||
{:busy, _, _, _} = busy ->
|
||||
{:noreply, busy}
|
||||
|
||||
{:ready, _, _, _} = ready ->
|
||||
:ets.insert(queue, {{now_in_native, holder}})
|
||||
{:noreply, ready}
|
||||
end
|
||||
end
|
||||
|
||||
defp dequeue(time, holder, queue, codel, ts) do
|
||||
case codel do
|
||||
%{next: next, delay: delay, target: target} when time >= next ->
|
||||
dequeue_first(time, delay > target, holder, queue, codel, ts)
|
||||
|
||||
%{slow: false} ->
|
||||
dequeue_fast(time, holder, queue, codel, ts)
|
||||
|
||||
%{slow: true, target: target} ->
|
||||
dequeue_slow(time, target * 2, holder, queue, codel, ts)
|
||||
end
|
||||
end
|
||||
|
||||
defp dequeue_first(time, slow?, holder, queue, codel, ts) do
|
||||
%{interval: interval} = codel
|
||||
next = time + interval
|
||||
|
||||
case :ets.first(queue) do
|
||||
{sent, _, from} = key ->
|
||||
:ets.delete(queue, key)
|
||||
delay = time - sent
|
||||
codel = %{codel | next: next, delay: delay, slow: slow?}
|
||||
go(delay, from, time, holder, queue, codel, ts)
|
||||
|
||||
:"$end_of_table" ->
|
||||
codel = %{codel | next: next, delay: 0, slow: slow?}
|
||||
{:ready, queue, codel, ts}
|
||||
end
|
||||
end
|
||||
|
||||
defp dequeue_fast(time, holder, queue, codel, ts) do
|
||||
case :ets.first(queue) do
|
||||
{sent, _, from} = key ->
|
||||
:ets.delete(queue, key)
|
||||
go(time - sent, from, time, holder, queue, codel, ts)
|
||||
|
||||
:"$end_of_table" ->
|
||||
{:ready, queue, %{codel | delay: 0}, ts}
|
||||
end
|
||||
end
|
||||
|
||||
defp dequeue_slow(time, timeout, holder, queue, codel, ts) do
|
||||
case :ets.first(queue) do
|
||||
{sent, _, from} = key when time - sent > timeout ->
|
||||
:ets.delete(queue, key)
|
||||
drop(time - sent, from)
|
||||
dequeue_slow(time, timeout, holder, queue, codel, ts)
|
||||
|
||||
{sent, _, from} = key ->
|
||||
:ets.delete(queue, key)
|
||||
go(time - sent, from, time, holder, queue, codel, ts)
|
||||
|
||||
:"$end_of_table" ->
|
||||
{:ready, queue, %{codel | delay: 0}, ts}
|
||||
end
|
||||
end
|
||||
|
||||
defp go(delay, from, time, holder, queue, %{delay: min} = codel, ts) do
|
||||
case Holder.handle_checkout(holder, from, queue, 0) do
|
||||
true when delay < min ->
|
||||
{:busy, queue, %{codel | delay: delay}, ts}
|
||||
|
||||
true ->
|
||||
{:busy, queue, codel, ts}
|
||||
|
||||
false ->
|
||||
dequeue(time, holder, queue, codel, ts)
|
||||
end
|
||||
end
|
||||
|
||||
defp drop(delay, from) do
|
||||
message = """
|
||||
[#{ancestor()}] connection not available and request was dropped from queue after #{delay}ms. \
|
||||
This means requests are coming in and your connection pool cannot serve them fast enough. \
|
||||
You can address this by:
|
||||
|
||||
1. Ensuring your database is available and that you can connect to it
|
||||
2. Tracking down slow queries and making sure they are running fast enough
|
||||
3. Increasing the pool_size (although this increases resource consumption)
|
||||
4. Allowing requests to wait longer by increasing :queue_target and :queue_interval
|
||||
|
||||
See DBConnection.start_link/2 for more information
|
||||
"""
|
||||
|
||||
err = DBConnection.ConnectionError.exception(message, :queue_timeout)
|
||||
|
||||
Holder.reply_error(from, err)
|
||||
end
|
||||
|
||||
defp ancestor do
|
||||
Process.get(:"$ancestors", []) |> Enum.find(&is_atom/1)
|
||||
end
|
||||
|
||||
defp start_opts(opts) do
|
||||
Keyword.take(opts, [:name, :spawn_opt])
|
||||
end
|
||||
|
||||
defp start_poll(now, last_sent, %{interval: interval} = codel) do
|
||||
timeout = now + interval
|
||||
poll = :erlang.start_timer(timeout, self(), {timeout, last_sent}, abs: true)
|
||||
%{codel | poll: poll}
|
||||
end
|
||||
|
||||
defp start_idle(now_in_native, %{idle_interval: interval} = codel) do
|
||||
timeout = System.convert_time_unit(now_in_native, :native, :millisecond) + interval
|
||||
idle = :erlang.start_timer(timeout, self(), now_in_native, abs: true)
|
||||
%{codel | idle: idle}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
defmodule DBConnection.ConnectionPool.Pool do
|
||||
@moduledoc false
|
||||
use Supervisor, restart: :temporary
|
||||
|
||||
def start_supervised(tag, mod, opts) do
|
||||
DBConnection.Watcher.watch(
|
||||
DBConnection.ConnectionPool.Supervisor,
|
||||
{DBConnection.ConnectionPool.Pool, {self(), tag, mod, opts}}
|
||||
)
|
||||
end
|
||||
|
||||
def start_link(arg) do
|
||||
Supervisor.start_link(__MODULE__, arg)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init({owner, tag, mod, opts}) do
|
||||
size = Keyword.get(opts, :pool_size, 1)
|
||||
|
||||
if size < 1, do: raise(ArgumentError, "pool size must be greater or equal to 1, got #{size}")
|
||||
|
||||
children = for id <- 1..size, do: conn(owner, tag, id, mod, opts)
|
||||
sup_opts = [strategy: :one_for_one] ++ Keyword.take(opts, [:max_restarts, :max_seconds])
|
||||
Supervisor.init(children, sup_opts)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp conn(owner, tag, id, mod, opts) do
|
||||
child_opts = [id: {mod, owner, id}] ++ Keyword.take(opts, [:shutdown])
|
||||
DBConnection.Connection.child_spec(mod, [pool_index: id] ++ opts, owner, tag, child_opts)
|
||||
end
|
||||
end
|
||||
448
phoenix/deps/db_connection/lib/db_connection/holder.ex
Normal file
448
phoenix/deps/db_connection/lib/db_connection/holder.ex
Normal file
@@ -0,0 +1,448 @@
|
||||
defmodule DBConnection.Holder do
|
||||
@moduledoc false
|
||||
require Record
|
||||
|
||||
alias DBConnection.Util
|
||||
|
||||
@queue true
|
||||
@timeout 15000
|
||||
@time_unit 1000
|
||||
|
||||
Record.defrecord(:conn, [:connection, :module, :state, :lock, :ts, deadline: nil, status: :ok])
|
||||
Record.defrecord(:pool_ref, [:pool, :reference, :deadline, :holder, :lock])
|
||||
|
||||
@type t :: :ets.tid()
|
||||
@type checkin_time :: non_neg_integer() | nil
|
||||
|
||||
## Holder API
|
||||
|
||||
@spec new(pid, reference, module, term) :: t
|
||||
def new(pool, ref, mod, state) do
|
||||
# Insert before setting heir so that pool can't receive empty table
|
||||
holder = :ets.new(__MODULE__, [:public, :ordered_set, decentralized_counters: true])
|
||||
|
||||
conn = conn(connection: self(), module: mod, state: state, ts: System.monotonic_time())
|
||||
true = :ets.insert_new(holder, conn)
|
||||
|
||||
:ets.setopts(holder, {:heir, pool, ref})
|
||||
holder
|
||||
end
|
||||
|
||||
@spec update(pid, reference, module, term) :: {:ok, t} | :error
|
||||
def update(pool, ref, mod, state) do
|
||||
holder = new(pool, ref, mod, state)
|
||||
|
||||
try do
|
||||
:ets.give_away(holder, pool, {:checkin, ref, System.monotonic_time()})
|
||||
{:ok, holder}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete(t) :: {module, term}
|
||||
def delete(holder) do
|
||||
[conn(module: module, state: state)] = :ets.lookup(holder, :conn)
|
||||
:ets.delete(holder)
|
||||
{module, state}
|
||||
end
|
||||
|
||||
## Pool API (invoked by caller)
|
||||
|
||||
@callback checkout(pool :: GenServer.server(), [pid], opts :: Keyword.t()) ::
|
||||
{:ok, pool_ref :: any, module, checkin_time, state :: any}
|
||||
| {:error, Exception.t()}
|
||||
def checkout(pool, callers, opts) do
|
||||
queue? = Keyword.get(opts, :queue, @queue)
|
||||
now = System.monotonic_time(@time_unit)
|
||||
timeout = abs_timeout(now, opts)
|
||||
|
||||
case checkout(pool, callers, queue?, now, timeout) do
|
||||
{:ok, _, _, _, _} = ok ->
|
||||
ok
|
||||
|
||||
{:error, %DBConnection.ConnectionError{} = connection_error} = error ->
|
||||
:telemetry.execute(
|
||||
[:db_connection, :connection_error],
|
||||
%{count: 1},
|
||||
%{
|
||||
error: connection_error,
|
||||
opts: opts
|
||||
}
|
||||
)
|
||||
|
||||
error
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
|
||||
{:redirect, caller, proxy} ->
|
||||
case checkout(proxy, [caller], opts) do
|
||||
{:ok, _, _, _, _} = ok ->
|
||||
ok
|
||||
|
||||
{:error, %DBConnection.ConnectionError{message: message} = exception} ->
|
||||
{:error,
|
||||
%{
|
||||
exception
|
||||
| message:
|
||||
"could not checkout the connection owned by #{Util.inspect_pid(caller)}. " <>
|
||||
"When using the sandbox, connections are shared, so this may imply " <>
|
||||
"another process is using a connection. Reason: #{message}"
|
||||
}}
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
|
||||
{:exit, reason} ->
|
||||
exit({reason, {__MODULE__, :checkout, [pool, opts]}})
|
||||
end
|
||||
end
|
||||
|
||||
@spec checkin(pool_ref :: any) :: :ok
|
||||
def checkin(pool_ref) do
|
||||
# Note we may call checkin after a disconnect/stop. For this reason, we choose
|
||||
# to not change the status on checkin but strictly speaking nobody can access
|
||||
# the holder after disconnect/stop unless they store a copy of %DBConnection{}.
|
||||
# Note status can't be :aborted as aborted is always reverted at the end of a
|
||||
# transaction.
|
||||
done(pool_ref, [{conn(:lock) + 1, nil}], :checkin, System.monotonic_time())
|
||||
end
|
||||
|
||||
@spec disconnect(pool_ref :: any, err :: Exception.t()) :: :ok
|
||||
def disconnect(pool_ref, err) do
|
||||
done(pool_ref, [{conn(:status) + 1, :error}], :disconnect, err)
|
||||
end
|
||||
|
||||
@spec stop(pool_ref :: any, err :: Exception.t()) :: :ok
|
||||
def stop(pool_ref, err) do
|
||||
done(pool_ref, [{conn(:status) + 1, :error}], :stop, err)
|
||||
end
|
||||
|
||||
@spec handle(pool_ref :: any, fun :: atom, args :: [term], Keyword.t()) :: tuple
|
||||
def handle(pool_ref, fun, args, opts) do
|
||||
handle_or_cleanup(:handle, pool_ref, fun, args, opts)
|
||||
end
|
||||
|
||||
@spec cleanup(pool_ref :: any, fun :: atom, args :: [term], Keyword.t()) :: tuple
|
||||
def cleanup(pool_ref, fun, args, opts) do
|
||||
handle_or_cleanup(:cleanup, pool_ref, fun, args, opts)
|
||||
end
|
||||
|
||||
defp handle_or_cleanup(type, pool_ref, fun, args, opts) do
|
||||
pool_ref(holder: holder, lock: lock) = pool_ref
|
||||
|
||||
try do
|
||||
:ets.lookup(holder, :conn)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
msg = "connection is closed because of an error, disconnect or timeout"
|
||||
{:disconnect, DBConnection.ConnectionError.exception(msg), _state = :unused}
|
||||
else
|
||||
[conn(lock: conn_lock)] when conn_lock != lock ->
|
||||
raise "an outdated connection has been given to DBConnection on #{fun}/#{length(args) + 2}"
|
||||
|
||||
[conn(status: :error)] ->
|
||||
msg = "connection is closed because of an error, disconnect or timeout"
|
||||
{:disconnect, DBConnection.ConnectionError.exception(msg), _state = :unused}
|
||||
|
||||
[conn(status: :aborted)] when type != :cleanup ->
|
||||
msg = "transaction rolling back"
|
||||
{:disconnect, DBConnection.ConnectionError.exception(msg), _state = :unused}
|
||||
|
||||
[conn(module: module, state: state)] ->
|
||||
holder_apply(holder, module, fun, args ++ [opts, state])
|
||||
end
|
||||
end
|
||||
|
||||
## Pool state helpers API (invoked by callers)
|
||||
|
||||
@spec put_state(pool_ref :: any, term) :: :ok
|
||||
def put_state(pool_ref(holder: sink_holder), state) do
|
||||
:ets.update_element(sink_holder, :conn, [{conn(:state) + 1, state}])
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec status?(pool_ref :: any, :ok | :aborted) :: boolean()
|
||||
def status?(pool_ref(holder: holder), status) do
|
||||
try do
|
||||
:ets.lookup_element(holder, :conn, conn(:status) + 1) == status
|
||||
rescue
|
||||
ArgumentError -> false
|
||||
end
|
||||
end
|
||||
|
||||
@spec put_status(pool_ref :: any, :ok | :aborted) :: boolean()
|
||||
def put_status(pool_ref(holder: holder), status) do
|
||||
try do
|
||||
:ets.update_element(holder, :conn, [{conn(:status) + 1, status}])
|
||||
rescue
|
||||
ArgumentError -> false
|
||||
end
|
||||
end
|
||||
|
||||
## Pool callbacks (invoked by pools)
|
||||
|
||||
@spec reply_redirect({pid, reference}, pid | :shared | :auto, GenServer.server()) :: :ok
|
||||
def reply_redirect(from, caller, redirect) do
|
||||
GenServer.reply(from, {:redirect, caller, redirect})
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec reply_error({pid, reference}, Exception.t()) :: :ok
|
||||
def reply_error(from, exception) do
|
||||
GenServer.reply(from, {:error, exception})
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec handle_checkout(t, {pid, reference}, reference, checkin_time) :: boolean
|
||||
def handle_checkout(holder, {pid, mref}, ref, checkin_time) do
|
||||
:ets.give_away(holder, pid, {mref, ref, checkin_time})
|
||||
rescue
|
||||
ArgumentError ->
|
||||
if Process.alive?(pid) or :ets.info(holder, :owner) != self() do
|
||||
raise ArgumentError, no_holder(holder, pid)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@spec handle_deadline(t, reference) :: boolean
|
||||
def handle_deadline(holder, deadline) do
|
||||
:ets.lookup_element(holder, :conn, conn(:deadline) + 1)
|
||||
rescue
|
||||
ArgumentError -> false
|
||||
else
|
||||
^deadline -> true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
@spec handle_ping(t) :: true
|
||||
def handle_ping(holder) do
|
||||
:ets.lookup(holder, :conn)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
raise ArgumentError, no_holder(holder, nil)
|
||||
else
|
||||
[conn(connection: conn, state: state)] ->
|
||||
DBConnection.Connection.ping({conn, holder}, state)
|
||||
:ets.delete(holder)
|
||||
true
|
||||
end
|
||||
|
||||
@spec handle_disconnect(t, Exception.t()) :: boolean
|
||||
def handle_disconnect(holder, err) do
|
||||
handle_done(holder, &DBConnection.Connection.disconnect/3, err)
|
||||
end
|
||||
|
||||
@spec handle_stop(t, term) :: boolean
|
||||
def handle_stop(holder, err) do
|
||||
handle_done(holder, &DBConnection.Connection.stop/3, err)
|
||||
end
|
||||
|
||||
@spec maybe_disconnect(t, integer, non_neg_integer) :: boolean()
|
||||
def maybe_disconnect(holder, start, interval_ms) do
|
||||
ts = :ets.lookup_element(holder, :conn, conn(:ts) + 1)
|
||||
|
||||
cond do
|
||||
ts >= start ->
|
||||
false
|
||||
|
||||
interval_ms == 0 ->
|
||||
true
|
||||
|
||||
true ->
|
||||
pid = :ets.lookup_element(holder, :conn, conn(:connection) + 1)
|
||||
System.monotonic_time() > hash_pid(pid, interval_ms) + start
|
||||
end
|
||||
rescue
|
||||
_ -> false
|
||||
else
|
||||
true ->
|
||||
opts = [message: "disconnect_all requested", severity: :debug]
|
||||
handle_disconnect(holder, DBConnection.ConnectionError.exception(opts))
|
||||
|
||||
false ->
|
||||
false
|
||||
end
|
||||
|
||||
## Private
|
||||
|
||||
defp checkout(pool, callers, queue?, start, timeout) do
|
||||
case GenServer.whereis(pool) do
|
||||
pid when node(pid) == node() ->
|
||||
checkout_call(pid, callers, queue?, start, timeout)
|
||||
|
||||
pid when node(pid) != node() ->
|
||||
{:exit, {:badnode, node(pid)}}
|
||||
|
||||
{_name, node} ->
|
||||
{:exit, {:badnode, node}}
|
||||
|
||||
nil ->
|
||||
{:exit, :noproc}
|
||||
end
|
||||
end
|
||||
|
||||
defp checkout_call(pid, callers, queue?, start, timeout) do
|
||||
lock = Process.monitor(pid)
|
||||
send(pid, {:db_connection, {self(), lock}, {:checkout, callers, start, queue?}})
|
||||
|
||||
receive do
|
||||
{:"ETS-TRANSFER", holder, pool, {^lock, ref, checkin_time}} ->
|
||||
Process.demonitor(lock, [:flush])
|
||||
{deadline, ops} = start_deadline(timeout, pool, ref, holder, start)
|
||||
:ets.update_element(holder, :conn, [{conn(:lock) + 1, lock} | ops])
|
||||
|
||||
pool_ref =
|
||||
pool_ref(pool: pool, reference: ref, deadline: deadline, holder: holder, lock: lock)
|
||||
|
||||
checkout_result(holder, pool_ref, checkin_time)
|
||||
|
||||
{^lock, reply} ->
|
||||
Process.demonitor(lock, [:flush])
|
||||
reply
|
||||
|
||||
{:DOWN, ^lock, _, _, reason} ->
|
||||
{:exit, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp checkout_result(holder, pool_ref, checkin_time) do
|
||||
try do
|
||||
:ets.lookup(holder, :conn)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
# Deadline could hit and be handled pool before using connection
|
||||
msg = "connection not available because deadline reached while in queue"
|
||||
{:error, DBConnection.ConnectionError.exception(msg)}
|
||||
else
|
||||
[conn(module: mod, state: state)] ->
|
||||
{:ok, pool_ref, mod, checkin_time, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp no_holder(holder, maybe_pid) do
|
||||
reason =
|
||||
case :ets.info(holder, :owner) do
|
||||
:undefined -> "does not exist"
|
||||
^maybe_pid -> "is being given to its current owner"
|
||||
owner when owner != self() -> "does not belong to the giving process"
|
||||
_ -> "could not be given away"
|
||||
end
|
||||
|
||||
call_reason =
|
||||
if maybe_pid do
|
||||
"Error happened when attempting to transfer to #{Util.inspect_pid(maybe_pid)} " <>
|
||||
"(alive: #{Process.alive?(maybe_pid)})"
|
||||
else
|
||||
"Error happened when looking up connection"
|
||||
end
|
||||
|
||||
"""
|
||||
#{inspect(__MODULE__)} #{inspect(holder)} #{reason}, pool inconsistent.
|
||||
#{call_reason}.
|
||||
|
||||
SELF: #{Util.inspect_pid(self())}
|
||||
ETS INFO: #{inspect(:ets.info(holder))}
|
||||
|
||||
Please report at https://github.com/elixir-ecto/db_connection/issues"
|
||||
"""
|
||||
end
|
||||
|
||||
defp holder_apply(holder, module, fun, args) do
|
||||
try do
|
||||
apply(module, fun, args)
|
||||
catch
|
||||
kind, reason ->
|
||||
{:catch, kind, reason, __STACKTRACE__}
|
||||
else
|
||||
result when is_tuple(result) ->
|
||||
state = :erlang.element(:erlang.tuple_size(result), result)
|
||||
|
||||
try do
|
||||
:ets.update_element(holder, :conn, {conn(:state) + 1, state})
|
||||
result
|
||||
rescue
|
||||
ArgumentError ->
|
||||
augment_disconnect(result)
|
||||
end
|
||||
|
||||
# If it is not a tuple, we just return it as is so we raise bad return.
|
||||
result ->
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp augment_disconnect({:disconnect, %DBConnection.ConnectionError{} = err, state}) do
|
||||
%{message: message} = err
|
||||
|
||||
message =
|
||||
message <>
|
||||
" (the connection was closed by the pool, " <>
|
||||
"possibly due to a timeout or because the pool has been terminated)"
|
||||
|
||||
{:disconnect, %{err | message: message}, state}
|
||||
end
|
||||
|
||||
defp augment_disconnect(result), do: result
|
||||
|
||||
defp done(pool_ref, ops, tag, info) do
|
||||
pool_ref(pool: pool, reference: ref, deadline: deadline, holder: holder) = pool_ref
|
||||
cancel_deadline(deadline)
|
||||
|
||||
try do
|
||||
:ets.update_element(holder, :conn, [{conn(:deadline) + 1, nil} | ops])
|
||||
:ets.give_away(holder, pool, {tag, ref, info})
|
||||
rescue
|
||||
ArgumentError -> :ok
|
||||
else
|
||||
true -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_done(holder, stop, err) do
|
||||
:ets.lookup(holder, :conn)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
false
|
||||
else
|
||||
[conn(connection: pid, deadline: deadline, state: state)] ->
|
||||
cancel_deadline(deadline)
|
||||
:ets.delete(holder)
|
||||
stop.({pid, holder}, err, state)
|
||||
true
|
||||
end
|
||||
|
||||
defp abs_timeout(now, opts) do
|
||||
case Keyword.get(opts, :timeout, @timeout) do
|
||||
:infinity -> Keyword.get(opts, :deadline)
|
||||
timeout -> min(now + timeout, Keyword.get(opts, :deadline))
|
||||
end
|
||||
end
|
||||
|
||||
defp start_deadline(nil, _, _, _, _) do
|
||||
{nil, []}
|
||||
end
|
||||
|
||||
defp start_deadline(timeout, pid, ref, holder, start) do
|
||||
deadline =
|
||||
:erlang.start_timer(timeout, pid, {ref, holder, self(), timeout - start}, abs: true)
|
||||
|
||||
{deadline, [{conn(:deadline) + 1, deadline}]}
|
||||
end
|
||||
|
||||
defp cancel_deadline(nil) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp cancel_deadline(deadline) do
|
||||
:erlang.cancel_timer(deadline, async: true, info: false)
|
||||
end
|
||||
|
||||
defp hash_pid(pid, interval_ms) do
|
||||
hash = :erlang.phash2(pid, interval_ms)
|
||||
System.convert_time_unit(hash, :millisecond, :native)
|
||||
end
|
||||
end
|
||||
83
phoenix/deps/db_connection/lib/db_connection/log_entry.ex
Normal file
83
phoenix/deps/db_connection/lib/db_connection/log_entry.ex
Normal file
@@ -0,0 +1,83 @@
|
||||
defmodule DBConnection.LogEntry do
|
||||
@moduledoc """
|
||||
Struct containing log entry information.
|
||||
|
||||
See `t:t/0` for information on the fields.
|
||||
"""
|
||||
|
||||
defstruct [
|
||||
:call,
|
||||
:query,
|
||||
:params,
|
||||
:result,
|
||||
:pool_time,
|
||||
:connection_time,
|
||||
:decode_time,
|
||||
:idle_time
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Log entry information.
|
||||
|
||||
* `:call` - The `DBConnection` function called
|
||||
* `:query` - The query used by the function
|
||||
* `:params` - The params passed to the function (if any)
|
||||
* `:result` - The result of the call
|
||||
* `:pool_time` - The length of time awaiting a connection from the pool (if
|
||||
the connection was not already checked out)
|
||||
* `:connection_time` - The length of time using the connection (if a
|
||||
connection was used)
|
||||
* `:decode_time` - The length of time decoding the result (if decoded the
|
||||
result using `DBConnection.Query.decode/3`)
|
||||
* `:idle_time` - The amount of time the connection was idle before use
|
||||
|
||||
All times are in the native time units of the VM, see
|
||||
`System.monotonic_time/0`.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
call: atom,
|
||||
query: any,
|
||||
params: any,
|
||||
result: {:ok, any} | {:ok, any, any} | {:error, Exception.t()},
|
||||
pool_time: non_neg_integer | nil,
|
||||
connection_time: non_neg_integer | nil,
|
||||
idle_time: non_neg_integer | nil,
|
||||
decode_time: non_neg_integer | nil
|
||||
}
|
||||
|
||||
@doc false
|
||||
def new(call, query, params, times, result) do
|
||||
entry = %__MODULE__{call: call, query: query, params: params, result: result}
|
||||
parse_times(times, entry)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp parse_times([], entry), do: entry
|
||||
|
||||
defp parse_times(times, entry) do
|
||||
stop = :erlang.monotonic_time()
|
||||
{_, entry} = Enum.reduce(times, {stop, entry}, &parse_time/2)
|
||||
entry
|
||||
end
|
||||
|
||||
defp parse_time({:decode, start}, {stop, entry}) do
|
||||
{start, %{entry | decode_time: stop - start}}
|
||||
end
|
||||
|
||||
defp parse_time({:checkout, start}, {stop, entry}) do
|
||||
{start, %{entry | pool_time: stop - start}}
|
||||
end
|
||||
|
||||
defp parse_time({:checkin, start}, {stop, entry}) do
|
||||
# The checkin time was most likely before checkout but it is
|
||||
# not guaranteed as they are tracked by different processes.
|
||||
# There should be no further measurements after checkin.
|
||||
{stop, %{entry | idle_time: max(stop - start, 0)}}
|
||||
end
|
||||
|
||||
defp parse_time({_, start}, {stop, entry}) do
|
||||
%{connection_time: connection_time} = entry
|
||||
{start, %{entry | connection_time: (connection_time || 0) + (stop - start)}}
|
||||
end
|
||||
end
|
||||
155
phoenix/deps/db_connection/lib/db_connection/ownership.ex
Normal file
155
phoenix/deps/db_connection/lib/db_connection/ownership.ex
Normal file
@@ -0,0 +1,155 @@
|
||||
defmodule DBConnection.OwnershipError do
|
||||
@moduledoc """
|
||||
An exception for when errors with ownership occur.
|
||||
"""
|
||||
|
||||
defexception [:message]
|
||||
|
||||
def exception(message), do: %DBConnection.OwnershipError{message: message}
|
||||
end
|
||||
|
||||
defmodule DBConnection.Ownership do
|
||||
@moduledoc """
|
||||
A DBConnection pool that requires explicit checkout and checkin
|
||||
as a mechanism to coordinate between processes.
|
||||
|
||||
## Options
|
||||
|
||||
* `:ownership_mode` - When mode is `:manual`, all connections must
|
||||
be explicitly checked out before by using `ownership_checkout/2`.
|
||||
Otherwise, mode is `:auto` and connections are checked out
|
||||
implicitly. `{:shared, owner}` mode is also supported so
|
||||
processes are allowed on demand. On all cases, checkins are
|
||||
explicit via `ownership_checkin/2`. Defaults to `:auto`.
|
||||
* `:ownership_timeout` - The maximum time (in milliseconds) that a process
|
||||
is allowed to own a connection or `:infinity`, default `120_000`.
|
||||
This timeout exists mostly for sanity checking purposes and can be increased
|
||||
at will, since DBConnection automatically checks in connections whenever
|
||||
there is a mode change.
|
||||
* `:ownership_log` - The `Logger.level` to log ownership changes, or `nil`
|
||||
not to log, default `nil`.
|
||||
|
||||
There are also two experimental options, `:post_checkout` and `:pre_checkin`
|
||||
which allows a developer to configure what happens when a connection is
|
||||
checked out and checked in. Those options are meant to be used during tests,
|
||||
and have the following behaviour:
|
||||
|
||||
* `:post_checkout` - it must be an anonymous function that receives the
|
||||
connection module, the connection state and it must return either
|
||||
`{:ok, connection_module, connection_state}` or
|
||||
`{:disconnect, err, connection_module, connection_state}`. This allows
|
||||
the developer to change the connection module on post checkout. However,
|
||||
in case of disconnects, the return `connection_module` must be the same
|
||||
as the `connection_module` given. Defaults to simply returning the given
|
||||
connection module and state.
|
||||
|
||||
* `:pre_checkin` - it must be an anonymous function that receives the
|
||||
checkin reason (`:checkin`, `{:disconnect, err}` or `{:stop, err}`),
|
||||
the connection module and the connection state returned by `post_checkout`.
|
||||
It must return either `{:ok, connection_module, connection_state}` or
|
||||
`{:disconnect, err, connection_module, connection_state}` where the connection
|
||||
module is the module given to `:post_checkout` Defaults to simply returning
|
||||
the given connection module and state.
|
||||
|
||||
## Callers lookup
|
||||
|
||||
When checking out, the ownership pool first looks if there is a connection
|
||||
assigned to the current process and then checks if there is a connection
|
||||
assigned to any of the processes listed under the `$callers` process
|
||||
dictionary entry. The `$callers` entry is set by default for tasks from
|
||||
Elixir v1.8.
|
||||
|
||||
You can also pass the `:caller` option on checkout with a pid and that
|
||||
pid will be looked up first, instead of `self()`, and then we fall back
|
||||
to `$callers`.
|
||||
"""
|
||||
|
||||
alias DBConnection.Ownership.Manager
|
||||
alias DBConnection.Holder
|
||||
|
||||
@behaviour DBConnection.Pool
|
||||
|
||||
@doc false
|
||||
defdelegate child_spec(args), to: Manager
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
defdelegate disconnect_all(pool, interval, opts), to: Manager
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
def checkout(pool, callers, opts) do
|
||||
case Manager.proxy_for(callers, opts) do
|
||||
{caller, pool} -> Holder.checkout(pool, [caller], opts)
|
||||
nil -> Holder.checkout(pool, callers, opts)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
defdelegate get_connection_metrics(pool), to: Manager
|
||||
|
||||
@doc """
|
||||
Explicitly checks a connection out from the ownership manager.
|
||||
|
||||
It may return `:ok` if the connection is checked out.
|
||||
`{:already, :owner | :allowed}` if the caller process already
|
||||
has a connection, or raise if there was an error.
|
||||
"""
|
||||
@spec ownership_checkout(GenServer.server(), Keyword.t()) ::
|
||||
:ok | {:already, :owner | :allowed}
|
||||
def ownership_checkout(manager, opts) do
|
||||
with {:ok, pid} <- Manager.checkout(manager, opts) do
|
||||
case Holder.checkout(pid, [self()], opts) do
|
||||
{:ok, pool_ref, _module, _idle_time, _state} ->
|
||||
Holder.checkin(pool_ref)
|
||||
|
||||
{:error, err} ->
|
||||
raise err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changes the ownership mode.
|
||||
|
||||
`mode` may be `:auto`, `:manual` or `{:shared, owner}`.
|
||||
|
||||
The operation will always succeed when setting the mode to
|
||||
`:auto` or `:manual`. It may fail with reason `:not_owner`
|
||||
or `:not_found` when setting `{:shared, pid}` and the
|
||||
given pid does not own any connection. May return
|
||||
`:already_shared` if another process set the ownership
|
||||
mode to `{:shared, _}` and is still alive.
|
||||
"""
|
||||
@spec ownership_mode(GenServer.server(), :auto | :manual | {:shared, pid}, Keyword.t()) ::
|
||||
:ok | :already_shared | :not_owner | :not_found
|
||||
defdelegate ownership_mode(manager, mode, opts), to: Manager, as: :mode
|
||||
|
||||
@doc """
|
||||
Checks a connection back in.
|
||||
|
||||
A connection can only be checked back in by its owner.
|
||||
"""
|
||||
@spec ownership_checkin(GenServer.server(), Keyword.t()) ::
|
||||
:ok | :not_owner | :not_found
|
||||
defdelegate ownership_checkin(manager, opts), to: Manager, as: :checkin
|
||||
|
||||
@doc """
|
||||
Allows the process given by `allow` to use the connection checked out
|
||||
by `owner_or_allowed`.
|
||||
|
||||
It may return `:ok` if the connection is checked out.
|
||||
`{:already, :owner | :allowed}` if the `allow` process already
|
||||
has a connection. `owner_or_allowed` may either be the owner or any
|
||||
other allowed process. Returns `:not_found` if the given process
|
||||
does not have any connection checked out.
|
||||
|
||||
Setting the `unallow_existing` option to `true` will remove the process given by `allow` from
|
||||
any existing allowance it may have (this is necessary because a given process can only be
|
||||
allowed on a single connection at a time).
|
||||
"""
|
||||
@spec ownership_allow(GenServer.server(), owner_or_allowed :: pid, allow :: pid, Keyword.t()) ::
|
||||
:ok | {:already, :owner | :allowed} | :not_found
|
||||
defdelegate ownership_allow(manager, owner, allow, opts), to: Manager, as: :allow
|
||||
end
|
||||
@@ -0,0 +1,439 @@
|
||||
defmodule DBConnection.Ownership.Manager do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
require Logger
|
||||
alias DBConnection.Ownership.Proxy
|
||||
alias DBConnection.Util
|
||||
|
||||
@timeout 5_000
|
||||
|
||||
@callback start_link({module, opts :: Keyword.t()}) ::
|
||||
GenServer.on_start()
|
||||
def start_link({module, opts}) do
|
||||
{owner_opts, pool_opts} = Keyword.split(opts, [:name])
|
||||
GenServer.start_link(__MODULE__, {module, owner_opts, pool_opts}, owner_opts)
|
||||
end
|
||||
|
||||
@callback disconnect_all(GenServer.server(), non_neg_integer, Keyword.t()) :: :ok
|
||||
def disconnect_all(pool, interval, opts) do
|
||||
inner_pool = GenServer.call(pool, :pool, :infinity)
|
||||
DBConnection.ConnectionPool.disconnect_all(inner_pool, interval, opts)
|
||||
end
|
||||
|
||||
@spec proxy_for(callers :: [pid], Keyword.t()) :: {caller :: pid, proxy :: pid} | nil
|
||||
def proxy_for(callers, opts) do
|
||||
case Keyword.fetch(opts, :name) do
|
||||
{:ok, name} ->
|
||||
Enum.find_value(callers, &List.first(:ets.lookup(name, &1)))
|
||||
|
||||
:error ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec checkout(GenServer.server(), Keyword.t()) ::
|
||||
{:ok, pid} | {:already, :owner | :allowed}
|
||||
def checkout(manager, opts) do
|
||||
GenServer.call(manager, {:checkout, opts}, :infinity)
|
||||
end
|
||||
|
||||
@spec checkin(GenServer.server(), Keyword.t()) ::
|
||||
:ok | :not_owner | :not_found
|
||||
def checkin(manager, opts) do
|
||||
timeout = Keyword.get(opts, :timeout, @timeout)
|
||||
GenServer.call(manager, :checkin, timeout)
|
||||
end
|
||||
|
||||
@spec mode(GenServer.server(), :auto | :manual | {:shared, pid}, Keyword.t()) ::
|
||||
:ok | :already_shared | :not_owner | :not_found
|
||||
def mode(manager, mode, opts)
|
||||
when mode in [:auto, :manual]
|
||||
when elem(mode, 0) == :shared and is_pid(elem(mode, 1)) do
|
||||
timeout = Keyword.get(opts, :timeout, @timeout)
|
||||
GenServer.call(manager, {:mode, mode}, timeout)
|
||||
end
|
||||
|
||||
@spec allow(GenServer.server(), parent :: pid, allow :: pid, Keyword.t()) ::
|
||||
:ok | {:already, :owner | :allowed} | :not_found
|
||||
def allow(manager, parent, allow, opts) do
|
||||
timeout = Keyword.get(opts, :timeout, @timeout)
|
||||
passed_opts = Keyword.take(opts, [:unallow_existing])
|
||||
GenServer.call(manager, {:allow, parent, allow, passed_opts}, timeout)
|
||||
end
|
||||
|
||||
@spec get_connection_metrics(GenServer.server()) ::
|
||||
{:ok, [DBConnection.Pool.connection_metrics()]} | :error
|
||||
def get_connection_metrics(manager) do
|
||||
GenServer.call(manager, :get_connection_metrics, :infinity)
|
||||
end
|
||||
|
||||
## Callbacks
|
||||
|
||||
@impl true
|
||||
def init({module, owner_opts, pool_opts}) do
|
||||
DBConnection.register_as_pool(module)
|
||||
|
||||
ets =
|
||||
case Keyword.fetch(owner_opts, :name) do
|
||||
{:ok, name} when is_atom(name) ->
|
||||
:ets.new(name, [
|
||||
:set,
|
||||
:named_table,
|
||||
:protected,
|
||||
read_concurrency: true,
|
||||
decentralized_counters: true
|
||||
])
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
# We can only start the connection pool directly because
|
||||
# neither the pool's GenServer nor the manager trap exits.
|
||||
# Otherwise we would need a supervisor plus a watcher process.
|
||||
pool_opts = Keyword.delete(pool_opts, :pool)
|
||||
{:ok, pool} = DBConnection.start_link(module, pool_opts)
|
||||
|
||||
log = Keyword.get(pool_opts, :ownership_log, nil)
|
||||
mode = Keyword.get(pool_opts, :ownership_mode, :auto)
|
||||
checkout_opts = Keyword.take(pool_opts, [:ownership_timeout, :queue_target, :queue_interval])
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
pool: pool,
|
||||
checkouts: %{},
|
||||
owners: %{},
|
||||
checkout_opts: checkout_opts,
|
||||
mode: mode,
|
||||
mode_ref: nil,
|
||||
ets: ets,
|
||||
log: log
|
||||
}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_connection_metrics, _from, %{pool: pool, owners: owners, log: log} = state) do
|
||||
pool_metrics = DBConnection.ConnectionPool.get_connection_metrics(pool)
|
||||
|
||||
proxy_metrics =
|
||||
owners
|
||||
|> Enum.map(fn {_, {proxy, _, _}} ->
|
||||
try do
|
||||
GenServer.call(proxy, :get_connection_metrics)
|
||||
catch
|
||||
:exit, reason ->
|
||||
if log do
|
||||
Logger.log(
|
||||
log,
|
||||
"Caught :exit while calling :get_connection_metrics due to #{inspect(reason)}"
|
||||
)
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
{:reply, pool_metrics ++ proxy_metrics, state}
|
||||
end
|
||||
|
||||
def handle_call(:pool, _from, %{pool: pool} = state) do
|
||||
{:reply, pool, state}
|
||||
end
|
||||
|
||||
def handle_call({:mode, {:shared, shared}}, {caller, _}, %{mode: {:shared, current}} = state) do
|
||||
cond do
|
||||
shared == current ->
|
||||
{:reply, :ok, state}
|
||||
|
||||
Process.alive?(current) ->
|
||||
{:reply, :already_shared, state}
|
||||
|
||||
true ->
|
||||
share_and_reply(state, shared, caller)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:mode, {:shared, shared}}, {caller, _}, state) do
|
||||
share_and_reply(state, shared, caller)
|
||||
end
|
||||
|
||||
def handle_call({:mode, mode}, _from, %{mode: mode} = state) do
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:mode, mode}, {caller, _}, state) do
|
||||
state = proxy_checkin_all_except(state, [], caller)
|
||||
{:reply, :ok, %{state | mode: mode, mode_ref: nil}}
|
||||
end
|
||||
|
||||
def handle_call(:checkin, {caller, _}, state) do
|
||||
{reply, state} = proxy_checkin(state, caller, caller)
|
||||
{:reply, reply, state}
|
||||
end
|
||||
|
||||
def handle_call({:allow, caller, allow, opts}, _from, %{checkouts: checkouts} = state) do
|
||||
unallow_existing = Keyword.get(opts, :unallow_existing, false)
|
||||
kind = already_checked_out(checkouts, allow)
|
||||
|
||||
if !unallow_existing && kind do
|
||||
{:reply, {:already, kind}, state}
|
||||
else
|
||||
case Map.get(checkouts, caller, :not_found) do
|
||||
{:owner, ref, proxy} ->
|
||||
state =
|
||||
if unallow_existing, do: owner_unallow(state, caller, allow, ref, proxy), else: state
|
||||
|
||||
{:reply, :ok, owner_allow(state, caller, allow, ref, proxy)}
|
||||
|
||||
{:allowed, ref, proxy} ->
|
||||
state =
|
||||
if unallow_existing, do: owner_unallow(state, caller, allow, ref, proxy), else: state
|
||||
|
||||
{:reply, :ok, owner_allow(state, caller, allow, ref, proxy)}
|
||||
|
||||
:not_found ->
|
||||
{:reply, :not_found, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:checkout, opts}, {caller, _}, %{checkouts: checkouts} = state) do
|
||||
if kind = already_checked_out(checkouts, caller) do
|
||||
{:reply, {:already, kind}, state}
|
||||
else
|
||||
{proxy, state} = proxy_checkout(state, caller, opts)
|
||||
{:reply, {:ok, proxy}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:db_connection, from, {:checkout, callers, _now, queue?}}, state) do
|
||||
%{checkouts: checkouts, mode: mode, checkout_opts: checkout_opts} = state
|
||||
caller = find_caller(callers, checkouts, mode)
|
||||
|
||||
case Map.get(checkouts, caller, :not_found) do
|
||||
{status, _ref, proxy} when status in [:owner, :allowed] ->
|
||||
DBConnection.Holder.reply_redirect(from, caller, proxy)
|
||||
{:noreply, state}
|
||||
|
||||
:not_found when mode == :auto ->
|
||||
{proxy, state} = proxy_checkout(state, caller, [queue: queue?] ++ checkout_opts)
|
||||
DBConnection.Holder.reply_redirect(from, caller, proxy)
|
||||
{:noreply, state}
|
||||
|
||||
:not_found when mode == :manual ->
|
||||
not_found(from, mode)
|
||||
{:noreply, state}
|
||||
|
||||
:not_found ->
|
||||
{:shared, shared} = mode
|
||||
{:owner, _ref, proxy} = Map.fetch!(checkouts, shared)
|
||||
DBConnection.Holder.reply_redirect(from, shared, proxy)
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, ref, _, _, _}, state) do
|
||||
{:noreply, state |> owner_down(ref) |> unshare(ref)}
|
||||
end
|
||||
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp already_checked_out(checkouts, pid) do
|
||||
case Map.get(checkouts, pid, :not_found) do
|
||||
{:owner, _, _} -> :owner
|
||||
{:allowed, _, _} -> :allowed
|
||||
:not_found -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp proxy_checkout(state, caller, opts) do
|
||||
%{pool: pool, checkouts: checkouts, owners: owners, ets: ets, log: log, mode: mode} = state
|
||||
|
||||
{:ok, proxy} =
|
||||
DynamicSupervisor.start_child(
|
||||
DBConnection.Ownership.Supervisor,
|
||||
{DBConnection.Ownership.Proxy, {caller, pool, opts}}
|
||||
)
|
||||
|
||||
if log do
|
||||
Logger.log(log, fn ->
|
||||
[
|
||||
Util.inspect_pid(caller),
|
||||
" checked out connection in ",
|
||||
inspect(mode),
|
||||
" mode using proxy ",
|
||||
Util.inspect_pid(proxy)
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
ref = Process.monitor(proxy)
|
||||
checkouts = Map.put(checkouts, caller, {:owner, ref, proxy})
|
||||
owners = Map.put(owners, ref, {proxy, caller, []})
|
||||
ets && :ets.insert(ets, {caller, proxy})
|
||||
{proxy, %{state | checkouts: checkouts, owners: owners}}
|
||||
end
|
||||
|
||||
defp proxy_checkin(state, maybe_owner, caller) do
|
||||
case get_and_update_in(state.checkouts, &Map.pop(&1, maybe_owner, :not_found)) do
|
||||
{{:owner, ref, proxy}, state} ->
|
||||
Proxy.stop(proxy, caller)
|
||||
{:ok, state |> owner_down(ref) |> unshare(ref)}
|
||||
|
||||
{{:allowed, _, _}, _} ->
|
||||
{:not_owner, state}
|
||||
|
||||
{:not_found, _} ->
|
||||
{:not_found, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp proxy_checkin_all_except(state, except, caller) do
|
||||
Enum.reduce(state.checkouts, state, fn {pid, _}, state ->
|
||||
if pid in except do
|
||||
state
|
||||
else
|
||||
{_, state} = proxy_checkin(state, pid, caller)
|
||||
state
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp owner_allow(%{ets: ets, log: log} = state, caller, allow, ref, proxy) do
|
||||
if log do
|
||||
Logger.log(log, fn ->
|
||||
[
|
||||
Util.inspect_pid(allow),
|
||||
" was allowed by ",
|
||||
Util.inspect_pid(caller),
|
||||
" on proxy ",
|
||||
Util.inspect_pid(proxy)
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
state = put_in(state.checkouts[allow], {:allowed, ref, proxy})
|
||||
|
||||
state =
|
||||
update_in(state.owners[ref], fn {proxy, caller, allowed} ->
|
||||
{proxy, caller, [allow | List.delete(allowed, allow)]}
|
||||
end)
|
||||
|
||||
ets && :ets.insert(ets, {allow, proxy})
|
||||
state
|
||||
end
|
||||
|
||||
defp owner_unallow(%{ets: ets, log: log} = state, caller, unallow, ref, proxy) do
|
||||
if log do
|
||||
Logger.log(log, fn ->
|
||||
[
|
||||
Util.inspect_pid(unallow),
|
||||
" was unallowed by ",
|
||||
Util.inspect_pid(caller),
|
||||
" on proxy ",
|
||||
Util.inspect_pid(proxy)
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
state = update_in(state.checkouts, &Map.delete(&1, unallow))
|
||||
|
||||
state =
|
||||
update_in(state.owners[ref], fn {proxy, caller, allowed} ->
|
||||
{proxy, caller, List.delete(allowed, unallow)}
|
||||
end)
|
||||
|
||||
ets && :ets.delete(ets, {unallow, proxy})
|
||||
state
|
||||
end
|
||||
|
||||
defp owner_down(%{ets: ets, log: log} = state, ref) do
|
||||
case get_and_update_in(state.owners, &Map.pop(&1, ref)) do
|
||||
{{proxy, caller, allowed}, state} ->
|
||||
Process.demonitor(ref, [:flush])
|
||||
entries = [caller | allowed]
|
||||
|
||||
if log do
|
||||
Logger.log(log, fn ->
|
||||
[
|
||||
Enum.map_join(entries, ", ", &Util.inspect_pid/1),
|
||||
" lost connection from proxy ",
|
||||
Util.inspect_pid(proxy)
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
ets && Enum.each(entries, &:ets.delete(ets, &1))
|
||||
update_in(state.checkouts, &Map.drop(&1, entries))
|
||||
|
||||
{nil, state} ->
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp share_and_reply(%{checkouts: checkouts} = state, shared, caller) do
|
||||
case Map.get(checkouts, shared, :not_found) do
|
||||
{:owner, ref, _} ->
|
||||
state = proxy_checkin_all_except(state, [shared], caller)
|
||||
{:reply, :ok, %{state | mode: {:shared, shared}, mode_ref: ref}}
|
||||
|
||||
{:allowed, _, _} ->
|
||||
{:reply, :not_owner, state}
|
||||
|
||||
:not_found ->
|
||||
{:reply, :not_found, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp unshare(%{mode_ref: ref} = state, ref) do
|
||||
%{state | mode: :manual, mode_ref: nil}
|
||||
end
|
||||
|
||||
defp unshare(state, _ref) do
|
||||
state
|
||||
end
|
||||
|
||||
defp find_caller(callers, checkouts, :manual) do
|
||||
Enum.find(callers, &Map.has_key?(checkouts, &1)) || hd(callers)
|
||||
end
|
||||
|
||||
defp find_caller([caller | _], _checkouts, _mode) do
|
||||
caller
|
||||
end
|
||||
|
||||
defp not_found({pid, _} = from, mode) do
|
||||
msg = """
|
||||
cannot find ownership process for #{Util.inspect_pid(pid)}
|
||||
using mode #{inspect(mode)}.
|
||||
|
||||
When using ownership, you must manage connections in one
|
||||
of the four ways:
|
||||
|
||||
* By explicitly checking out a connection
|
||||
* By explicitly allowing a spawned process
|
||||
* By running the pool in shared mode
|
||||
* By using :caller option with allowed process
|
||||
|
||||
The first two options require every new process to explicitly
|
||||
check a connection out or be allowed by calling checkout or
|
||||
allow respectively.
|
||||
|
||||
The third option requires a {:shared, pid} mode to be set.
|
||||
If using shared mode in tests, make sure your tests are not
|
||||
async.
|
||||
|
||||
The fourth option requires [caller: pid] to be used when
|
||||
checking out a connection from the pool. The caller process
|
||||
should already be allowed on a connection.
|
||||
|
||||
If you are reading this error, it means you have not done one
|
||||
of the steps above or that the owner process has crashed.
|
||||
"""
|
||||
|
||||
DBConnection.Holder.reply_error(from, DBConnection.OwnershipError.exception(msg))
|
||||
end
|
||||
end
|
||||
334
phoenix/deps/db_connection/lib/db_connection/ownership/proxy.ex
Normal file
334
phoenix/deps/db_connection/lib/db_connection/ownership/proxy.ex
Normal file
@@ -0,0 +1,334 @@
|
||||
defmodule DBConnection.Ownership.Proxy do
|
||||
@moduledoc false
|
||||
|
||||
alias DBConnection.Holder
|
||||
alias DBConnection.Util
|
||||
use GenServer, restart: :temporary
|
||||
|
||||
@time_unit 1000
|
||||
@ownership_timeout 120_000
|
||||
@queue_target 50
|
||||
@queue_interval 1000
|
||||
|
||||
def start_link({caller, pool, pool_opts}) do
|
||||
GenServer.start_link(__MODULE__, {caller, pool, pool_opts}, [])
|
||||
end
|
||||
|
||||
def stop(proxy, caller) do
|
||||
GenServer.cast(proxy, {:stop, caller})
|
||||
end
|
||||
|
||||
# Callbacks
|
||||
|
||||
@impl true
|
||||
def init({caller, pool, pool_opts}) do
|
||||
pool_opts =
|
||||
pool_opts
|
||||
|> Keyword.put(:timeout, :infinity)
|
||||
|> Keyword.delete(:deadline)
|
||||
|
||||
owner_ref = Process.monitor(caller)
|
||||
ownership_timeout = Keyword.get(pool_opts, :ownership_timeout, @ownership_timeout)
|
||||
timeout = Keyword.get(pool_opts, :queue_target, @queue_target) * 2
|
||||
interval = Keyword.get(pool_opts, :queue_interval, @queue_interval)
|
||||
|
||||
pre_checkin = Keyword.get(pool_opts, :pre_checkin, fn _, mod, state -> {:ok, mod, state} end)
|
||||
post_checkout = Keyword.get(pool_opts, :post_checkout, &{:ok, &1, &2})
|
||||
|
||||
state = %{
|
||||
client: nil,
|
||||
timer: nil,
|
||||
holder: nil,
|
||||
timeout: timeout,
|
||||
interval: interval,
|
||||
poll: nil,
|
||||
owner: {caller, owner_ref},
|
||||
pool: pool,
|
||||
pool_ref: nil,
|
||||
pool_opts: pool_opts,
|
||||
queue: :queue.new(),
|
||||
mod: nil,
|
||||
pre_checkin: pre_checkin,
|
||||
post_checkout: post_checkout,
|
||||
ownership_timer: start_timer(caller, ownership_timeout)
|
||||
}
|
||||
|
||||
now = System.monotonic_time(@time_unit)
|
||||
{:ok, start_poll(now, state)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:DOWN, ref, _, pid, _reason}, %{owner: {_, ref}} = state) do
|
||||
shutdown("owner #{Util.inspect_pid(pid)} exited", state)
|
||||
end
|
||||
|
||||
def handle_info({:timeout, deadline, {_ref, holder, pid, len}}, %{holder: holder} = state) do
|
||||
if Holder.handle_deadline(holder, deadline) do
|
||||
message =
|
||||
"client #{Util.inspect_pid(pid)} timed out because " <>
|
||||
"it queued and checked out the connection for longer than #{len}ms"
|
||||
|
||||
shutdown(message, state)
|
||||
else
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:timeout, timer, {__MODULE__, pid, timeout}},
|
||||
%{ownership_timer: timer} = state
|
||||
) do
|
||||
message =
|
||||
"owner #{Util.inspect_pid(pid)} timed out because " <>
|
||||
"it owned the connection for longer than #{timeout}ms (set via the :ownership_timeout option)"
|
||||
|
||||
# We don't invoke shutdown because this is always a disconnect, even if there is no client.
|
||||
# On the other hand, those timeouts are unlikely to trigger, as it defaults to 2 mins.
|
||||
pool_disconnect(DBConnection.ConnectionError.exception(message), false, state)
|
||||
end
|
||||
|
||||
def handle_info({:timeout, poll, time}, %{poll: poll} = state) do
|
||||
state = timeout(time, state)
|
||||
{:noreply, start_poll(time, state)}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:db_connection, from, {:checkout, _caller, _now, _queue?}},
|
||||
%{holder: nil} = state
|
||||
) do
|
||||
%{pool: pool, pool_opts: pool_opts, owner: {_, owner_ref}, post_checkout: post_checkout} =
|
||||
state
|
||||
|
||||
case Holder.checkout(pool, [self()], pool_opts) do
|
||||
{:ok, pool_ref, original_mod, _idle_time, conn_state} ->
|
||||
case post_checkout.(original_mod, conn_state) do
|
||||
{:ok, conn_mod, conn_state} ->
|
||||
holder = Holder.new(self(), owner_ref, conn_mod, conn_state)
|
||||
state = %{state | pool_ref: pool_ref, holder: holder, mod: original_mod}
|
||||
checkout(from, state)
|
||||
|
||||
{:disconnect, err, ^original_mod, _conn_state} ->
|
||||
Holder.disconnect(pool_ref, err)
|
||||
Holder.reply_error(from, err)
|
||||
{:stop, {:shutdown, err}, state}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
Holder.reply_error(from, err)
|
||||
{:stop, {:shutdown, err}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:db_connection, from, {:checkout, _caller, _now, _queue?}},
|
||||
%{client: nil} = state
|
||||
) do
|
||||
checkout(from, state)
|
||||
end
|
||||
|
||||
def handle_info({:db_connection, from, {:checkout, _caller, now, queue?}}, state) do
|
||||
if queue? do
|
||||
%{queue: queue} = state
|
||||
queue = :queue.in({now, from}, queue)
|
||||
{:noreply, %{state | queue: queue}}
|
||||
else
|
||||
message = "connection not available and queuing is disabled"
|
||||
err = DBConnection.ConnectionError.exception(message)
|
||||
Holder.reply_error(from, err)
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:"ETS-TRANSFER", holder, _, {msg, ref, extra}},
|
||||
%{holder: holder, client: {_, ref, _}} = state
|
||||
) do
|
||||
case msg do
|
||||
:checkin -> checkin(state)
|
||||
:disconnect -> pool_disconnect(extra, true, state)
|
||||
:stop -> pool_stop(extra, state)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:"ETS-TRANSFER", holder, pid, ref}, %{holder: holder, owner: {_, ref}} = state) do
|
||||
shutdown("client #{Util.inspect_pid(pid)} exited", state)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:stop, caller}, %{owner: {owner, _}} = state) do
|
||||
message =
|
||||
"#{Util.inspect_pid(caller)} checked in the connection owned by #{Util.inspect_pid(owner)}"
|
||||
|
||||
message =
|
||||
case pruned_stacktrace(caller) do
|
||||
[] ->
|
||||
message
|
||||
|
||||
current_stack ->
|
||||
message <>
|
||||
"\n\n#{Util.inspect_pid(caller)} triggered the checkin at location:\n\n" <>
|
||||
Exception.format_stacktrace(current_stack)
|
||||
end
|
||||
|
||||
shutdown(message, state)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(
|
||||
:get_connection_metrics,
|
||||
_,
|
||||
%{queue: queue, holder: holder, client: client} = state
|
||||
) do
|
||||
connection_metrics = %{
|
||||
source: {:proxy, self()},
|
||||
ready_conn_count:
|
||||
if is_nil(holder) or not is_nil(client) do
|
||||
0
|
||||
else
|
||||
1
|
||||
end,
|
||||
checkout_queue_length: :queue.len(queue)
|
||||
}
|
||||
|
||||
{:reply, connection_metrics, state}
|
||||
end
|
||||
|
||||
defp checkout({pid, ref} = from, %{holder: holder} = state) do
|
||||
if Holder.handle_checkout(holder, from, ref, nil) do
|
||||
{:noreply, %{state | client: {pid, ref, pruned_stacktrace(pid)}}}
|
||||
else
|
||||
next(state)
|
||||
end
|
||||
end
|
||||
|
||||
defp checkin(state) do
|
||||
next(%{state | client: nil})
|
||||
end
|
||||
|
||||
defp next(%{queue: queue} = state) do
|
||||
case :queue.out(queue) do
|
||||
{{:value, {_, from}}, queue} ->
|
||||
checkout(from, %{state | queue: queue})
|
||||
|
||||
{:empty, queue} ->
|
||||
{:noreply, %{state | queue: queue}}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_timer(_, :infinity), do: nil
|
||||
|
||||
defp start_timer(pid, timeout) do
|
||||
:erlang.start_timer(timeout, self(), {__MODULE__, pid, timeout})
|
||||
end
|
||||
|
||||
# If shutting down but it has no client, checkin
|
||||
defp shutdown(reason, %{client: nil} = state) do
|
||||
pool_checkin(reason, state)
|
||||
end
|
||||
|
||||
# If shutting down but it has a client, disconnect
|
||||
defp shutdown(reason, %{client: {client, _, checkout_stack}} = state) do
|
||||
reason =
|
||||
case pruned_stacktrace(client) do
|
||||
[] ->
|
||||
reason
|
||||
|
||||
current_stack ->
|
||||
reason <>
|
||||
"""
|
||||
\n\nClient #{Util.inspect_pid(client)} is still using a connection from owner at location:
|
||||
|
||||
#{Exception.format_stacktrace(current_stack)}
|
||||
The connection itself was checked out by #{Util.inspect_pid(client)} at location:
|
||||
|
||||
#{Exception.format_stacktrace(checkout_stack)}
|
||||
"""
|
||||
end
|
||||
|
||||
err = DBConnection.ConnectionError.exception(reason)
|
||||
pool_disconnect(err, false, state)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp pool_checkin(reason, state) do
|
||||
checkin = fn pool_ref, _ -> Holder.checkin(pool_ref) end
|
||||
pool_done(reason, state, :checkin, false, checkin, &Holder.disconnect/2)
|
||||
end
|
||||
|
||||
defp pool_disconnect(err, keep_alive?, state) do
|
||||
disconnect = &Holder.disconnect/2
|
||||
pool_done(err, state, {:disconnect, err}, keep_alive?, disconnect, disconnect)
|
||||
end
|
||||
|
||||
defp pool_stop(err, state) do
|
||||
stop = &Holder.stop/2
|
||||
pool_done(err, state, {:stop, err}, false, stop, stop)
|
||||
end
|
||||
|
||||
defp pool_done(err, state, op, keep_alive?, done, stop_or_disconnect) do
|
||||
%{holder: holder, pool_ref: pool_ref, pre_checkin: pre_checkin, mod: original_mod} = state
|
||||
|
||||
if holder do
|
||||
{conn_mod, conn_state} = Holder.delete(holder)
|
||||
|
||||
case pre_checkin.(op, conn_mod, conn_state) do
|
||||
{:ok, ^original_mod, conn_state} ->
|
||||
Holder.put_state(pool_ref, conn_state)
|
||||
done.(pool_ref, err)
|
||||
|
||||
if keep_alive? do
|
||||
{:noreply, %{state | holder: nil}}
|
||||
else
|
||||
{:stop, {:shutdown, err}, state}
|
||||
end
|
||||
|
||||
{:disconnect, err, ^original_mod, conn_state} ->
|
||||
Holder.put_state(pool_ref, conn_state)
|
||||
stop_or_disconnect.(pool_ref, err)
|
||||
{:stop, {:shutdown, err}, state}
|
||||
end
|
||||
else
|
||||
{:stop, {:shutdown, err}, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_poll(now, %{interval: interval} = state) do
|
||||
timeout = now + interval
|
||||
poll = :erlang.start_timer(timeout, self(), timeout, abs: true)
|
||||
%{state | poll: poll}
|
||||
end
|
||||
|
||||
defp timeout(time, %{queue: queue, timeout: timeout} = state) do
|
||||
case :queue.out(queue) do
|
||||
{{:value, {sent, from}}, queue} when sent + timeout < time ->
|
||||
drop(time - sent, from)
|
||||
timeout(time, %{state | queue: queue})
|
||||
|
||||
{_, _} ->
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp drop(delay, from) do
|
||||
message =
|
||||
"connection not available and request was dropped from queue after #{delay}ms. " <>
|
||||
"You can configure how long requests wait in the queue using :queue_target and " <>
|
||||
":queue_interval. See DBConnection.start_link/2 for more information"
|
||||
|
||||
err = DBConnection.ConnectionError.exception(message, :queue_timeout)
|
||||
Holder.reply_error(from, err)
|
||||
end
|
||||
|
||||
@prune_modules [:gen, GenServer, DBConnection, DBConnection.Holder, DBConnection.Ownership]
|
||||
|
||||
defp pruned_stacktrace(pid) do
|
||||
case Process.info(pid, :current_stacktrace) do
|
||||
{:current_stacktrace, stacktrace} ->
|
||||
Enum.drop_while(stacktrace, &match?({mod, _, _, _} when mod in @prune_modules, &1))
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
19
phoenix/deps/db_connection/lib/db_connection/pool.ex
Normal file
19
phoenix/deps/db_connection/lib/db_connection/pool.ex
Normal file
@@ -0,0 +1,19 @@
|
||||
defmodule DBConnection.Pool do
|
||||
@moduledoc false
|
||||
|
||||
@type pool :: GenServer.server()
|
||||
@type connection_metrics :: %{
|
||||
source: {:pool | :proxy, pid()},
|
||||
ready_conn_count: non_neg_integer(),
|
||||
checkout_queue_length: non_neg_integer()
|
||||
}
|
||||
|
||||
@callback disconnect_all(pool, interval :: term, options :: keyword) :: :ok
|
||||
|
||||
@callback checkout(pool, callers :: [pid], options :: keyword) ::
|
||||
{:ok, pool_ref :: term, module, checkin_time :: non_neg_integer() | nil,
|
||||
state :: term}
|
||||
| {:error, Exception.t()}
|
||||
|
||||
@callback get_connection_metrics(pool :: pool()) :: [connection_metrics()]
|
||||
end
|
||||
57
phoenix/deps/db_connection/lib/db_connection/query.ex
Normal file
57
phoenix/deps/db_connection/lib/db_connection/query.ex
Normal file
@@ -0,0 +1,57 @@
|
||||
defprotocol DBConnection.Query do
|
||||
@moduledoc """
|
||||
The `DBConnection.Query` protocol is responsible for preparing and
|
||||
encoding queries.
|
||||
|
||||
All `DBConnection.Query` functions are executed in the caller process which
|
||||
means it's safe to, for example, raise exceptions or do blocking calls as
|
||||
they won't affect the connection process.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Parse a query.
|
||||
|
||||
This function is called to parse a query term before it is prepared using a
|
||||
connection callback module.
|
||||
|
||||
See `DBConnection.prepare/3`.
|
||||
"""
|
||||
@spec parse(any, Keyword.t()) :: any
|
||||
def parse(query, opts)
|
||||
|
||||
@doc """
|
||||
Describe a query.
|
||||
|
||||
This function is called to describe a query after it is prepared using a
|
||||
connection callback module.
|
||||
|
||||
See `DBConnection.prepare/3`.
|
||||
"""
|
||||
@spec describe(any, Keyword.t()) :: any
|
||||
def describe(query, opts)
|
||||
|
||||
@doc """
|
||||
Encode parameters using a query.
|
||||
|
||||
This function is called to encode a query before it is executed using a
|
||||
connection callback module.
|
||||
|
||||
If this function raises `DBConnection.EncodeError`, then the query is
|
||||
prepared once again.
|
||||
|
||||
See `DBConnection.execute/3`.
|
||||
"""
|
||||
@spec encode(any, any, Keyword.t()) :: any
|
||||
def encode(query, params, opts)
|
||||
|
||||
@doc """
|
||||
Decode a result using a query.
|
||||
|
||||
This function is called to decode a result after it is returned by a
|
||||
connection callback module.
|
||||
|
||||
See `DBConnection.execute/3`.
|
||||
"""
|
||||
@spec decode(any, any, Keyword.t()) :: any
|
||||
def decode(query, result, opts)
|
||||
end
|
||||
47
phoenix/deps/db_connection/lib/db_connection/task.ex
Normal file
47
phoenix/deps/db_connection/lib/db_connection/task.ex
Normal file
@@ -0,0 +1,47 @@
|
||||
defmodule DBConnection.Task do
|
||||
@moduledoc false
|
||||
@name __MODULE__
|
||||
|
||||
require DBConnection.Holder
|
||||
|
||||
def run_child(mod, state, fun, opts) do
|
||||
arg = [fun, self(), opts]
|
||||
{:ok, pid} = Task.Supervisor.start_child(@name, __MODULE__, :init, arg)
|
||||
ref = Process.monitor(pid)
|
||||
_ = DBConnection.Holder.update(pid, ref, mod, state)
|
||||
{pid, ref}
|
||||
end
|
||||
|
||||
def init(fun, parent, opts) do
|
||||
try do
|
||||
Process.link(parent)
|
||||
catch
|
||||
:error, :noproc ->
|
||||
exit({:shutdown, :noproc})
|
||||
end
|
||||
|
||||
receive do
|
||||
{:"ETS-TRANSFER", holder, ^parent, {:checkin, ref, _extra}} ->
|
||||
Process.unlink(parent)
|
||||
pool_ref = DBConnection.Holder.pool_ref(pool: parent, reference: ref, holder: holder)
|
||||
checkout = {:via, __MODULE__, pool_ref}
|
||||
_ = DBConnection.run(checkout, make_fun(fun), [pool: __MODULE__] ++ opts)
|
||||
exit(:normal)
|
||||
end
|
||||
end
|
||||
|
||||
def checkout({:via, __MODULE__, pool_ref}, _callers, _opts) do
|
||||
{:ok, pool_ref, _mod = :unused, _idle_time = nil, _state = :unused}
|
||||
end
|
||||
|
||||
defp make_fun(fun) when is_function(fun, 1) do
|
||||
fun
|
||||
end
|
||||
|
||||
defp make_fun(mfargs) do
|
||||
fn conn ->
|
||||
{mod, fun, args} = mfargs
|
||||
apply(mod, fun, [conn | args])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,116 @@
|
||||
defmodule DBConnection.TelemetryListener do
|
||||
@moduledoc """
|
||||
A connection listener that emits telemetry events for connection and disconnection
|
||||
|
||||
It monitors connection processes and ensures that disconnection events are
|
||||
always emitted.
|
||||
|
||||
## Usage
|
||||
|
||||
Start the listener, and pass it under the `:connection_listeners` option when
|
||||
starting DBConnection:
|
||||
|
||||
{:ok, pid} = DBConnection.TelemetryListener.start_link()
|
||||
{:ok, _conn} = DBConnection.start_link(SomeModule, connection_listeners: [pid])
|
||||
|
||||
# Using a tag, which will be sent in telemetry metadata
|
||||
{:ok, _conn} = DBConnection.start_link(SomeModule, connection_listeners: {[pid], :my_tag})
|
||||
|
||||
# Or, with a Supervisor:
|
||||
Supervisor.start_link([
|
||||
{DBConnection.TelemetryListener, name: MyListener},
|
||||
DBConnection.child_spec(SomeModule, connection_listeners: {[MyListener], :my_tag})
|
||||
])
|
||||
|
||||
When using with Ecto, you can pass the `connection_listeners` option to Ecto, and we
|
||||
recommend passing the repository as the tag. In your supervision tree:
|
||||
|
||||
Supervisor.start_link([
|
||||
{DBConnection.TelemetryListener, name: MyApp.DBListener},
|
||||
{MyApp.Repo, connection_listeners: {[MyApp.DBListener], MyApp.Repo})
|
||||
])
|
||||
|
||||
## Telemetry events
|
||||
|
||||
### Connected
|
||||
|
||||
`[:db_connection, :connected]` - Executed after a connection is established.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:count` - Always 1
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:pid` - The connection pid
|
||||
* `:tag` - The connection pool tag
|
||||
|
||||
### Disconnected
|
||||
|
||||
`[:db_connection, :disconnected]` - Executed after a disconnect.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:count` - Always 1
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:pid` - The connection pid
|
||||
* `:tag` - The connection pool tag
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
@doc "Starts a telemetry listener"
|
||||
@spec start_link(GenServer.options()) :: {:ok, pid()}
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, nil, opts)
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def init(nil) do
|
||||
{:ok, %{monitoring: %{}}}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info({:connected, pid, tag}, state) do
|
||||
handle_connected(pid, tag, state)
|
||||
end
|
||||
|
||||
def handle_info({:connected, pid}, state) do
|
||||
handle_connected(pid, nil, state)
|
||||
end
|
||||
|
||||
def handle_info({:disconnected, pid, _}, state) do
|
||||
handle_disconnected(pid, state)
|
||||
end
|
||||
|
||||
def handle_info({:disconnected, pid}, state) do
|
||||
handle_disconnected(pid, state)
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
|
||||
handle_disconnected(pid, state)
|
||||
end
|
||||
|
||||
defp handle_connected(pid, tag, state) do
|
||||
:telemetry.execute([:db_connection, :connected], %{count: 1}, %{tag: tag, pid: pid})
|
||||
ref = Process.monitor(pid)
|
||||
|
||||
{:noreply, put_in(state.monitoring[pid], {ref, tag})}
|
||||
end
|
||||
|
||||
defp handle_disconnected(pid, state) do
|
||||
case state.monitoring[pid] do
|
||||
# Already handled. We may receive two messages: one from monitor and one
|
||||
# from listener. For this reason, we need to handle both.
|
||||
nil ->
|
||||
{:noreply, state}
|
||||
|
||||
{ref, tag} ->
|
||||
Process.demonitor(ref, [:flush])
|
||||
:telemetry.execute([:db_connection, :disconnected], %{count: 1}, %{tag: tag, pid: pid})
|
||||
{:noreply, %{state | monitoring: Map.delete(state.monitoring, pid)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
62
phoenix/deps/db_connection/lib/db_connection/util.ex
Normal file
62
phoenix/deps/db_connection/lib/db_connection/util.ex
Normal file
@@ -0,0 +1,62 @@
|
||||
defmodule DBConnection.Util do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Inspect a pid, including the process label if possible.
|
||||
"""
|
||||
def inspect_pid(pid) when is_pid(pid) do
|
||||
with :undefined <- get_label(pid),
|
||||
:undefined <- get_name(pid),
|
||||
:undefined <- get_initial_call(pid) do
|
||||
inspect(pid)
|
||||
else
|
||||
label_or_name_or_call -> "#{inspect(pid)} (#{inspect(label_or_name_or_call)})"
|
||||
end
|
||||
end
|
||||
|
||||
def inspect_pid(other), do: inspect(other)
|
||||
|
||||
defp get_name(pid) do
|
||||
try do
|
||||
Process.info(pid, :registered_name)
|
||||
rescue
|
||||
_ -> :undefined
|
||||
else
|
||||
{:registered_name, name} when is_atom(name) -> name
|
||||
_ -> :undefined
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set a process label if `Process.set_label/1` is available.
|
||||
"""
|
||||
def set_label(label) do
|
||||
if function_exported?(Process, :set_label, 1) do
|
||||
Process.set_label(label)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Get a process label if `:proc_lib.get_label/1` is available.
|
||||
defp get_label(pid) do
|
||||
if function_exported?(:proc_lib, :get_label, 1) do
|
||||
# Avoid a compiler warning if the function isn't
|
||||
# defined in your version of Erlang/OTP
|
||||
apply(:proc_lib, :get_label, [pid])
|
||||
else
|
||||
# mimic return value of
|
||||
# `:proc_lib.get_label/1` when none is set.
|
||||
# Don't resort to using `Process.info(pid, :dictionary)`,
|
||||
# as this is not efficient.
|
||||
:undefined
|
||||
end
|
||||
end
|
||||
|
||||
defp get_initial_call(pid) do
|
||||
case Process.info(pid, :initial_call) do
|
||||
{:initial_call, {mod, _, _}} -> mod
|
||||
_ -> :undefined
|
||||
end
|
||||
end
|
||||
end
|
||||
65
phoenix/deps/db_connection/lib/db_connection/watcher.ex
Normal file
65
phoenix/deps/db_connection/lib/db_connection/watcher.ex
Normal file
@@ -0,0 +1,65 @@
|
||||
defmodule DBConnection.Watcher do
|
||||
@moduledoc false
|
||||
@name __MODULE__
|
||||
|
||||
use GenServer
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, :ok, name: @name)
|
||||
end
|
||||
|
||||
def watch(supervisor, args) do
|
||||
GenServer.call(@name, {:watch, supervisor, args}, :infinity)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(:ok) do
|
||||
Process.flag(:trap_exit, true)
|
||||
{:ok, {%{}, %{}}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:watch, supervisor, args}, {caller_pid, _ref}, {caller_refs, started_refs}) do
|
||||
case DynamicSupervisor.start_child(supervisor, args) do
|
||||
{:ok, started_pid} ->
|
||||
Process.link(caller_pid)
|
||||
caller_ref = Process.monitor(caller_pid)
|
||||
started_ref = Process.monitor(started_pid)
|
||||
caller_refs = Map.put(caller_refs, caller_ref, {supervisor, started_pid, started_ref})
|
||||
started_refs = Map.put(started_refs, started_ref, {caller_pid, caller_ref})
|
||||
{:reply, {:ok, started_pid}, {caller_refs, started_refs}}
|
||||
|
||||
other ->
|
||||
{:reply, other, {caller_refs, started_refs}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:DOWN, ref, _, _, _}, {caller_refs, started_refs}) do
|
||||
case caller_refs do
|
||||
%{^ref => {supervisor, started_pid, started_ref}} ->
|
||||
Process.demonitor(started_ref, [:flush])
|
||||
DynamicSupervisor.terminate_child(supervisor, started_pid)
|
||||
{:noreply, {Map.delete(caller_refs, ref), Map.delete(started_refs, started_ref)}}
|
||||
|
||||
%{} ->
|
||||
%{^ref => {caller_pid, caller_ref}} = started_refs
|
||||
Process.demonitor(caller_ref, [:flush])
|
||||
Process.exit(caller_pid, :kill)
|
||||
{:noreply, {Map.delete(caller_refs, caller_ref), Map.delete(started_refs, ref)}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:EXIT, _, _}, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate(_, {_, started_refs}) do
|
||||
for {_, {caller_pid, _}} <- started_refs do
|
||||
Process.exit(caller_pid, :kill)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
84
phoenix/deps/db_connection/mix.exs
Normal file
84
phoenix/deps/db_connection/mix.exs
Normal file
@@ -0,0 +1,84 @@
|
||||
defmodule DBConnection.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
@source_url "https://github.com/elixir-ecto/db_connection"
|
||||
@pools [:connection_pool, :ownership]
|
||||
@version "2.9.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :db_connection,
|
||||
version: @version,
|
||||
elixir: "~> 1.11",
|
||||
deps: deps(),
|
||||
docs: docs(),
|
||||
description: description(),
|
||||
package: package(),
|
||||
build_per_environment: false,
|
||||
consolidate_protocols: false,
|
||||
test_paths: test_paths(Mix.env()),
|
||||
aliases: ["test.all": ["test", "test.pools"], "test.pools": &test_pools/1],
|
||||
preferred_cli_env: ["test.all": :test]
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger],
|
||||
mod: {DBConnection.App, []}
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
|
||||
{:telemetry, "~> 0.4 or ~> 1.0"}
|
||||
]
|
||||
end
|
||||
|
||||
defp docs do
|
||||
[
|
||||
source_url: @source_url,
|
||||
source_ref: "v#{@version}",
|
||||
main: DBConnection,
|
||||
extras: ["CHANGELOG.md"]
|
||||
]
|
||||
end
|
||||
|
||||
defp description do
|
||||
"""
|
||||
Database connection behaviour for database transactions and connection pooling
|
||||
"""
|
||||
end
|
||||
|
||||
defp package do
|
||||
%{
|
||||
licenses: ["Apache-2.0"],
|
||||
maintainers: ["James Fish", "José Valim"],
|
||||
links: %{"GitHub" => @source_url}
|
||||
}
|
||||
end
|
||||
|
||||
defp test_paths(pool) when pool in @pools, do: ["integration_test/#{pool}"]
|
||||
defp test_paths(_), do: ["test"]
|
||||
|
||||
defp test_pools(args) do
|
||||
for env <- @pools, do: env_run(env, args)
|
||||
end
|
||||
|
||||
defp env_run(env, args) do
|
||||
args = if IO.ANSI.enabled?(), do: ["--color" | args], else: ["--no-color" | args]
|
||||
|
||||
IO.puts("==> Running tests for MIX_ENV=#{env} mix test")
|
||||
|
||||
{_, res} =
|
||||
System.cmd("mix", ["test" | args],
|
||||
into: IO.binstream(:stdio, :line),
|
||||
env: [{"MIX_ENV", to_string(env)}]
|
||||
)
|
||||
|
||||
if res > 0 do
|
||||
System.at_exit(fn _ -> exit({:shutdown, 1}) end)
|
||||
end
|
||||
end
|
||||
end
|
||||
3
phoenix/deps/decimal/.formatter.exs
Normal file
3
phoenix/deps/decimal/.formatter.exs
Normal file
@@ -0,0 +1,3 @@
|
||||
[
|
||||
inputs: ["{bench,mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
BIN
phoenix/deps/decimal/.hex
Normal file
BIN
phoenix/deps/decimal/.hex
Normal file
Binary file not shown.
150
phoenix/deps/decimal/CHANGELOG.md
Normal file
150
phoenix/deps/decimal/CHANGELOG.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v2.3.0 (2024-12-13)
|
||||
|
||||
* Implement the upcoming [`JSON.Encoder`](https://hexdocs.pm/elixir/main/JSON.Encoder.html)
|
||||
protocol
|
||||
|
||||
## v2.2.0 (2024-11-13)
|
||||
|
||||
* Add `Decimal.gte?/2` and `Decimal.lte?/2`
|
||||
* Add `Decimal.compare/3` and `Decimal.eq?/3` with threshold as parameter
|
||||
|
||||
## v2.1.1 (2023-04-26)
|
||||
|
||||
Decimal v2.1 requires Elixir v1.8+.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix `Decimal.compare/2` when comparing against `0`
|
||||
|
||||
## v2.1.0 (2023-04-26)
|
||||
|
||||
Decimal v2.1 requires Elixir v1.8+.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Improve error message from `Decimal.to_integer/1` during precision loss
|
||||
* `Inspect` protocol implementation returns strings in the `Decimal.new(...)` format
|
||||
* Add `Decimal.scale/1`
|
||||
* Optimize `Decimal.compare/2` for numbers with large exponents
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix `Decimal.integer?/1` spec
|
||||
* Fix `Decimal.integer?/1` check on 0 with >1 significant digits
|
||||
|
||||
## v2.0.0 (2020-09-08)
|
||||
|
||||
Decimal v2.0 requires Elixir v1.2+.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `Decimal.integer?/1`
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Change `Decimal.compare/2` to return `:lt | :eq | :gt`
|
||||
* Change `Decimal.cast/1` to return `{:ok, t} | :error`
|
||||
* Change `Decimal.parse/1` to return `{t, binary} | :error`
|
||||
* Remove `:message` and `:result` fields from `Decimal.Error`
|
||||
* Remove sNaN
|
||||
* Rename qNaN to NaN
|
||||
* Remove deprecated support for floats in `Decimal.new/1`
|
||||
* Remove deprecated `Decimal.minus/1`
|
||||
* Remove deprecated `Decimal.plus/1`
|
||||
* Remove deprecated `Decimal.reduce/1`
|
||||
* Remove deprecated `Decimal.with_context/2`, `Decimal.get_context/1`, `Decimal.set_context/1`,
|
||||
and `Decimal.update_context/1`
|
||||
* Remove deprecated `Decimal.decimal?/1`
|
||||
|
||||
### Deprecations
|
||||
|
||||
* Deprecate `Decimal.cmp/2`
|
||||
|
||||
## v1.9.0 (2020-09-08)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `Decimal.negate/1`
|
||||
* Add `Decimal.apply_context/1`
|
||||
* Add `Decimal.normalize/1`
|
||||
* Add `Decimal.Context.with/2`, `Decimal.Context.get/1`, `Decimal.Context.set/2`,
|
||||
and `Decimal.Context.update/1`
|
||||
* Add `Decimal.is_decimal/1`
|
||||
|
||||
### Deprecations
|
||||
|
||||
* Deprecate `Decimal.minus/1` in favour of the new `Decimal.negate/1`
|
||||
* Deprecate `Decimal.plus/1` in favour of the new `Decimal.apply_context/1`
|
||||
* Deprecate `Decimal.reduce/1` in favour of the new `Decimal.normalize/1`
|
||||
* Deprecate `Decimal.with_context/2`, `Decimal.get_context/1`, `Decimal.set_context/2`,
|
||||
and `Decimal.update_context/1` in favour of new functions on the `Decimal.Context` module
|
||||
* Deprecate `Decimal.decimal?/1` in favour of the new `Decimal.is_decimal/1`
|
||||
|
||||
## v1.8.1 (2019-12-20)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix Decimal.compare/2 with string arguments
|
||||
* Set :signal on error
|
||||
|
||||
## v1.8.0 (2019-06-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `Decimal.cast/1`
|
||||
* Add `Decimal.eq?/2`, `Decimal.gt?/2`, and `Decimal.lt?/2`
|
||||
* Add guards to `Decimal.new/3` to prevent invalid Decimal numbers
|
||||
|
||||
## v1.7.0 (2019-02-16)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `Decimal.sqrt/1`
|
||||
|
||||
## v1.6.0 (2018-11-22)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Support for canonical XSD representation on `Decimal.to_string/2`
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix exponent off-by-one when converting from decimal to float
|
||||
* Fix negative?/1 and positive?/1 specs
|
||||
|
||||
### Deprecations
|
||||
|
||||
* Deprecate passing float to `Decimal.new/1` in favor of `Decimal.from_float/1`
|
||||
|
||||
## v1.5.0 (2018-03-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `Decimal.positive?/1` and `Decimal.negative?/1`
|
||||
* Accept integers and strings in arithmetic functions, e.g.: `Decimal.add(1, "2.0")`
|
||||
* Add `Decimal.from_float/1`
|
||||
|
||||
### Soft deprecations (no warnings emitted)
|
||||
|
||||
* Soft deprecate passing float to `new/1` in favor of `from_float/1`
|
||||
|
||||
## v1.4.1 (2017-10-12)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Include the given value as part of the error reason
|
||||
* Fix `:half_even` `:lists.last` bug (empty signif)
|
||||
* Fix error message for round
|
||||
* Fix `:half_down` rounding error when remainder is greater than 5
|
||||
* Fix `Decimal.new/1` float conversion with bigger precision than 4
|
||||
* Fix precision default value
|
||||
|
||||
## v1.4.0 (2017-06-25)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `Decimal.to_integer/1` for large coefficients
|
||||
* Fix rounding of ~0 values
|
||||
* Fix errors when comparing and adding two infinities
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user