update
This commit is contained in:
30
phoenix/deps/plug/.formatter.exs
Normal file
30
phoenix/deps/plug/.formatter.exs
Normal file
@@ -0,0 +1,30 @@
|
||||
# Used by "mix format" and to export configuration.
|
||||
export_locals_without_parens = [
|
||||
plug: 1,
|
||||
plug: 2,
|
||||
forward: 2,
|
||||
forward: 3,
|
||||
forward: 4,
|
||||
match: 2,
|
||||
match: 3,
|
||||
get: 2,
|
||||
get: 3,
|
||||
head: 2,
|
||||
head: 3,
|
||||
post: 2,
|
||||
post: 3,
|
||||
put: 2,
|
||||
put: 3,
|
||||
patch: 2,
|
||||
patch: 3,
|
||||
delete: 2,
|
||||
delete: 3,
|
||||
options: 2,
|
||||
options: 3
|
||||
]
|
||||
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
locals_without_parens: export_locals_without_parens,
|
||||
export: [locals_without_parens: export_locals_without_parens]
|
||||
]
|
||||
BIN
phoenix/deps/plug/.hex
Normal file
BIN
phoenix/deps/plug/.hex
Normal file
Binary file not shown.
405
phoenix/deps/plug/CHANGELOG.md
Normal file
405
phoenix/deps/plug/CHANGELOG.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# Changelog
|
||||
|
||||
## v1.19.1 (2025-12-09)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.SSL] Fix `cypher_suite: :strong` compatibility
|
||||
|
||||
## v1.19.0 (2025-12-08)
|
||||
|
||||
This release requires Elixir v1.14+ and it bumps the recommended :strong and :compatible SSL/TLS ciphers suite to align with modern security standards, prioritizing TLS 1.3 and 1.2. Support for the insecure TLS 1.0 and 1.1 protocols are removed in accordance with RFC 8996.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.Router] Allow colon for named segments to be escaped
|
||||
* [Plug.SSL] Prioritize TLS 1.3 and 1.2 ciphers
|
||||
* [Plug.SSL] Allow excluding redirects based on hosts, paths, or the connection
|
||||
* [Plug.Static] Add `:raise_on_missing_only`
|
||||
* [Plug.Upload] Partition the uploader to improve performance
|
||||
* [Plug.Upload] Add API for deleting files
|
||||
|
||||
### Deprecations
|
||||
|
||||
* [Plug.Conn.Adapter] Deprecate `:owner` field
|
||||
|
||||
## v1.18.1 (2025-07-01)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.Debugger] Do not include code snippets in rendered markdown
|
||||
* [Plug.RewriteOn] Add support to rewrite nonstandard headers
|
||||
|
||||
## v1.18.0 (2025-05-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.Conn] Define optional `get_sock_data/1` and `get_ssl_data/1` callbacks
|
||||
* [Plug.RequestID] Allow metadata key to be customizable
|
||||
* [Plug.Router] Allow match to dispatch to function plugs
|
||||
|
||||
## v1.17.0 (2025-03-14)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.Debugger] Add dark mode and other UI improvements
|
||||
* [Plug.Debugger] Link `Module.function/arity` to hexdocs in exception messages
|
||||
* [Plug.Debugger] Support `__RELATIVEFILE__` to `PLUG_EDITOR` replacements
|
||||
* [Plug.SSL] Add SSL validation support for `certs_keys`
|
||||
|
||||
### Deprecations
|
||||
|
||||
* [Plug.Conn.Adapter] Make `push` an optional callback as it is no longer supported by browsers
|
||||
* [Plug.Conn] Deprecate `req_cookies`, `cookies`, and `resp_cookies` fields in favor of functions
|
||||
* [Plug.Conn] Deprecate `owner` field. Tracking responses is now part of adapters
|
||||
* [Plug.Test] Deprecate `use Plug.Test` in favor of imports
|
||||
|
||||
## v1.16.2 (2025-03-14)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Avoid XSS injection in the debug error page
|
||||
|
||||
## v1.16.1 (2024-06-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Optimize cookie parsing by 10x (10x faster, 10x less memory) on Erlang/OTP 26+
|
||||
|
||||
## v1.16.0 (2024-05-18)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Support x-forwarded-for in Plug.RewriteOn
|
||||
* Support MFArgs in Plug.RewriteOn
|
||||
* Add immutable directive to versioned requests in `Plug.Static`
|
||||
* Support disabling MIME type handling in `Plug.Static`
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix bug with discarded connection state in `Plug.Debugger`
|
||||
* Parse media types with underscores in them
|
||||
* Do not crash on `max_age` set to nil (for consistency)
|
||||
|
||||
## v1.15.3 (2024-01-16)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Allow setting the port on the connection in tests
|
||||
* Allow returning `{:ok, payload}` on inform
|
||||
* Allow custom exceptions in `validate_utf8` option
|
||||
* Allow skipping sent body on chunked replies
|
||||
|
||||
## v1.15.2 (2023-11-14)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `:assign_as` option to `Plug.RequestId`
|
||||
* Improve performance of `Plug.RequestId`
|
||||
* Avoid clashes between Plug nodes
|
||||
* Add specs to `Plug.BasicAuth`
|
||||
* Fix a bug with non-string `_method` body parameters in `Plug.MethodOverride`
|
||||
|
||||
## v1.15.1 (2023-10-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Relax requirement on `plug_crypto`
|
||||
|
||||
## v1.15.0 (2023-10-01)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `Plug.Conn.get_session/3` for default value
|
||||
* Allow `Plug.SSL.configure/1` to accept all :ssl options
|
||||
* Optimize query decoding by 15% to 45% - this removes the previously deprecated `:limit` MFA and `:include_unnamed_parts_at` from MULTIPART. This may be backwards incompatible for applications that were relying on ambiguous arguments, such as `user[][key]=1&user[][key]=2`, which has unspecified parsing behaviour
|
||||
|
||||
## v1.14.2 (2023-03-23)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Properly deprecate `Plug.Adapters.Cowboy` before removal
|
||||
|
||||
## v1.14.1 (2023-03-17)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `nest_all_json` option to JSON parser
|
||||
* Make action on Plug.Debugger page look like a button
|
||||
* Better formatting of exceptions on the error page
|
||||
* Provide stronger response header validation
|
||||
|
||||
## v1.14.0 (2022-10-31)
|
||||
|
||||
Require Elixir v1.10+.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `Plug.Conn.prepend_req_headers/2` and `Plug.Conn.merge_req_headers/2`
|
||||
* Support adapter upgrades with `Plug.Conn.upgrade_adapter/3`
|
||||
* Add "Copy to Markdown" button in exception page
|
||||
* Support exclusive use of tlsv1.3
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Make sure last parameter works within maps
|
||||
|
||||
### Deprecations
|
||||
|
||||
* Deprecate server pushes as they are no longer supported by browsers
|
||||
|
||||
## v1.13.6 (2022-04-14)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix compile-time dependencies in Plug.Builder
|
||||
|
||||
## v1.13.5 (2022-04-11)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Support `:via` in `Plug.Router.forward/2`
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix compile-time deps in Plug.Builder
|
||||
* Do not require routes to be compile-time binaries in `Plug.Router.forward/2`
|
||||
|
||||
## v1.13.4 (2022-03-10)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Improve deprecation warnings
|
||||
|
||||
## v1.13.3 (2022-02-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.Builder] Introduce `:copy_opts_to_assign` instead of `builder_opts/0`
|
||||
* [Plug.Router] Do not introduce compile-time dependencies in `Plug.Router`
|
||||
|
||||
## v1.13.2 (2022-02-04)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Router] Properly fix regression on Plug.Router helper function accidentally renamed
|
||||
|
||||
## v1.13.1 (2022-02-03)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Router] Fix regression on Plug.Router helper function accidentally renamed
|
||||
|
||||
## v1.13.0 (2022-02-02)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.Builder] Do not add compile-time deps to literal options in function plugs
|
||||
* [Plug.Parsers.MULTIPART] Allow custom conversion of multipart to parameters
|
||||
* [Plug.Router] Allow suffix matches in the router (such as `/feeds/:name.atom`)
|
||||
* [Plug.Session] Allow a list of `:rotating_options` for rotating session cookies
|
||||
* [Plug.Static] Allow a list of `:encodings` to be given for handling static assets
|
||||
* [Plug.Test] Raise an error when providing path not starting with "/"
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Upload] Normalize paths coming from environment variables
|
||||
|
||||
### Deprecations
|
||||
|
||||
* [Plug.Router] Mixing prefix matches with globs is deprecated
|
||||
* [Plug.Parsers.MULTIPART] Deprecate `:include_unnamed_parts_at`
|
||||
|
||||
## v1.12.1 (2021-08-01)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug] Make sure module plugs are compile time dependencies if init mode is compile-time
|
||||
|
||||
## v1.12.0 (2021-07-22)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug] Accept mime v2.0
|
||||
* [Plug] Accept telemetry v1.0
|
||||
* [Plug.Conn] Improve performance of UTF-8 validation
|
||||
* [Plug.Conn.Adapter] Add API for creating a connection
|
||||
* [Plug.Static] Allow MFA in `:from`
|
||||
|
||||
## v1.11.1 (2021-03-08)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.Upload] Allow transfer of ownership in Plug.Upload
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Debugger] Drop CSP Header when showing error via Plug.Debugger
|
||||
* [Plug.Test] Populate `query_params` from `Plug.Test.conn/3`
|
||||
|
||||
## v1.11.0 (2020-10-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.RewriteOn] Add a new public to handle `x-forwarded` headers
|
||||
* [Plug.Router] Add macro for `head` requests
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.CSRFProtection] Do not crash if request body params are not available
|
||||
* [Plug.Conn.Query] Conform `www-url-encoded` parsing to whatwg spec
|
||||
|
||||
### Deprecations
|
||||
|
||||
* [Plug.Parsers.MULTIPART] Deprecate passing MFA to MULTIPART in favor of a more composable approach
|
||||
|
||||
## v1.10.4 (2020-08-07)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Conn] Automatically set secure when deleting cookies to fix compatibility with SameSite
|
||||
|
||||
## v1.10.3 (2020-06-10)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.SSL] Allow host exclusion to be checked dynamically
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Router] Fix router telemetry event to follow Telemetry specification. This corrects the telemetry event added on v1.10.1.
|
||||
|
||||
## v1.10.2 (2020-06-06)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug] Make `:telemetry` a required dependency
|
||||
* [Plug.Test] Populate `:query_string` when params are passed in
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug] Add `Plug.run/3` for running multiple Plugs at runtime
|
||||
* [Plug] Add `Plug.forward/4` for forwarding between Plugs
|
||||
|
||||
## v1.10.1 (2020-05-15)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.Conn] Add option to disable uft-8 validation on query strings
|
||||
* [Plug.Conn] Support `:same_site` option when writing cookies
|
||||
* [Plug.Router] Add router dispatch telemetry events
|
||||
* [Plug.SSL] Support `:x_forwarded_host` and `:x_forwarded_port` on `:rewrite_on`
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Test] Ensure parameters are converted to string keys
|
||||
|
||||
## v1.10.0 (2020-03-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.BasicAuth] Add `Plug.BasicAuth`
|
||||
* [Plug.Conn] Add built-in support for signed and encrypted cookies
|
||||
* [Plug.Exception] Allow to use atoms as statuses in the `plug_status` field for exceptions
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Router] Handle malformed URI as bad requests
|
||||
|
||||
## v1.9.0 (2020-02-07)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Conn.Cookies] Make `decode` split on `;` only, remove `$`-prefix condition
|
||||
* [Plug.CSRFProtection] Generate url safe CSRF masks
|
||||
* [Plug.Parsers] Treat invalid content-types as parsing errors unless `:pass` is given
|
||||
* [Plug.Parsers] Ensure parameters are merged when falling back to `:pass` clause
|
||||
* [Plug.Parsers] Use HTTP status code 414 when query string is too long
|
||||
* [Plug.SSL] Rewrite port when rewriting a request coming to a standard port
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug] Make Plug fully compatible with new Elixir child specs
|
||||
* [Plug.Exception] Add actions to exceptions that implement `Plug.Exception` and render actions in `Plug.Debugger` error page
|
||||
* [Plug.Parsers] Add option to skip utf8 validation
|
||||
* [Plug.Parsers] Make multipart support MFA for `:length` limit
|
||||
* [Plug.Static] Accept MFA for `:header` option
|
||||
|
||||
### Notes
|
||||
* When implementing the `Plug.Exception` protocol, if the new `actions` function is not implemented, a warning will printed during compilation.
|
||||
|
||||
## v1.8.3 (2019-07-28)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Builder] Ensure init_mode option is respected within the Plug.Builder DSL itself
|
||||
* [Plug.Session] Fix dropping session with custom max_age
|
||||
|
||||
## v1.8.2 (2019-06-01)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.CSRFProtection] Increase entropy and ensure forwards compatibility with future URL-safe CSRF tokens
|
||||
|
||||
## v1.8.1 (2019-06-01)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.CSRFProtection] Allow state to be dumped from the session and provide an API to validate both state and tokens
|
||||
* [Plug.Session.Store] Add `get/1` to retrieve the store from a module/atom
|
||||
* [Plug.Static] Support Nginx range requests
|
||||
* [Plug.Telemetry] Allow extra options in `Plug.Telemetry` metadata
|
||||
|
||||
## v1.8.0 (2019-03-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug.Conn] Add `get_session/1` for retrieving the whole session
|
||||
* [Plug.CSRFProtection] Add `Plug.CSRFProtection.load_state/2` and `Plug.CSRFProtection.dump_state/0` to allow tokens to be generated in other processes
|
||||
* [Plug.Parsers] Allow unnamed parts in multipart parser via `:include_unnamed_parts_at`
|
||||
* [Plug.Router] Wrap router dispatch in a connection checkpoint to avoid losing information attached to the connection in error cases
|
||||
* [Plug.Telemetry] Add `Plug.Telemetry` to facilitate with telemetry integration
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.Conn.Status] Use IANA registered status code for HTTP 425
|
||||
* [Plug.RequestID] Reduce RequestID size by relying on base64 encoding
|
||||
* [Plug.Static] Ensure etags are quoted correctly
|
||||
* [Plug.Static] Ensure vary header is set in 304 response
|
||||
* [Plug.Static] Omit content-encoding header in 304 responses
|
||||
|
||||
## v1.7.2 (2019-02-09)
|
||||
|
||||
* [Plug.Parser.MULTIPART] Support UTF-8 filename encoding in multipart parser
|
||||
* [Plug.Router] Add `builder_opts` support to `:dispatch` plug
|
||||
* [Plug.SSL] Do not disable client renegotiation
|
||||
* [Plug.Upload] Raise when we can't write to disk during upload
|
||||
|
||||
## v1.7.1 (2018-10-24)
|
||||
|
||||
* [Plug.Adapters.Cowboy] Less verbose output when plug_cowboy is missing
|
||||
* [Plug.Adapters.Cowboy2] Less verbose output when plug_cowboy is missing
|
||||
|
||||
## v1.7.0 (2018-10-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Plug] Require Elixir v1.4+
|
||||
* [Plug.Session] Support MFAs for cookie session secrets
|
||||
* [Plug.Test] Add `put_peer_data`
|
||||
* [Plug.Adapters.Cowboy] Extract into [plug_cowboy][plug_cowboy]
|
||||
* [Plug.Adapters.Cowboy2] Extract into [plug_cowboy][plug_cowboy]
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Plug.SSL] Don't redirect excluded hosts on Plug.SSL
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* [Plug] Applications may need to add `:plug_cowboy` to your deps to use this version
|
||||
|
||||
## v1.6
|
||||
|
||||
See [CHANGELOG in the v1.6 branch](https://github.com/elixir-plug/plug/blob/v1.6/CHANGELOG.md).
|
||||
|
||||
[plug_cowboy]: https://github.com/elixir-plug/plug_cowboy
|
||||
13
phoenix/deps/plug/LICENSE
Normal file
13
phoenix/deps/plug/LICENSE
Normal file
@@ -0,0 +1,13 @@
|
||||
Copyright (c) 2013 Plataformatec.
|
||||
|
||||
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.
|
||||
372
phoenix/deps/plug/README.md
Normal file
372
phoenix/deps/plug/README.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# Plug
|
||||
|
||||
[](https://github.com/elixir-plug/plug/actions?query=workflow%3A%22CI%22)
|
||||
[](https://hex.pm/packages/plug)
|
||||
[](https://hexdocs.pm/plug/)
|
||||
|
||||
Plug is:
|
||||
|
||||
1. A specification for composing web applications with functions
|
||||
2. Connection adapters for different web servers in the Erlang VM
|
||||
|
||||
In other words, Plug allows you to build web applications from small pieces and run them on different web servers. Plug is used by web frameworks such as [Phoenix](https://phoenixframework.org) to manage requests, responses, and websockets. This documentation will show some high-level examples and introduce the Plug's main building blocks.
|
||||
|
||||
## Installation
|
||||
|
||||
In order to use Plug, you need a webserver and its bindings for Plug.
|
||||
There are two options at the moment:
|
||||
|
||||
1. Use the Cowboy webserver (Erlang-based) by adding the `plug_cowboy` package to your `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:plug_cowboy, "~> 2.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
2. Use the Bandit webserver (Elixir-based) by adding the `bandit` package to your `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:bandit, "~> 1.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
## Hello world: request/response
|
||||
|
||||
This is a minimal hello world example, using the Cowboy webserver:
|
||||
|
||||
```elixir
|
||||
Mix.install([:plug, :plug_cowboy])
|
||||
|
||||
defmodule MyPlug do
|
||||
import Plug.Conn
|
||||
|
||||
def init(options) do
|
||||
# initialize options
|
||||
options
|
||||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
conn
|
||||
|> put_resp_content_type("text/plain")
|
||||
|> send_resp(200, "Hello world")
|
||||
end
|
||||
end
|
||||
|
||||
require Logger
|
||||
webserver = {Plug.Cowboy, plug: MyPlug, scheme: :http, options: [port: 4000]}
|
||||
{:ok, _} = Supervisor.start_link([webserver], strategy: :one_for_one)
|
||||
Logger.info("Plug now running on localhost:4000")
|
||||
Process.sleep(:infinity)
|
||||
```
|
||||
|
||||
Save that snippet to a file and execute it as `elixir hello_world.exs`.
|
||||
Access <http://localhost:4000/> and you should be greeted!
|
||||
|
||||
In the example above, we wrote our first **module plug**, called `MyPlug`.
|
||||
Module plugs must define the `init/1` function and the `call/2` function.
|
||||
`call/2` is invoked with the connection and the options returned by `init/1`.
|
||||
|
||||
## Hello world: websockets
|
||||
|
||||
Plug v1.14 includes a connection `upgrade` API, which means it provides WebSocket
|
||||
support out of the box. Let's see an example, this time using the Bandit webserver
|
||||
and the `websocket_adapter` project for the WebSocket bits. Since we need different
|
||||
routes, we will use the built-in `Plug.Router` for that:
|
||||
|
||||
```elixir
|
||||
Mix.install([:bandit, :websock_adapter])
|
||||
|
||||
defmodule EchoServer do
|
||||
def init(options) do
|
||||
{:ok, options}
|
||||
end
|
||||
|
||||
def handle_in({"ping", [opcode: :text]}, state) do
|
||||
{:reply, :ok, {:text, "pong"}, state}
|
||||
end
|
||||
|
||||
def terminate(:timeout, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Router do
|
||||
use Plug.Router
|
||||
|
||||
plug Plug.Logger
|
||||
plug :match
|
||||
plug :dispatch
|
||||
|
||||
get "/" do
|
||||
send_resp(conn, 200, """
|
||||
Use the JavaScript console to interact using websockets
|
||||
|
||||
sock = new WebSocket("ws://localhost:4000/websocket")
|
||||
sock.addEventListener("message", console.log)
|
||||
sock.addEventListener("open", () => sock.send("ping"))
|
||||
""")
|
||||
end
|
||||
|
||||
get "/websocket" do
|
||||
conn
|
||||
|> WebSockAdapter.upgrade(EchoServer, [], timeout: 60_000)
|
||||
|> halt()
|
||||
end
|
||||
|
||||
match _ do
|
||||
send_resp(conn, 404, "not found")
|
||||
end
|
||||
end
|
||||
|
||||
require Logger
|
||||
webserver = {Bandit, plug: Router, scheme: :http, port: 4000}
|
||||
{:ok, _} = Supervisor.start_link([webserver], strategy: :one_for_one)
|
||||
Logger.info("Plug now running on localhost:4000")
|
||||
Process.sleep(:infinity)
|
||||
```
|
||||
|
||||
Save that snippet to a file and execute it as `elixir websockets.exs`.
|
||||
Access <http://localhost:4000/> and you should see messages in your browser
|
||||
console.
|
||||
|
||||
This time, we used `Plug.Router`, which allows us to define the routes
|
||||
used by our web application and a series of steps/plugs, such as
|
||||
`plug Plug.Logger`, to be executed on every request.
|
||||
|
||||
Furthermore, as you can see, Plug abstracts the different webservers.
|
||||
When booting up your application, the difference is between choosing
|
||||
`Plug.Cowboy` or `Bandit`.
|
||||
|
||||
For now, we have directly started the server in a throw-away supervisor but,
|
||||
for production deployments, you want to start them in application
|
||||
supervision tree. See the [Supervised handlers](#supervised-handlers) section next.
|
||||
|
||||
## Supervised handlers
|
||||
|
||||
On a production system, you likely want to start your Plug pipeline under your application's supervision tree. Start a new Elixir project with the `--sup` flag:
|
||||
|
||||
```shell
|
||||
$ mix new my_app --sup
|
||||
```
|
||||
|
||||
Add `:plug_cowboy` (or `:bandit`) as a dependency to your `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:plug_cowboy, "~> 2.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Now update `lib/my_app/application.ex` as follows:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Application do
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@moduledoc false
|
||||
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
# List all child processes to be supervised
|
||||
children = [
|
||||
{Plug.Cowboy, scheme: :http, plug: MyPlug, options: [port: 4001]}
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Finally create `lib/my_app/my_plug.ex` with the `MyPlug` module.
|
||||
|
||||
Now run `mix run --no-halt` and it will start your application with a web server running at <http://localhost:4001>.
|
||||
|
||||
## Plugs and the `Plug.Conn` struct
|
||||
|
||||
In the hello world example, we defined our first plug called `MyPlug`. There are two types of plugs, module plugs and function plugs.
|
||||
|
||||
A module plug implements an `init/1` function to initialize the options and a `call/2` function which receives the connection and initialized options and returns the connection:
|
||||
|
||||
```elixir
|
||||
defmodule MyPlug do
|
||||
def init([]), do: false
|
||||
def call(conn, _opts), do: conn
|
||||
end
|
||||
```
|
||||
|
||||
A function plug takes the connection, a set of options as arguments, and returns the connection:
|
||||
|
||||
```elixir
|
||||
def hello_world_plug(conn, _opts) do
|
||||
conn
|
||||
|> put_resp_content_type("text/plain")
|
||||
|> send_resp(200, "Hello world")
|
||||
end
|
||||
```
|
||||
|
||||
A connection is represented by the `%Plug.Conn{}` struct:
|
||||
|
||||
```elixir
|
||||
%Plug.Conn{
|
||||
host: "www.example.com",
|
||||
path_info: ["bar", "baz"],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Data can be read directly from the connection and also pattern matched on. Manipulating the connection often happens with the use of the functions defined in the `Plug.Conn` module. In our example, both `put_resp_content_type/2` and `send_resp/3` are defined in `Plug.Conn`.
|
||||
|
||||
Remember that, as everything else in Elixir, **a connection is immutable**, so every manipulation returns a new copy of the connection:
|
||||
|
||||
```elixir
|
||||
conn = put_resp_content_type(conn, "text/plain")
|
||||
conn = send_resp(conn, 200, "ok")
|
||||
conn
|
||||
```
|
||||
|
||||
Finally, keep in mind that a connection is a **direct interface to the underlying web server**. When you call `send_resp/3` above, it will immediately send the given status and body back to the client. This makes features like streaming a breeze to work with.
|
||||
|
||||
## `Plug.Router`
|
||||
|
||||
To write a "router" plug that dispatches based on the path and method of incoming requests, Plug provides `Plug.Router`:
|
||||
|
||||
```elixir
|
||||
defmodule MyRouter do
|
||||
use Plug.Router
|
||||
|
||||
plug :match
|
||||
plug :dispatch
|
||||
|
||||
get "/hello" do
|
||||
send_resp(conn, 200, "world")
|
||||
end
|
||||
|
||||
forward "/users", to: UsersRouter
|
||||
|
||||
match _ do
|
||||
send_resp(conn, 404, "oops")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The router is a plug. Not only that: it contains its own plug pipeline too. The example above says that when the router is invoked, it will invoke the `:match` plug, represented by a local (imported) `match/2` function, and then call the `:dispatch` plug which will execute the matched code.
|
||||
|
||||
Plug ships with many plugs that you can add to the router plug pipeline, allowing you to plug something before a route matches or before a route is dispatched to. For example, if you want to add logging to the router, just do:
|
||||
|
||||
```elixir
|
||||
plug Plug.Logger
|
||||
plug :match
|
||||
plug :dispatch
|
||||
```
|
||||
|
||||
Note `Plug.Router` compiles all of your routes into a single function and relies on the Erlang VM to optimize the underlying routes into a tree lookup, instead of a linear lookup that would instead match route-per-route. This means route lookups are extremely fast in Plug!
|
||||
|
||||
This also means that a catch all `match` block is recommended to be defined as in the example above, otherwise routing fails with a function clause error (as it would in any regular Elixir function).
|
||||
|
||||
Each route needs to return the connection as per the Plug specification. See the `Plug.Router` docs for more information.
|
||||
|
||||
## Testing plugs
|
||||
|
||||
Plug ships with a `Plug.Test` module that makes testing your plugs easy. Here is how we can test the router from above (or any other plug):
|
||||
|
||||
```elixir
|
||||
defmodule MyPlugTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Plug.Test
|
||||
import Plug.Conn
|
||||
|
||||
@opts MyRouter.init([])
|
||||
|
||||
test "returns hello world" do
|
||||
# Create a test connection
|
||||
conn = conn(:get, "/hello")
|
||||
|
||||
# Invoke the plug
|
||||
conn = MyRouter.call(conn, @opts)
|
||||
|
||||
# Assert the response and status
|
||||
assert conn.state == :sent
|
||||
assert conn.status == 200
|
||||
assert conn.resp_body == "world"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Available plugs
|
||||
|
||||
This project aims to ship with different plugs that can be re-used across applications:
|
||||
|
||||
* `Plug.BasicAuth` - provides Basic HTTP authentication;
|
||||
* `Plug.CSRFProtection` - adds Cross-Site Request Forgery protection to your application. Typically required if you are using `Plug.Session`;
|
||||
* `Plug.Head` - converts HEAD requests to GET requests;
|
||||
* `Plug.Logger` - logs requests;
|
||||
* `Plug.MethodOverride` - overrides a request method with one specified in the request parameters;
|
||||
* `Plug.Parsers` - responsible for parsing the request body given its content-type;
|
||||
* `Plug.RequestId` - sets up a request ID to be used in logs;
|
||||
* `Plug.RewriteOn` - rewrite the request's host/port/protocol from `x-forwarded-*` headers;
|
||||
* `Plug.Session` - handles session management and storage;
|
||||
* `Plug.SSL` - enforces requests through SSL;
|
||||
* `Plug.Static` - serves static files;
|
||||
* `Plug.Telemetry` - instruments the plug pipeline with `:telemetry` events;
|
||||
|
||||
You can go into more details about each of them [in our docs](http://hexdocs.pm/plug/).
|
||||
|
||||
## Helper modules
|
||||
|
||||
Modules that can be used after you use `Plug.Router` or `Plug.Builder` to help development:
|
||||
|
||||
* `Plug.Debugger` - shows a helpful debugging page every time there is a failure in a request;
|
||||
* `Plug.ErrorHandler` - allows developers to customize error pages in case of crashes instead of sending a blank one;
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome everyone to contribute to Plug and help us tackle existing issues!
|
||||
|
||||
Use the [issue tracker][issues] for bug reports or feature requests. Open a [pull request][pulls] when you are ready to contribute. When submitting a pull request you should not update the `CHANGELOG.md`.
|
||||
|
||||
If you are planning to contribute documentation, [please check our best practices for writing documentation][writing-docs].
|
||||
|
||||
Finally, remember all interactions in our official spaces follow our [Code of Conduct][code-of-conduct].
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Branch | Support |
|
||||
|--------|--------------------------|
|
||||
| v1.15 | Bug fixes |
|
||||
| v1.14 | Security patches only |
|
||||
| v1.13 | Security patches only |
|
||||
| v1.12 | Security patches only |
|
||||
| v1.11 | Security patches only |
|
||||
| v1.10 | Security patches only |
|
||||
| v1.9 | Unsupported from 10/2023 |
|
||||
| v1.8 | Unsupported from 01/2023 |
|
||||
| v1.7 | Unsupported from 01/2022 |
|
||||
| v1.6 | Unsupported from 01/2022 |
|
||||
| v1.5 | Unsupported from 03/2021 |
|
||||
| v1.4 | Unsupported from 12/2018 |
|
||||
| v1.3 | Unsupported from 12/2018 |
|
||||
| v1.2 | Unsupported from 06/2018 |
|
||||
| v1.1 | Unsupported from 01/2018 |
|
||||
| v1.0 | Unsupported from 05/2017 |
|
||||
|
||||
## License
|
||||
|
||||
Plug source code is released under Apache License 2.0.
|
||||
Check LICENSE file for more information.
|
||||
|
||||
[issues]: https://github.com/elixir-plug/plug/issues
|
||||
[pulls]: https://github.com/elixir-plug/plug/pulls
|
||||
[code-of-conduct]: https://github.com/elixir-lang/elixir/blob/master/CODE_OF_CONDUCT.md
|
||||
[writing-docs]: https://hexdocs.pm/elixir/writing-documentation.html
|
||||
55
phoenix/deps/plug/hex_metadata.config
Normal file
55
phoenix/deps/plug/hex_metadata.config
Normal file
@@ -0,0 +1,55 @@
|
||||
{<<"links">>,
|
||||
[{<<"Changelog">>,
|
||||
<<"https://github.com/elixir-plug/plug/blob/main/CHANGELOG.md">>},
|
||||
{<<"GitHub">>,<<"https://github.com/elixir-plug/plug">>}]}.
|
||||
{<<"name">>,<<"plug">>}.
|
||||
{<<"version">>,<<"1.19.1">>}.
|
||||
{<<"description">>,<<"Compose web applications with functions">>}.
|
||||
{<<"elixir">>,<<"~> 1.14">>}.
|
||||
{<<"files">>,
|
||||
[<<"lib">>,<<"lib/plug">>,<<"lib/plug/html.ex">>,
|
||||
<<"lib/plug/csrf_protection.ex">>,<<"lib/plug/rewrite_on.ex">>,
|
||||
<<"lib/plug/parsers">>,<<"lib/plug/parsers/multipart.ex">>,
|
||||
<<"lib/plug/parsers/urlencoded.ex">>,<<"lib/plug/parsers/json.ex">>,
|
||||
<<"lib/plug/telemetry.ex">>,<<"lib/plug/parsers.ex">>,
|
||||
<<"lib/plug/session.ex">>,<<"lib/plug/test.ex">>,<<"lib/plug/logger.ex">>,
|
||||
<<"lib/plug/mime.ex">>,<<"lib/plug/upload.ex">>,<<"lib/plug/router.ex">>,
|
||||
<<"lib/plug/error_handler.ex">>,<<"lib/plug/adapters">>,
|
||||
<<"lib/plug/adapters/cowboy.ex">>,<<"lib/plug/adapters/test">>,
|
||||
<<"lib/plug/adapters/test/conn.ex">>,<<"lib/plug/conn.ex">>,
|
||||
<<"lib/plug/ssl.ex">>,<<"lib/plug/templates">>,
|
||||
<<"lib/plug/templates/debugger.html.eex">>,
|
||||
<<"lib/plug/templates/debugger.md.eex">>,<<"lib/plug/basic_auth.ex">>,
|
||||
<<"lib/plug/debugger.ex">>,<<"lib/plug/builder.ex">>,
|
||||
<<"lib/plug/request_id.ex">>,<<"lib/plug/application.ex">>,
|
||||
<<"lib/plug/head.ex">>,<<"lib/plug/conn">>,<<"lib/plug/conn/adapter.ex">>,
|
||||
<<"lib/plug/conn/cookies.ex">>,<<"lib/plug/conn/query.ex">>,
|
||||
<<"lib/plug/conn/unfetched.ex">>,<<"lib/plug/conn/wrapper_error.ex">>,
|
||||
<<"lib/plug/conn/status.ex">>,<<"lib/plug/conn/utils.ex">>,
|
||||
<<"lib/plug/exceptions.ex">>,<<"lib/plug/upload">>,
|
||||
<<"lib/plug/upload/terminator.ex">>,<<"lib/plug/upload/supervisor.ex">>,
|
||||
<<"lib/plug/method_override.ex">>,<<"lib/plug/static.ex">>,
|
||||
<<"lib/plug/session">>,<<"lib/plug/session/store.ex">>,
|
||||
<<"lib/plug/session/ets.ex">>,<<"lib/plug/session/cookie.ex">>,
|
||||
<<"lib/plug/router">>,<<"lib/plug/router/utils.ex">>,<<"lib/plug.ex">>,
|
||||
<<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>,<<"LICENSE">>,<<"src">>,
|
||||
<<"src/plug_multipart.erl">>,<<".formatter.exs">>]}.
|
||||
{<<"app">>,<<"plug">>}.
|
||||
{<<"licenses">>,[<<"Apache-2.0">>]}.
|
||||
{<<"requirements">>,
|
||||
[[{<<"name">>,<<"mime">>},
|
||||
{<<"app">>,<<"mime">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 1.0 or ~> 2.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"plug_crypto">>},
|
||||
{<<"app">>,<<"plug_crypto">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 1.1.1 or ~> 1.2 or ~> 2.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"telemetry">>},
|
||||
{<<"app">>,<<"telemetry">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 0.4.3 or ~> 1.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}]]}.
|
||||
{<<"build_tools">>,[<<"mix">>]}.
|
||||
179
phoenix/deps/plug/lib/plug.ex
Normal file
179
phoenix/deps/plug/lib/plug.ex
Normal file
@@ -0,0 +1,179 @@
|
||||
defmodule Plug do
|
||||
@moduledoc """
|
||||
The plug specification.
|
||||
|
||||
## Types of plugs
|
||||
|
||||
There are two kind of plugs: function plugs and module plugs.
|
||||
|
||||
### Function plugs
|
||||
|
||||
A function plug is by definition any function that receives a connection
|
||||
and a set of options and returns a connection. Function plugs must have
|
||||
the following type signature:
|
||||
|
||||
(Plug.Conn.t, Plug.opts) :: Plug.Conn.t
|
||||
|
||||
### Module plugs
|
||||
|
||||
A module plug is an extension of the function plug. It is a module that must
|
||||
export:
|
||||
|
||||
* a `c:call/2` function with the signature defined above
|
||||
* an `c:init/1` function which takes a set of options and initializes it.
|
||||
|
||||
The result returned by `c:init/1` is passed as second argument to `c:call/2`. Note
|
||||
that `c:init/1` may be called during compilation and as such it must not return
|
||||
pids, ports or values that are specific to the runtime.
|
||||
|
||||
The API expected by a module plug is defined as a behaviour by the
|
||||
`Plug` module (this module).
|
||||
|
||||
## Examples
|
||||
|
||||
Here's an example of a function plug:
|
||||
|
||||
def json_header_plug(conn, _opts) do
|
||||
Plug.Conn.put_resp_content_type(conn, "application/json")
|
||||
end
|
||||
|
||||
Here's an example of a module plug:
|
||||
|
||||
defmodule JSONHeaderPlug do
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
def init(opts) do
|
||||
opts
|
||||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
put_resp_content_type(conn, "application/json")
|
||||
end
|
||||
end
|
||||
|
||||
## The Plug pipeline
|
||||
|
||||
The `Plug.Builder` module provides conveniences for building plug pipelines.
|
||||
"""
|
||||
|
||||
@type opts ::
|
||||
binary
|
||||
| tuple
|
||||
| atom
|
||||
| integer
|
||||
| float
|
||||
| [opts]
|
||||
| %{optional(opts) => opts}
|
||||
| MapSet.t()
|
||||
|
||||
@callback init(opts) :: opts
|
||||
@callback call(conn :: Plug.Conn.t(), opts) :: Plug.Conn.t()
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Run a series of plugs at runtime.
|
||||
|
||||
The plugs given here can be either a tuple, representing a module plug
|
||||
and their options, or a simple function that receives a connection and
|
||||
returns a connection.
|
||||
|
||||
If any plug halts, the connection won't invoke the remaining plugs. If the
|
||||
given connection was already halted, none of the plugs are invoked either.
|
||||
|
||||
While `Plug.Builder` is designed to operate at compile-time, the `run` function
|
||||
serves as a straightforward alternative for runtime executions.
|
||||
|
||||
## Examples
|
||||
|
||||
Plug.run(conn, [{Plug.Head, []}, &IO.inspect/1])
|
||||
|
||||
## Options
|
||||
|
||||
* `:log_on_halt` - a log level to be used if a plug halts
|
||||
|
||||
"""
|
||||
@spec run(Plug.Conn.t(), [{module, opts} | (Plug.Conn.t() -> Plug.Conn.t())], Keyword.t()) ::
|
||||
Plug.Conn.t()
|
||||
def run(conn, plugs, opts \\ [])
|
||||
|
||||
def run(%Plug.Conn{halted: true} = conn, _plugs, _opts),
|
||||
do: conn
|
||||
|
||||
def run(%Plug.Conn{} = conn, plugs, opts),
|
||||
do: do_run(conn, plugs, Keyword.get(opts, :log_on_halt))
|
||||
|
||||
defp do_run(conn, [{mod, opts} | plugs], level) when is_atom(mod) do
|
||||
case mod.call(conn, mod.init(opts)) do
|
||||
%Plug.Conn{halted: true} = conn ->
|
||||
level && Logger.log(level, "Plug halted in #{inspect(mod)}.call/2")
|
||||
conn
|
||||
|
||||
%Plug.Conn{} = conn ->
|
||||
do_run(conn, plugs, level)
|
||||
|
||||
other ->
|
||||
raise "expected #{inspect(mod)} to return Plug.Conn, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp do_run(conn, [fun | plugs], level) when is_function(fun, 1) do
|
||||
case fun.(conn) do
|
||||
%Plug.Conn{halted: true} = conn ->
|
||||
level && Logger.log(level, "Plug halted in #{inspect(fun)}")
|
||||
conn
|
||||
|
||||
%Plug.Conn{} = conn ->
|
||||
do_run(conn, plugs, level)
|
||||
|
||||
other ->
|
||||
raise "expected #{inspect(fun)} to return Plug.Conn, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp do_run(conn, [], _level), do: conn
|
||||
|
||||
@doc """
|
||||
Forwards requests to another plug while setting the connection to a trailing subpath of the request.
|
||||
|
||||
The `path_info` on the forwarded connection will only include the request path trailing segments
|
||||
supplied to the `forward` function. The `conn.script_name` attribute retains the correct base path,
|
||||
e.g., url generation.
|
||||
|
||||
## Example
|
||||
|
||||
defmodule Router do
|
||||
@behaviour Plug
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, opts) do
|
||||
case conn do
|
||||
# Match subdomain
|
||||
%{host: "admin." <> _} ->
|
||||
AdminRouter.call(conn, opts)
|
||||
|
||||
# Match path on localhost
|
||||
%{host: "localhost", path_info: ["admin" | rest]} ->
|
||||
Plug.forward(conn, rest, AdminRouter, opts)
|
||||
|
||||
_ ->
|
||||
MainRouter.call(conn, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
@spec forward(Plug.Conn.t(), [String.t()], atom, Plug.opts()) :: Plug.Conn.t()
|
||||
def forward(%Plug.Conn{path_info: path, script_name: script} = conn, new_path, target, opts) do
|
||||
{base, split_path} = Enum.split(path, length(path) - length(new_path))
|
||||
|
||||
conn = do_forward(target, %{conn | path_info: split_path, script_name: script ++ base}, opts)
|
||||
%{conn | path_info: path, script_name: script}
|
||||
end
|
||||
|
||||
defp do_forward({mod, fun}, conn, opts), do: apply(mod, fun, [conn, opts])
|
||||
defp do_forward(mod, conn, opts), do: mod.call(conn, opts)
|
||||
end
|
||||
55
phoenix/deps/plug/lib/plug/adapters/cowboy.ex
Normal file
55
phoenix/deps/plug/lib/plug/adapters/cowboy.ex
Normal file
@@ -0,0 +1,55 @@
|
||||
defmodule Plug.Adapters.Cowboy do
|
||||
@moduledoc false
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Plug.Cowboy.http/3 instead"
|
||||
def http(plug, opts, cowboy_options \\ []) do
|
||||
unless using_plug_cowboy?(), do: warn_and_raise()
|
||||
Plug.Cowboy.http(plug, opts, cowboy_options)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Plug.Cowboy.https/3 instead"
|
||||
def https(plug, opts, cowboy_options \\ []) do
|
||||
unless using_plug_cowboy?(), do: warn_and_raise()
|
||||
Plug.Cowboy.https(plug, opts, cowboy_options)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Plug.Cowboy.shutdown/1 instead"
|
||||
def shutdown(ref) do
|
||||
unless using_plug_cowboy?(), do: warn_and_raise()
|
||||
Plug.Cowboy.shutdown(ref)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Plug.Cowboy.child_spec/4 instead"
|
||||
def child_spec(scheme, plug, opts, cowboy_options \\ []) do
|
||||
unless using_plug_cowboy?(), do: warn_and_raise()
|
||||
Plug.Cowboy.child_spec(scheme, plug, opts, cowboy_options)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Plug.Cowboy.child_spec/1 instead"
|
||||
def child_spec(opts) do
|
||||
unless using_plug_cowboy?(), do: warn_and_raise()
|
||||
Plug.Cowboy.child_spec(opts)
|
||||
end
|
||||
|
||||
defp using_plug_cowboy?() do
|
||||
Code.ensure_loaded?(Plug.Cowboy)
|
||||
end
|
||||
|
||||
defp warn_and_raise() do
|
||||
error = """
|
||||
please add the following dependency to your mix.exs:
|
||||
{:plug_cowboy, "~> 1.0"}
|
||||
This dependency is required by Plug.Adapters.Cowboy
|
||||
which you may be using directly or indirectly.
|
||||
Note you no longer need to depend on :cowboy directly.
|
||||
"""
|
||||
|
||||
IO.warn(error, [])
|
||||
:erlang.raise(:exit, "plug_cowboy dependency missing", [])
|
||||
end
|
||||
end
|
||||
243
phoenix/deps/plug/lib/plug/adapters/test/conn.ex
Normal file
243
phoenix/deps/plug/lib/plug/adapters/test/conn.ex
Normal file
@@ -0,0 +1,243 @@
|
||||
defmodule Plug.Adapters.Test.Conn do
|
||||
@behaviour Plug.Conn.Adapter
|
||||
@already_sent Plug.Conn.Adapter.already_sent()
|
||||
@moduledoc false
|
||||
|
||||
## Test helpers
|
||||
|
||||
def conn(%Plug.Conn{} = conn, method, uri, body_or_params) do
|
||||
maybe_flush()
|
||||
uri = URI.parse(uri)
|
||||
|
||||
if is_binary(uri.path) and not String.starts_with?(uri.path, "/") do
|
||||
# TODO: Convert to an error
|
||||
IO.warn("the URI path used in plug tests must start with \"/\", got: #{inspect(uri.path)}")
|
||||
end
|
||||
|
||||
method = method |> to_string |> String.upcase()
|
||||
query = uri.query || ""
|
||||
owner = self()
|
||||
|
||||
{params, {body, body_params}, {query, query_params}, req_headers} =
|
||||
body_or_params(body_or_params, query, conn.req_headers, method)
|
||||
|
||||
state = %{
|
||||
method: method,
|
||||
params: params,
|
||||
req_body: body,
|
||||
chunks: nil,
|
||||
ref: make_ref(),
|
||||
owner: owner,
|
||||
http_protocol: get_from_adapter(conn, :get_http_protocol, :"HTTP/1.1"),
|
||||
peer_data:
|
||||
get_from_adapter(conn, :get_peer_data, %{
|
||||
address: {127, 0, 0, 1},
|
||||
port: 111_317,
|
||||
ssl_cert: nil
|
||||
}),
|
||||
sock_data:
|
||||
get_from_adapter(conn, :get_sock_data, %{
|
||||
address: {127, 0, 0, 1},
|
||||
port: 111_318
|
||||
}),
|
||||
ssl_data: get_from_adapter(conn, :get_ssl_data, nil)
|
||||
}
|
||||
|
||||
conn_port = if conn.port != 0, do: conn.port, else: 80
|
||||
|
||||
%{
|
||||
conn
|
||||
| adapter: {__MODULE__, state},
|
||||
host: uri.host || conn.host || "www.example.com",
|
||||
method: method,
|
||||
path_info: split_path(uri.path),
|
||||
port: uri.port || conn_port,
|
||||
remote_ip: conn.remote_ip || {127, 0, 0, 1},
|
||||
req_headers: req_headers,
|
||||
request_path: uri.path,
|
||||
query_string: query,
|
||||
query_params: query_params || %Plug.Conn.Unfetched{aspect: :query_params},
|
||||
body_params: body_params || %Plug.Conn.Unfetched{aspect: :body_params},
|
||||
params: params || %Plug.Conn.Unfetched{aspect: :params},
|
||||
scheme: (uri.scheme || "http") |> String.downcase() |> String.to_atom()
|
||||
}
|
||||
end
|
||||
|
||||
## Connection adapter
|
||||
|
||||
def send_resp(%{method: "HEAD"} = state, status, headers, _body) do
|
||||
do_send(state, status, headers, "")
|
||||
end
|
||||
|
||||
def send_resp(state, status, headers, body) do
|
||||
do_send(state, status, headers, IO.iodata_to_binary(body))
|
||||
end
|
||||
|
||||
def send_file(%{method: "HEAD"} = state, status, headers, _path, _offset, _length) do
|
||||
do_send(state, status, headers, "")
|
||||
end
|
||||
|
||||
def send_file(state, status, headers, path, offset, length) do
|
||||
%File.Stat{type: :regular, size: size} = File.stat!(path)
|
||||
|
||||
length =
|
||||
cond do
|
||||
length == :all -> size
|
||||
is_integer(length) -> length
|
||||
end
|
||||
|
||||
{:ok, data} =
|
||||
File.open!(path, [:read, :binary], fn device ->
|
||||
:file.pread(device, offset, length)
|
||||
end)
|
||||
|
||||
do_send(state, status, headers, data)
|
||||
end
|
||||
|
||||
def send_chunked(%{owner: owner} = state, _status, _headers) do
|
||||
send(owner, @already_sent)
|
||||
{:ok, "", %{state | chunks: ""}}
|
||||
end
|
||||
|
||||
def chunk(%{method: "HEAD"} = state, _body), do: {:ok, "", state}
|
||||
|
||||
def chunk(%{chunks: chunks} = state, body) do
|
||||
body = chunks <> IO.iodata_to_binary(body)
|
||||
{:ok, body, %{state | chunks: body}}
|
||||
end
|
||||
|
||||
defp do_send(%{owner: owner, ref: ref} = state, status, headers, body) do
|
||||
send(owner, @already_sent)
|
||||
send(owner, {ref, {status, headers, body}})
|
||||
{:ok, body, state}
|
||||
end
|
||||
|
||||
def read_req_body(%{req_body: body} = state, opts \\ []) do
|
||||
size = min(byte_size(body), Keyword.get(opts, :length, 8_000_000))
|
||||
data = :binary.part(body, 0, size)
|
||||
rest = :binary.part(body, size, byte_size(body) - size)
|
||||
|
||||
tag =
|
||||
case rest do
|
||||
"" -> :ok
|
||||
_ -> :more
|
||||
end
|
||||
|
||||
{tag, data, %{state | req_body: rest}}
|
||||
end
|
||||
|
||||
def inform(%{owner: owner, ref: ref}, status, headers) do
|
||||
send(owner, {ref, :inform, {status, headers}})
|
||||
:ok
|
||||
end
|
||||
|
||||
def upgrade(%{owner: owner, ref: ref}, :not_supported = protocol, opts) do
|
||||
send(owner, {ref, :upgrade, {protocol, opts}})
|
||||
{:error, :not_supported}
|
||||
end
|
||||
|
||||
def upgrade(%{owner: owner, ref: ref} = state, protocol, opts) do
|
||||
send(owner, {ref, :upgrade, {protocol, opts}})
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def push(%{owner: owner, ref: ref}, path, headers) do
|
||||
send(owner, {ref, :push, {path, headers}})
|
||||
:ok
|
||||
end
|
||||
|
||||
def get_peer_data(payload) do
|
||||
Map.fetch!(payload, :peer_data)
|
||||
end
|
||||
|
||||
def get_sock_data(payload) do
|
||||
Map.fetch!(payload, :sock_data)
|
||||
end
|
||||
|
||||
def get_ssl_data(payload) do
|
||||
Map.fetch!(payload, :ssl_data)
|
||||
end
|
||||
|
||||
def get_http_protocol(payload) do
|
||||
Map.fetch!(payload, :http_protocol)
|
||||
end
|
||||
|
||||
## Private helpers
|
||||
|
||||
defp get_from_adapter(conn, op, default) do
|
||||
case conn.adapter do
|
||||
{Plug.MissingAdapter, _} -> default
|
||||
{adapter, payload} -> apply(adapter, op, [payload])
|
||||
end
|
||||
end
|
||||
|
||||
defp body_or_params(nil, query, headers, _method), do: {nil, {"", nil}, {query, nil}, headers}
|
||||
|
||||
defp body_or_params(body, query, headers, _method) when is_binary(body) do
|
||||
{nil, {body, nil}, {query, nil}, headers}
|
||||
end
|
||||
|
||||
defp body_or_params(params, query, headers, method) when is_list(params) do
|
||||
body_or_params(Enum.into(params, %{}), query, headers, method)
|
||||
end
|
||||
|
||||
defp body_or_params(params, query, headers, method)
|
||||
when is_map(params) and method in ["GET", "HEAD"] do
|
||||
params = stringify_params(params, &to_string/1)
|
||||
|
||||
from_query = Plug.Conn.Query.decode(query)
|
||||
params = Map.merge(from_query, params)
|
||||
|
||||
query =
|
||||
params
|
||||
|> Map.merge(from_query)
|
||||
|> Plug.Conn.Query.encode()
|
||||
|
||||
{params, {"", nil}, {query, params}, headers}
|
||||
end
|
||||
|
||||
defp body_or_params(params, query, headers, _method) when is_map(params) do
|
||||
content_type_header = {"content-type", "multipart/mixed; boundary=plug_conn_test"}
|
||||
content_type = List.keyfind(headers, "content-type", 0, content_type_header)
|
||||
headers = List.keystore(headers, "content-type", 0, content_type)
|
||||
|
||||
body_params = stringify_params(params, & &1)
|
||||
query_params = Plug.Conn.Query.decode(query)
|
||||
params = Map.merge(query_params, body_params)
|
||||
|
||||
{params, {"--plug_conn_test--", body_params}, {query, query_params}, headers}
|
||||
end
|
||||
|
||||
defp stringify_params([{_, _} | _] = params, value_fun),
|
||||
do: Enum.into(params, %{}, &stringify_kv(&1, value_fun))
|
||||
|
||||
defp stringify_params([_ | _] = params, value_fun),
|
||||
do: Enum.map(params, &stringify_params(&1, value_fun))
|
||||
|
||||
defp stringify_params(%{__struct__: mod} = struct, _value_fun) when is_atom(mod), do: struct
|
||||
defp stringify_params(fun, _value_fun) when is_function(fun), do: fun
|
||||
|
||||
defp stringify_params(%{} = params, value_fun),
|
||||
do: Enum.into(params, %{}, &stringify_kv(&1, value_fun))
|
||||
|
||||
defp stringify_params(other, value_fun), do: value_fun.(other)
|
||||
|
||||
defp stringify_kv({k, v}, value_fun), do: {to_string(k), stringify_params(v, value_fun)}
|
||||
|
||||
defp split_path(nil), do: []
|
||||
|
||||
defp split_path(path) do
|
||||
segments = :binary.split(path, "/", [:global])
|
||||
for segment <- segments, segment != "", do: segment
|
||||
end
|
||||
|
||||
@already_sent {:plug_conn, :sent}
|
||||
|
||||
defp maybe_flush() do
|
||||
receive do
|
||||
@already_sent -> :ok
|
||||
after
|
||||
0 -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
16
phoenix/deps/plug/lib/plug/application.ex
Normal file
16
phoenix/deps/plug/lib/plug/application.ex
Normal file
@@ -0,0 +1,16 @@
|
||||
defmodule Plug.Application do
|
||||
@moduledoc false
|
||||
use Application
|
||||
|
||||
def start(_, _) do
|
||||
# While Plug.Crypto provides its own cache, Plug ship its own too,
|
||||
# both to keep storages separate and for backwards compatibility.
|
||||
Plug.Keys = :ets.new(Plug.Keys, [:named_table, :public, read_concurrency: true])
|
||||
|
||||
children = [
|
||||
Plug.Upload.Supervisor
|
||||
]
|
||||
|
||||
Supervisor.start_link(children, name: __MODULE__, strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
170
phoenix/deps/plug/lib/plug/basic_auth.ex
Normal file
170
phoenix/deps/plug/lib/plug/basic_auth.ex
Normal file
@@ -0,0 +1,170 @@
|
||||
defmodule Plug.BasicAuth do
|
||||
@moduledoc """
|
||||
Functionality for providing Basic HTTP authentication.
|
||||
|
||||
It is recommended to only use this module in production
|
||||
if SSL is enabled and enforced. See `Plug.SSL` for more
|
||||
information.
|
||||
|
||||
## Compile-time usage
|
||||
|
||||
If you have a single username and password, you can use
|
||||
the `basic_auth/2` plug:
|
||||
|
||||
import Plug.BasicAuth
|
||||
plug :basic_auth, username: "hello", password: "secret"
|
||||
|
||||
Or if you would rather put those in a config file:
|
||||
|
||||
# lib/your_app.ex
|
||||
import Plug.BasicAuth
|
||||
plug :basic_auth, Application.compile_env(:my_app, :basic_auth)
|
||||
|
||||
# config/config.exs
|
||||
config :my_app, :basic_auth, username: "hello", password: "secret"
|
||||
|
||||
Once the user first accesses the page, the request will be denied
|
||||
with reason 401 and the request is halted. The browser will then
|
||||
prompt the user for username and password. If they match, then the
|
||||
request succeeds.
|
||||
|
||||
Both approaches shown above rely on static configuration. Let's see
|
||||
alternatives.
|
||||
|
||||
## Runtime-time usage
|
||||
|
||||
As any other Plug, we can use the `basic_auth` at runtime by simply
|
||||
wrapping it in a function:
|
||||
|
||||
plug :auth
|
||||
|
||||
defp auth(conn, _opts) do
|
||||
username = System.fetch_env!("AUTH_USERNAME")
|
||||
password = System.fetch_env!("AUTH_PASSWORD")
|
||||
Plug.BasicAuth.basic_auth(conn, username: username, password: password)
|
||||
end
|
||||
|
||||
This approach is useful when both username and password are specified
|
||||
upfront and available at runtime. However, you may also want to compute
|
||||
a different password for each different user. In those cases, we can use
|
||||
the low-level API.
|
||||
|
||||
## Low-level usage
|
||||
|
||||
If you want to provide your own authentication logic on top of Basic HTTP
|
||||
auth, you can use the low-level functions. As an example, we define `:auth`
|
||||
plug that extracts username and password from the request headers, compares
|
||||
them against the database, and either assigns a `:current_user` on success
|
||||
or responds with an error on failure.
|
||||
|
||||
plug :auth
|
||||
|
||||
defp auth(conn, _opts) do
|
||||
with {user, pass} <- Plug.BasicAuth.parse_basic_auth(conn),
|
||||
%User{} = user <- MyApp.Accounts.find_by_username_and_password(user, pass) do
|
||||
assign(conn, :current_user, user)
|
||||
else
|
||||
_ -> conn |> Plug.BasicAuth.request_basic_auth() |> halt()
|
||||
end
|
||||
end
|
||||
|
||||
Keep in mind that:
|
||||
|
||||
* The supplied `user` and `pass` may be empty strings;
|
||||
|
||||
* If you are comparing the username and password with existing strings,
|
||||
do not use `==/2` or pattern matching. Use `Plug.Crypto.secure_compare/2`
|
||||
instead.
|
||||
|
||||
"""
|
||||
import Plug.Conn
|
||||
|
||||
@doc """
|
||||
Higher level usage of Basic HTTP auth.
|
||||
|
||||
See the module docs for examples.
|
||||
|
||||
## Options
|
||||
|
||||
* `:username` - the expected username
|
||||
* `:password` - the expected password
|
||||
* `:realm` - the authentication realm. The value is not fully
|
||||
sanitized, so do not accept user input as the realm and use
|
||||
strings with only alphanumeric characters and space
|
||||
|
||||
"""
|
||||
@spec basic_auth(Plug.Conn.t(), [auth_option]) :: Plug.Conn.t()
|
||||
when auth_option: {:username, String.t()} | {:password, String.t()} | {:realm, String.t()}
|
||||
def basic_auth(%Plug.Conn{} = conn, options \\ []) when is_list(options) do
|
||||
username = Keyword.fetch!(options, :username)
|
||||
password = Keyword.fetch!(options, :password)
|
||||
|
||||
with {request_username, request_password} <- parse_basic_auth(conn),
|
||||
valid_username? = Plug.Crypto.secure_compare(username, request_username),
|
||||
valid_password? = Plug.Crypto.secure_compare(password, request_password),
|
||||
true <- valid_username? and valid_password? do
|
||||
conn
|
||||
else
|
||||
_ -> conn |> request_basic_auth(options) |> halt()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses the request username and password from Basic HTTP auth.
|
||||
|
||||
It returns either `{user, pass}` or `:error`. Note the username
|
||||
and password may be empty strings. When comparing the username
|
||||
and password with the expected values, be sure to use
|
||||
`Plug.Crypto.secure_compare/2`.
|
||||
|
||||
See the module docs for examples.
|
||||
"""
|
||||
@spec parse_basic_auth(Plug.Conn.t()) :: {user :: String.t(), password :: String.t()} | :error
|
||||
def parse_basic_auth(%Plug.Conn{} = conn) do
|
||||
with ["Basic " <> encoded_user_and_pass] <- get_req_header(conn, "authorization"),
|
||||
{:ok, decoded_user_and_pass} <- Base.decode64(encoded_user_and_pass),
|
||||
[user, pass] <- :binary.split(decoded_user_and_pass, ":") do
|
||||
{user, pass}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a basic authentication header.
|
||||
|
||||
This can be used during tests:
|
||||
|
||||
put_req_header(conn, "authorization", encode_basic_auth("hello", "world"))
|
||||
|
||||
"""
|
||||
@spec encode_basic_auth(String.t(), String.t()) :: String.t()
|
||||
def encode_basic_auth(user, pass) when is_binary(user) and is_binary(pass) do
|
||||
"Basic " <> Base.encode64("#{user}:#{pass}")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Requests basic authentication from the client.
|
||||
|
||||
It sets the response to status 401 with "Unauthorized" as body.
|
||||
The response is not sent though (nor the connection is halted),
|
||||
allowing developers to further customize it.
|
||||
|
||||
## Options
|
||||
|
||||
* `:realm` - the authentication realm. The value is not fully
|
||||
sanitized, so do not accept user input as the realm and use
|
||||
strings with only alphanumeric characters and space
|
||||
|
||||
"""
|
||||
@spec request_basic_auth(Plug.Conn.t(), [option]) :: Plug.Conn.t()
|
||||
when option: {:realm, String.t()}
|
||||
def request_basic_auth(%Plug.Conn{} = conn, options \\ []) when is_list(options) do
|
||||
realm = Keyword.get(options, :realm, "Application")
|
||||
escaped_realm = String.replace(realm, "\"", "")
|
||||
|
||||
conn
|
||||
|> put_resp_header("www-authenticate", "Basic realm=\"#{escaped_realm}\"")
|
||||
|> resp(401, "Unauthorized")
|
||||
end
|
||||
end
|
||||
440
phoenix/deps/plug/lib/plug/builder.ex
Normal file
440
phoenix/deps/plug/lib/plug/builder.ex
Normal file
@@ -0,0 +1,440 @@
|
||||
defmodule Plug.Builder do
|
||||
@moduledoc """
|
||||
Conveniences for building plugs.
|
||||
|
||||
You can use this module to build a plug pipeline:
|
||||
|
||||
defmodule MyApp do
|
||||
use Plug.Builder
|
||||
|
||||
plug Plug.Logger
|
||||
plug :hello, upper: true
|
||||
|
||||
# A function from another module can be plugged too, provided it's
|
||||
# imported into the current module first.
|
||||
import AnotherModule, only: [interesting_plug: 2]
|
||||
plug :interesting_plug
|
||||
|
||||
def hello(conn, opts) do
|
||||
body = if opts[:upper], do: "WORLD", else: "world"
|
||||
send_resp(conn, 200, body)
|
||||
end
|
||||
end
|
||||
|
||||
The `plug/2` macro forms a pipeline by defining multiple plugs. Each plug
|
||||
in the pipeline is executed from top to bottom. In the example above, the
|
||||
`Plug.Logger` module plug is called before the `:hello` function plug, so
|
||||
the function plug will be called on the module plug's resulting connection.
|
||||
|
||||
`Plug.Builder` imports the `Plug.Conn` module so functions like `send_resp/3`
|
||||
are available.
|
||||
|
||||
## Options
|
||||
|
||||
When used, the following options are accepted by `Plug.Builder`:
|
||||
|
||||
* `:init_mode` - the environment to initialize the plug's options, one of
|
||||
`:compile` or `:runtime`. The default value is `:compile`.
|
||||
|
||||
* `:log_on_halt` - accepts the level to log whenever the request is halted
|
||||
|
||||
* `:copy_opts_to_assign` - an `atom` representing an assign. When supplied,
|
||||
it will copy the options given to the Plug initialization to the given
|
||||
connection assign
|
||||
|
||||
## Plug behaviour
|
||||
|
||||
`Plug.Builder` defines the `init/1` and `call/2` functions by implementing
|
||||
the `Plug` behaviour.
|
||||
|
||||
By implementing the Plug API, `Plug.Builder` guarantees this module is a plug
|
||||
and can be handed to a web server or used as part of another pipeline.
|
||||
|
||||
## Conditional plugs
|
||||
|
||||
Sometimes you may want to conditionally invoke a Plug in a pipeline. For example,
|
||||
you may want to invoke `Plug.Parsers` only under certain routes. This can be done
|
||||
by wrapping the module plug in a function plug. Instead of:
|
||||
|
||||
plug Plug.Parsers, parsers: [:urlencoded, :multipart], pass: ["text/*"]
|
||||
|
||||
You can write:
|
||||
|
||||
plug :conditional_parser
|
||||
|
||||
defp conditional_parser(%Plug.Conn{path_info: ["noparser" | _]} = conn, _opts) do
|
||||
conn
|
||||
end
|
||||
|
||||
@parser Plug.Parsers.init(parsers: [:urlencoded, :multipart], pass: ["text/*"])
|
||||
defp conditional_parser(conn, _opts) do
|
||||
Plug.Parsers.call(conn, @parser)
|
||||
end
|
||||
|
||||
The above will invoke `Plug.Parsers` on all routes, except the ones under `/noparser`
|
||||
|
||||
## Overriding the default Plug API functions
|
||||
|
||||
Both the `init/1` and `call/2` functions defined by `Plug.Builder` can be
|
||||
manually overridden. For example, the `init/1` function provided by
|
||||
`Plug.Builder` returns the options that it receives as an argument, but its
|
||||
behaviour can be customized:
|
||||
|
||||
defmodule PlugWithCustomOptions do
|
||||
use Plug.Builder
|
||||
plug Plug.Logger
|
||||
|
||||
def init(opts) do
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
The `call/2` function that `Plug.Builder` provides is used internally to
|
||||
execute all the plugs listed using the `plug` macro, so overriding the
|
||||
`call/2` function generally implies using `super` in order to still call the
|
||||
plug chain:
|
||||
|
||||
defmodule PlugWithCustomCall do
|
||||
use Plug.Builder
|
||||
plug Plug.Logger
|
||||
plug Plug.Head
|
||||
|
||||
def call(conn, opts) do
|
||||
conn
|
||||
|> super(opts) # calls Plug.Logger and Plug.Head
|
||||
|> assign(:called_all_plugs, true)
|
||||
end
|
||||
end
|
||||
|
||||
## Halting a plug pipeline
|
||||
|
||||
`Plug.Conn.halt/1` halts a plug pipeline. `Plug.Builder` prevents plugs
|
||||
downstream from being invoked and returns the current connection.
|
||||
In the following example, the `Plug.Logger` plug never gets
|
||||
called:
|
||||
|
||||
defmodule PlugUsingHalt do
|
||||
use Plug.Builder
|
||||
|
||||
plug :stopper
|
||||
plug Plug.Logger
|
||||
|
||||
def stopper(conn, _opts) do
|
||||
halt(conn)
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
@type plug :: module | atom
|
||||
|
||||
@doc false
|
||||
defmacro __using__(opts) do
|
||||
quote do
|
||||
@behaviour Plug
|
||||
@plug_builder_opts unquote(opts)
|
||||
|
||||
def init(opts) do
|
||||
opts
|
||||
end
|
||||
|
||||
def call(conn, opts) do
|
||||
plug_builder_call(conn, opts)
|
||||
end
|
||||
|
||||
defoverridable Plug
|
||||
|
||||
import Plug.Conn
|
||||
import Plug.Builder, only: [plug: 1, plug: 2, builder_opts: 0]
|
||||
|
||||
Module.register_attribute(__MODULE__, :plugs, accumulate: true)
|
||||
@before_compile Plug.Builder
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __before_compile__(env) do
|
||||
plugs = Module.get_attribute(env.module, :plugs)
|
||||
|
||||
plugs =
|
||||
if builder_ref = get_plug_builder_ref(env.module) do
|
||||
traverse(plugs, builder_ref)
|
||||
else
|
||||
plugs
|
||||
end
|
||||
|
||||
builder_opts = Module.get_attribute(env.module, :plug_builder_opts)
|
||||
{conn, body} = Plug.Builder.compile(env, plugs, builder_opts)
|
||||
|
||||
compile_time =
|
||||
if builder_opts[:init_mode] == :runtime do
|
||||
[]
|
||||
else
|
||||
for triplet <- plugs,
|
||||
{plug, _, _} = triplet,
|
||||
module_plug?(plug) do
|
||||
quote(do: unquote(plug).__info__(:module))
|
||||
end
|
||||
end
|
||||
|
||||
plug_builder_call =
|
||||
if assign = builder_opts[:copy_opts_to_assign] do
|
||||
quote do
|
||||
defp plug_builder_call(conn, opts) do
|
||||
unquote(conn) = Plug.Conn.assign(conn, unquote(assign), opts)
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
else
|
||||
quote do
|
||||
defp plug_builder_call(unquote(conn), opts), do: unquote(body)
|
||||
end
|
||||
end
|
||||
|
||||
quote do
|
||||
unquote_splicing(compile_time)
|
||||
unquote(plug_builder_call)
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse(tuple, ref) when is_tuple(tuple) do
|
||||
tuple |> Tuple.to_list() |> traverse(ref) |> List.to_tuple()
|
||||
end
|
||||
|
||||
defp traverse(map, ref) when is_map(map) do
|
||||
map |> Map.to_list() |> traverse(ref) |> Map.new()
|
||||
end
|
||||
|
||||
defp traverse(list, ref) when is_list(list) do
|
||||
Enum.map(list, &traverse(&1, ref))
|
||||
end
|
||||
|
||||
defp traverse(ref, ref) do
|
||||
{:unquote, [], [quote(do: opts)]}
|
||||
end
|
||||
|
||||
defp traverse(term, _ref) do
|
||||
term
|
||||
end
|
||||
|
||||
@doc """
|
||||
A macro that stores a new plug. `opts` will be passed unchanged to the new
|
||||
plug.
|
||||
|
||||
This macro doesn't add any guards when adding the new plug to the pipeline;
|
||||
for more information about adding plugs with guards see `compile/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
plug Plug.Logger # plug module
|
||||
plug :foo, some_options: true # plug function
|
||||
|
||||
"""
|
||||
defmacro plug(plug, opts \\ []) do
|
||||
# We always expand it but the @before_compile callback adds compile
|
||||
# time dependencies back depending on the builder's init mode.
|
||||
plug = expand_alias(plug, __CALLER__)
|
||||
|
||||
# If we are sure we don't have a module plug, the options are all
|
||||
# runtime options too.
|
||||
opts =
|
||||
if is_atom(plug) and not module_plug?(plug) and Macro.quoted_literal?(opts) do
|
||||
Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
quote do
|
||||
@plugs {unquote(plug), unquote(opts), true}
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_alias({:__aliases__, _, _} = alias, env),
|
||||
do: Macro.expand(alias, %{env | function: {:init, 1}})
|
||||
|
||||
defp expand_alias(other, _env), do: other
|
||||
|
||||
@doc """
|
||||
Using `builder_opts/0` is deprecated.
|
||||
|
||||
Instead use `:copy_opts_to_assign` on `use Plug.Builder`.
|
||||
"""
|
||||
@deprecated "Pass :copy_opts_to_assign on \"use Plug.Builder\""
|
||||
defmacro builder_opts() do
|
||||
quote do
|
||||
Plug.Builder.__builder_opts__(__MODULE__)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __builder_opts__(module) do
|
||||
get_plug_builder_ref(module) || generate_plug_builder_ref(module)
|
||||
end
|
||||
|
||||
defp get_plug_builder_ref(module) do
|
||||
Module.get_attribute(module, :plug_builder_ref)
|
||||
end
|
||||
|
||||
defp generate_plug_builder_ref(module) do
|
||||
ref = make_ref()
|
||||
Module.put_attribute(module, :plug_builder_ref, ref)
|
||||
ref
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles a plug pipeline.
|
||||
|
||||
Each element of the plug pipeline (according to the type signature of this
|
||||
function) has the form:
|
||||
|
||||
{plug_name, options, guards}
|
||||
|
||||
Note that this function expects a reversed pipeline (with the last plug that
|
||||
has to be called coming first in the pipeline).
|
||||
|
||||
The function returns a tuple with the first element being a quoted reference
|
||||
to the connection and the second element being the compiled quoted pipeline.
|
||||
|
||||
## Examples
|
||||
|
||||
Plug.Builder.compile(env, [
|
||||
{Plug.Logger, [], true}, # no guards, as added by the Plug.Builder.plug/2 macro
|
||||
{Plug.Head, [], quote(do: a when is_binary(a))}
|
||||
], [])
|
||||
|
||||
"""
|
||||
@spec compile(Macro.Env.t(), [{plug, Plug.opts(), Macro.t()}], Keyword.t()) ::
|
||||
{Macro.t(), Macro.t()}
|
||||
def compile(env, pipeline, builder_opts) do
|
||||
conn = quote do: conn
|
||||
init_mode = builder_opts[:init_mode] || :compile
|
||||
|
||||
unless init_mode in [:compile, :runtime] do
|
||||
raise ArgumentError, """
|
||||
invalid :init_mode when compiling #{inspect(env.module)}.
|
||||
|
||||
Supported values include :compile or :runtime. Got: #{inspect(init_mode)}
|
||||
"""
|
||||
end
|
||||
|
||||
ast =
|
||||
Enum.reduce(pipeline, conn, fn {plug, opts, guards}, acc ->
|
||||
{plug, opts, guards}
|
||||
|> init_plug(init_mode)
|
||||
|> quote_plug(init_mode, acc, env, builder_opts)
|
||||
end)
|
||||
|
||||
{conn, ast}
|
||||
end
|
||||
|
||||
defp module_plug?(plug), do: match?(~c"Elixir." ++ _, Atom.to_charlist(plug))
|
||||
|
||||
# Initializes the options of a plug in the configured init_mode.
|
||||
defp init_plug({plug, opts, guards}, init_mode) do
|
||||
if module_plug?(plug) do
|
||||
init_module_plug(plug, opts, guards, init_mode)
|
||||
else
|
||||
init_fun_plug(plug, opts, guards)
|
||||
end
|
||||
end
|
||||
|
||||
defp init_module_plug(plug, opts, guards, :compile) do
|
||||
initialized_opts = plug.init(opts)
|
||||
|
||||
if function_exported?(plug, :call, 2) do
|
||||
{:module, plug, escape(initialized_opts), guards}
|
||||
else
|
||||
raise ArgumentError, "#{inspect(plug)} plug must implement call/2"
|
||||
end
|
||||
end
|
||||
|
||||
defp init_module_plug(plug, opts, guards, :runtime) do
|
||||
{:module, plug, quote(do: unquote(plug).init(unquote(escape(opts)))), guards}
|
||||
end
|
||||
|
||||
defp init_fun_plug(plug, opts, guards) do
|
||||
{:function, plug, escape(opts), guards}
|
||||
end
|
||||
|
||||
defp escape(opts) do
|
||||
Macro.escape(opts, unquote: true)
|
||||
end
|
||||
|
||||
defp quote_plug({:module, plug, opts, guards}, :compile, acc, env, builder_opts) do
|
||||
# Elixir v1.13/1.14 do not add a compile time dependency on require,
|
||||
# so we build the alias and expand it to simulate the behaviour.
|
||||
parts = [:"Elixir" | Enum.map(Module.split(plug), &String.to_atom/1)]
|
||||
alias = {:__aliases__, [line: env.line], parts}
|
||||
_ = Macro.expand(alias, env)
|
||||
|
||||
quote_plug(:module, plug, opts, guards, acc, env, builder_opts)
|
||||
end
|
||||
|
||||
defp quote_plug({plug_type, plug, opts, guards}, _init_mode, acc, env, builder_opts) do
|
||||
quote_plug(plug_type, plug, opts, guards, acc, env, builder_opts)
|
||||
end
|
||||
|
||||
# `acc` is a series of nested plug calls in the form of plug3(plug2(plug1(conn))).
|
||||
# `quote_plug` wraps a new plug around that series of calls.
|
||||
defp quote_plug(plug_type, plug, opts, guards, acc, env, builder_opts) do
|
||||
call = quote_plug_call(plug_type, plug, opts)
|
||||
|
||||
error_message =
|
||||
case plug_type do
|
||||
:module -> "expected #{inspect(plug)}.call/2 to return a Plug.Conn"
|
||||
:function -> "expected #{plug}/2 to return a Plug.Conn"
|
||||
end <> ", all plugs must receive a connection (conn) and return a connection"
|
||||
|
||||
quote generated: true do
|
||||
case unquote(compile_guards(call, guards)) do
|
||||
%Plug.Conn{halted: true} = conn ->
|
||||
unquote(log_halt(plug_type, plug, env, builder_opts))
|
||||
conn
|
||||
|
||||
%Plug.Conn{} = conn ->
|
||||
unquote(acc)
|
||||
|
||||
other ->
|
||||
raise unquote(error_message) <> ", got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp quote_plug_call(:function, plug, opts) do
|
||||
quote do: unquote(plug)(conn, unquote(opts))
|
||||
end
|
||||
|
||||
defp quote_plug_call(:module, plug, opts) do
|
||||
quote do: unquote(plug).call(conn, unquote(opts))
|
||||
end
|
||||
|
||||
defp compile_guards(call, true) do
|
||||
call
|
||||
end
|
||||
|
||||
defp compile_guards(call, guards) do
|
||||
quote do
|
||||
case true do
|
||||
true when unquote(guards) -> unquote(call)
|
||||
true -> conn
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp log_halt(plug_type, plug, env, builder_opts) do
|
||||
if level = builder_opts[:log_on_halt] do
|
||||
message =
|
||||
case plug_type do
|
||||
:module -> "#{inspect(env.module)} halted in #{inspect(plug)}.call/2"
|
||||
:function -> "#{inspect(env.module)} halted in #{inspect(plug)}/2"
|
||||
end
|
||||
|
||||
quote do
|
||||
require Logger
|
||||
# Matching, to make Dialyzer happy on code executing Plug.Builder.compile/3
|
||||
_ = Logger.unquote(level)(unquote(message))
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
2024
phoenix/deps/plug/lib/plug/conn.ex
Normal file
2024
phoenix/deps/plug/lib/plug/conn.ex
Normal file
File diff suppressed because it is too large
Load Diff
209
phoenix/deps/plug/lib/plug/conn/adapter.ex
Normal file
209
phoenix/deps/plug/lib/plug/conn/adapter.ex
Normal file
@@ -0,0 +1,209 @@
|
||||
defmodule Plug.Conn.Adapter do
|
||||
@moduledoc """
|
||||
Specification of the connection adapter API implemented by webservers.
|
||||
|
||||
## Implementation recommendations
|
||||
|
||||
The `owner` field of `Plug.Conn` is deprecated and no longer needs to
|
||||
be set by adapters. If you don't set the `owner` field, it is the
|
||||
responsibility of the adapters to track the owner and send the
|
||||
`already_sent/0` message below on any of the `send_*` callbacks.
|
||||
"""
|
||||
alias Plug.Conn
|
||||
|
||||
@type http_protocol :: :"HTTP/1" | :"HTTP/1.1" | :"HTTP/2" | atom
|
||||
@type payload :: term
|
||||
@type peer_data :: %{
|
||||
address: :inet.ip_address(),
|
||||
port: :inet.port_number(),
|
||||
ssl_cert: binary | nil
|
||||
}
|
||||
@type sock_data :: %{
|
||||
address: :inet.ip_address(),
|
||||
port: :inet.port_number()
|
||||
}
|
||||
@type ssl_data :: :ssl.connection_info() | nil
|
||||
|
||||
@doc """
|
||||
The message to send to the request process on send callbacks.
|
||||
"""
|
||||
def already_sent do
|
||||
{:plug_conn, :sent}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Function used by adapters to create a new connection.
|
||||
"""
|
||||
def conn(adapter, method, uri, remote_ip, req_headers) do
|
||||
%URI{path: path, host: host, port: port, query: qs, scheme: scheme} = uri
|
||||
|
||||
%Plug.Conn{
|
||||
adapter: adapter,
|
||||
host: host,
|
||||
method: method,
|
||||
path_info: split_path(path),
|
||||
port: port,
|
||||
remote_ip: remote_ip,
|
||||
query_string: qs || "",
|
||||
req_headers: req_headers,
|
||||
request_path: path,
|
||||
scheme: String.to_atom(scheme)
|
||||
}
|
||||
end
|
||||
|
||||
defp split_path(path) do
|
||||
segments = :binary.split(path, "/", [:global])
|
||||
for segment <- segments, segment != "", do: segment
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends the given status, headers and body as a response
|
||||
back to the client.
|
||||
|
||||
If the request has method `"HEAD"`, the adapter should
|
||||
not send the response to the client.
|
||||
|
||||
Webservers are advised to return `nil` as the sent_body,
|
||||
as the body can no longer be manipulated. However, the
|
||||
test implementation returns the actual body so it can
|
||||
be used during testing.
|
||||
|
||||
Webservers must send a `{:plug_conn, :sent}` message to the
|
||||
process that called `Plug.Conn.Adapter.conn/5`.
|
||||
"""
|
||||
@callback send_resp(
|
||||
payload,
|
||||
status :: Conn.status(),
|
||||
headers :: Conn.headers(),
|
||||
body :: Conn.body()
|
||||
) ::
|
||||
{:ok, sent_body :: binary | nil, payload}
|
||||
|
||||
@doc """
|
||||
Sends the given status, headers and file as a response
|
||||
back to the client.
|
||||
|
||||
If the request has method `"HEAD"`, the adapter should
|
||||
not send the response to the client.
|
||||
|
||||
Webservers are advised to return `nil` as the sent_body,
|
||||
as the body can no longer be manipulated. However, the
|
||||
test implementation returns the actual body so it can
|
||||
be used during testing.
|
||||
|
||||
Webservers must send a `{:plug_conn, :sent}` message to the
|
||||
process that called `Plug.Conn.Adapter.conn/5`.
|
||||
"""
|
||||
@callback send_file(
|
||||
payload,
|
||||
status :: Conn.status(),
|
||||
headers :: Conn.headers(),
|
||||
file :: binary,
|
||||
offset :: integer,
|
||||
length :: integer | :all
|
||||
) :: {:ok, sent_body :: binary | nil, payload}
|
||||
|
||||
@doc """
|
||||
Sends the given status, headers as the beginning of
|
||||
a chunked response to the client. If the connection uses a
|
||||
protocol (such as HTTP/2) which does not support chunked encoding,
|
||||
the response should be sent in a streaming manner using the
|
||||
protocol's framing method. Likewise if the passed headers include
|
||||
a `content-length` header, the response should be streamed using
|
||||
content length framing.
|
||||
|
||||
Webservers are advised to return `nil` as the sent_body,
|
||||
since this function does not actually produce a body.
|
||||
However, the test implementation returns an empty binary
|
||||
as the body in order to be consistent with the built-up
|
||||
body returned by subsequent calls to the test implementation's
|
||||
`chunk/2` function
|
||||
|
||||
Webservers must send a `{:plug_conn, :sent}` message to the
|
||||
process that called `Plug.Conn.Adapter.conn/5`.
|
||||
"""
|
||||
@callback send_chunked(payload, status :: Conn.status(), headers :: Conn.headers()) ::
|
||||
{:ok, sent_body :: binary | nil, payload}
|
||||
|
||||
@doc """
|
||||
Sends a chunk in the chunked response.
|
||||
|
||||
If the request has method `"HEAD"`, the adapter should
|
||||
not send the response to the client.
|
||||
|
||||
Webservers are advised to return `nil` as the sent_body,
|
||||
since the complete sent body depends on the sum of all
|
||||
calls to this function. However, the test implementation
|
||||
tracks the overall body and payload so it can be used
|
||||
during testing.
|
||||
"""
|
||||
@callback chunk(payload, body :: Conn.body()) ::
|
||||
:ok | {:ok, sent_body :: binary | nil, payload} | {:error, term}
|
||||
|
||||
@doc """
|
||||
Reads the request body.
|
||||
|
||||
Read the docs in `Plug.Conn.read_body/2` for the supported
|
||||
options and expected behaviour.
|
||||
"""
|
||||
@callback read_req_body(payload, options :: Keyword.t()) ::
|
||||
{:ok, data :: binary, payload}
|
||||
| {:more, data :: binary, payload}
|
||||
| {:error, term}
|
||||
|
||||
@doc """
|
||||
Push a resource to the client.
|
||||
|
||||
If the adapter does not support server push then `{:error, :not_supported}`
|
||||
should be returned.
|
||||
|
||||
This callback no longer needs to be implemented, as browsers no longer support server push.
|
||||
"""
|
||||
@callback push(payload, path :: String.t(), headers :: Keyword.t()) :: :ok | {:error, term}
|
||||
|
||||
@doc """
|
||||
Send an informational response to the client.
|
||||
|
||||
If the adapter does not support inform, then `{:error, :not_supported}`
|
||||
should be returned.
|
||||
"""
|
||||
@callback inform(payload, status :: Conn.status(), headers :: Keyword.t()) ::
|
||||
:ok | {:ok, payload()} | {:error, term()}
|
||||
|
||||
@doc """
|
||||
Attempt to upgrade the connection with the client.
|
||||
|
||||
If the adapter does not support the indicated upgrade, then `{:error, :not_supported}` should be
|
||||
be returned.
|
||||
|
||||
If the adapter supports the indicated upgrade but is unable to proceed with it (due to
|
||||
a negotiation error, invalid opts being passed to this function, or some other reason), then an
|
||||
arbitrary error may be returned. Note that an adapter does not need to process the actual
|
||||
upgrade within this function; it is a wholly supported failure mode for an adapter to attempt
|
||||
the upgrade process later in the connection lifecycle and fail at that point.
|
||||
"""
|
||||
@callback upgrade(payload, protocol :: atom, opts :: term) :: {:ok, payload} | {:error, term}
|
||||
|
||||
@doc """
|
||||
Returns peer information such as the address, port and ssl cert.
|
||||
"""
|
||||
@callback get_peer_data(payload) :: peer_data()
|
||||
|
||||
@doc """
|
||||
Returns sock (local-side) information such as the address and port.
|
||||
"""
|
||||
@callback get_sock_data(payload) :: sock_data()
|
||||
|
||||
@doc """
|
||||
Returns details of the negotiated SSL connection, if present. If the connection is not SSL,
|
||||
returns nil
|
||||
"""
|
||||
@callback get_ssl_data(payload) :: ssl_data()
|
||||
|
||||
@doc """
|
||||
Returns the HTTP protocol and its version.
|
||||
"""
|
||||
@callback get_http_protocol(payload) :: http_protocol
|
||||
|
||||
@optional_callbacks push: 3, get_sock_data: 1, get_ssl_data: 1
|
||||
end
|
||||
153
phoenix/deps/plug/lib/plug/conn/cookies.ex
Normal file
153
phoenix/deps/plug/lib/plug/conn/cookies.ex
Normal file
@@ -0,0 +1,153 @@
|
||||
defmodule Plug.Conn.Cookies do
|
||||
@moduledoc """
|
||||
Conveniences for encoding and decoding cookies.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Decodes the given cookies as given in either a request or response header.
|
||||
|
||||
If a cookie is invalid, it is automatically discarded from the result.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> decode("key1=value1;key2=value2")
|
||||
%{"key1" => "value1", "key2" => "value2"}
|
||||
|
||||
"""
|
||||
def decode(cookie) when is_binary(cookie) do
|
||||
Map.new(decode_kv(cookie, []))
|
||||
end
|
||||
|
||||
defp decode_kv("", acc), do: acc
|
||||
defp decode_kv(<<h, t::binary>>, acc) when h in [?\s, ?\t], do: decode_kv(t, acc)
|
||||
defp decode_kv(kv, acc) when is_binary(kv), do: decode_key(kv, "", acc)
|
||||
|
||||
defp decode_key(<<h, t::binary>>, _key, acc) when h in [?\s, ?\t, ?\r, ?\n, ?\v, ?\f],
|
||||
do: skip_until_cc(t, acc)
|
||||
|
||||
defp decode_key(<<?;, t::binary>>, _key, acc), do: decode_kv(t, acc)
|
||||
defp decode_key(<<?=, t::binary>>, "", acc), do: skip_until_cc(t, acc)
|
||||
defp decode_key(<<?=, t::binary>>, key, acc), do: decode_value(t, "", 0, key, acc)
|
||||
defp decode_key(<<h, t::binary>>, key, acc), do: decode_key(t, <<key::binary, h>>, acc)
|
||||
defp decode_key(<<>>, _key, acc), do: acc
|
||||
|
||||
defp decode_value(<<?;, t::binary>>, value, spaces, key, acc),
|
||||
do: decode_kv(t, [{key, trim_spaces(value, spaces)} | acc])
|
||||
|
||||
defp decode_value(<<?\s, t::binary>>, value, spaces, key, acc),
|
||||
do: decode_value(t, <<value::binary, ?\s>>, spaces + 1, key, acc)
|
||||
|
||||
defp decode_value(<<h, t::binary>>, _value, _spaces, _key, acc)
|
||||
when h in [?\t, ?\r, ?\n, ?\v, ?\f],
|
||||
do: skip_until_cc(t, acc)
|
||||
|
||||
defp decode_value(<<h, t::binary>>, value, _spaces, key, acc),
|
||||
do: decode_value(t, <<value::binary, h>>, 0, key, acc)
|
||||
|
||||
defp decode_value(<<>>, value, spaces, key, acc),
|
||||
do: [{key, trim_spaces(value, spaces)} | acc]
|
||||
|
||||
defp skip_until_cc(<<?;, t::binary>>, acc), do: decode_kv(t, acc)
|
||||
defp skip_until_cc(<<_, t::binary>>, acc), do: skip_until_cc(t, acc)
|
||||
defp skip_until_cc(<<>>, acc), do: acc
|
||||
|
||||
defp trim_spaces(value, 0), do: value
|
||||
defp trim_spaces(value, spaces), do: binary_part(value, 0, byte_size(value) - spaces)
|
||||
|
||||
@doc """
|
||||
Encodes the given cookies as expected in a response header.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> encode("key1", %{value: "value1"})
|
||||
"key1=value1; path=/; HttpOnly"
|
||||
|
||||
iex> encode("key1", %{value: "value1", secure: true, path: "/example", http_only: false})
|
||||
"key1=value1; path=/example; secure"
|
||||
"""
|
||||
def encode(key, opts \\ %{}) when is_map(opts) do
|
||||
value = Map.get(opts, :value)
|
||||
path = Map.get(opts, :path, "/")
|
||||
|
||||
IO.iodata_to_binary([
|
||||
"#{key}=#{value}; path=#{path}",
|
||||
emit_if(opts[:domain], &["; domain=", &1]),
|
||||
emit_if(opts[:max_age], &encode_max_age(&1, opts)),
|
||||
emit_if(Map.get(opts, :secure, false), "; secure"),
|
||||
emit_if(Map.get(opts, :http_only, true), "; HttpOnly"),
|
||||
emit_if(Map.get(opts, :same_site, nil), &encode_same_site/1),
|
||||
emit_if(opts[:extra], &["; ", &1])
|
||||
])
|
||||
end
|
||||
|
||||
defp encode_max_age(max_age, opts) do
|
||||
time = Map.get(opts, :universal_time) || :calendar.universal_time()
|
||||
time = add_seconds(time, max_age)
|
||||
["; expires=", rfc2822(time), "; max-age=", Integer.to_string(max_age)]
|
||||
end
|
||||
|
||||
defp encode_same_site(value) when is_binary(value), do: "; SameSite=#{value}"
|
||||
|
||||
defp emit_if(value, fun_or_string) do
|
||||
cond do
|
||||
!value ->
|
||||
[]
|
||||
|
||||
is_function(fun_or_string) ->
|
||||
fun_or_string.(value)
|
||||
|
||||
is_binary(fun_or_string) ->
|
||||
fun_or_string
|
||||
end
|
||||
end
|
||||
|
||||
defp pad(number) when number in 0..9, do: <<?0, ?0 + number>>
|
||||
defp pad(number), do: Integer.to_string(number)
|
||||
|
||||
defp rfc2822({{year, month, day} = date, {hour, minute, second}}) do
|
||||
# Sat, 17 Apr 2010 14:00:00 GMT
|
||||
[
|
||||
weekday_name(:calendar.day_of_the_week(date)),
|
||||
?,,
|
||||
?\s,
|
||||
pad(day),
|
||||
?\s,
|
||||
month_name(month),
|
||||
?\s,
|
||||
Integer.to_string(year),
|
||||
?\s,
|
||||
pad(hour),
|
||||
?:,
|
||||
pad(minute),
|
||||
?:,
|
||||
pad(second),
|
||||
" GMT"
|
||||
]
|
||||
end
|
||||
|
||||
defp weekday_name(1), do: "Mon"
|
||||
defp weekday_name(2), do: "Tue"
|
||||
defp weekday_name(3), do: "Wed"
|
||||
defp weekday_name(4), do: "Thu"
|
||||
defp weekday_name(5), do: "Fri"
|
||||
defp weekday_name(6), do: "Sat"
|
||||
defp weekday_name(7), do: "Sun"
|
||||
|
||||
defp month_name(1), do: "Jan"
|
||||
defp month_name(2), do: "Feb"
|
||||
defp month_name(3), do: "Mar"
|
||||
defp month_name(4), do: "Apr"
|
||||
defp month_name(5), do: "May"
|
||||
defp month_name(6), do: "Jun"
|
||||
defp month_name(7), do: "Jul"
|
||||
defp month_name(8), do: "Aug"
|
||||
defp month_name(9), do: "Sep"
|
||||
defp month_name(10), do: "Oct"
|
||||
defp month_name(11), do: "Nov"
|
||||
defp month_name(12), do: "Dec"
|
||||
|
||||
defp add_seconds(time, seconds_to_add) do
|
||||
time_seconds = :calendar.datetime_to_gregorian_seconds(time)
|
||||
:calendar.gregorian_seconds_to_datetime(time_seconds + seconds_to_add)
|
||||
end
|
||||
end
|
||||
457
phoenix/deps/plug/lib/plug/conn/query.ex
Normal file
457
phoenix/deps/plug/lib/plug/conn/query.ex
Normal file
@@ -0,0 +1,457 @@
|
||||
defmodule Plug.Conn.Query do
|
||||
@moduledoc """
|
||||
Conveniences for decoding and encoding URL-encoded queries.
|
||||
|
||||
Plug allows developers to build query strings that map to
|
||||
Elixir structures in order to make manipulation of such structures
|
||||
easier on the server side. Here are some examples:
|
||||
|
||||
iex> decode("foo=bar")["foo"]
|
||||
"bar"
|
||||
|
||||
If a value is given more than once, the last value takes precedence:
|
||||
|
||||
iex> decode("foo=bar&foo=baz")["foo"]
|
||||
"baz"
|
||||
|
||||
Nested structures can be created via `[key]`:
|
||||
|
||||
iex> decode("foo[bar]=baz")["foo"]["bar"]
|
||||
"baz"
|
||||
|
||||
Lists are created with `[]`:
|
||||
|
||||
iex> decode("foo[]=bar&foo[]=baz")["foo"]
|
||||
["bar", "baz"]
|
||||
|
||||
> #### Nesting inside lists {: .error}
|
||||
>
|
||||
> Nesting inside lists is ambiguous and unspecified behaviour.
|
||||
> Therefore, you should not rely on the decoding behaviour of
|
||||
> `user[][foo]=1&user[][bar]=2`.
|
||||
>
|
||||
> As an alternative, you can explicitly specify the keys:
|
||||
>
|
||||
> # If foo and bar belong to the same entry
|
||||
> user[0][foo]=1&user[0][bar]=2
|
||||
>
|
||||
> # If foo and bar are different entries
|
||||
> user[0][foo]=1&user[1][bar]=2
|
||||
|
||||
Keys without values are treated as empty strings,
|
||||
according to https://url.spec.whatwg.org/#application/x-www-form-urlencoded:
|
||||
|
||||
iex> decode("foo")["foo"]
|
||||
""
|
||||
|
||||
Maps can be encoded:
|
||||
|
||||
iex> encode(%{foo: "bar"})
|
||||
"foo=bar"
|
||||
|
||||
Encoding keyword lists preserves the order of the fields:
|
||||
|
||||
iex> encode([foo: "bar", baz: "bat"])
|
||||
"foo=bar&baz=bat"
|
||||
|
||||
When encoding keyword lists with duplicate keys, the key that comes first
|
||||
takes precedence:
|
||||
|
||||
iex> encode([foo: "bar", foo: "bat"])
|
||||
"foo=bar"
|
||||
|
||||
Encoding named lists:
|
||||
|
||||
iex> encode(%{foo: ["bar", "baz"]})
|
||||
"foo[]=bar&foo[]=baz"
|
||||
|
||||
Encoding nested structures:
|
||||
|
||||
iex> encode(%{foo: %{bar: "baz"}})
|
||||
"foo[bar]=baz"
|
||||
|
||||
It is only possible to encode maps inside lists if those maps have exactly one element.
|
||||
In this case it is possible to encode the parameters using maps instead of lists:
|
||||
|
||||
iex> encode(%{"list" => [%{"a" => 1, "b" => 2}]})
|
||||
** (ArgumentError) cannot encode maps inside lists when the map has 0 or more than 1 element, got: %{\"a\" => 1, \"b\" => 2}
|
||||
|
||||
iex> encode(%{"list" => %{0 => %{"a" => 1, "b" => 2}}})
|
||||
"list[0][a]=1&list[0][b]=2"
|
||||
|
||||
For stateful decoding, see `decode_init/0`, `decode_each/2`, and `decode_done/2`.
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
Stateful decoder accumulator.
|
||||
|
||||
See `decode_init/0`, `decode_each/2`, and `decode_done/2`.
|
||||
"""
|
||||
@typedoc since: "1.16.0"
|
||||
@opaque decoder() :: map()
|
||||
|
||||
@doc """
|
||||
Decodes the given `query`.
|
||||
|
||||
The `query` is assumed to be encoded in the "x-www-form-urlencoded" format.
|
||||
The format is decoded at first. Then, if `validate_utf8` is `true`, the decoded
|
||||
result is validated for proper UTF-8 encoding. `validate_utf8` may also be
|
||||
an atom with a custom exception to raise.
|
||||
|
||||
`initial` is the initial "accumulator" where decoded values will be added.
|
||||
|
||||
`invalid_exception` is the exception module for the exception to raise on
|
||||
errors with decoding.
|
||||
"""
|
||||
@spec decode(String.t(), keyword(), module(), boolean()) :: %{optional(String.t()) => term()}
|
||||
def decode(
|
||||
query,
|
||||
initial \\ [],
|
||||
invalid_exception \\ Plug.Conn.InvalidQueryError,
|
||||
validate_utf8 \\ true
|
||||
)
|
||||
|
||||
def decode("", initial, _invalid_exception, _validate_utf8) do
|
||||
Map.new(initial)
|
||||
end
|
||||
|
||||
def decode(query, initial, invalid_exception, validate_utf8)
|
||||
when is_binary(query) do
|
||||
parts = :binary.split(query, "&", [:global])
|
||||
|
||||
parts
|
||||
|> Enum.reduce(decode_init(), &decode_www_pair(&1, &2, invalid_exception, validate_utf8))
|
||||
|> decode_done(initial)
|
||||
end
|
||||
|
||||
defp decode_www_pair("", acc, _invalid_exception, _validate_utf8) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp decode_www_pair(binary, acc, invalid_exception, validate_utf8) do
|
||||
current =
|
||||
case :binary.split(binary, "=") do
|
||||
[key, value] ->
|
||||
{decode_www_form(key, invalid_exception, validate_utf8),
|
||||
decode_www_form(value, invalid_exception, validate_utf8)}
|
||||
|
||||
[key] ->
|
||||
{decode_www_form(key, invalid_exception, validate_utf8), ""}
|
||||
end
|
||||
|
||||
decode_each(current, acc)
|
||||
end
|
||||
|
||||
defp decode_www_form(value, invalid_exception, validate_utf8) do
|
||||
binary = URI.decode_www_form(value)
|
||||
|
||||
case validate_utf8 do
|
||||
true -> Plug.Conn.Utils.validate_utf8!(binary, invalid_exception, "urlencoded params")
|
||||
false -> :ok
|
||||
module -> Plug.Conn.Utils.validate_utf8!(binary, module, "urlencoded params")
|
||||
end
|
||||
|
||||
binary
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts a stateful decoder.
|
||||
|
||||
Use `decode_each/2` and `decode_done/2` to decode and complete.
|
||||
See `decode_each/2` for examples.
|
||||
"""
|
||||
@spec decode_init() :: decoder()
|
||||
def decode_init(), do: %{root: []}
|
||||
|
||||
@doc """
|
||||
Decodes the given `pair` tuple.
|
||||
|
||||
It parses the key and stores the value into the current
|
||||
accumulator `decoder`. The keys and values are not assumed to be
|
||||
encoded in `"x-www-form-urlencoded"`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> decoder = Plug.Conn.Query.decode_init()
|
||||
iex> decoder = Plug.Conn.Query.decode_each({"foo", "bar"}, decoder)
|
||||
iex> decoder = Plug.Conn.Query.decode_each({"baz", "bat"}, decoder)
|
||||
iex> Plug.Conn.Query.decode_done(decoder)
|
||||
%{"baz" => "bat", "foo" => "bar"}
|
||||
|
||||
"""
|
||||
@spec decode_each({term(), term()}, decoder()) :: decoder()
|
||||
def decode_each(pair, decoder)
|
||||
|
||||
def decode_each({"", value}, map) do
|
||||
insert_keys([{:root, ""}], value, map)
|
||||
end
|
||||
|
||||
def decode_each({key, value}, map) do
|
||||
# Examples:
|
||||
#
|
||||
# users
|
||||
# #=> [{:root, "users"}]
|
||||
#
|
||||
# users[foo]
|
||||
# #=> [{"users", "foo"}, {:root, "users"}]
|
||||
#
|
||||
# users[foo][bar]
|
||||
# #=> [{"users[foo]", "bar"}, {"users", "foo"}, {:root, "users"}]
|
||||
#
|
||||
keys =
|
||||
with ?] <- :binary.last(key),
|
||||
{pos, 1} when pos > 0 <- :binary.match(key, "[") do
|
||||
value = binary_part(key, 0, pos)
|
||||
pos = pos + 1
|
||||
rest = binary_part(key, pos, byte_size(key) - pos)
|
||||
split_keys(rest, key, pos, pos, value, [{:root, value}])
|
||||
else
|
||||
_ -> [{:root, key}]
|
||||
end
|
||||
|
||||
insert_keys(keys, value, map)
|
||||
end
|
||||
|
||||
defp split_keys(<<?], ?[, rest::binary>>, binary, current_pos, start_pos, level, acc) do
|
||||
value = split_key(binary, current_pos, start_pos)
|
||||
next_level = binary_part(binary, 0, current_pos + 1)
|
||||
split_keys(rest, binary, current_pos + 2, current_pos + 2, next_level, [{level, value} | acc])
|
||||
end
|
||||
|
||||
defp split_keys(<<?]>>, binary, current_pos, start_pos, level, acc) do
|
||||
value = split_key(binary, current_pos, start_pos)
|
||||
[{level, value} | acc]
|
||||
end
|
||||
|
||||
defp split_keys(<<_, rest::binary>>, binary, current_pos, start_pos, level, acc) do
|
||||
split_keys(rest, binary, current_pos + 1, start_pos, level, acc)
|
||||
end
|
||||
|
||||
defp split_key(_binary, start, start), do: nil
|
||||
defp split_key(binary, current, start), do: binary_part(binary, start, current - start)
|
||||
|
||||
defp insert_keys([{level, key} | rest], value, map) do
|
||||
case map do
|
||||
%{^level => entries} -> %{map | level => [{key, value} | entries]}
|
||||
%{} -> insert_keys(rest, [:pointer | level], Map.put(map, level, [{key, value}]))
|
||||
end
|
||||
end
|
||||
|
||||
defp insert_keys([], _value, map) do
|
||||
map
|
||||
end
|
||||
|
||||
@doc """
|
||||
Finishes stateful decoding and returns a map with the decoded pairs.
|
||||
|
||||
`decoder` is the stateful decoder returned by `decode_init/0` and `decode_each/2`.
|
||||
`initial` is an enumerable of key-value pairs that functions as the initial
|
||||
accumulator for the returned map (see examples below).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> decoder = Plug.Conn.Query.decode_init()
|
||||
iex> decoder = Plug.Conn.Query.decode_each({"foo", "bar"}, decoder)
|
||||
iex> Plug.Conn.Query.decode_done(decoder, %{"initial" => true})
|
||||
%{"foo" => "bar", "initial" => true}
|
||||
|
||||
"""
|
||||
@spec decode_done(decoder(), Enumerable.t()) :: %{optional(String.t()) => term()}
|
||||
def decode_done(%{root: root} = decoder, initial \\ []) do
|
||||
finalize_map(root, Enum.to_list(initial), decoder)
|
||||
end
|
||||
|
||||
defp finalize_pointer(key, map) do
|
||||
case Map.fetch!(map, key) do
|
||||
[{nil, _} | _] = entries -> finalize_list(entries, [], map)
|
||||
entries -> finalize_map(entries, [], map)
|
||||
end
|
||||
end
|
||||
|
||||
defp finalize_map([{key, [:pointer | pointer]} | rest], acc, map),
|
||||
do: finalize_map(rest, [{key, finalize_pointer(pointer, map)} | acc], map)
|
||||
|
||||
defp finalize_map([{nil, _} | rest], acc, map),
|
||||
do: finalize_map(rest, acc, map)
|
||||
|
||||
defp finalize_map([{_, _} = kv | rest], acc, map),
|
||||
do: finalize_map(rest, [kv | acc], map)
|
||||
|
||||
defp finalize_map([], acc, _map),
|
||||
do: Map.new(acc)
|
||||
|
||||
defp finalize_list([{nil, [:pointer | pointer]} | rest], acc, map),
|
||||
do: finalize_list(rest, [finalize_pointer(pointer, map) | acc], map)
|
||||
|
||||
defp finalize_list([{nil, value} | rest], acc, map),
|
||||
do: finalize_list(rest, [value | acc], map)
|
||||
|
||||
defp finalize_list([{_, _} | rest], acc, map),
|
||||
do: finalize_list(rest, acc, map)
|
||||
|
||||
defp finalize_list([], acc, _map),
|
||||
do: acc
|
||||
|
||||
@doc """
|
||||
Decodes the given tuple and stores it in the given accumulator.
|
||||
|
||||
It parses the key and stores the value into the current
|
||||
accumulator. The keys and values are not assumed to be
|
||||
encoded in "x-www-form-urlencoded".
|
||||
|
||||
Parameter lists are added to the accumulator in reverse
|
||||
order, so be sure to pass the parameters in reverse order.
|
||||
"""
|
||||
@deprecated "Use decode_init/0, decode_each/2, and decode_done/2 instead"
|
||||
def decode_pair({key, value} = _pair, acc) do
|
||||
if key != "" and :binary.last(key) == ?] do
|
||||
# Remove trailing ]
|
||||
subkey = :binary.part(key, 0, byte_size(key) - 1)
|
||||
|
||||
# Split the first [ then we will split on remaining ][.
|
||||
#
|
||||
# users[address][street #=> [ "users", "address][street" ]
|
||||
#
|
||||
assign_split(:binary.split(subkey, "["), value, acc, :binary.compile_pattern("]["))
|
||||
else
|
||||
assign_map(acc, key, value)
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_split(["", rest], value, acc, pattern) do
|
||||
parts = :binary.split(rest, pattern)
|
||||
|
||||
case acc do
|
||||
[_ | _] -> [assign_split(parts, value, :none, pattern) | acc]
|
||||
:none -> [assign_split(parts, value, :none, pattern)]
|
||||
_ -> acc
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_split([key, rest], value, acc, pattern) do
|
||||
parts = :binary.split(rest, pattern)
|
||||
|
||||
case acc do
|
||||
%{^key => current} when is_list(current) or is_map(current) ->
|
||||
Map.put(acc, key, assign_split(parts, value, current, pattern))
|
||||
|
||||
%{^key => _} ->
|
||||
acc
|
||||
|
||||
%{} ->
|
||||
Map.put(acc, key, assign_split(parts, value, :none, pattern))
|
||||
|
||||
_ ->
|
||||
%{key => assign_split(parts, value, :none, pattern)}
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_split([""], nil, acc, _pattern) do
|
||||
case acc do
|
||||
[_ | _] -> acc
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_split([""], value, acc, _pattern) do
|
||||
case acc do
|
||||
[_ | _] -> [value | acc]
|
||||
:none -> [value]
|
||||
_ -> acc
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_split([key], value, acc, _pattern) do
|
||||
assign_map(acc, key, value)
|
||||
end
|
||||
|
||||
defp assign_map(acc, key, value) do
|
||||
case acc do
|
||||
%{^key => _} -> acc
|
||||
%{} -> Map.put(acc, key, value)
|
||||
_ -> %{key => value}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes the given map or list of tuples.
|
||||
"""
|
||||
@spec encode(Enumerable.t(), (term() -> binary())) :: binary()
|
||||
def encode(kv, encoder \\ &to_string/1) do
|
||||
IO.iodata_to_binary(encode_pair("", kv, encoder))
|
||||
end
|
||||
|
||||
# covers structs
|
||||
defp encode_pair(field, %{__struct__: struct} = map, encoder) when is_atom(struct) do
|
||||
[field, ?= | encode_value(map, encoder)]
|
||||
end
|
||||
|
||||
# covers maps
|
||||
defp encode_pair(parent_field, %{} = map, encoder) do
|
||||
encode_kv(map, parent_field, encoder)
|
||||
end
|
||||
|
||||
# covers keyword lists
|
||||
defp encode_pair(parent_field, list, encoder) when is_list(list) and is_tuple(hd(list)) do
|
||||
encode_kv(Enum.uniq_by(list, &elem(&1, 0)), parent_field, encoder)
|
||||
end
|
||||
|
||||
# covers non-keyword lists
|
||||
defp encode_pair(parent_field, list, encoder) when is_list(list) do
|
||||
mapper = fn
|
||||
value when is_map(value) and map_size(value) != 1 ->
|
||||
raise ArgumentError,
|
||||
"cannot encode maps inside lists when the map has 0 or more than 1 element, " <>
|
||||
"got: #{inspect(value)}"
|
||||
|
||||
value ->
|
||||
[?&, encode_pair(parent_field <> "[]", value, encoder)]
|
||||
end
|
||||
|
||||
list
|
||||
|> Enum.flat_map(mapper)
|
||||
|> prune()
|
||||
end
|
||||
|
||||
# covers nil
|
||||
defp encode_pair(field, nil, _encoder) do
|
||||
[field, ?=]
|
||||
end
|
||||
|
||||
# encoder fallback
|
||||
defp encode_pair(field, value, encoder) do
|
||||
[field, ?= | encode_value(value, encoder)]
|
||||
end
|
||||
|
||||
defp encode_kv(kv, parent_field, encoder) do
|
||||
mapper = fn
|
||||
{_, value} when value in [%{}, []] ->
|
||||
[]
|
||||
|
||||
{field, value} ->
|
||||
field =
|
||||
if parent_field == "" do
|
||||
encode_key(field)
|
||||
else
|
||||
parent_field <> "[" <> encode_key(field) <> "]"
|
||||
end
|
||||
|
||||
[?&, encode_pair(field, value, encoder)]
|
||||
end
|
||||
|
||||
kv
|
||||
|> Enum.flat_map(mapper)
|
||||
|> prune()
|
||||
end
|
||||
|
||||
defp encode_key(item) do
|
||||
item |> to_string |> URI.encode_www_form()
|
||||
end
|
||||
|
||||
defp encode_value(item, encoder) do
|
||||
item |> encoder.() |> URI.encode_www_form()
|
||||
end
|
||||
|
||||
defp prune([?& | t]), do: t
|
||||
defp prune([]), do: []
|
||||
end
|
||||
183
phoenix/deps/plug/lib/plug/conn/status.ex
Normal file
183
phoenix/deps/plug/lib/plug/conn/status.ex
Normal file
@@ -0,0 +1,183 @@
|
||||
defmodule Plug.Conn.Status do
|
||||
@moduledoc """
|
||||
Conveniences for working with status codes.
|
||||
"""
|
||||
|
||||
custom_statuses = Application.compile_env(:plug, :statuses, %{})
|
||||
|
||||
aliased_statuses = [
|
||||
{422, :unprocessable_entity}
|
||||
]
|
||||
|
||||
statuses = %{
|
||||
100 => "Continue",
|
||||
101 => "Switching Protocols",
|
||||
102 => "Processing",
|
||||
103 => "Early Hints",
|
||||
200 => "OK",
|
||||
201 => "Created",
|
||||
202 => "Accepted",
|
||||
203 => "Non-Authoritative Information",
|
||||
204 => "No Content",
|
||||
205 => "Reset Content",
|
||||
206 => "Partial Content",
|
||||
207 => "Multi-Status",
|
||||
208 => "Already Reported",
|
||||
226 => "IM Used",
|
||||
300 => "Multiple Choices",
|
||||
301 => "Moved Permanently",
|
||||
302 => "Found",
|
||||
303 => "See Other",
|
||||
304 => "Not Modified",
|
||||
305 => "Use Proxy",
|
||||
306 => "Switch Proxy",
|
||||
307 => "Temporary Redirect",
|
||||
308 => "Permanent Redirect",
|
||||
400 => "Bad Request",
|
||||
401 => "Unauthorized",
|
||||
402 => "Payment Required",
|
||||
403 => "Forbidden",
|
||||
404 => "Not Found",
|
||||
405 => "Method Not Allowed",
|
||||
406 => "Not Acceptable",
|
||||
407 => "Proxy Authentication Required",
|
||||
408 => "Request Timeout",
|
||||
409 => "Conflict",
|
||||
410 => "Gone",
|
||||
411 => "Length Required",
|
||||
412 => "Precondition Failed",
|
||||
413 => "Request Entity Too Large",
|
||||
414 => "Request-URI Too Long",
|
||||
415 => "Unsupported Media Type",
|
||||
416 => "Requested Range Not Satisfiable",
|
||||
417 => "Expectation Failed",
|
||||
418 => "I'm a teapot",
|
||||
421 => "Misdirected Request",
|
||||
422 => "Unprocessable Content",
|
||||
423 => "Locked",
|
||||
424 => "Failed Dependency",
|
||||
425 => "Too Early",
|
||||
426 => "Upgrade Required",
|
||||
428 => "Precondition Required",
|
||||
429 => "Too Many Requests",
|
||||
431 => "Request Header Fields Too Large",
|
||||
451 => "Unavailable For Legal Reasons",
|
||||
500 => "Internal Server Error",
|
||||
501 => "Not Implemented",
|
||||
502 => "Bad Gateway",
|
||||
503 => "Service Unavailable",
|
||||
504 => "Gateway Timeout",
|
||||
505 => "HTTP Version Not Supported",
|
||||
506 => "Variant Also Negotiates",
|
||||
507 => "Insufficient Storage",
|
||||
508 => "Loop Detected",
|
||||
510 => "Not Extended",
|
||||
511 => "Network Authentication Required"
|
||||
}
|
||||
|
||||
reason_phrase_to_atom = fn reason_phrase ->
|
||||
reason_phrase
|
||||
|> String.downcase()
|
||||
|> String.replace("'", "")
|
||||
|> String.replace(~r/[^a-z0-9]/, "_")
|
||||
|> String.to_atom()
|
||||
end
|
||||
|
||||
status_map_to_doc = fn statuses ->
|
||||
statuses
|
||||
|> Enum.sort_by(&elem(&1, 0))
|
||||
|> Enum.map(fn {code, reason_phrase} ->
|
||||
atom = reason_phrase_to_atom.(reason_phrase)
|
||||
" * `#{inspect(atom)}` - #{code}\n"
|
||||
end)
|
||||
end
|
||||
|
||||
custom_status_doc =
|
||||
if custom_statuses != %{} do
|
||||
"""
|
||||
## Custom status codes
|
||||
|
||||
#{status_map_to_doc.(custom_statuses)}
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the status code given an integer or a known atom.
|
||||
|
||||
## Known status codes
|
||||
|
||||
The following status codes can be given as atoms with their
|
||||
respective value shown next:
|
||||
|
||||
#{status_map_to_doc.(statuses)}
|
||||
#{custom_status_doc}
|
||||
"""
|
||||
@spec code(integer | atom) :: integer
|
||||
def code(integer_or_atom)
|
||||
|
||||
def code(integer) when integer in 100..999 do
|
||||
integer
|
||||
end
|
||||
|
||||
for {code, reason_phrase} <- statuses do
|
||||
atom = reason_phrase_to_atom.(reason_phrase)
|
||||
def code(unquote(atom)), do: unquote(code)
|
||||
end
|
||||
|
||||
for {code, aliased_status} <- aliased_statuses do
|
||||
def code(unquote(aliased_status)), do: unquote(code)
|
||||
end
|
||||
|
||||
# This ensures that both the default and custom statuses will work
|
||||
for {code, reason_phrase} <- custom_statuses do
|
||||
atom = reason_phrase_to_atom.(reason_phrase)
|
||||
def code(unquote(atom)), do: unquote(code)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the atom for given integer.
|
||||
|
||||
See `code/1` for the mapping.
|
||||
"""
|
||||
@spec reason_atom(integer) :: atom
|
||||
def reason_atom(code)
|
||||
|
||||
for {code, reason_phrase} <- Map.merge(statuses, custom_statuses) do
|
||||
atom = reason_phrase_to_atom.(reason_phrase)
|
||||
def reason_atom(unquote(code)), do: unquote(atom)
|
||||
end
|
||||
|
||||
def reason_atom(code) do
|
||||
raise ArgumentError, "unknown status code #{inspect(code)}"
|
||||
end
|
||||
|
||||
@spec reason_phrase(integer) :: String.t()
|
||||
def reason_phrase(integer)
|
||||
|
||||
for {code, phrase} <- Map.merge(statuses, custom_statuses) do
|
||||
def reason_phrase(unquote(code)), do: unquote(phrase)
|
||||
end
|
||||
|
||||
def reason_phrase(code) do
|
||||
raise ArgumentError, """
|
||||
unknown status code #{inspect(code)}
|
||||
|
||||
Custom codes can be defined in the configuration for the :plug application,
|
||||
under the :statuses key (which contains a map of status codes as keys and
|
||||
reason phrases as values). For example:
|
||||
|
||||
config :plug, :statuses, %{998 => "Not An RFC Status Code"}
|
||||
|
||||
After defining the config for custom statuses, Plug must be recompiled for
|
||||
the changes to take place using:
|
||||
|
||||
MIX_ENV=dev mix deps.clean plug --build
|
||||
|
||||
Doing this will allow the use of the integer status code 998 as
|
||||
well as the atom :not_an_rfc_status_code in many Plug functions.
|
||||
For example:
|
||||
|
||||
put_status(conn, :not_an_rfc_status_code)
|
||||
"""
|
||||
end
|
||||
end
|
||||
47
phoenix/deps/plug/lib/plug/conn/unfetched.ex
Normal file
47
phoenix/deps/plug/lib/plug/conn/unfetched.ex
Normal file
@@ -0,0 +1,47 @@
|
||||
defmodule Plug.Conn.Unfetched do
|
||||
@moduledoc """
|
||||
A struct used as default on unfetched fields.
|
||||
|
||||
The `:aspect` key of the struct specifies what field is still unfetched.
|
||||
|
||||
## Examples
|
||||
|
||||
unfetched = %Plug.Conn.Unfetched{aspect: :cookies}
|
||||
|
||||
"""
|
||||
|
||||
defstruct [:aspect]
|
||||
@type t :: %__MODULE__{aspect: atom()}
|
||||
|
||||
@behaviour Access
|
||||
|
||||
def fetch(%{aspect: aspect}, key) do
|
||||
raise_unfetched(__ENV__.function, aspect, key)
|
||||
end
|
||||
|
||||
def get(%{aspect: aspect}, key, _value) do
|
||||
raise_unfetched(__ENV__.function, aspect, key)
|
||||
end
|
||||
|
||||
def get_and_update(%{aspect: aspect}, key, _fun) do
|
||||
raise_unfetched(__ENV__.function, aspect, key)
|
||||
end
|
||||
|
||||
def pop(%{aspect: aspect}, key) do
|
||||
raise_unfetched(__ENV__.function, aspect, key)
|
||||
end
|
||||
|
||||
defp raise_unfetched({access, _}, aspect, key) do
|
||||
raise ArgumentError,
|
||||
"cannot #{access} key #{inspect(key)} from conn.#{aspect} " <>
|
||||
"because they were not fetched" <> hint(aspect)
|
||||
end
|
||||
|
||||
defp hint(aspect) when aspect in [:cookies, :query_params],
|
||||
do: ". Call Plug.Conn.fetch_#{aspect}/2, either as a plug or directly, to fetch it"
|
||||
|
||||
defp hint(aspect) when aspect in [:params, :body_params],
|
||||
do: ". Configure and invoke Plug.Parsers to set params based on the request"
|
||||
|
||||
defp hint(_), do: ""
|
||||
end
|
||||
341
phoenix/deps/plug/lib/plug/conn/utils.ex
Normal file
341
phoenix/deps/plug/lib/plug/conn/utils.ex
Normal file
@@ -0,0 +1,341 @@
|
||||
defmodule Plug.Conn.Utils do
|
||||
@moduledoc """
|
||||
Utilities for working with connection data
|
||||
"""
|
||||
|
||||
@type params :: %{optional(binary) => binary}
|
||||
|
||||
@upper ?A..?Z
|
||||
@lower ?a..?z
|
||||
@alpha ?0..?9
|
||||
@other [?., ?-, ?+, ?_]
|
||||
@space [?\s, ?\t]
|
||||
@specials ~c|()<>@,;:\\"/[]?={}|
|
||||
|
||||
@doc ~S"""
|
||||
Parses media types (with wildcards).
|
||||
|
||||
Type and subtype are case insensitive while the
|
||||
sensitiveness of params depends on their keys and
|
||||
therefore are not handled by this parser.
|
||||
|
||||
Returns:
|
||||
|
||||
* `{:ok, type, subtype, map_of_params}` if everything goes fine
|
||||
* `:error` if the media type isn't valid
|
||||
|
||||
## Examples
|
||||
|
||||
iex> media_type "text/plain"
|
||||
{:ok, "text", "plain", %{}}
|
||||
|
||||
iex> media_type "APPLICATION/vnd.ms-data+XML"
|
||||
{:ok, "application", "vnd.ms-data+xml", %{}}
|
||||
|
||||
iex> media_type "application/media_control+xml"
|
||||
{:ok, "application", "media_control+xml", %{}}
|
||||
|
||||
iex> media_type "text/*; q=1.0"
|
||||
{:ok, "text", "*", %{"q" => "1.0"}}
|
||||
|
||||
iex> media_type "*/*; q=1.0"
|
||||
{:ok, "*", "*", %{"q" => "1.0"}}
|
||||
|
||||
iex> media_type "x y"
|
||||
:error
|
||||
|
||||
iex> media_type "*/html"
|
||||
:error
|
||||
|
||||
iex> media_type "/"
|
||||
:error
|
||||
|
||||
iex> media_type "x/y z"
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec media_type(binary) :: {:ok, type :: binary, subtype :: binary, params} | :error
|
||||
def media_type(binary) when is_binary(binary) do
|
||||
case strip_spaces(binary) do
|
||||
"*/*" <> t -> mt_params(t, "*", "*")
|
||||
t -> mt_first(t, "")
|
||||
end
|
||||
end
|
||||
|
||||
defp mt_first(<<?/, t::binary>>, acc) when acc != "", do: mt_wildcard(t, acc)
|
||||
|
||||
defp mt_first(<<h, t::binary>>, acc) when h in @upper,
|
||||
do: mt_first(t, <<acc::binary, downcase_char(h)>>)
|
||||
|
||||
defp mt_first(<<h, t::binary>>, acc) when h in @lower or h in @alpha or h == ?-,
|
||||
do: mt_first(t, <<acc::binary, h>>)
|
||||
|
||||
defp mt_first(_, _acc), do: :error
|
||||
|
||||
defp mt_wildcard(<<?*, t::binary>>, first), do: mt_params(t, first, "*")
|
||||
defp mt_wildcard(t, first), do: mt_second(t, "", first)
|
||||
|
||||
defp mt_second(<<h, t::binary>>, acc, first) when h in @upper,
|
||||
do: mt_second(t, <<acc::binary, downcase_char(h)>>, first)
|
||||
|
||||
defp mt_second(<<h, t::binary>>, acc, first) when h in @lower or h in @alpha or h in @other,
|
||||
do: mt_second(t, <<acc::binary, h>>, first)
|
||||
|
||||
defp mt_second(t, acc, first), do: mt_params(t, first, acc)
|
||||
|
||||
defp mt_params(t, first, second) do
|
||||
case strip_spaces(t) do
|
||||
"" -> {:ok, first, second, %{}}
|
||||
";" <> t -> {:ok, first, second, params(t)}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Parses content type (without wildcards).
|
||||
|
||||
It is similar to `media_type/1` except wildcards are
|
||||
not accepted in the type nor in the subtype.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> content_type "x-sample/json; charset=utf-8"
|
||||
{:ok, "x-sample", "json", %{"charset" => "utf-8"}}
|
||||
|
||||
iex> content_type "x-sample/json ; charset=utf-8 ; foo=bar"
|
||||
{:ok, "x-sample", "json", %{"charset" => "utf-8", "foo" => "bar"}}
|
||||
|
||||
iex> content_type "\r\n text/plain;\r\n charset=utf-8\r\n"
|
||||
{:ok, "text", "plain", %{"charset" => "utf-8"}}
|
||||
|
||||
iex> content_type "text/plain"
|
||||
{:ok, "text", "plain", %{}}
|
||||
|
||||
iex> content_type "x/*"
|
||||
:error
|
||||
|
||||
iex> content_type "*/*"
|
||||
:error
|
||||
|
||||
iex> content_type "something"
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec content_type(binary) :: {:ok, type :: binary, subtype :: binary, params} | :error
|
||||
def content_type(binary) do
|
||||
case media_type(binary) do
|
||||
{:ok, _, "*", _} -> :error
|
||||
{:ok, _, _, _} = ok -> ok
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Parses headers parameters.
|
||||
|
||||
Keys are case insensitive and downcased,
|
||||
invalid key-value pairs are discarded.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> params("foo=bar")
|
||||
%{"foo" => "bar"}
|
||||
|
||||
iex> params(" foo=bar ")
|
||||
%{"foo" => "bar"}
|
||||
|
||||
iex> params("FOO=bar")
|
||||
%{"foo" => "bar"}
|
||||
|
||||
iex> params("Foo=bar; baz=BOING")
|
||||
%{"foo" => "bar", "baz" => "BOING"}
|
||||
|
||||
iex> params("foo=BAR ; wat")
|
||||
%{"foo" => "BAR"}
|
||||
|
||||
iex> params("foo=\"bar\"; baz=\"boing\"")
|
||||
%{"foo" => "bar", "baz" => "boing"}
|
||||
|
||||
iex> params("foo=\"bar;\"; baz=\"boing\"")
|
||||
%{"foo" => "bar;", "baz" => "boing"}
|
||||
|
||||
iex> params("=")
|
||||
%{}
|
||||
|
||||
iex> params(";")
|
||||
%{}
|
||||
|
||||
iex> params("foo=")
|
||||
%{}
|
||||
|
||||
"""
|
||||
@spec params(binary) :: params
|
||||
def params(t) do
|
||||
t
|
||||
|> split_semicolon("", [], false)
|
||||
|> Enum.reduce(%{}, ¶ms/2)
|
||||
end
|
||||
|
||||
defp params(param, acc) do
|
||||
case params_key(strip_spaces(param), "") do
|
||||
{k, v} -> Map.put(acc, k, v)
|
||||
false -> acc
|
||||
end
|
||||
end
|
||||
|
||||
defp params_key(<<?=, t::binary>>, acc) when acc != "", do: params_value(t, acc)
|
||||
|
||||
defp params_key(<<h, _::binary>>, _acc)
|
||||
when h in @specials or h in @space or h < 32 or h === 127,
|
||||
do: false
|
||||
|
||||
defp params_key(<<h, t::binary>>, acc), do: params_key(t, <<acc::binary, downcase_char(h)>>)
|
||||
defp params_key(<<>>, _acc), do: false
|
||||
|
||||
defp params_value(token, key) do
|
||||
case token(token) do
|
||||
false -> false
|
||||
value -> {key, value}
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Parses a value as defined in [RFC-1341](http://www.w3.org/Protocols/rfc1341/4_Content-Type.html).
|
||||
|
||||
For convenience, trims whitespace at the end of the token.
|
||||
Returns `false` if the token is invalid.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> token("foo")
|
||||
"foo"
|
||||
|
||||
iex> token("foo-bar")
|
||||
"foo-bar"
|
||||
|
||||
iex> token("<foo>")
|
||||
false
|
||||
|
||||
iex> token(~s["<foo>"])
|
||||
"<foo>"
|
||||
|
||||
iex> token(~S["<f\oo>\"<b\ar>"])
|
||||
"<foo>\"<bar>"
|
||||
|
||||
iex> token(~s["])
|
||||
false
|
||||
|
||||
iex> token("foo ")
|
||||
"foo"
|
||||
|
||||
iex> token("foo bar")
|
||||
false
|
||||
|
||||
iex> token("")
|
||||
false
|
||||
|
||||
iex> token(" ")
|
||||
""
|
||||
|
||||
"""
|
||||
@spec token(binary) :: binary | false
|
||||
def token(""), do: false
|
||||
def token(<<?", quoted::binary>>), do: quoted_token(quoted, "")
|
||||
def token(token), do: unquoted_token(token, "")
|
||||
|
||||
defp quoted_token(<<>>, _acc), do: false
|
||||
defp quoted_token(<<?", t::binary>>, acc), do: strip_spaces(t) == "" and acc
|
||||
defp quoted_token(<<?\\, h, t::binary>>, acc), do: quoted_token(t, <<acc::binary, h>>)
|
||||
defp quoted_token(<<h, t::binary>>, acc), do: quoted_token(t, <<acc::binary, h>>)
|
||||
|
||||
defp unquoted_token(<<>>, acc), do: acc
|
||||
defp unquoted_token("\r\n" <> t, acc), do: strip_spaces(t) == "" and acc
|
||||
defp unquoted_token(<<h, t::binary>>, acc) when h in @space, do: strip_spaces(t) == "" and acc
|
||||
|
||||
defp unquoted_token(<<h, _::binary>>, _acc) when h in @specials or h < 32 or h === 127,
|
||||
do: false
|
||||
|
||||
defp unquoted_token(<<h, t::binary>>, acc), do: unquoted_token(t, <<acc::binary, h>>)
|
||||
|
||||
@doc """
|
||||
Parses a comma-separated list of header values.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list("foo, bar")
|
||||
["foo", "bar"]
|
||||
|
||||
iex> list("foobar")
|
||||
["foobar"]
|
||||
|
||||
iex> list("")
|
||||
[]
|
||||
|
||||
iex> list("empties, , are,, filtered")
|
||||
["empties", "are", "filtered"]
|
||||
|
||||
iex> list("whitespace , , ,, is ,definitely,optional")
|
||||
["whitespace", "is", "definitely", "optional"]
|
||||
|
||||
"""
|
||||
@spec list(binary) :: [binary]
|
||||
def list(binary) do
|
||||
for elem <- :binary.split(binary, ",", [:global]),
|
||||
stripped = strip_spaces(elem),
|
||||
stripped != "",
|
||||
do: stripped
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates the given binary is valid UTF-8.
|
||||
"""
|
||||
@spec validate_utf8!(binary, module, binary) :: :ok | no_return
|
||||
def validate_utf8!(binary, exception, context)
|
||||
|
||||
def validate_utf8!(<<binary::binary>>, exception, context) do
|
||||
do_validate_utf8!(binary, exception, context)
|
||||
end
|
||||
|
||||
defp do_validate_utf8!(<<_::utf8, rest::bits>>, exception, context) do
|
||||
do_validate_utf8!(rest, exception, context)
|
||||
end
|
||||
|
||||
defp do_validate_utf8!(<<byte, _::bits>>, exception, context) do
|
||||
raise exception, "invalid UTF-8 on #{context}, got byte #{byte}"
|
||||
end
|
||||
|
||||
defp do_validate_utf8!(<<>>, _exception, _context) do
|
||||
:ok
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp strip_spaces("\r\n" <> t), do: strip_spaces(t)
|
||||
defp strip_spaces(<<h, t::binary>>) when h in [?\s, ?\t], do: strip_spaces(t)
|
||||
defp strip_spaces(t), do: trim_trailing(t)
|
||||
|
||||
defp trim_trailing(binary), do: trim_trailing(binary, byte_size(binary))
|
||||
|
||||
defp trim_trailing(binary, pos) do
|
||||
if pos > 0 and :binary.at(binary, pos - 1) in [?\s, ?\t] do
|
||||
trim_trailing(binary, pos - 1)
|
||||
else
|
||||
binary_part(binary, 0, pos)
|
||||
end
|
||||
end
|
||||
|
||||
defp downcase_char(char) when char in @upper, do: char + 32
|
||||
defp downcase_char(char), do: char
|
||||
|
||||
defp split_semicolon(<<>>, <<>>, acc, _), do: acc
|
||||
defp split_semicolon(<<>>, buffer, acc, _), do: [buffer | acc]
|
||||
|
||||
defp split_semicolon(<<?", rest::binary>>, buffer, acc, quoted?),
|
||||
do: split_semicolon(rest, <<buffer::binary, ?">>, acc, not quoted?)
|
||||
|
||||
defp split_semicolon(<<?;, rest::binary>>, buffer, acc, false),
|
||||
do: split_semicolon(rest, <<>>, [buffer | acc], false)
|
||||
|
||||
defp split_semicolon(<<char, rest::binary>>, buffer, acc, quoted?),
|
||||
do: split_semicolon(rest, <<buffer::binary, char>>, acc, quoted?)
|
||||
end
|
||||
38
phoenix/deps/plug/lib/plug/conn/wrapper_error.ex
Normal file
38
phoenix/deps/plug/lib/plug/conn/wrapper_error.ex
Normal file
@@ -0,0 +1,38 @@
|
||||
defmodule Plug.Conn.WrapperError do
|
||||
@moduledoc """
|
||||
Wraps the connection in an error which is meant
|
||||
to be handled upper in the stack.
|
||||
|
||||
Used by both `Plug.Debugger` and `Plug.ErrorHandler`.
|
||||
"""
|
||||
defexception [:conn, :kind, :reason, :stack]
|
||||
|
||||
def message(%{kind: kind, reason: reason, stack: stack}) do
|
||||
Exception.format_banner(kind, reason, stack)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reraises an error or a wrapped one.
|
||||
"""
|
||||
def reraise(%__MODULE__{stack: stack} = reason) do
|
||||
:erlang.raise(:error, reason, stack)
|
||||
end
|
||||
|
||||
@deprecated "Use reraise/1 or reraise/4 instead"
|
||||
def reraise(conn, kind, reason) do
|
||||
reraise(conn, kind, reason, [])
|
||||
end
|
||||
|
||||
def reraise(_conn, :error, %__MODULE__{stack: stack} = reason, _stack) do
|
||||
:erlang.raise(:error, reason, stack)
|
||||
end
|
||||
|
||||
def reraise(conn, :error, reason, stack) do
|
||||
wrapper = %__MODULE__{conn: conn, kind: :error, reason: reason, stack: stack}
|
||||
:erlang.raise(:error, wrapper, stack)
|
||||
end
|
||||
|
||||
def reraise(_conn, kind, reason, stack) do
|
||||
:erlang.raise(kind, reason, stack)
|
||||
end
|
||||
end
|
||||
448
phoenix/deps/plug/lib/plug/csrf_protection.ex
Normal file
448
phoenix/deps/plug/lib/plug/csrf_protection.ex
Normal file
@@ -0,0 +1,448 @@
|
||||
defmodule Plug.CSRFProtection do
|
||||
@moduledoc """
|
||||
Plug to protect from cross-site request forgery.
|
||||
|
||||
For this plug to work, it expects a session to have been
|
||||
previously fetched. It will then compare the token stored
|
||||
in the session with the one sent by the request to determine
|
||||
the validity of the request. For an invalid request the action
|
||||
taken is based on the `:with` option.
|
||||
|
||||
The token may be sent by the request either via the params
|
||||
with key "_csrf_token" or a header with name "x-csrf-token".
|
||||
|
||||
GET requests are not protected, as they should not have any
|
||||
side-effect or change your application state. JavaScript
|
||||
requests are an exception: by using a script tag, external
|
||||
websites can embed server-side generated JavaScript, which
|
||||
can leak information. For this reason, this plug also forbids
|
||||
any GET JavaScript request that is not XHR (or AJAX).
|
||||
|
||||
Note that it is recommended to enable CSRFProtection whenever
|
||||
a session is used, even for JSON requests. For example, Chrome
|
||||
had a bug that allowed POST requests to be triggered with
|
||||
arbitrary content-type, making JSON exploitable. More info:
|
||||
https://bugs.chromium.org/p/chromium/issues/detail?id=490015
|
||||
|
||||
Finally, we recommend developers to invoke `delete_csrf_token/0`
|
||||
every time after they log a user in, to avoid CSRF fixation
|
||||
attacks.
|
||||
|
||||
## Token generation
|
||||
|
||||
This plug won't generate tokens automatically. Instead, tokens
|
||||
will be generated only when required by calling `get_csrf_token/0`.
|
||||
In case you are generating the token for certain specific URL,
|
||||
you should use `get_csrf_token_for/1` as that will avoid tokens
|
||||
from being leaked to other applications.
|
||||
|
||||
Once a token is generated, it is cached in the process dictionary.
|
||||
The CSRF token is usually generated inside forms which may be
|
||||
isolated from `Plug.Conn`. Storing them in the process dictionary
|
||||
allows them to be generated as a side-effect only when necessary,
|
||||
becoming one of those rare situations where using the process
|
||||
dictionary is useful.
|
||||
|
||||
## Cross-host protection
|
||||
|
||||
If you are sending data to a full URI, such as `//subdomain.host.com/path`
|
||||
or `//external.com/path`, instead of a simple path such as `/path`, you may
|
||||
want to consider using `get_csrf_token_for/1`, as that will encode the host
|
||||
in the CSRF token. Once received, Plug will only consider the CSRF token to
|
||||
be valid if the `host` encoded in the token is the same as the one in
|
||||
`conn.host`.
|
||||
|
||||
Therefore, if you get a warning that the host does not match, it is either
|
||||
because someone is attempting to steal CSRF tokens or because you have a
|
||||
misconfigured host configuration.
|
||||
|
||||
For example, if you are running your application behind a proxy, the browser
|
||||
will send a request to the proxy with `www.example.com` but the proxy will
|
||||
request you using an internal IP. In such cases, it is common for proxies
|
||||
to attach information such as `"x-forwarded-host"` that contains the original
|
||||
host.
|
||||
|
||||
This may also happen on redirects. If you have a POST request to `foo.example.com`
|
||||
that redirects to `bar.example.com` with status 307, the token will contain a
|
||||
different host than the one in the request.
|
||||
|
||||
You can pass the `:allow_hosts` option to control any host that you may want
|
||||
to allow. The values in `:allow_hosts` may either be a full host name or a
|
||||
host suffix. For example: `["www.example.com", ".subdomain.example.com"]`
|
||||
will allow the exact host of `"www.example.com"` and any host that ends with
|
||||
`".subdomain.example.com"`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:session_key` - the name of the key in session to store the token under
|
||||
* `:allow_hosts` - a list with hosts to allow on cross-host tokens
|
||||
* `:with` - should be one of `:exception` or `:clear_session`. Defaults to
|
||||
`:exception`.
|
||||
* `:exception` - for invalid requests, this plug will raise
|
||||
`Plug.CSRFProtection.InvalidCSRFTokenError`.
|
||||
* `:clear_session` - for invalid requests, this plug will set an empty
|
||||
session for only this request. Also any changes to the session during this
|
||||
request will be ignored.
|
||||
|
||||
## Disabling
|
||||
|
||||
You may disable this plug by doing
|
||||
`Plug.Conn.put_private(conn, :plug_skip_csrf_protection, true)`. This was made
|
||||
available for disabling `Plug.CSRFProtection` in tests and not for dynamically
|
||||
skipping `Plug.CSRFProtection` in production code. If you want specific routes to
|
||||
skip `Plug.CSRFProtection`, then use a different stack of plugs for that route that
|
||||
does not include `Plug.CSRFProtection`.
|
||||
|
||||
## Examples
|
||||
|
||||
plug Plug.Session, ...
|
||||
plug :fetch_session
|
||||
plug Plug.CSRFProtection
|
||||
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
require Logger
|
||||
|
||||
alias Plug.Crypto.KeyGenerator
|
||||
alias Plug.Crypto.MessageVerifier
|
||||
|
||||
@behaviour Plug
|
||||
@unprotected_methods ~w(HEAD GET OPTIONS)
|
||||
@digest Base.url_encode64("HS256", padding: false) <> "."
|
||||
|
||||
# The token size value should not generate padding
|
||||
@token_size 18
|
||||
@encoded_token_size 24
|
||||
@double_encoded_token_size 32
|
||||
|
||||
defmodule InvalidCSRFTokenError do
|
||||
@moduledoc "Error raised when CSRF token is invalid."
|
||||
|
||||
message =
|
||||
"invalid CSRF (Cross Site Request Forgery) token, please make sure that:\n\n" <>
|
||||
" * The session cookie is being sent and session is loaded\n" <>
|
||||
" * The request include a valid '_csrf_token' param or 'x-csrf-token' header"
|
||||
|
||||
defexception message: message, plug_status: 403
|
||||
end
|
||||
|
||||
defmodule InvalidCrossOriginRequestError do
|
||||
@moduledoc "Error raised when non-XHR requests are used for Javascript responses."
|
||||
|
||||
message =
|
||||
"security warning: an embedded <script> tag on another site requested " <>
|
||||
"protected JavaScript (if you know what you're doing, disable forgery " <>
|
||||
"protection for this route)"
|
||||
|
||||
defexception message: message, plug_status: 403
|
||||
end
|
||||
|
||||
## API
|
||||
|
||||
@doc """
|
||||
Load CSRF state into the process dictionary.
|
||||
|
||||
This can be used to load CSRF state into another process.
|
||||
See `dump_state/0` and `dump_state_from_session/2` for dumping it.
|
||||
|
||||
## Examples
|
||||
|
||||
To dump the state from the current process and load into another one:
|
||||
|
||||
csrf_state = Plug.CSRFProtection.dump_state()
|
||||
secret_key_base = conn.secret_key_base
|
||||
|
||||
Task.async(fn ->
|
||||
Plug.CSRFProtection.load_state(secret_key_base, csrf_state)
|
||||
end)
|
||||
|
||||
If you have a session but the CSRF state was not loaded into the
|
||||
current process, you can dump the state from the session:
|
||||
|
||||
csrf_state = Plug.CSRFProtection.dump_state_from_session(session["_csrf_token"])
|
||||
|
||||
Task.async(fn ->
|
||||
Plug.CSRFProtection.load_state(secret_key_base, csrf_state)
|
||||
end)
|
||||
|
||||
"""
|
||||
def load_state(secret_key_base, csrf_state) when is_binary(csrf_state) or is_nil(csrf_state) do
|
||||
Process.put(:plug_unmasked_csrf_token, csrf_state)
|
||||
Process.put(:plug_csrf_token_per_host, %{secret_key_base: secret_key_base})
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dump CSRF state from the process dictionary.
|
||||
|
||||
This allows it to be loaded in another process.
|
||||
|
||||
See `load_state/2` for more information.
|
||||
"""
|
||||
def dump_state() do
|
||||
unmasked_csrf_token()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dumps the CSRF state from the session token.
|
||||
|
||||
It expects the value of `get_session(conn, "_csrf_token")`
|
||||
as input. It returns `nil` if the given token is not valid.
|
||||
"""
|
||||
def dump_state_from_session(session_token) do
|
||||
if is_binary(session_token) and byte_size(session_token) == @encoded_token_size do
|
||||
session_token
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates the `csrf_token` against the state.
|
||||
|
||||
This is the mechanism used by the Plug itself to match the token
|
||||
received in the request (via headers or parameters) with the state
|
||||
(typically stored in the session).
|
||||
"""
|
||||
def valid_state_and_csrf_token?(state, csrf_token) do
|
||||
with <<state_token::@encoded_token_size-binary>> <-
|
||||
state,
|
||||
<<user_token::@double_encoded_token_size-binary, mask::@encoded_token_size-binary>> <-
|
||||
csrf_token do
|
||||
valid_masked_token?(state_token, user_token, mask)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the CSRF token.
|
||||
|
||||
Generates a token and stores it in the process
|
||||
dictionary if one does not exist.
|
||||
"""
|
||||
def get_csrf_token do
|
||||
if token = Process.get(:plug_masked_csrf_token) do
|
||||
token
|
||||
else
|
||||
token = mask(unmasked_csrf_token())
|
||||
Process.put(:plug_masked_csrf_token, token)
|
||||
token
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the CSRF token for the associated URL (as a string or a URI struct).
|
||||
|
||||
If the URL has a host, a CSRF token that is tied to that
|
||||
host will be generated. If it is a relative path URL, a
|
||||
simple token emitted with `get_csrf_token/0` will be used.
|
||||
"""
|
||||
def get_csrf_token_for(url) when is_binary(url) do
|
||||
case url do
|
||||
<<"/">> -> get_csrf_token()
|
||||
<<"/", not_slash, _::binary>> when not_slash != ?/ -> get_csrf_token()
|
||||
_ -> get_csrf_token_for(URI.parse(url))
|
||||
end
|
||||
end
|
||||
|
||||
def get_csrf_token_for(%URI{host: nil}) do
|
||||
get_csrf_token()
|
||||
end
|
||||
|
||||
def get_csrf_token_for(%URI{host: host}) do
|
||||
case Process.get(:plug_csrf_token_per_host) do
|
||||
%{^host => token} ->
|
||||
token
|
||||
|
||||
%{secret_key_base: secret} = secrets ->
|
||||
unmasked = unmasked_csrf_token()
|
||||
message = generate_token() <> host
|
||||
key = KeyGenerator.generate(secret, unmasked)
|
||||
token = MessageVerifier.sign(message, key)
|
||||
Process.put(:plug_csrf_token_per_host, Map.put(secrets, host, token))
|
||||
token
|
||||
|
||||
_ ->
|
||||
raise "cannot generate CSRF token for a host because get_csrf_token_for/1 is invoked " <>
|
||||
"in a separate process than the one that started the request"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the CSRF token from the process dictionary.
|
||||
|
||||
This will force the token to be deleted once the response is sent.
|
||||
If you want to refresh the CSRF state, you can call `get_csrf_token/0`
|
||||
after `delete_csrf_token/0` to ensure a new token is generated.
|
||||
"""
|
||||
def delete_csrf_token do
|
||||
case Process.get(:plug_csrf_token_per_host) do
|
||||
%{secret_key_base: secret_key_base} ->
|
||||
Process.put(:plug_csrf_token_per_host, %{secret_key_base: secret_key_base})
|
||||
Process.put(:plug_unmasked_csrf_token, :delete)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
Process.delete(:plug_masked_csrf_token)
|
||||
end
|
||||
|
||||
## Plug
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
session_key = Keyword.get(opts, :session_key, "_csrf_token")
|
||||
mode = Keyword.get(opts, :with, :exception)
|
||||
allow_hosts = Keyword.get(opts, :allow_hosts, [])
|
||||
{session_key, mode, allow_hosts}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def call(conn, {session_key, mode, allow_hosts}) do
|
||||
csrf_token = dump_state_from_session(get_session(conn, session_key))
|
||||
load_state(conn.secret_key_base, csrf_token)
|
||||
|
||||
conn =
|
||||
cond do
|
||||
verified_request?(conn, csrf_token, allow_hosts) ->
|
||||
conn
|
||||
|
||||
mode == :clear_session ->
|
||||
conn |> configure_session(ignore: true) |> clear_session()
|
||||
|
||||
mode == :exception ->
|
||||
raise InvalidCSRFTokenError
|
||||
|
||||
true ->
|
||||
raise ArgumentError,
|
||||
"option :with should be one of :exception or :clear_session, got #{inspect(mode)}"
|
||||
end
|
||||
|
||||
register_before_send(conn, &ensure_same_origin_and_csrf_token!(&1, session_key, csrf_token))
|
||||
end
|
||||
|
||||
## Verification
|
||||
|
||||
defp verified_request?(conn, csrf_token, allow_hosts) do
|
||||
conn.method in @unprotected_methods ||
|
||||
valid_csrf_token?(conn, csrf_token, body_csrf_token(conn), allow_hosts) ||
|
||||
valid_csrf_token?(conn, csrf_token, header_csrf_token(conn), allow_hosts) ||
|
||||
skip_csrf_protection?(conn)
|
||||
end
|
||||
|
||||
defp header_csrf_token(conn), do: List.first(get_req_header(conn, "x-csrf-token"))
|
||||
|
||||
defp body_csrf_token(%{body_params: %{"_csrf_token" => csrf_token}}), do: csrf_token
|
||||
defp body_csrf_token(_), do: nil
|
||||
|
||||
defp valid_csrf_token?(
|
||||
_conn,
|
||||
<<csrf_token::@encoded_token_size-binary>>,
|
||||
<<user_token::@double_encoded_token_size-binary, mask::@encoded_token_size-binary>>,
|
||||
_allow_hosts
|
||||
) do
|
||||
valid_masked_token?(csrf_token, user_token, mask)
|
||||
end
|
||||
|
||||
defp valid_csrf_token?(
|
||||
conn,
|
||||
<<csrf_token::@encoded_token_size-binary>>,
|
||||
<<@digest, _::binary>> = signed_user_token,
|
||||
allow_hosts
|
||||
) do
|
||||
key = KeyGenerator.generate(conn.secret_key_base, csrf_token)
|
||||
|
||||
case MessageVerifier.verify(signed_user_token, key) do
|
||||
{:ok, <<_::@encoded_token_size-binary, host::binary>>} ->
|
||||
if host == conn.host or Enum.any?(allow_hosts, &allowed_host?(&1, host)) do
|
||||
true
|
||||
else
|
||||
Logger.error("""
|
||||
Plug.CSRFProtection generated token for host #{inspect(host)} \
|
||||
but the host for the current request is #{inspect(conn.host)}. \
|
||||
See Plug.CSRFProtection documentation for more information.
|
||||
""")
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
:error ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_csrf_token?(_conn, _csrf_token, _user_token, _allowed_host), do: false
|
||||
|
||||
defp valid_masked_token?(csrf_token, user_token, mask) do
|
||||
case Base.url_decode64(user_token) do
|
||||
{:ok, user_token} -> Plug.Crypto.masked_compare(csrf_token, user_token, mask)
|
||||
:error -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp allowed_host?("." <> _ = allowed, host), do: String.ends_with?(host, allowed)
|
||||
defp allowed_host?(allowed, host), do: allowed == host
|
||||
|
||||
## Before send
|
||||
|
||||
defp ensure_same_origin_and_csrf_token!(conn, session_key, csrf_token) do
|
||||
if cross_origin_js?(conn) do
|
||||
raise InvalidCrossOriginRequestError
|
||||
end
|
||||
|
||||
ensure_csrf_token(conn, session_key, csrf_token)
|
||||
end
|
||||
|
||||
defp cross_origin_js?(%Plug.Conn{method: "GET"} = conn),
|
||||
do: not skip_csrf_protection?(conn) and not xhr?(conn) and js_content_type?(conn)
|
||||
|
||||
defp cross_origin_js?(%Plug.Conn{}), do: false
|
||||
|
||||
defp js_content_type?(conn) do
|
||||
conn
|
||||
|> get_resp_header("content-type")
|
||||
|> Enum.any?(&String.starts_with?(&1, ~w(text/javascript application/javascript)))
|
||||
end
|
||||
|
||||
defp xhr?(conn) do
|
||||
"XMLHttpRequest" in get_req_header(conn, "x-requested-with")
|
||||
end
|
||||
|
||||
defp ensure_csrf_token(conn, session_key, csrf_token) do
|
||||
Process.delete(:plug_masked_csrf_token)
|
||||
|
||||
case Process.delete(:plug_unmasked_csrf_token) do
|
||||
^csrf_token -> conn
|
||||
nil -> conn
|
||||
:delete -> delete_session(conn, session_key)
|
||||
current -> put_session(conn, session_key, current)
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp skip_csrf_protection?(%Plug.Conn{private: %{plug_skip_csrf_protection: true}}), do: true
|
||||
defp skip_csrf_protection?(%Plug.Conn{}), do: false
|
||||
|
||||
defp mask(token) do
|
||||
mask = generate_token()
|
||||
Base.url_encode64(Plug.Crypto.mask(token, mask)) <> mask
|
||||
end
|
||||
|
||||
defp unmasked_csrf_token do
|
||||
case Process.get(:plug_unmasked_csrf_token) do
|
||||
token when is_binary(token) ->
|
||||
token
|
||||
|
||||
_ ->
|
||||
token = generate_token()
|
||||
Process.put(:plug_unmasked_csrf_token, token)
|
||||
token
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_token do
|
||||
Base.url_encode64(:crypto.strong_rand_bytes(@token_size))
|
||||
end
|
||||
end
|
||||
589
phoenix/deps/plug/lib/plug/debugger.ex
Normal file
589
phoenix/deps/plug/lib/plug/debugger.ex
Normal file
File diff suppressed because one or more lines are too long
122
phoenix/deps/plug/lib/plug/error_handler.ex
Normal file
122
phoenix/deps/plug/lib/plug/error_handler.ex
Normal file
@@ -0,0 +1,122 @@
|
||||
defmodule Plug.ErrorHandler do
|
||||
@moduledoc """
|
||||
A module to be used in your existing plugs in order to provide
|
||||
error handling.
|
||||
|
||||
defmodule AppRouter do
|
||||
use Plug.Router
|
||||
use Plug.ErrorHandler
|
||||
|
||||
plug :match
|
||||
plug :dispatch
|
||||
|
||||
get "/hello" do
|
||||
send_resp(conn, 200, "world")
|
||||
end
|
||||
|
||||
@impl Plug.ErrorHandler
|
||||
def handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do
|
||||
send_resp(conn, conn.status, "Something went wrong")
|
||||
end
|
||||
end
|
||||
|
||||
Once this module is used, a callback named `handle_errors/2` should
|
||||
be defined in your plug. This callback will receive the connection
|
||||
already updated with a proper status code for the given exception.
|
||||
The second argument is a map containing:
|
||||
|
||||
* the exception kind (`:throw`, `:error` or `:exit`),
|
||||
* the reason (an exception for errors or a term for others)
|
||||
* the stacktrace
|
||||
|
||||
After the callback is invoked, the error is re-raised.
|
||||
|
||||
It is advised to do as little work as possible when handling errors
|
||||
and avoid accessing data like parameters and session, as the parsing
|
||||
of those is what could have led the error to trigger in the first place.
|
||||
|
||||
Also notice that these pages are going to be shown in production. If
|
||||
you are looking for error handling to help during development, consider
|
||||
using `Plug.Debugger`.
|
||||
|
||||
**Note:** If this module is used with `Plug.Debugger`, it must be used
|
||||
after `Plug.Debugger`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Handle errors from plugs.
|
||||
|
||||
Called when an exception is raised during the processing of a plug.
|
||||
"""
|
||||
@callback handle_errors(Plug.Conn.t(), %{
|
||||
kind: :error | :throw | :exit,
|
||||
reason: Exception.t() | term(),
|
||||
stack: Exception.stacktrace()
|
||||
}) :: no_return()
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote location: :keep do
|
||||
@before_compile Plug.ErrorHandler
|
||||
|
||||
@behaviour Plug.ErrorHandler
|
||||
|
||||
@impl Plug.ErrorHandler
|
||||
def handle_errors(conn, assigns) do
|
||||
Plug.Conn.send_resp(conn, conn.status, "Something went wrong")
|
||||
end
|
||||
|
||||
defoverridable handle_errors: 2
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __before_compile__(_env) do
|
||||
quote location: :keep do
|
||||
defoverridable call: 2
|
||||
|
||||
def call(conn, opts) do
|
||||
try do
|
||||
super(conn, opts)
|
||||
rescue
|
||||
e in Plug.Conn.WrapperError ->
|
||||
%{conn: conn, kind: kind, reason: reason, stack: stack} = e
|
||||
Plug.ErrorHandler.__catch__(conn, kind, e, reason, stack, &handle_errors/2)
|
||||
catch
|
||||
kind, reason ->
|
||||
Plug.ErrorHandler.__catch__(
|
||||
conn,
|
||||
kind,
|
||||
reason,
|
||||
reason,
|
||||
__STACKTRACE__,
|
||||
&handle_errors/2
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@already_sent {:plug_conn, :sent}
|
||||
|
||||
@doc false
|
||||
def __catch__(conn, kind, reason, wrapped_reason, stack, handle_errors) do
|
||||
receive do
|
||||
@already_sent ->
|
||||
send(self(), @already_sent)
|
||||
after
|
||||
0 ->
|
||||
normalized_reason = Exception.normalize(kind, wrapped_reason, stack)
|
||||
|
||||
conn
|
||||
|> Plug.Conn.put_status(status(kind, normalized_reason))
|
||||
|> handle_errors.(%{kind: kind, reason: normalized_reason, stack: stack})
|
||||
end
|
||||
|
||||
:erlang.raise(kind, reason, stack)
|
||||
end
|
||||
|
||||
defp status(:error, error), do: Plug.Exception.status(error)
|
||||
defp status(:throw, _throw), do: 500
|
||||
defp status(:exit, _exit), do: 500
|
||||
end
|
||||
70
phoenix/deps/plug/lib/plug/exceptions.ex
Normal file
70
phoenix/deps/plug/lib/plug/exceptions.ex
Normal file
@@ -0,0 +1,70 @@
|
||||
# This file defines the Plug.Exception protocol and
|
||||
# the exceptions that implement such protocol.
|
||||
|
||||
defprotocol Plug.Exception do
|
||||
@moduledoc """
|
||||
A protocol that extends exceptions to be status-code aware.
|
||||
|
||||
By default, it looks for an implementation of the protocol,
|
||||
otherwise checks if the exception has the `:plug_status` field
|
||||
or simply returns 500.
|
||||
"""
|
||||
|
||||
@fallback_to_any true
|
||||
|
||||
@type action :: %{label: String.t(), handler: {module(), atom(), list()}}
|
||||
|
||||
@doc """
|
||||
Receives an exception and returns its HTTP status code.
|
||||
"""
|
||||
@spec status(t) :: Plug.Conn.status()
|
||||
def status(exception)
|
||||
|
||||
@doc """
|
||||
Receives an exception and returns the possible actions that could be triggered for that error.
|
||||
Should return a list of actions in the following structure:
|
||||
|
||||
%{
|
||||
label: "Text that will be displayed in the button",
|
||||
handler: {Module, :function, [args]}
|
||||
}
|
||||
|
||||
Where:
|
||||
|
||||
* `label` a string/binary that names this action
|
||||
* `handler` a MFArgs that will be executed when this action is triggered
|
||||
|
||||
It will be rendered in the `Plug.Debugger` generated error page as buttons showing the `label`
|
||||
that upon pressing executes the MFArgs defined in the `handler`.
|
||||
|
||||
## Examples
|
||||
|
||||
defimpl Plug.Exception, for: ActionableExample do
|
||||
def actions(_), do: [%{label: "Print HI", handler: {IO, :puts, ["Hi!"]}}]
|
||||
end
|
||||
"""
|
||||
@spec actions(t) :: [action()]
|
||||
def actions(exception)
|
||||
end
|
||||
|
||||
defimpl Plug.Exception, for: Any do
|
||||
def status(%{plug_status: status}), do: Plug.Conn.Status.code(status)
|
||||
def status(_), do: 500
|
||||
def actions(_exception), do: []
|
||||
end
|
||||
|
||||
defmodule Plug.BadRequestError do
|
||||
@moduledoc """
|
||||
The request will not be processed due to a client error.
|
||||
"""
|
||||
|
||||
defexception message: "could not process the request due to client error", plug_status: 400
|
||||
end
|
||||
|
||||
defmodule Plug.TimeoutError do
|
||||
@moduledoc """
|
||||
Timeout while waiting for the request.
|
||||
"""
|
||||
|
||||
defexception message: "timeout while waiting for request data", plug_status: 408
|
||||
end
|
||||
20
phoenix/deps/plug/lib/plug/head.ex
Normal file
20
phoenix/deps/plug/lib/plug/head.ex
Normal file
@@ -0,0 +1,20 @@
|
||||
defmodule Plug.Head do
|
||||
@moduledoc """
|
||||
A Plug to convert `HEAD` requests to `GET` requests.
|
||||
|
||||
## Examples
|
||||
|
||||
plug Plug.Head
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
alias Plug.Conn
|
||||
|
||||
@impl true
|
||||
def init([]), do: []
|
||||
|
||||
@impl true
|
||||
def call(%Conn{method: "HEAD"} = conn, []), do: %{conn | method: "GET"}
|
||||
def call(conn, []), do: conn
|
||||
end
|
||||
81
phoenix/deps/plug/lib/plug/html.ex
Normal file
81
phoenix/deps/plug/lib/plug/html.ex
Normal file
@@ -0,0 +1,81 @@
|
||||
defmodule Plug.HTML do
|
||||
@moduledoc """
|
||||
Conveniences for generating HTML.
|
||||
"""
|
||||
|
||||
@doc ~S"""
|
||||
Escapes the given HTML to string.
|
||||
|
||||
iex> Plug.HTML.html_escape("foo")
|
||||
"foo"
|
||||
|
||||
iex> Plug.HTML.html_escape("<foo>")
|
||||
"<foo>"
|
||||
|
||||
iex> Plug.HTML.html_escape("quotes: \" & \'")
|
||||
"quotes: " & '"
|
||||
"""
|
||||
@spec html_escape(String.t()) :: String.t()
|
||||
def html_escape(data) when is_binary(data) do
|
||||
IO.iodata_to_binary(to_iodata(data, 0, data, []))
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Escapes the given HTML to iodata.
|
||||
|
||||
iex> Plug.HTML.html_escape_to_iodata("foo")
|
||||
"foo"
|
||||
|
||||
iex> Plug.HTML.html_escape_to_iodata("<foo>")
|
||||
[[[] | "<"], "foo" | ">"]
|
||||
|
||||
iex> Plug.HTML.html_escape_to_iodata("quotes: \" & \'")
|
||||
[[[[], "quotes: " | """], " " | "&"], " " | "'"]
|
||||
|
||||
"""
|
||||
@spec html_escape_to_iodata(String.t()) :: iodata
|
||||
def html_escape_to_iodata(data) when is_binary(data) do
|
||||
to_iodata(data, 0, data, [])
|
||||
end
|
||||
|
||||
escapes = [
|
||||
{?<, "<"},
|
||||
{?>, ">"},
|
||||
{?&, "&"},
|
||||
{?", """},
|
||||
{?', "'"}
|
||||
]
|
||||
|
||||
for {match, insert} <- escapes do
|
||||
defp to_iodata(<<unquote(match), rest::bits>>, skip, original, acc) do
|
||||
to_iodata(rest, skip + 1, original, [acc | unquote(insert)])
|
||||
end
|
||||
end
|
||||
|
||||
defp to_iodata(<<_char, rest::bits>>, skip, original, acc) do
|
||||
to_iodata(rest, skip, original, acc, 1)
|
||||
end
|
||||
|
||||
defp to_iodata(<<>>, _skip, _original, acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
for {match, insert} <- escapes do
|
||||
defp to_iodata(<<unquote(match), rest::bits>>, skip, original, acc, len) do
|
||||
part = binary_part(original, skip, len)
|
||||
to_iodata(rest, skip + len + 1, original, [acc, part | unquote(insert)])
|
||||
end
|
||||
end
|
||||
|
||||
defp to_iodata(<<_char, rest::bits>>, skip, original, acc, len) do
|
||||
to_iodata(rest, skip, original, acc, len + 1)
|
||||
end
|
||||
|
||||
defp to_iodata(<<>>, 0, original, _acc, _len) do
|
||||
original
|
||||
end
|
||||
|
||||
defp to_iodata(<<>>, skip, original, acc, len) do
|
||||
[acc | binary_part(original, skip, len)]
|
||||
end
|
||||
end
|
||||
56
phoenix/deps/plug/lib/plug/logger.ex
Normal file
56
phoenix/deps/plug/lib/plug/logger.ex
Normal file
@@ -0,0 +1,56 @@
|
||||
defmodule Plug.Logger do
|
||||
@moduledoc """
|
||||
A plug for logging basic request information in the format:
|
||||
|
||||
GET /index.html
|
||||
Sent 200 in 572ms
|
||||
|
||||
To use it, just plug it into the desired module.
|
||||
|
||||
plug Plug.Logger, log: :debug
|
||||
|
||||
## Options
|
||||
|
||||
* `:log` - The log level at which this plug should log its request info.
|
||||
Default is `:info`.
|
||||
The [list of supported levels](https://hexdocs.pm/logger/Logger.html#module-levels)
|
||||
is available in the `Logger` documentation.
|
||||
|
||||
"""
|
||||
|
||||
require Logger
|
||||
alias Plug.Conn
|
||||
@behaviour Plug
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
Keyword.get(opts, :log, :info)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def call(conn, level) do
|
||||
Logger.log(level, fn ->
|
||||
[conn.method, ?\s, conn.request_path]
|
||||
end)
|
||||
|
||||
start = System.monotonic_time()
|
||||
|
||||
Conn.register_before_send(conn, fn conn ->
|
||||
Logger.log(level, fn ->
|
||||
stop = System.monotonic_time()
|
||||
diff = System.convert_time_unit(stop - start, :native, :microsecond)
|
||||
status = Integer.to_string(conn.status)
|
||||
|
||||
[connection_type(conn), ?\s, status, " in ", formatted_diff(diff)]
|
||||
end)
|
||||
|
||||
conn
|
||||
end)
|
||||
end
|
||||
|
||||
defp formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"]
|
||||
defp formatted_diff(diff), do: [Integer.to_string(diff), "µs"]
|
||||
|
||||
defp connection_type(%{state: :set_chunked}), do: "Chunked"
|
||||
defp connection_type(_), do: "Sent"
|
||||
end
|
||||
69
phoenix/deps/plug/lib/plug/method_override.ex
Normal file
69
phoenix/deps/plug/lib/plug/method_override.ex
Normal file
@@ -0,0 +1,69 @@
|
||||
defmodule Plug.MethodOverride do
|
||||
@moduledoc """
|
||||
This plug overrides the request's `POST` method with the method defined in
|
||||
the `_method` request parameter.
|
||||
|
||||
The `POST` method can be overridden only by these HTTP methods:
|
||||
|
||||
* `PUT`
|
||||
* `PATCH`
|
||||
* `DELETE`
|
||||
|
||||
This plug only replaces the request method if the `_method` request
|
||||
parameter is a string. If the `_method` request parameter is not a string,
|
||||
the request method is not changed.
|
||||
|
||||
> #### Parse Body Parameters First {: .info}
|
||||
>
|
||||
> This plug expects the body parameters to be **already fetched and
|
||||
> parsed**. Those can be fetched with `Plug.Parsers`.
|
||||
|
||||
This plug doesn't accept any options.
|
||||
|
||||
To recap, here are all the conditions that the request must meet in order
|
||||
for this plug to replace the `:method` field in the `Plug.Conn`:
|
||||
|
||||
1. The conn's request `:method` must be `POST`.
|
||||
1. The conn's `:body_params` must have been fetched already (for example,
|
||||
with `Plug.Parsers`).
|
||||
1. The conn's `:body_params` must have a `_method` field that is a string
|
||||
and whose value is `"PUT"`, `"PATCH"`, or `"DELETE"` (case insensitive).
|
||||
|
||||
## Usage
|
||||
|
||||
# You'll need to fetch and parse parameters first, for example:
|
||||
# plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json]
|
||||
|
||||
plug Plug.MethodOverride
|
||||
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
@allowed_methods ~w(DELETE PUT PATCH)
|
||||
|
||||
@impl true
|
||||
def init([]), do: []
|
||||
|
||||
@impl true
|
||||
def call(%Plug.Conn{method: "POST", body_params: body_params} = conn, []),
|
||||
do: override_method(conn, body_params)
|
||||
|
||||
def call(%Plug.Conn{} = conn, []), do: conn
|
||||
|
||||
defp override_method(conn, %Plug.Conn.Unfetched{}) do
|
||||
# Just skip it because maybe it is a content-type that
|
||||
# we could not parse as parameters (for example, text/gps)
|
||||
conn
|
||||
end
|
||||
|
||||
defp override_method(%Plug.Conn{} = conn, body_params) do
|
||||
with method when is_binary(method) <- body_params["_method"] || "",
|
||||
method = String.upcase(method, :ascii),
|
||||
true <- method in @allowed_methods do
|
||||
%{conn | method: method}
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
end
|
||||
67
phoenix/deps/plug/lib/plug/mime.ex
Normal file
67
phoenix/deps/plug/lib/plug/mime.ex
Normal file
@@ -0,0 +1,67 @@
|
||||
defmodule Plug.MIME do
|
||||
@moduledoc false
|
||||
|
||||
if Application.compile_env(:plug, :mimes) do
|
||||
IO.puts(:stderr, """
|
||||
warning: you have set the :mimes configuration for the :plug
|
||||
application but it is no longer supported. Instead of:
|
||||
|
||||
config :plug, :mimes, %{...}
|
||||
|
||||
You must write:
|
||||
|
||||
config :mime, :types, %{...}
|
||||
|
||||
After adding the configuration, MIME needs to be recompiled.
|
||||
If you are using mix, it can be done with:
|
||||
|
||||
$ mix deps.clean mime --build
|
||||
$ mix deps.get
|
||||
|
||||
""")
|
||||
end
|
||||
|
||||
@deprecated "Use MIME.extensions(type) != [] instead"
|
||||
def valid?(type) do
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"Plug.MIME.valid?/1 is deprecated, please use MIME.extensions(type) != [] instead\n" <>
|
||||
Exception.format_stacktrace()
|
||||
)
|
||||
|
||||
MIME.extensions(type) != []
|
||||
end
|
||||
|
||||
@deprecated "Use MIME.extensions/1 instead"
|
||||
def extensions(type) do
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"Plug.MIME.extensions/1 is deprecated, please use MIME.extensions/1 instead\n" <>
|
||||
Exception.format_stacktrace()
|
||||
)
|
||||
|
||||
MIME.extensions(type)
|
||||
end
|
||||
|
||||
@deprecated "Use MIME.type/1 instead"
|
||||
def type(file_extension) do
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"Plug.MIME.type/1 is deprecated, please use MIME.type/1 instead\n" <>
|
||||
Exception.format_stacktrace()
|
||||
)
|
||||
|
||||
MIME.type(file_extension)
|
||||
end
|
||||
|
||||
@deprecated "Use MIME.from_path/1 instead"
|
||||
def path(path) do
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"Plug.MIME.path/1 is deprecated, please use MIME.from_path/1 instead\n" <>
|
||||
Exception.format_stacktrace()
|
||||
)
|
||||
|
||||
MIME.from_path(path)
|
||||
end
|
||||
end
|
||||
388
phoenix/deps/plug/lib/plug/parsers.ex
Normal file
388
phoenix/deps/plug/lib/plug/parsers.ex
Normal file
@@ -0,0 +1,388 @@
|
||||
defmodule Plug.Parsers do
|
||||
defmodule RequestTooLargeError do
|
||||
@moduledoc """
|
||||
Error raised when the request is too large.
|
||||
"""
|
||||
|
||||
defexception message:
|
||||
"the request is too large. If you are willing to process " <>
|
||||
"larger requests, please give a :length to Plug.Parsers",
|
||||
plug_status: 413
|
||||
end
|
||||
|
||||
defmodule UnsupportedMediaTypeError do
|
||||
@moduledoc """
|
||||
Error raised when the request body cannot be parsed.
|
||||
"""
|
||||
|
||||
defexception media_type: nil, plug_status: 415
|
||||
|
||||
def message(exception) do
|
||||
"unsupported media type #{exception.media_type}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadEncodingError do
|
||||
@moduledoc """
|
||||
Raised when the request body contains bad encoding.
|
||||
"""
|
||||
|
||||
defexception message: nil, plug_status: 400
|
||||
end
|
||||
|
||||
defmodule ParseError do
|
||||
@moduledoc """
|
||||
Error raised when the request body is malformed.
|
||||
"""
|
||||
|
||||
defexception exception: nil, plug_status: 400
|
||||
|
||||
def message(%{exception: exception}) do
|
||||
"malformed request, a #{inspect(exception.__struct__)} exception was raised " <>
|
||||
"with message #{inspect(Exception.message(exception))}"
|
||||
end
|
||||
end
|
||||
|
||||
@moduledoc """
|
||||
A plug for parsing the request body.
|
||||
|
||||
It invokes a list of `:parsers`, which are activated based on the
|
||||
request content-type. Custom parsers are also supported by defining
|
||||
a module that implements the behaviour defined by this module.
|
||||
|
||||
Once a connection goes through this plug, it will have `:body_params`
|
||||
set to the map of params parsed by one of the parsers listed in
|
||||
`:parsers` and `:params` set to the result of merging the `:body_params`
|
||||
and `:query_params`. In case `:query_params` have not yet been parsed,
|
||||
`Plug.Conn.fetch_query_params/2` is automatically invoked.
|
||||
|
||||
This plug will raise `Plug.Parsers.UnsupportedMediaTypeError` by default if
|
||||
the request cannot be parsed by any of the given types and the MIME type has
|
||||
not been explicitly accepted with the `:pass` option.
|
||||
|
||||
`Plug.Parsers.RequestTooLargeError` will be raised if the request goes over
|
||||
the given limit. The default length is 8MB and it can be customized by passing
|
||||
the `:length` option to the Plug. `:read_timeout` and `:read_length`, as
|
||||
described by `Plug.Conn.read_body/2`, are also supported.
|
||||
|
||||
Parsers may raise a `Plug.Parsers.ParseError` if the request has a malformed
|
||||
body.
|
||||
|
||||
This plug only parses the body if the request method is one of the following:
|
||||
|
||||
* `POST`
|
||||
* `PUT`
|
||||
* `PATCH`
|
||||
* `DELETE`
|
||||
|
||||
For requests with a different request method, this plug will only fetch the
|
||||
query params.
|
||||
|
||||
## Options
|
||||
|
||||
* `:parsers` - a list of modules or atoms of built-in parsers to be
|
||||
invoked for parsing. These modules need to implement the behaviour
|
||||
outlined in this module.
|
||||
|
||||
* `:pass` - an optional list of MIME type strings that are allowed
|
||||
to pass through. Any mime not handled by a parser and not explicitly
|
||||
listed in `:pass` will `raise UnsupportedMediaTypeError`. For example:
|
||||
|
||||
* `["*/*"]` - never raises
|
||||
* `["text/html", "application/*"]` - doesn't raise for those values
|
||||
* `[]` - always raises (default)
|
||||
|
||||
* `:query_string_length` - the maximum allowed size for query strings
|
||||
|
||||
* `:validate_utf8` - boolean that tells whether or not we want to
|
||||
validate that parsed binaries are utf8 strings.
|
||||
|
||||
* `:body_reader` - an optional replacement (or wrapper) for
|
||||
`Plug.Conn.read_body/2` to provide a function that gives access to the
|
||||
raw body before it is parsed and discarded. It is in the standard format
|
||||
of `{Module, :function, [args]}` (MFA) and defaults to
|
||||
`{Plug.Conn, :read_body, []}`. Note that this option is not used by
|
||||
`Plug.Parsers.MULTIPART` which relies instead on other functions defined
|
||||
in `Plug.Conn`.
|
||||
|
||||
All other options given to this Plug are forwarded to the parsers.
|
||||
|
||||
## Examples
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart],
|
||||
pass: ["text/*"]
|
||||
|
||||
Any other option given to Plug.Parsers is forwarded to the underlying
|
||||
parsers. Therefore, you can use a JSON parser and pass the `:json_decoder`
|
||||
option at the root:
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, :json],
|
||||
json_decoder: Jason
|
||||
|
||||
Or directly to the parser itself:
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, {:json, json_decoder: Jason}]
|
||||
|
||||
It is also possible to pass the `:json_decoder` as a `{module, function, args}` tuple,
|
||||
useful for passing options to the JSON decoder:
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:json],
|
||||
json_decoder: {Jason, :decode!, [[floats: :decimals]]}
|
||||
|
||||
A common set of shared options given to Plug.Parsers is `:length`,
|
||||
`:read_length` and `:read_timeout`, which customizes the maximum
|
||||
request length you want to accept. For example, to support file
|
||||
uploads, you can do:
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart],
|
||||
length: 20_000_000
|
||||
|
||||
However, the above will increase the maximum length of all request
|
||||
types. If you want to increase the limit only for multipart requests
|
||||
(which is typically the ones used for file uploads), you can do:
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [
|
||||
:urlencoded,
|
||||
{:multipart, length: 20_000_000} # Increase to 20MB max upload
|
||||
]
|
||||
|
||||
## Built-in parsers
|
||||
|
||||
Plug ships with the following parsers:
|
||||
|
||||
* `Plug.Parsers.URLENCODED` - parses `application/x-www-form-urlencoded`
|
||||
requests (can be used as `:urlencoded` as well in the `:parsers` option)
|
||||
* `Plug.Parsers.MULTIPART` - parses `multipart/form-data` and
|
||||
`multipart/mixed` requests (can be used as `:multipart` as well in the
|
||||
`:parsers` option)
|
||||
* `Plug.Parsers.JSON` - parses `application/json` requests with the given
|
||||
`:json_decoder` (can be used as `:json` as well in the `:parsers` option)
|
||||
|
||||
## File handling
|
||||
|
||||
If a file is uploaded via any of the parsers, Plug will
|
||||
stream the uploaded contents to a file in a temporary directory in order to
|
||||
avoid loading the whole file into memory. For such, the `:plug` application
|
||||
needs to be started in order for file uploads to work. More details on how the
|
||||
uploaded file is handled can be found in the documentation for `Plug.Upload`.
|
||||
|
||||
When a file is uploaded, the request parameter that identifies that file will
|
||||
be a `Plug.Upload` struct with information about the uploaded file (e.g.
|
||||
filename and content type) and about where the file is stored.
|
||||
|
||||
The temporary directory where files are streamed to can be customized by
|
||||
setting the `PLUG_TMPDIR` environment variable on the host system. If
|
||||
`PLUG_TMPDIR` isn't set, Plug will look at some environment
|
||||
variables which usually hold the value of the system's temporary directory
|
||||
(like `TMPDIR` or `TMP`). If no value is found in any of those variables,
|
||||
`/tmp` is used as a default.
|
||||
|
||||
## Custom body reader
|
||||
|
||||
Sometimes you may want to customize how a parser reads the body from the
|
||||
connection. For example, you may want to cache the body to perform verification
|
||||
later, such as HTTP Signature Verification. This can be achieved with a custom
|
||||
body reader that would read the body and store it in the connection, such as:
|
||||
|
||||
defmodule CacheBodyReader do
|
||||
def read_body(conn, opts) do
|
||||
with {:ok, body, conn} <- Plug.Conn.read_body(conn, opts) do
|
||||
conn = update_in(conn.assigns[:raw_body], &[body | &1 || []])
|
||||
{:ok, body, conn}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
which could then be set as:
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, :json],
|
||||
pass: ["text/*"],
|
||||
body_reader: {CacheBodyReader, :read_body, []},
|
||||
json_decoder: Jason
|
||||
|
||||
"""
|
||||
|
||||
alias Plug.Conn
|
||||
|
||||
@callback init(opts :: Keyword.t()) :: Plug.opts()
|
||||
|
||||
@doc """
|
||||
Attempts to parse the connection's request body given the content-type type,
|
||||
subtype, and its parameters.
|
||||
|
||||
The arguments are:
|
||||
|
||||
* the `Plug.Conn` connection
|
||||
* `type`, the content-type type (e.g., `"x-sample"` for the
|
||||
`"x-sample/json"` content-type)
|
||||
* `subtype`, the content-type subtype (e.g., `"json"` for the
|
||||
`"x-sample/json"` content-type)
|
||||
* `params`, the content-type parameters (e.g., `%{"foo" => "bar"}`
|
||||
for the `"text/plain; foo=bar"` content-type)
|
||||
|
||||
This function should return:
|
||||
|
||||
* `{:ok, body_params, conn}` if the parser is able to handle the given
|
||||
content-type; `body_params` should be a map
|
||||
* `{:next, conn}` if the next parser should be invoked
|
||||
* `{:error, :too_large, conn}` if the request goes over the given limit
|
||||
|
||||
"""
|
||||
@callback parse(
|
||||
conn :: Conn.t(),
|
||||
type :: binary,
|
||||
subtype :: binary,
|
||||
params :: Conn.Utils.params(),
|
||||
opts :: Plug.opts()
|
||||
) ::
|
||||
{:ok, Conn.params(), Conn.t()}
|
||||
| {:error, :too_large, Conn.t()}
|
||||
| {:next, Conn.t()}
|
||||
|
||||
@behaviour Plug
|
||||
@methods ~w(POST PUT PATCH DELETE)
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
{parsers, opts} = Keyword.pop(opts, :parsers)
|
||||
{pass, opts} = Keyword.pop(opts, :pass, [])
|
||||
{query_string_length, opts} = Keyword.pop(opts, :query_string_length, 1_000_000)
|
||||
validate_utf8 = Keyword.get(opts, :validate_utf8, true)
|
||||
|
||||
unless parsers do
|
||||
raise ArgumentError, "Plug.Parsers expects a set of parsers to be given in :parsers"
|
||||
end
|
||||
|
||||
{convert_parsers(parsers, opts), pass, query_string_length, validate_utf8}
|
||||
end
|
||||
|
||||
defp convert_parsers(parsers, root_opts) do
|
||||
for parser <- parsers do
|
||||
{parser, opts} =
|
||||
case parser do
|
||||
{parser, opts} when is_atom(parser) and is_list(opts) ->
|
||||
{parser, Keyword.merge(root_opts, opts)}
|
||||
|
||||
parser when is_atom(parser) ->
|
||||
{parser, root_opts}
|
||||
end
|
||||
|
||||
module =
|
||||
case Atom.to_string(parser) do
|
||||
"Elixir." <> _ -> parser
|
||||
reference -> Module.concat(Plug.Parsers, String.upcase(reference))
|
||||
end
|
||||
|
||||
{module, module.init(opts)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def call(%{method: method, body_params: %Plug.Conn.Unfetched{}} = conn, options)
|
||||
when method in @methods do
|
||||
{parsers, pass, query_string_length, validate_utf8} = options
|
||||
%{req_headers: req_headers} = conn
|
||||
|
||||
conn =
|
||||
Conn.fetch_query_params(conn,
|
||||
length: query_string_length,
|
||||
validate_utf8: validate_utf8
|
||||
)
|
||||
|
||||
case List.keyfind(req_headers, "content-type", 0) do
|
||||
{"content-type", ct} ->
|
||||
case Conn.Utils.content_type(ct) do
|
||||
{:ok, type, subtype, params} ->
|
||||
reduce(
|
||||
conn,
|
||||
parsers,
|
||||
type,
|
||||
subtype,
|
||||
params,
|
||||
pass,
|
||||
query_string_length,
|
||||
validate_utf8
|
||||
)
|
||||
|
||||
:error ->
|
||||
reduce(conn, parsers, ct, "", %{}, pass, query_string_length, validate_utf8)
|
||||
end
|
||||
|
||||
_ ->
|
||||
{conn, params} = merge_params(conn, %{}, query_string_length, validate_utf8)
|
||||
|
||||
%{conn | params: params, body_params: %{}}
|
||||
end
|
||||
end
|
||||
|
||||
def call(%{body_params: body_params} = conn, {_, _, query_string_length, validate_utf8}) do
|
||||
body_params = make_empty_if_unfetched(body_params)
|
||||
{conn, params} = merge_params(conn, body_params, query_string_length, validate_utf8)
|
||||
%{conn | params: params, body_params: body_params}
|
||||
end
|
||||
|
||||
defp reduce(
|
||||
conn,
|
||||
[{parser, options} | rest],
|
||||
type,
|
||||
subtype,
|
||||
params,
|
||||
pass,
|
||||
query_string_length,
|
||||
validate_utf8
|
||||
) do
|
||||
case parser.parse(conn, type, subtype, params, options) do
|
||||
{:ok, body, conn} ->
|
||||
{conn, params} = merge_params(conn, body, query_string_length, validate_utf8)
|
||||
%{conn | params: params, body_params: body}
|
||||
|
||||
{:next, conn} ->
|
||||
reduce(conn, rest, type, subtype, params, pass, query_string_length, validate_utf8)
|
||||
|
||||
{:error, :too_large, _conn} ->
|
||||
raise RequestTooLargeError
|
||||
end
|
||||
end
|
||||
|
||||
defp reduce(conn, [], type, subtype, _params, pass, query_string_length, validate_utf8) do
|
||||
if accepted_mime?(type, subtype, pass) do
|
||||
{conn, params} = merge_params(conn, %{}, query_string_length, validate_utf8)
|
||||
%{conn | params: params}
|
||||
else
|
||||
raise UnsupportedMediaTypeError, media_type: "#{type}/#{subtype}"
|
||||
end
|
||||
end
|
||||
|
||||
defp accepted_mime?(_type, _subtype, ["*/*"]),
|
||||
do: true
|
||||
|
||||
defp accepted_mime?(type, subtype, pass),
|
||||
do: "#{type}/#{subtype}" in pass || "#{type}/*" in pass
|
||||
|
||||
defp merge_params(conn, body_params, query_string_length, validate_utf8) do
|
||||
%{params: params, path_params: path_params} = conn
|
||||
params = make_empty_if_unfetched(params)
|
||||
|
||||
conn =
|
||||
Plug.Conn.fetch_query_params(conn,
|
||||
length: query_string_length,
|
||||
validate_utf8: validate_utf8
|
||||
)
|
||||
|
||||
{conn,
|
||||
conn.query_params
|
||||
|> Map.merge(params)
|
||||
|> Map.merge(body_params)
|
||||
|> Map.merge(path_params)}
|
||||
end
|
||||
|
||||
defp make_empty_if_unfetched(%Plug.Conn.Unfetched{}), do: %{}
|
||||
defp make_empty_if_unfetched(params), do: params
|
||||
end
|
||||
116
phoenix/deps/plug/lib/plug/parsers/json.ex
Normal file
116
phoenix/deps/plug/lib/plug/parsers/json.ex
Normal file
@@ -0,0 +1,116 @@
|
||||
defmodule Plug.Parsers.JSON do
|
||||
@moduledoc """
|
||||
Parses JSON request body.
|
||||
|
||||
JSON documents that aren't maps (arrays, strings, numbers, etc) are parsed
|
||||
into a `"_json"` key to allow proper param merging.
|
||||
|
||||
An empty request body is parsed as an empty map.
|
||||
|
||||
## Options
|
||||
|
||||
All options supported by `Plug.Conn.read_body/2` are also supported here.
|
||||
They are repeated here for convenience:
|
||||
|
||||
* `:length` - sets the maximum number of bytes to read from the request,
|
||||
defaults to 8_000_000 bytes
|
||||
* `:read_length` - sets the amount of bytes to read at one time from the
|
||||
underlying socket to fill the chunk, defaults to 1_000_000 bytes
|
||||
* `:read_timeout` - sets the timeout for each socket read, defaults to
|
||||
15_000ms
|
||||
|
||||
So by default, `Plug.Parsers` will read 1_000_000 bytes at a time from the
|
||||
socket with an overall limit of 8_000_000 bytes.
|
||||
|
||||
The option `:nest_all_json`, when true, specifies all parsed JSON (including maps)
|
||||
are parsed into a `"_json"` key.
|
||||
"""
|
||||
|
||||
@behaviour Plug.Parsers
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
{decoder, opts} = Keyword.pop(opts, :json_decoder)
|
||||
{body_reader, opts} = Keyword.pop(opts, :body_reader, {Plug.Conn, :read_body, []})
|
||||
decoder = validate_decoder!(decoder)
|
||||
{body_reader, decoder, opts}
|
||||
end
|
||||
|
||||
defp validate_decoder!(nil) do
|
||||
raise ArgumentError, "JSON parser expects a :json_decoder option"
|
||||
end
|
||||
|
||||
defp validate_decoder!({module, fun, args} = mfa)
|
||||
when is_atom(module) and is_atom(fun) and is_list(args) do
|
||||
arity = length(args) + 1
|
||||
|
||||
if Code.ensure_compiled(module) != {:module, module} do
|
||||
raise ArgumentError,
|
||||
"invalid :json_decoder option. The module #{inspect(module)} is not " <>
|
||||
"loaded and could not be found"
|
||||
end
|
||||
|
||||
if not function_exported?(module, fun, arity) do
|
||||
raise ArgumentError,
|
||||
"invalid :json_decoder option. The module #{inspect(module)} must " <>
|
||||
"implement #{fun}/#{arity}"
|
||||
end
|
||||
|
||||
mfa
|
||||
end
|
||||
|
||||
defp validate_decoder!(decoder) when is_atom(decoder) do
|
||||
validate_decoder!({decoder, :decode!, []})
|
||||
end
|
||||
|
||||
defp validate_decoder!(decoder) do
|
||||
raise ArgumentError,
|
||||
"the :json_decoder option expects a module, or a three-element " <>
|
||||
"tuple in the form of {module, function, extra_args}, got: #{inspect(decoder)}"
|
||||
end
|
||||
|
||||
@impl true
|
||||
def parse(conn, "application", subtype, _headers, {{mod, fun, args}, decoder, opts}) do
|
||||
if subtype == "json" or String.ends_with?(subtype, "+json") do
|
||||
apply(mod, fun, [conn, opts | args]) |> decode(decoder, opts)
|
||||
else
|
||||
{:next, conn}
|
||||
end
|
||||
end
|
||||
|
||||
def parse(conn, _type, _subtype, _headers, _opts) do
|
||||
{:next, conn}
|
||||
end
|
||||
|
||||
defp decode({:ok, "", conn}, _decoder, _opts) do
|
||||
{:ok, %{}, conn}
|
||||
end
|
||||
|
||||
defp decode({:ok, body, conn}, {module, fun, args}, opts) do
|
||||
nest_all = Keyword.get(opts, :nest_all_json, false)
|
||||
|
||||
try do
|
||||
apply(module, fun, [body | args])
|
||||
rescue
|
||||
e -> raise Plug.Parsers.ParseError, exception: e
|
||||
else
|
||||
terms when is_map(terms) and not nest_all ->
|
||||
{:ok, terms, conn}
|
||||
|
||||
terms ->
|
||||
{:ok, %{"_json" => terms}, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp decode({:more, _, conn}, _decoder, _opts) do
|
||||
{:error, :too_large, conn}
|
||||
end
|
||||
|
||||
defp decode({:error, :timeout}, _decoder, _opts) do
|
||||
raise Plug.TimeoutError
|
||||
end
|
||||
|
||||
defp decode({:error, _}, _decoder, _opts) do
|
||||
raise Plug.BadRequestError
|
||||
end
|
||||
end
|
||||
311
phoenix/deps/plug/lib/plug/parsers/multipart.ex
Normal file
311
phoenix/deps/plug/lib/plug/parsers/multipart.ex
Normal file
@@ -0,0 +1,311 @@
|
||||
defmodule Plug.Parsers.MULTIPART do
|
||||
@moduledoc """
|
||||
Parses multipart request body.
|
||||
|
||||
## Options
|
||||
|
||||
All options supported by `Plug.Conn.read_body/2` are also supported here.
|
||||
They are repeated here for convenience:
|
||||
|
||||
* `:length` - sets the maximum number of bytes to read from the request,
|
||||
defaults to 8_000_000 bytes
|
||||
|
||||
* `:read_length` - sets the amount of bytes to read at one time from the
|
||||
underlying socket to fill the chunk, defaults to 1_000_000 bytes
|
||||
|
||||
* `:read_timeout` - sets the timeout for each socket read, defaults to
|
||||
15_000ms
|
||||
|
||||
So by default, `Plug.Parsers` will read 1_000_000 bytes at a time from the
|
||||
socket with an overall limit of 8_000_000 bytes.
|
||||
|
||||
Besides the options supported by `Plug.Conn.read_body/2`, the multipart parser
|
||||
also checks for:
|
||||
|
||||
* `:headers` - containing the same `:length`, `:read_length`
|
||||
and `:read_timeout` options which are used explicitly for parsing multipart
|
||||
headers
|
||||
|
||||
* `:validate_utf8` - specifies whether multipart body parts should be validated
|
||||
as utf8 binaries. It is either a boolean or a custom exception to raise
|
||||
|
||||
* `:multipart_to_params` - a MFA that receives the multipart headers and the
|
||||
connection and it must return a tuple of `{:ok, params, conn}`
|
||||
|
||||
## Multipart to params
|
||||
|
||||
Once all multiparts are collected, they must be converted to params and this
|
||||
can be customize with a MFA. The default implementation of this function
|
||||
is equivalent to:
|
||||
|
||||
def multipart_to_params(parts, conn) do
|
||||
acc =
|
||||
for {name, _headers, body} <- Enum.reverse(parts),
|
||||
name != nil,
|
||||
reduce: Plug.Conn.Query.decode_init() do
|
||||
acc -> Plug.Conn.Query.decode_each({name, body}, acc)
|
||||
end
|
||||
|
||||
{:ok, Plug.Conn.Query.decode_done(acc), conn}
|
||||
end
|
||||
|
||||
As you can notice, it discards all multiparts without a name. If you want
|
||||
to keep the unnamed parts, you can store all of them under a known prefix,
|
||||
such as:
|
||||
|
||||
def multipart_to_params(parts, conn) do
|
||||
acc =
|
||||
for {name, _headers, body} <- Enum.reverse(parts),
|
||||
name != nil,
|
||||
reduce: Plug.Conn.Query.decode_init() do
|
||||
acc -> Plug.Conn.Query.decode_each({name || "_parts[]", body}, acc)
|
||||
end
|
||||
|
||||
{:ok, Plug.Conn.Query.decode_done(acc), conn}
|
||||
end
|
||||
|
||||
## Dynamic configuration
|
||||
|
||||
If you need to dynamically configure how `Plug.Parsers.MULTIPART` behave,
|
||||
for example, based on the connection or another system parameter, one option
|
||||
is to create your own parser that wraps it:
|
||||
|
||||
defmodule MyMultipart do
|
||||
@multipart Plug.Parsers.MULTIPART
|
||||
|
||||
def init(opts) do
|
||||
opts
|
||||
end
|
||||
|
||||
def parse(conn, "multipart", subtype, headers, opts) do
|
||||
length = System.fetch_env!("UPLOAD_LIMIT") |> String.to_integer
|
||||
opts = @multipart.init([length: length] ++ opts)
|
||||
@multipart.parse(conn, "multipart", subtype, headers, opts)
|
||||
end
|
||||
|
||||
def parse(conn, _type, _subtype, _headers, _opts) do
|
||||
{:next, conn}
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
@behaviour Plug.Parsers
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
# Remove the length from options as it would attempt
|
||||
# to eagerly read the body on the limit value.
|
||||
{limit, opts} = Keyword.pop(opts, :length, 8_000_000)
|
||||
|
||||
# The read length is now our effective length per call.
|
||||
{read_length, opts} = Keyword.pop(opts, :read_length, 1_000_000)
|
||||
opts = [length: read_length, read_length: read_length] ++ opts
|
||||
|
||||
# The header options are handled individually.
|
||||
{headers_opts, opts} = Keyword.pop(opts, :headers, [])
|
||||
|
||||
unless is_integer(limit) do
|
||||
raise ":length option for Plug.Parsers.MULTIPART must be an integer"
|
||||
end
|
||||
|
||||
m2p = opts[:multipart_to_params] || {__MODULE__, :multipart_to_params, [opts]}
|
||||
{m2p, limit, headers_opts, opts}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def parse(conn, "multipart", subtype, _headers, opts_tuple)
|
||||
when subtype in ["form-data", "mixed"] do
|
||||
try do
|
||||
parse_multipart(conn, opts_tuple)
|
||||
rescue
|
||||
# Do not ignore upload errors
|
||||
e in [Plug.UploadError, Plug.Parsers.BadEncodingError] ->
|
||||
reraise e, __STACKTRACE__
|
||||
|
||||
# All others are wrapped
|
||||
e ->
|
||||
reraise Plug.Parsers.ParseError.exception(exception: e), __STACKTRACE__
|
||||
end
|
||||
end
|
||||
|
||||
def parse(conn, _type, _subtype, _headers, _opts) do
|
||||
{:next, conn}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def multipart_to_params(acc, conn, _opts) do
|
||||
acc =
|
||||
List.foldr(acc, Plug.Conn.Query.decode_init(), fn
|
||||
{nil, _headers, _body}, acc -> acc
|
||||
{name, _headers, body}, acc -> Plug.Conn.Query.decode_each({name, body}, acc)
|
||||
end)
|
||||
|
||||
{:ok, Plug.Conn.Query.decode_done(acc), conn}
|
||||
end
|
||||
|
||||
## Multipart
|
||||
|
||||
defp parse_multipart(conn, {m2p, limit, headers_opts, opts}) do
|
||||
read_result = Plug.Conn.read_part_headers(conn, headers_opts)
|
||||
{:ok, limit, acc, conn} = parse_multipart(read_result, limit, opts, headers_opts, [])
|
||||
|
||||
if limit > 0 do
|
||||
{mod, fun, args} = m2p
|
||||
apply(mod, fun, [acc, conn | args])
|
||||
else
|
||||
{:error, :too_large, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_multipart({:ok, headers, conn}, limit, opts, headers_opts, acc) when limit >= 0 do
|
||||
{conn, limit, acc} = parse_multipart_headers(headers, conn, limit, opts, acc)
|
||||
read_result = Plug.Conn.read_part_headers(conn, headers_opts)
|
||||
parse_multipart(read_result, limit, opts, headers_opts, acc)
|
||||
end
|
||||
|
||||
defp parse_multipart({:ok, _headers, conn}, limit, _opts, _headers_opts, acc) do
|
||||
{:ok, limit, acc, conn}
|
||||
end
|
||||
|
||||
defp parse_multipart({:done, conn}, limit, _opts, _headers_opts, acc) do
|
||||
{:ok, limit, acc, conn}
|
||||
end
|
||||
|
||||
defp parse_multipart_headers(headers, conn, limit, opts, acc) do
|
||||
case multipart_type(headers) do
|
||||
{:binary, name} ->
|
||||
{:ok, limit, body, conn} =
|
||||
parse_multipart_body(Plug.Conn.read_part_body(conn, opts), limit, opts, "")
|
||||
|
||||
case Keyword.get(opts, :validate_utf8, true) do
|
||||
true ->
|
||||
Plug.Conn.Utils.validate_utf8!(body, Plug.Parsers.BadEncodingError, "multipart body")
|
||||
|
||||
false ->
|
||||
:ok
|
||||
|
||||
module ->
|
||||
Plug.Conn.Utils.validate_utf8!(body, module, "multipart body")
|
||||
end
|
||||
|
||||
{conn, limit, [{name, headers, body} | acc]}
|
||||
|
||||
{:file, name, path, %Plug.Upload{} = uploaded} ->
|
||||
{:ok, file} = File.open(path, [:write, :binary, :delayed_write, :raw])
|
||||
|
||||
{:ok, limit, conn} =
|
||||
parse_multipart_file(Plug.Conn.read_part_body(conn, opts), limit, opts, file)
|
||||
|
||||
:ok = File.close(file)
|
||||
{conn, limit, [{name, headers, uploaded} | acc]}
|
||||
|
||||
:skip ->
|
||||
{conn, limit, acc}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_multipart_body({:more, tail, conn}, limit, opts, body)
|
||||
when limit >= byte_size(tail) do
|
||||
read_result = Plug.Conn.read_part_body(conn, opts)
|
||||
parse_multipart_body(read_result, limit - byte_size(tail), opts, body <> tail)
|
||||
end
|
||||
|
||||
defp parse_multipart_body({:more, tail, conn}, limit, _opts, body) do
|
||||
{:ok, limit - byte_size(tail), body, conn}
|
||||
end
|
||||
|
||||
defp parse_multipart_body({:ok, tail, conn}, limit, _opts, body)
|
||||
when limit >= byte_size(tail) do
|
||||
{:ok, limit - byte_size(tail), body <> tail, conn}
|
||||
end
|
||||
|
||||
defp parse_multipart_body({:ok, tail, conn}, limit, _opts, body) do
|
||||
{:ok, limit - byte_size(tail), body, conn}
|
||||
end
|
||||
|
||||
defp parse_multipart_file({:more, tail, conn}, limit, opts, file)
|
||||
when limit >= byte_size(tail) do
|
||||
binwrite!(file, tail)
|
||||
read_result = Plug.Conn.read_part_body(conn, opts)
|
||||
parse_multipart_file(read_result, limit - byte_size(tail), opts, file)
|
||||
end
|
||||
|
||||
defp parse_multipart_file({:more, tail, conn}, limit, _opts, _file) do
|
||||
{:ok, limit - byte_size(tail), conn}
|
||||
end
|
||||
|
||||
defp parse_multipart_file({:ok, tail, conn}, limit, _opts, file)
|
||||
when limit >= byte_size(tail) do
|
||||
binwrite!(file, tail)
|
||||
{:ok, limit - byte_size(tail), conn}
|
||||
end
|
||||
|
||||
defp parse_multipart_file({:ok, tail, conn}, limit, _opts, _file) do
|
||||
{:ok, limit - byte_size(tail), conn}
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp binwrite!(device, contents) do
|
||||
case IO.binwrite(device, contents) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
raise Plug.UploadError,
|
||||
"could not write to file #{inspect(device)} during upload " <>
|
||||
"due to reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp multipart_type(headers) do
|
||||
with {_, disposition} <- List.keyfind(headers, "content-disposition", 0),
|
||||
[_, params] <- :binary.split(disposition, ";"),
|
||||
%{"name" => name} = params <- Plug.Conn.Utils.params(params) do
|
||||
handle_disposition(params, name, headers)
|
||||
else
|
||||
_ -> {:binary, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_disposition(params, name, headers) do
|
||||
case params do
|
||||
%{"filename" => ""} ->
|
||||
:skip
|
||||
|
||||
%{"filename" => filename} ->
|
||||
path = Plug.Upload.random_file!("multipart")
|
||||
content_type = get_header(headers, "content-type")
|
||||
upload = %Plug.Upload{filename: filename, path: path, content_type: content_type}
|
||||
{:file, name, path, upload}
|
||||
|
||||
%{"filename*" => ""} ->
|
||||
:skip
|
||||
|
||||
%{"filename*" => "utf-8''" <> filename} ->
|
||||
filename = URI.decode(filename)
|
||||
|
||||
Plug.Conn.Utils.validate_utf8!(
|
||||
filename,
|
||||
Plug.Parsers.BadEncodingError,
|
||||
"multipart filename"
|
||||
)
|
||||
|
||||
path = Plug.Upload.random_file!("multipart")
|
||||
content_type = get_header(headers, "content-type")
|
||||
upload = %Plug.Upload{filename: filename, path: path, content_type: content_type}
|
||||
{:file, name, path, upload}
|
||||
|
||||
%{} ->
|
||||
{:binary, name}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_header(headers, key) do
|
||||
case List.keyfind(headers, key, 0) do
|
||||
{^key, value} -> value
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
57
phoenix/deps/plug/lib/plug/parsers/urlencoded.ex
Normal file
57
phoenix/deps/plug/lib/plug/parsers/urlencoded.ex
Normal file
@@ -0,0 +1,57 @@
|
||||
defmodule Plug.Parsers.URLENCODED do
|
||||
@moduledoc """
|
||||
Parses urlencoded request body.
|
||||
|
||||
## Options
|
||||
|
||||
All options supported by `Plug.Conn.read_body/2` are also supported here.
|
||||
They are repeated here for convenience:
|
||||
|
||||
* `:length` - sets the maximum number of bytes to read from the request,
|
||||
defaults to 1_000_000 bytes
|
||||
* `:read_length` - sets the amount of bytes to read at one time from the
|
||||
underlying socket to fill the chunk, defaults to 1_000_000 bytes
|
||||
* `:read_timeout` - sets the timeout for each socket read, defaults to
|
||||
15_000ms
|
||||
|
||||
So by default, `Plug.Parsers` will read 1_000_000 bytes at a time from the
|
||||
socket with an overall limit of 8_000_000 bytes.
|
||||
"""
|
||||
|
||||
@behaviour Plug.Parsers
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
opts = Keyword.put_new(opts, :length, 1_000_000)
|
||||
Keyword.pop(opts, :body_reader, {Plug.Conn, :read_body, []})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def parse(conn, "application", "x-www-form-urlencoded", _headers, {{mod, fun, args}, opts}) do
|
||||
case apply(mod, fun, [conn, opts | args]) do
|
||||
{:ok, body, conn} ->
|
||||
validate_utf8 = Keyword.get(opts, :validate_utf8, true)
|
||||
|
||||
{:ok,
|
||||
Plug.Conn.Query.decode(
|
||||
body,
|
||||
%{},
|
||||
Plug.Parsers.BadEncodingError,
|
||||
validate_utf8
|
||||
), conn}
|
||||
|
||||
{:more, _data, conn} ->
|
||||
{:error, :too_large, conn}
|
||||
|
||||
{:error, :timeout} ->
|
||||
raise Plug.TimeoutError
|
||||
|
||||
{:error, _} ->
|
||||
raise Plug.BadRequestError
|
||||
end
|
||||
end
|
||||
|
||||
def parse(conn, _type, _subtype, _headers, _opts) do
|
||||
{:next, conn}
|
||||
end
|
||||
end
|
||||
99
phoenix/deps/plug/lib/plug/request_id.ex
Normal file
99
phoenix/deps/plug/lib/plug/request_id.ex
Normal file
@@ -0,0 +1,99 @@
|
||||
defmodule Plug.RequestId do
|
||||
@moduledoc """
|
||||
A plug for generating a unique request ID for each request.
|
||||
|
||||
The generated request ID will be in the format:
|
||||
|
||||
```
|
||||
GEBMr97eLMHtGWsAAAVj
|
||||
```
|
||||
|
||||
If a request ID already exists in a configured HTTP request header (see options below),
|
||||
then this plug will use that value, *assuming it is between 20 and 200 characters*.
|
||||
If such header is not present, this plug will generate a new request ID.
|
||||
|
||||
The request ID is added to the `Logger` metadata as `:request_id`, and to the
|
||||
response as the configured HTTP response header (see options below). To see the
|
||||
request ID in your log output, configure your logger formatter to include the `:request_id`
|
||||
metadata. For example:
|
||||
|
||||
config :logger, :default_formatter, metadata: [:request_id]
|
||||
|
||||
We recommend to include this metadata configuration in your production
|
||||
configuration file.
|
||||
|
||||
> #### Programmatic access to the request ID {: .tip}
|
||||
>
|
||||
> To access the request ID programmatically, use the `:assign_as` option (see below)
|
||||
> to assign the request ID to a key in `conn.assigns`, and then fetch it from there.
|
||||
|
||||
## Usage
|
||||
|
||||
To use this plug, just plug it into the desired module:
|
||||
|
||||
plug Plug.RequestId
|
||||
|
||||
## Options
|
||||
|
||||
* `:http_header` - The name of the HTTP *request* header to check for
|
||||
existing request IDs. This is also the HTTP *response* header that will be
|
||||
set with the request id. Default value is `"x-request-id"`.
|
||||
|
||||
plug Plug.RequestId, http_header: "custom-request-id"
|
||||
|
||||
* `:assign_as` - The name of the key that will be used to store the
|
||||
discovered or generated request id in `conn.assigns`. If not provided,
|
||||
the request id will not be stored. *Available since v1.16.0*.
|
||||
|
||||
plug Plug.RequestId, assign_as: :plug_request_id
|
||||
|
||||
* `:logger_metadata_key` - The name of the key that will be used to store the
|
||||
discovered or generated request id in `Logger` metadata. If not provided,
|
||||
the request ID Logger metadata will be stored as `:request_id`. *Available
|
||||
since v1.18.0*.
|
||||
|
||||
plug Plug.RequestId, logger_metadata_key: :my_request_id
|
||||
|
||||
"""
|
||||
|
||||
alias Plug.Conn
|
||||
@behaviour Plug
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
{
|
||||
Keyword.get(opts, :http_header, "x-request-id"),
|
||||
Keyword.get(opts, :assign_as),
|
||||
Keyword.get(opts, :logger_metadata_key, :request_id)
|
||||
}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def call(conn, {header, assign_as, logger_metadata_key}) do
|
||||
request_id = get_request_id(conn, header)
|
||||
|
||||
Logger.metadata([{logger_metadata_key, request_id}])
|
||||
conn = if assign_as, do: Conn.assign(conn, assign_as, request_id), else: conn
|
||||
|
||||
Conn.put_resp_header(conn, header, request_id)
|
||||
end
|
||||
|
||||
defp get_request_id(conn, header) do
|
||||
case Conn.get_req_header(conn, header) do
|
||||
[] -> generate_request_id()
|
||||
[val | _] -> if valid_request_id?(val), do: val, else: generate_request_id()
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_request_id do
|
||||
binary = <<
|
||||
System.system_time(:nanosecond)::64,
|
||||
:erlang.phash2({node(), self()}, 16_777_216)::24,
|
||||
:erlang.unique_integer()::32
|
||||
>>
|
||||
|
||||
Base.url_encode64(binary)
|
||||
end
|
||||
|
||||
defp valid_request_id?(s), do: byte_size(s) in 20..200
|
||||
end
|
||||
135
phoenix/deps/plug/lib/plug/rewrite_on.ex
Normal file
135
phoenix/deps/plug/lib/plug/rewrite_on.ex
Normal file
@@ -0,0 +1,135 @@
|
||||
defmodule Plug.RewriteOn do
|
||||
@moduledoc """
|
||||
A plug to rewrite the request's host/port/protocol from `x-forwarded-*` headers.
|
||||
|
||||
If your Plug application is behind a proxy that handles HTTPS, you may
|
||||
need to tell Plug to parse the proper protocol from the `x-forwarded-*`
|
||||
header.
|
||||
|
||||
plug Plug.RewriteOn, [:x_forwarded_host, :x_forwarded_port, :x_forwarded_proto]
|
||||
|
||||
The supported values are:
|
||||
|
||||
* `:x_forwarded_for` - to override the remote IP based on the "x-forwarded-for" header
|
||||
* `:x_forwarded_host` - to override the host based on the "x-forwarded-host" header
|
||||
* `:x_forwarded_port` - to override the port based on the "x-forwarded-port" header
|
||||
* `:x_forwarded_proto` - to override the protocol based on the "x-forwarded-proto" header
|
||||
|
||||
Some HTTPS proxies use nonstandard headers, which can be specified in the list via tuples:
|
||||
|
||||
* `{:remote_ip, header}` - to override the remote IP based on a custom header
|
||||
* `{:host, header}` - to override the host based on a custom header
|
||||
* `{:port, header}` - to override the port based on a custom header
|
||||
* `{:scheme, header}` - to override the protocol based on a custom header
|
||||
|
||||
A tuple representing a Module-Function-Args can also be given as argument
|
||||
instead of a list.
|
||||
|
||||
Since rewriting the scheme based on `x-forwarded-*` headers can open up
|
||||
security vulnerabilities, only use this plug if:
|
||||
|
||||
* your app is behind a proxy
|
||||
* your proxy strips the given `x-forwarded-*` headers from all incoming requests
|
||||
* your proxy sets the `x-forwarded-*` headers and sends it to Plug
|
||||
"""
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn, only: [get_req_header: 2]
|
||||
|
||||
@impl true
|
||||
def init({_m, _f, _a} = header), do: header
|
||||
def init(header), do: List.wrap(header)
|
||||
|
||||
@impl true
|
||||
def call(conn, [:x_forwarded_for | rewrite_on]) do
|
||||
call(conn, [{:remote_ip, "x-forwarded-for"} | rewrite_on])
|
||||
end
|
||||
|
||||
def call(conn, [:x_forwarded_proto | rewrite_on]) do
|
||||
call(conn, [{:scheme, "x-forwarded-proto"} | rewrite_on])
|
||||
end
|
||||
|
||||
def call(conn, [:x_forwarded_port | rewrite_on]) do
|
||||
call(conn, [{:port, "x-forwarded-port"} | rewrite_on])
|
||||
end
|
||||
|
||||
def call(conn, [:x_forwarded_host | rewrite_on]) do
|
||||
call(conn, [{:host, "x-forwarded-host"} | rewrite_on])
|
||||
end
|
||||
|
||||
def call(conn, [{:remote_ip, header} | rewrite_on]) do
|
||||
conn
|
||||
|> put_remote_ip(get_req_header(conn, header))
|
||||
|> call(rewrite_on)
|
||||
end
|
||||
|
||||
def call(conn, [{:scheme, header} | rewrite_on]) do
|
||||
conn
|
||||
|> put_scheme(get_req_header(conn, header))
|
||||
|> call(rewrite_on)
|
||||
end
|
||||
|
||||
def call(conn, [{:port, header} | rewrite_on]) do
|
||||
conn
|
||||
|> put_port(get_req_header(conn, header))
|
||||
|> call(rewrite_on)
|
||||
end
|
||||
|
||||
def call(conn, [{:host, header} | rewrite_on]) do
|
||||
conn
|
||||
|> put_host(get_req_header(conn, header))
|
||||
|> call(rewrite_on)
|
||||
end
|
||||
|
||||
def call(_conn, [other | _rewrite_on]) do
|
||||
raise "unknown rewrite: #{inspect(other)}"
|
||||
end
|
||||
|
||||
def call(conn, []) do
|
||||
conn
|
||||
end
|
||||
|
||||
def call(conn, {mod, fun, args}) do
|
||||
call(conn, apply(mod, fun, args))
|
||||
end
|
||||
|
||||
defp put_scheme(%{scheme: :http, port: 80} = conn, ["https"]),
|
||||
do: %{conn | scheme: :https, port: 443}
|
||||
|
||||
defp put_scheme(conn, ["https"]),
|
||||
do: %{conn | scheme: :https}
|
||||
|
||||
defp put_scheme(%{scheme: :https, port: 443} = conn, ["http"]),
|
||||
do: %{conn | scheme: :http, port: 80}
|
||||
|
||||
defp put_scheme(conn, ["http"]),
|
||||
do: %{conn | scheme: :http}
|
||||
|
||||
defp put_scheme(conn, _scheme),
|
||||
do: conn
|
||||
|
||||
defp put_host(conn, [proper_host]),
|
||||
do: %{conn | host: proper_host}
|
||||
|
||||
defp put_host(conn, _),
|
||||
do: conn
|
||||
|
||||
defp put_port(conn, headers) do
|
||||
with [header] <- headers,
|
||||
{port, ""} <- Integer.parse(header) do
|
||||
%{conn | port: port}
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
|
||||
defp put_remote_ip(conn, headers) do
|
||||
with [header] <- headers,
|
||||
[client | _] <- :binary.split(header, ","),
|
||||
{:ok, remote_ip} <- :inet.parse_address(String.to_charlist(client)) do
|
||||
%{conn | remote_ip: remote_ip}
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
end
|
||||
665
phoenix/deps/plug/lib/plug/router.ex
Normal file
665
phoenix/deps/plug/lib/plug/router.ex
Normal file
@@ -0,0 +1,665 @@
|
||||
defmodule Plug.Router do
|
||||
@moduledoc ~S"""
|
||||
A DSL to define a routing algorithm that works with Plug.
|
||||
|
||||
It provides a set of macros to generate routes. For example:
|
||||
|
||||
defmodule AppRouter do
|
||||
use Plug.Router
|
||||
|
||||
plug :match
|
||||
plug :dispatch
|
||||
|
||||
get "/hello" do
|
||||
send_resp(conn, 200, "world")
|
||||
end
|
||||
|
||||
match _ do
|
||||
send_resp(conn, 404, "oops")
|
||||
end
|
||||
end
|
||||
|
||||
Each route receives a `conn` variable containing a `Plug.Conn`
|
||||
struct and it needs to return a connection, as per the Plug spec.
|
||||
A catch-all `match` is recommended to be defined as in the example
|
||||
above, otherwise routing fails with a function clause error.
|
||||
|
||||
The router is itself a plug, which means it can be invoked as:
|
||||
|
||||
AppRouter.call(conn, AppRouter.init([]))
|
||||
|
||||
Each `Plug.Router` has a plug pipeline, defined by `Plug.Builder`,
|
||||
and by default it requires two plugs: `:match` and `:dispatch`.
|
||||
`:match` is responsible for finding a matching route which is
|
||||
then forwarded to `:dispatch`. This means users can easily hook
|
||||
into the router mechanism and add behaviour before match, before
|
||||
dispatch, or after both. See the `Plug.Builder` module for more
|
||||
information.
|
||||
|
||||
## Routes
|
||||
|
||||
get "/hello" do
|
||||
send_resp(conn, 200, "world")
|
||||
end
|
||||
|
||||
In the example above, a request will only match if it is a `GET`
|
||||
request and the route is "/hello". The supported HTTP methods are
|
||||
`get`, `post`, `put`, `patch`, `delete` and `options`.
|
||||
|
||||
A route can also specify parameters which will then be available
|
||||
in the function body:
|
||||
|
||||
get "/hello/:name" do
|
||||
send_resp(conn, 200, "hello #{name}")
|
||||
end
|
||||
|
||||
This means the name can also be used in guards:
|
||||
|
||||
get "/hello/:name" when name in ~w(foo bar) do
|
||||
send_resp(conn, 200, "hello #{name}")
|
||||
end
|
||||
|
||||
The `:name` parameter will also be available in the function body as
|
||||
`conn.params["name"]` and `conn.path_params["name"]`.
|
||||
|
||||
The identifier always starts with `:` and must be followed by letters,
|
||||
numbers, and underscores, like any Elixir variable. It is possible for
|
||||
identifiers to be either prefixed or suffixed by other words. For example,
|
||||
you can include a suffix such as a dot delimited file extension:
|
||||
|
||||
get "/hello/:name.json" do
|
||||
send_resp(conn, 200, "hello #{name}")
|
||||
end
|
||||
|
||||
The above will match `/hello/foo.json` but not `/hello/foo`.
|
||||
Other delimiters such as `-`, `@` may be used to denote suffixes.
|
||||
|
||||
Identifier matching can be escaped using the `\` character:
|
||||
|
||||
get "/hello/\\:greet" do
|
||||
send_resp(conn, 200, "hello")
|
||||
end
|
||||
|
||||
The above will only match `/hello/:greet`.
|
||||
|
||||
Routes allow for globbing which will match the remaining parts
|
||||
of a route. A glob match is done with the `*` character followed
|
||||
by the variable name. Typically you prefix the variable name with
|
||||
underscore to discard it:
|
||||
|
||||
get "/hello/*_rest" do
|
||||
send_resp(conn, 200, "matches all routes starting with /hello")
|
||||
end
|
||||
|
||||
But you can also assign the glob to any variable. The contents will
|
||||
always be a list:
|
||||
|
||||
get "/hello/*glob" do
|
||||
send_resp(conn, 200, "route after /hello: #{inspect glob}")
|
||||
end
|
||||
|
||||
Similarly to `:identifiers`, globs are also escaped using the
|
||||
`\` character:
|
||||
|
||||
get "/hello/\\*glob" do
|
||||
send_resp(conn, 200, "this is not a glob route")
|
||||
end
|
||||
|
||||
The above will only match `/hello/*glob`.
|
||||
|
||||
Opposite to `:identifiers`, globs do not allow prefix nor suffix
|
||||
matches.
|
||||
|
||||
Finally, a general `match` function is also supported:
|
||||
|
||||
match "/hello" do
|
||||
send_resp(conn, 200, "world")
|
||||
end
|
||||
|
||||
A `match` will match any route regardless of the HTTP method.
|
||||
Check `match/3` for more information on how route compilation
|
||||
works and a list of supported options.
|
||||
|
||||
## Parameter Parsing
|
||||
|
||||
Handling request data can be done through the
|
||||
[`Plug.Parsers`](https://hexdocs.pm/plug/Plug.Parsers.html#content) plug. It
|
||||
provides support for parsing URL-encoded, form-data, and JSON data as well as
|
||||
providing a behaviour that others parsers can adopt.
|
||||
|
||||
Here is an example of `Plug.Parsers` can be used in a `Plug.Router` router to
|
||||
parse the JSON-encoded body of a POST request:
|
||||
|
||||
defmodule AppRouter do
|
||||
use Plug.Router
|
||||
|
||||
plug :match
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:json],
|
||||
pass: ["application/json"],
|
||||
json_decoder: Jason
|
||||
|
||||
plug :dispatch
|
||||
|
||||
post "/hello" do
|
||||
IO.inspect conn.body_params # Prints JSON POST body
|
||||
send_resp(conn, 200, "Success!")
|
||||
end
|
||||
end
|
||||
|
||||
It is important that `Plug.Parsers` is placed before the `:dispatch` plug in
|
||||
the pipeline, otherwise the matched clause route will not receive the parsed
|
||||
body in its `Plug.Conn` argument when dispatched.
|
||||
|
||||
`Plug.Parsers` can also be plugged between `:match` and `:dispatch` (like in
|
||||
the example above): this means that `Plug.Parsers` will run only if there is a
|
||||
matching route. This can be useful to perform actions such as authentication
|
||||
*before* parsing the body, which should only be parsed if a route matches
|
||||
afterwards.
|
||||
|
||||
## Error handling
|
||||
|
||||
In case something goes wrong in a request, the router by default
|
||||
will crash, without returning any response to the client. This
|
||||
behaviour can be configured in two ways, by using two different
|
||||
modules:
|
||||
|
||||
* `Plug.ErrorHandler` - allows the developer to customize exactly
|
||||
which page is sent to the client via the `handle_errors/2` function;
|
||||
|
||||
* `Plug.Debugger` - automatically shows debugging and request information
|
||||
about the failure. This module is recommended to be used only in a
|
||||
development environment.
|
||||
|
||||
Here is an example of how both modules could be used in an application:
|
||||
|
||||
defmodule AppRouter do
|
||||
use Plug.Router
|
||||
|
||||
if Mix.env == :dev do
|
||||
use Plug.Debugger
|
||||
end
|
||||
|
||||
use Plug.ErrorHandler
|
||||
|
||||
plug :match
|
||||
plug :dispatch
|
||||
|
||||
get "/hello" do
|
||||
send_resp(conn, 200, "world")
|
||||
end
|
||||
|
||||
defp handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do
|
||||
send_resp(conn, conn.status, "Something went wrong")
|
||||
end
|
||||
end
|
||||
|
||||
## Passing data between routes and plugs
|
||||
|
||||
It is also possible to assign data to the `Plug.Conn` that will
|
||||
be available to any plug invoked after the `:match` plug.
|
||||
This is very useful if you want a matched route to customize how
|
||||
later plugs will behave.
|
||||
|
||||
You can use `:assigns` (which contains user data) or `:private`
|
||||
(which contains library/framework data) for this. For example:
|
||||
|
||||
get "/hello", assigns: %{an_option: :a_value} do
|
||||
send_resp(conn, 200, "world")
|
||||
end
|
||||
|
||||
In the example above, `conn.assigns[:an_option]` will be available
|
||||
to all plugs invoked after `:match`. Such plugs can read from
|
||||
`conn.assigns` (or `conn.private`) to configure their behaviour
|
||||
based on the matched route.
|
||||
|
||||
## `use` options
|
||||
|
||||
All of the options given to `use Plug.Router` are forwarded to
|
||||
`Plug.Builder`. See the `Plug.Builder` module for more information.
|
||||
|
||||
## Telemetry
|
||||
|
||||
The router emits the following telemetry events:
|
||||
|
||||
* `[:plug, :router_dispatch, :start]` - dispatched before dispatching to a matched route
|
||||
* Measurement: `%{system_time: System.system_time}`
|
||||
* Metadata: `%{telemetry_span_context: term(), conn: Plug.Conn.t, route: binary, router: module}`
|
||||
|
||||
* `[:plug, :router_dispatch, :exception]` - dispatched after exceptions on dispatching a route
|
||||
* Measurement: `%{duration: native_time}`
|
||||
* Metadata: `%{telemetry_span_context: term(), conn: Plug.Conn.t, route: binary, router: module, kind: :throw | :error | :exit, reason: term(), stacktrace: list()}`
|
||||
|
||||
* `[:plug, :router_dispatch, :stop]` - dispatched after successfully dispatching a matched route
|
||||
* Measurement: `%{duration: native_time}`
|
||||
* Metadata: `%{telemetry_span_context: term(), conn: Plug.Conn.t, route: binary, router: module}`
|
||||
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(opts) do
|
||||
quote location: :keep do
|
||||
import Plug.Router
|
||||
@plug_router_to %{}
|
||||
@before_compile Plug.Router
|
||||
|
||||
use Plug.Builder, unquote(opts)
|
||||
|
||||
@doc false
|
||||
def match(conn, _opts) do
|
||||
do_match(conn, conn.method, Plug.Router.Utils.decode_path_info!(conn), conn.host)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def dispatch(%Plug.Conn{} = conn, opts) do
|
||||
{path, fun} = Map.fetch!(conn.private, :plug_route)
|
||||
|
||||
try do
|
||||
:telemetry.span(
|
||||
[:plug, :router_dispatch],
|
||||
%{conn: conn, route: path, router: __MODULE__},
|
||||
fn ->
|
||||
conn = fun.(conn, opts)
|
||||
{conn, %{conn: conn, route: path, router: __MODULE__}}
|
||||
end
|
||||
)
|
||||
catch
|
||||
kind, reason ->
|
||||
Plug.Conn.WrapperError.reraise(conn, kind, reason, __STACKTRACE__)
|
||||
end
|
||||
end
|
||||
|
||||
defoverridable match: 2, dispatch: 2
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __before_compile__(env) do
|
||||
unless Module.defines?(env.module, {:do_match, 4}) do
|
||||
raise "no routes defined in module #{inspect(env.module)} using Plug.Router"
|
||||
end
|
||||
|
||||
router_to = Module.get_attribute(env.module, :plug_router_to)
|
||||
init_mode = Module.get_attribute(env.module, :plug_builder_opts)[:init_mode]
|
||||
|
||||
defs =
|
||||
for {callback, {target, opts}} <- router_to do
|
||||
case {init_mode, Atom.to_string(target)} do
|
||||
{:runtime, "Elixir." <> _} ->
|
||||
quote do
|
||||
defp unquote(callback)(conn, _opts) do
|
||||
unquote(target).call(conn, unquote(target).init(unquote(Macro.escape(opts))))
|
||||
end
|
||||
end
|
||||
|
||||
{_, "Elixir." <> _} ->
|
||||
opts = target.init(opts)
|
||||
|
||||
quote do
|
||||
defp unquote(callback)(conn, _opts) do
|
||||
require unquote(target)
|
||||
unquote(target).call(conn, unquote(Macro.escape(opts)))
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
quote do
|
||||
defp unquote(callback)(conn, _opts) do
|
||||
unquote(target)(conn, unquote(Macro.escape(opts)))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
quote do
|
||||
unquote_splicing(defs)
|
||||
import Plug.Router, only: []
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the path of the route that the request was matched to.
|
||||
"""
|
||||
@spec match_path(Plug.Conn.t()) :: String.t()
|
||||
def match_path(%Plug.Conn{} = conn) do
|
||||
{path, _fun} = Map.fetch!(conn.private, :plug_route)
|
||||
path
|
||||
end
|
||||
|
||||
## Match
|
||||
|
||||
@doc """
|
||||
Main API to define routes.
|
||||
|
||||
It accepts an expression representing the path and many options
|
||||
allowing the match to be configured.
|
||||
|
||||
The route can dispatch either to a function body or a Plug module.
|
||||
|
||||
## Examples
|
||||
|
||||
match "/foo/bar", via: :get do
|
||||
send_resp(conn, 200, "hello world")
|
||||
end
|
||||
|
||||
match "/baz", to: MyPlug, init_opts: [an_option: :a_value]
|
||||
|
||||
## Options
|
||||
|
||||
`match/3` and the other route macros accept the following options:
|
||||
|
||||
* `:host` - the host which the route should match. Defaults to `nil`,
|
||||
meaning no host match, but can be a string like "example.com" or a
|
||||
string ending with ".", like "subdomain." for a subdomain match.
|
||||
|
||||
* `:private` - assigns values to `conn.private` for use in the match
|
||||
|
||||
* `:assigns` - assigns values to `conn.assigns` for use in the match
|
||||
|
||||
* `:via` - matches the route against some specific HTTP method(s) specified
|
||||
as an atom, like `:get` or `:put`, or a list, like `[:get, :post]`.
|
||||
|
||||
* `:do` - contains the implementation to be invoked in case
|
||||
the route matches.
|
||||
|
||||
* `:to` - a Plug that will be called in case the route matches.
|
||||
|
||||
* `:init_opts` - the options for the target Plug given by `:to`.
|
||||
|
||||
A route should specify only one of `:do` or `:to` options.
|
||||
"""
|
||||
defmacro match(path, options, contents \\ []) do
|
||||
compile(nil, path, options, contents, __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dispatches to the path only if the request is a GET request.
|
||||
See `match/3` for more examples.
|
||||
"""
|
||||
defmacro get(path, options, contents \\ []) do
|
||||
compile(:get, path, options, contents, __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dispatches to the path only if the request is a HEAD request.
|
||||
See `match/3` for more examples.
|
||||
"""
|
||||
defmacro head(path, options, contents \\ []) do
|
||||
compile(:head, path, options, contents, __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dispatches to the path only if the request is a POST request.
|
||||
See `match/3` for more examples.
|
||||
"""
|
||||
defmacro post(path, options, contents \\ []) do
|
||||
compile(:post, path, options, contents, __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dispatches to the path only if the request is a PUT request.
|
||||
See `match/3` for more examples.
|
||||
"""
|
||||
defmacro put(path, options, contents \\ []) do
|
||||
compile(:put, path, options, contents, __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dispatches to the path only if the request is a PATCH request.
|
||||
See `match/3` for more examples.
|
||||
"""
|
||||
defmacro patch(path, options, contents \\ []) do
|
||||
compile(:patch, path, options, contents, __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dispatches to the path only if the request is a DELETE request.
|
||||
See `match/3` for more examples.
|
||||
"""
|
||||
defmacro delete(path, options, contents \\ []) do
|
||||
compile(:delete, path, options, contents, __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dispatches to the path only if the request is an OPTIONS request.
|
||||
See `match/3` for more examples.
|
||||
"""
|
||||
defmacro options(path, options, contents \\ []) do
|
||||
compile(:options, path, options, contents, __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Forwards requests to another Plug. The `path_info` of the forwarded
|
||||
connection will exclude the portion of the path specified in the
|
||||
call to `forward`. If the path contains any parameters, those will
|
||||
be available in the target Plug in `conn.params` and `conn.path_params`.
|
||||
|
||||
## Options
|
||||
|
||||
`forward` accepts the following options:
|
||||
|
||||
* `:to` - a Plug the requests will be forwarded to.
|
||||
* `:init_opts` - the options for the target Plug. It is the preferred
|
||||
mechanism for passing options to the target Plug.
|
||||
* `:host` - a string representing the host or subdomain, exactly like in
|
||||
`match/3`.
|
||||
* `:private` - values for `conn.private`, exactly like in `match/3`.
|
||||
* `:assigns` - values for `conn.assigns`, exactly like in `match/3`.
|
||||
|
||||
If `:init_opts` is undefined, then all remaining options are passed
|
||||
to the target plug.
|
||||
|
||||
## Examples
|
||||
|
||||
forward "/users", to: UserRouter
|
||||
|
||||
Assuming the above code, a request to `/users/sign_in` will be forwarded to
|
||||
the `UserRouter` plug, which will receive what it will see as a request to
|
||||
`/sign_in`.
|
||||
|
||||
forward "/foo/:bar/qux", to: FooPlug
|
||||
|
||||
Here, a request to `/foo/BAZ/qux` will be forwarded to the `FooPlug`
|
||||
plug, which will receive what it will see as a request to `/`,
|
||||
and `conn.params["bar"]` will be set to `"BAZ"`.
|
||||
|
||||
Some other examples:
|
||||
|
||||
forward "/foo/bar", to: :foo_bar_plug, host: "foobar."
|
||||
forward "/baz", to: BazPlug, init_opts: [plug_specific_option: true]
|
||||
|
||||
"""
|
||||
defmacro forward(path, options) do
|
||||
quote bind_quoted: [path: path, options: options] do
|
||||
{target, options} = Keyword.pop(options, :to)
|
||||
{options, plug_options} = Keyword.split(options, [:via, :host, :private, :assigns])
|
||||
plug_options = Keyword.get(plug_options, :init_opts, plug_options)
|
||||
|
||||
if is_nil(target) or not is_atom(target) do
|
||||
raise ArgumentError, message: "expected :to to be an alias or an atom"
|
||||
end
|
||||
|
||||
{target, target_opts} =
|
||||
case Atom.to_string(target) do
|
||||
"Elixir." <> _ -> {target, target.init(plug_options)}
|
||||
_ -> {{__MODULE__, target}, plug_options}
|
||||
end
|
||||
|
||||
@plug_forward_target target
|
||||
@plug_forward_opts target_opts
|
||||
|
||||
# Delegate the matching to the match/3 macro along with the options
|
||||
# specified by Keyword.split/2.
|
||||
match path <> "/*glob", options do
|
||||
Plug.forward(
|
||||
var!(conn),
|
||||
var!(glob),
|
||||
@plug_forward_target,
|
||||
@plug_forward_opts
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Match Helpers
|
||||
|
||||
@doc false
|
||||
def __route__(method, path, guards, options) do
|
||||
{method, guards} = build_methods(List.wrap(method || options[:via]), guards)
|
||||
{params, match, guards, post_match} = Plug.Router.Utils.build_path_clause(path, guards)
|
||||
params = Plug.Router.Utils.build_path_params_match(params)
|
||||
private = extract_merger(options, :private)
|
||||
assigns = extract_merger(options, :assigns)
|
||||
host_match = Plug.Router.Utils.build_host_match(options[:host])
|
||||
{quote(do: conn), method, match, post_match, params, host_match, guards, private, assigns}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __put_route__(conn, path, fun) do
|
||||
Plug.Conn.put_private(conn, :plug_route, {append_match_path(conn, path), fun})
|
||||
end
|
||||
|
||||
defp append_match_path(%Plug.Conn{private: %{plug_route: {base_path, _}}}, path) do
|
||||
base_path <> path
|
||||
end
|
||||
|
||||
defp append_match_path(%Plug.Conn{}, path) do
|
||||
path
|
||||
end
|
||||
|
||||
# Entry point for both forward and match that is actually
|
||||
# responsible to compile the route.
|
||||
defp compile(method, expr, options, contents, caller) do
|
||||
{callback, options} =
|
||||
cond do
|
||||
Keyword.has_key?(contents, :do) ->
|
||||
{wrap_function_do(contents[:do]), expand_options(options, caller)}
|
||||
|
||||
Keyword.has_key?(options, :do) ->
|
||||
{body, options} = Keyword.pop(options, :do)
|
||||
{wrap_function_do(body), expand_options(options, caller)}
|
||||
|
||||
options[:to] ->
|
||||
options = expand_options(options, caller)
|
||||
|
||||
callback =
|
||||
quote unquote: false do
|
||||
&(unquote(callback) / 2)
|
||||
end
|
||||
|
||||
options =
|
||||
quote do
|
||||
{callback, options} = Plug.Router.__to__(unquote(caller.module), unquote(options))
|
||||
options
|
||||
end
|
||||
|
||||
{callback, options}
|
||||
|
||||
true ->
|
||||
raise ArgumentError, message: "expected one of :to or :do to be given as option"
|
||||
end
|
||||
|
||||
{path, guards} = extract_path_and_guards(expr)
|
||||
|
||||
quote bind_quoted: [
|
||||
method: method,
|
||||
path: path,
|
||||
options: options,
|
||||
guards: Macro.escape(guards, unquote: true),
|
||||
callback: Macro.escape(callback, unquote: true)
|
||||
] do
|
||||
route = Plug.Router.__route__(method, path, guards, options)
|
||||
{conn, method, match, post_match, params, host, guards, private, assigns} = route
|
||||
|
||||
defp do_match(unquote(conn), unquote(method), unquote(match), unquote(host))
|
||||
when unquote(guards) do
|
||||
unquote_splicing(post_match)
|
||||
unquote(private)
|
||||
unquote(assigns)
|
||||
|
||||
params = unquote({:%{}, [], params})
|
||||
|
||||
merge_params = fn
|
||||
%Plug.Conn.Unfetched{} -> params
|
||||
fetched -> Map.merge(fetched, params)
|
||||
end
|
||||
|
||||
conn = update_in(unquote(conn).params, merge_params)
|
||||
conn = update_in(conn.path_params, merge_params)
|
||||
|
||||
Plug.Router.__put_route__(conn, unquote(path), unquote(callback))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __to__(module, options) do
|
||||
{to, options} = Keyword.pop(options, :to)
|
||||
{init_opts, options} = Keyword.pop(options, :init_opts, [])
|
||||
|
||||
router_to = Module.get_attribute(module, :plug_router_to)
|
||||
callback = :"plug_router_to_#{map_size(router_to)}"
|
||||
router_to = Map.put(router_to, callback, {to, init_opts})
|
||||
|
||||
Module.put_attribute(module, :plug_router_to, router_to)
|
||||
{Macro.var(callback, nil), options}
|
||||
end
|
||||
|
||||
defp wrap_function_do(body) do
|
||||
quote do
|
||||
fn var!(conn), var!(opts) ->
|
||||
_ = var!(opts)
|
||||
unquote(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_options(opts, caller) do
|
||||
if Macro.quoted_literal?(opts) do
|
||||
Macro.prewalk(opts, &expand_alias(&1, caller))
|
||||
else
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_alias({:__aliases__, _, _} = alias, env),
|
||||
do: Macro.expand(alias, %{env | function: {:init, 1}})
|
||||
|
||||
defp expand_alias(other, _env), do: other
|
||||
|
||||
defp extract_merger(options, key) when is_list(options) do
|
||||
if option = Keyword.get(options, key) do
|
||||
quote do
|
||||
conn = update_in(conn.unquote(key), &Map.merge(&1, unquote(Macro.escape(option))))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Convert the verbs given with `:via` into a variable and guard set that can
|
||||
# be added to the dispatch clause.
|
||||
defp build_methods([], guards) do
|
||||
{quote(do: _), guards}
|
||||
end
|
||||
|
||||
defp build_methods([method], guards) do
|
||||
{Plug.Router.Utils.normalize_method(method), guards}
|
||||
end
|
||||
|
||||
defp build_methods(methods, guards) do
|
||||
methods = Enum.map(methods, &Plug.Router.Utils.normalize_method(&1))
|
||||
var = quote do: method
|
||||
guards = join_guards(quote(do: unquote(var) in unquote(methods)), guards)
|
||||
{var, guards}
|
||||
end
|
||||
|
||||
defp join_guards(fst, true), do: fst
|
||||
defp join_guards(fst, snd), do: quote(do: unquote(fst) and unquote(snd))
|
||||
|
||||
# Extract the path and guards from the path.
|
||||
defp extract_path_and_guards({:when, _, [path, guards]}), do: {extract_path(path), guards}
|
||||
defp extract_path_and_guards(path), do: {extract_path(path), true}
|
||||
|
||||
defp extract_path({:_, _, var}) when is_atom(var), do: "/*_path"
|
||||
defp extract_path(path), do: path
|
||||
end
|
||||
286
phoenix/deps/plug/lib/plug/router/utils.ex
Normal file
286
phoenix/deps/plug/lib/plug/router/utils.ex
Normal file
@@ -0,0 +1,286 @@
|
||||
defmodule Plug.Router.InvalidSpecError do
|
||||
defexception message: "invalid route specification"
|
||||
end
|
||||
|
||||
defmodule Plug.Router.MalformedURIError do
|
||||
defexception message: "malformed URI", plug_status: 400
|
||||
end
|
||||
|
||||
defmodule Plug.Router.Utils do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Decodes path information for dispatching.
|
||||
"""
|
||||
def decode_path_info!(conn) do
|
||||
Enum.map(conn.path_info, &URI.decode/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a given method to its connection representation.
|
||||
|
||||
The request method is stored in the `Plug.Conn` struct as an uppercase string
|
||||
(like `"GET"` or `"POST"`). This function converts `method` to that
|
||||
representation.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Plug.Router.Utils.normalize_method(:get)
|
||||
"GET"
|
||||
|
||||
"""
|
||||
def normalize_method(method) do
|
||||
method |> to_string |> String.upcase()
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Builds the pattern that will be used to match against the request's host
|
||||
(provided via the `:host`) option.
|
||||
|
||||
If `host` is `nil`, a wildcard match (`_`) will be returned. If `host` ends
|
||||
with a dot, a match like `"host." <> _` will be returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Plug.Router.Utils.build_host_match(nil)
|
||||
{:_, [], Plug.Router.Utils}
|
||||
|
||||
iex> Plug.Router.Utils.build_host_match("foo.com")
|
||||
"foo.com"
|
||||
|
||||
iex> "api." |> Plug.Router.Utils.build_host_match() |> Macro.to_string()
|
||||
"\"api.\" <> _"
|
||||
|
||||
"""
|
||||
def build_host_match(host) do
|
||||
cond do
|
||||
is_nil(host) -> quote do: _
|
||||
String.last(host) == "." -> quote do: unquote(host) <> _
|
||||
is_binary(host) -> host
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a representation that will only match routes
|
||||
according to the given `spec`.
|
||||
|
||||
If a non-binary spec is given, it is assumed to be
|
||||
custom match arguments and they are simply returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Plug.Router.Utils.build_path_match("/foo/:id")
|
||||
{[:id], ["foo", {:id, [], nil}]}
|
||||
|
||||
"""
|
||||
def build_path_match(path, context \\ nil) when is_binary(path) do
|
||||
case build_path_clause(path, true, context) do
|
||||
{params, match, true, _post_match} ->
|
||||
{Enum.map(params, &String.to_atom(&1)), match}
|
||||
|
||||
{_, _, _, _} ->
|
||||
raise Plug.Router.InvalidSpecError,
|
||||
"invalid dynamic path. Only letters, numbers, and underscore are allowed after : in " <>
|
||||
inspect(path)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a list of path param names and var match pairs.
|
||||
|
||||
This is used to build parameter maps from existing variables.
|
||||
Excludes variables with underscore.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Plug.Router.Utils.build_path_params_match(["id"])
|
||||
[{"id", {:id, [], nil}}]
|
||||
iex> Plug.Router.Utils.build_path_params_match(["_id"])
|
||||
[]
|
||||
|
||||
iex> Plug.Router.Utils.build_path_params_match([:id])
|
||||
[{"id", {:id, [], nil}}]
|
||||
iex> Plug.Router.Utils.build_path_params_match([:_id])
|
||||
[]
|
||||
|
||||
"""
|
||||
def build_path_params_match(params, context \\ nil)
|
||||
|
||||
def build_path_params_match([param | _] = params, context) when is_binary(param) do
|
||||
params
|
||||
|> Enum.reject(&match?("_" <> _, &1))
|
||||
|> Enum.map(&{&1, Macro.var(String.to_atom(&1), context)})
|
||||
end
|
||||
|
||||
def build_path_params_match([param | _] = params, context) when is_atom(param) do
|
||||
params
|
||||
|> Enum.map(&{Atom.to_string(&1), Macro.var(&1, context)})
|
||||
|> Enum.reject(&match?({"_" <> _var, _macro}, &1))
|
||||
end
|
||||
|
||||
def build_path_params_match([], _context) do
|
||||
[]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a clause with match, guards, and post matches,
|
||||
including the known parameters.
|
||||
"""
|
||||
def build_path_clause(path, guard, context \\ nil) when is_binary(path) do
|
||||
compiled = :binary.compile_pattern(["\\:", "\\*", ":", "*"])
|
||||
|
||||
{params, match, guards, post_match} =
|
||||
path
|
||||
|> split()
|
||||
|> build_path_clause([], [], [], [], context, compiled)
|
||||
|
||||
if guard != true and guards != [] do
|
||||
raise ArgumentError, "cannot use \"when\" guards in route when using suffix matches"
|
||||
end
|
||||
|
||||
params = params |> Enum.uniq() |> Enum.reverse()
|
||||
guards = Enum.reduce(guards, guard, "e(do: unquote(&1) and unquote(&2)))
|
||||
{params, match, guards, post_match}
|
||||
end
|
||||
|
||||
defp build_path_clause([segment | rest], params, match, guards, post_match, context, compiled) do
|
||||
case :binary.matches(segment, compiled) do
|
||||
[] ->
|
||||
build_path_clause(rest, params, [segment | match], guards, post_match, context, compiled)
|
||||
|
||||
[{prefix_size, match_length}] when match_length == 2 ->
|
||||
suffix_size = byte_size(segment) - prefix_size - 2
|
||||
|
||||
<<prefix::binary-size(prefix_size), ?\\, char, suffix::binary-size(suffix_size)>> =
|
||||
segment
|
||||
|
||||
escaped_segment = [prefix <> <<char>> <> suffix | match]
|
||||
build_path_clause(rest, params, escaped_segment, guards, post_match, context, compiled)
|
||||
|
||||
[{prefix_size, _}] ->
|
||||
suffix_size = byte_size(segment) - prefix_size - 1
|
||||
<<prefix::binary-size(prefix_size), char, suffix::binary-size(suffix_size)>> = segment
|
||||
{param, suffix} = parse_suffix(suffix)
|
||||
params = [param | params]
|
||||
var = Macro.var(String.to_atom(param), context)
|
||||
|
||||
case char do
|
||||
?* when suffix != "" ->
|
||||
raise Plug.Router.InvalidSpecError,
|
||||
"globs (*var) cannot be followed by suffixes, got: #{inspect(segment)}"
|
||||
|
||||
?* when rest != [] ->
|
||||
raise Plug.Router.InvalidSpecError,
|
||||
"globs (*var) must always be in the last path, got glob in: #{inspect(segment)}"
|
||||
|
||||
?* ->
|
||||
submatch =
|
||||
if prefix != "" do
|
||||
IO.warn("""
|
||||
doing a prefix match with globs is deprecated, invalid segment #{inspect(segment)}.
|
||||
|
||||
You can either replace by a single segment match:
|
||||
|
||||
/foo/bar-:var
|
||||
|
||||
Or by mixing single segment match with globs:
|
||||
|
||||
/foo/bar-:var/*rest
|
||||
""")
|
||||
|
||||
quote do: [unquote(prefix) <> _ | _] = unquote(var)
|
||||
else
|
||||
var
|
||||
end
|
||||
|
||||
match =
|
||||
case match do
|
||||
[] ->
|
||||
submatch
|
||||
|
||||
[last | match] ->
|
||||
Enum.reverse([quote(do: unquote(last) | unquote(submatch)) | match])
|
||||
end
|
||||
|
||||
{params, match, guards, post_match}
|
||||
|
||||
?: ->
|
||||
match =
|
||||
if prefix == "",
|
||||
do: [var | match],
|
||||
else: [quote(do: unquote(prefix) <> unquote(var)) | match]
|
||||
|
||||
{post_match, guards} =
|
||||
if suffix == "" do
|
||||
{post_match, guards}
|
||||
else
|
||||
guard =
|
||||
quote do
|
||||
binary_part(
|
||||
unquote(var),
|
||||
byte_size(unquote(var)) - unquote(byte_size(suffix)),
|
||||
unquote(byte_size(suffix))
|
||||
) == unquote(suffix)
|
||||
end
|
||||
|
||||
trim =
|
||||
quote do
|
||||
unquote(var) = String.trim_trailing(unquote(var), unquote(suffix))
|
||||
end
|
||||
|
||||
{[trim | post_match], [guard | guards]}
|
||||
end
|
||||
|
||||
build_path_clause(rest, params, match, guards, post_match, context, compiled)
|
||||
end
|
||||
|
||||
[_ | _] ->
|
||||
raise Plug.Router.InvalidSpecError,
|
||||
"only one dynamic entry (:var or *glob) per path segment is allowed, got: " <>
|
||||
inspect(segment)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_path_clause([], params, match, guards, post_match, _context, _compiled) do
|
||||
{params, Enum.reverse(match), guards, post_match}
|
||||
end
|
||||
|
||||
defp parse_suffix(<<h, t::binary>>) when h in ?a..?z or h == ?_,
|
||||
do: parse_suffix(t, <<h>>)
|
||||
|
||||
defp parse_suffix(suffix) do
|
||||
raise Plug.Router.InvalidSpecError,
|
||||
"invalid dynamic path. The characters : and * must be immediately followed by " <>
|
||||
"lowercase letters or underscore, got: :#{suffix}"
|
||||
end
|
||||
|
||||
defp parse_suffix(<<h, t::binary>>, acc)
|
||||
when h in ?a..?z or h in ?A..?Z or h in ?0..?9 or h == ?_,
|
||||
do: parse_suffix(t, <<acc::binary, h>>)
|
||||
|
||||
defp parse_suffix(rest, acc),
|
||||
do: {acc, rest}
|
||||
|
||||
@doc """
|
||||
Splits the given path into several segments.
|
||||
It ignores both leading and trailing slashes in the path.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Plug.Router.Utils.split("/foo/bar")
|
||||
["foo", "bar"]
|
||||
|
||||
iex> Plug.Router.Utils.split("/:id/*")
|
||||
[":id", "*"]
|
||||
|
||||
iex> Plug.Router.Utils.split("/foo//*_bar")
|
||||
["foo", "*_bar"]
|
||||
|
||||
"""
|
||||
def split(bin) do
|
||||
for segment <- String.split(bin, "/"), segment != "", do: segment
|
||||
end
|
||||
|
||||
@deprecated "Use Plug.forward/4 instead"
|
||||
defdelegate forward(conn, new_path, target, opts), to: Plug
|
||||
end
|
||||
140
phoenix/deps/plug/lib/plug/session.ex
Normal file
140
phoenix/deps/plug/lib/plug/session.ex
Normal file
@@ -0,0 +1,140 @@
|
||||
defmodule Plug.Session do
|
||||
@moduledoc """
|
||||
A plug to handle session cookies and session stores.
|
||||
|
||||
The session is accessed via functions on `Plug.Conn`. Cookies and
|
||||
session have to be fetched with `Plug.Conn.fetch_session/1` before the
|
||||
session can be accessed.
|
||||
|
||||
The session is also lazy. Once configured, a cookie header with the
|
||||
session will only be sent to the client if something is written to the
|
||||
session in the first place.
|
||||
|
||||
When using `Plug.Session`, also consider using `Plug.CSRFProtection`
|
||||
to avoid Cross Site Request Forgery attacks.
|
||||
|
||||
## Session stores
|
||||
|
||||
See `Plug.Session.Store` for the specification session stores are required to
|
||||
implement.
|
||||
|
||||
Plug ships with the following session stores:
|
||||
|
||||
* `Plug.Session.ETS`
|
||||
* `Plug.Session.COOKIE`
|
||||
|
||||
## Options
|
||||
|
||||
* `:store` - session store module (required);
|
||||
* `:key` - session cookie key (required);
|
||||
* `:domain` - see `Plug.Conn.put_resp_cookie/4`;
|
||||
* `:max_age` - see `Plug.Conn.put_resp_cookie/4`;
|
||||
* `:path` - see `Plug.Conn.put_resp_cookie/4`;
|
||||
* `:secure` - see `Plug.Conn.put_resp_cookie/4`;
|
||||
* `:http_only` - see `Plug.Conn.put_resp_cookie/4`;
|
||||
* `:same_site` - see `Plug.Conn.put_resp_cookie/4`;
|
||||
* `:extra` - see `Plug.Conn.put_resp_cookie/4`;
|
||||
|
||||
Additional options can be given to the session store, see the store's
|
||||
documentation for the options it accepts.
|
||||
|
||||
## Examples
|
||||
|
||||
plug Plug.Session, store: :ets, key: "_my_app_session", table: :session
|
||||
"""
|
||||
|
||||
alias Plug.Conn
|
||||
@behaviour Plug
|
||||
|
||||
@cookie_opts [:domain, :max_age, :path, :secure, :http_only, :extra, :same_site]
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
store = Plug.Session.Store.get(Keyword.fetch!(opts, :store))
|
||||
key = Keyword.fetch!(opts, :key)
|
||||
cookie_opts = Keyword.take(opts, @cookie_opts)
|
||||
store_opts = Keyword.drop(opts, [:store, :key] ++ @cookie_opts)
|
||||
store_config = store.init(store_opts)
|
||||
|
||||
%{
|
||||
store: store,
|
||||
store_config: store_config,
|
||||
key: key,
|
||||
cookie_opts: cookie_opts
|
||||
}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def call(conn, config) do
|
||||
Conn.put_private(conn, :plug_session_fetch, fetch_session(config))
|
||||
end
|
||||
|
||||
defp fetch_session(config) do
|
||||
%{store: store, store_config: store_config, key: key} = config
|
||||
|
||||
fn conn ->
|
||||
{sid, session} =
|
||||
if cookie = Plug.Conn.get_cookies(conn)[key] do
|
||||
store.get(conn, cookie, store_config)
|
||||
else
|
||||
{nil, %{}}
|
||||
end
|
||||
|
||||
session = Map.merge(session, Map.get(conn.private, :plug_session, %{}))
|
||||
|
||||
conn
|
||||
|> Conn.put_private(:plug_session, session)
|
||||
|> Conn.put_private(:plug_session_fetch, :done)
|
||||
|> Conn.register_before_send(before_send(sid, config))
|
||||
end
|
||||
end
|
||||
|
||||
defp before_send(sid, config) do
|
||||
fn conn ->
|
||||
case Map.get(conn.private, :plug_session_info) do
|
||||
:write ->
|
||||
value = put_session(sid, conn, config)
|
||||
put_cookie(value, conn, config)
|
||||
|
||||
:drop ->
|
||||
drop_session(sid, conn, config)
|
||||
|
||||
:renew ->
|
||||
renew_session(sid, conn, config)
|
||||
|
||||
:ignore ->
|
||||
conn
|
||||
|
||||
nil ->
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp drop_session(sid, conn, config) do
|
||||
if sid do
|
||||
delete_session(sid, conn, config)
|
||||
delete_cookie(conn, config)
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
defp renew_session(sid, conn, config) do
|
||||
if sid, do: delete_session(sid, conn, config)
|
||||
value = put_session(nil, conn, config)
|
||||
put_cookie(value, conn, config)
|
||||
end
|
||||
|
||||
defp put_session(sid, conn, %{store: store, store_config: store_config}),
|
||||
do: store.put(conn, sid, conn.private[:plug_session], store_config)
|
||||
|
||||
defp delete_session(sid, conn, %{store: store, store_config: store_config}),
|
||||
do: store.delete(conn, sid, store_config)
|
||||
|
||||
defp put_cookie(value, conn, %{cookie_opts: cookie_opts, key: key}),
|
||||
do: Conn.put_resp_cookie(conn, key, value, cookie_opts)
|
||||
|
||||
defp delete_cookie(conn, %{cookie_opts: cookie_opts, key: key}),
|
||||
do: Conn.delete_resp_cookie(conn, key, cookie_opts)
|
||||
end
|
||||
246
phoenix/deps/plug/lib/plug/session/cookie.ex
Normal file
246
phoenix/deps/plug/lib/plug/session/cookie.ex
Normal file
@@ -0,0 +1,246 @@
|
||||
defmodule Plug.Session.COOKIE do
|
||||
@moduledoc """
|
||||
Stores the session in a cookie.
|
||||
|
||||
This cookie store is based on `Plug.Crypto.MessageVerifier`
|
||||
and `Plug.Crypto.MessageEncryptor` which encrypts and signs
|
||||
each cookie to ensure they can't be read nor tampered with.
|
||||
|
||||
Since this store uses crypto features, it requires you to
|
||||
set the `:secret_key_base` field in your connection. This
|
||||
can be easily achieved with a plug:
|
||||
|
||||
plug :put_secret_key_base
|
||||
|
||||
def put_secret_key_base(conn, _) do
|
||||
put_in conn.secret_key_base, "-- LONG STRING WITH AT LEAST 64 BYTES --"
|
||||
end
|
||||
|
||||
## Options
|
||||
|
||||
* `:secret_key_base` - the secret key base to built the cookie
|
||||
signing/encryption on top of. If one is given on initialization,
|
||||
the cookie store can precompute all relevant values at compilation
|
||||
time. Otherwise, the value is taken from `conn.secret_key_base`
|
||||
and cached.
|
||||
|
||||
* `:encryption_salt` - a salt used with `conn.secret_key_base` to generate
|
||||
a key for encrypting/decrypting a cookie, can be either a binary or
|
||||
an MFA returning a binary;
|
||||
|
||||
* `:signing_salt` - a salt used with `conn.secret_key_base` to generate a
|
||||
key for signing/verifying a cookie, can be either a binary or
|
||||
an MFA returning a binary;
|
||||
|
||||
* `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator`
|
||||
when generating the encryption and signing keys. Defaults to 1000;
|
||||
|
||||
* `:key_length` - option passed to `Plug.Crypto.KeyGenerator`
|
||||
when generating the encryption and signing keys. Defaults to 32;
|
||||
|
||||
* `:key_digest` - option passed to `Plug.Crypto.KeyGenerator`
|
||||
when generating the encryption and signing keys. Defaults to `:sha256`;
|
||||
|
||||
* `:serializer` - cookie serializer module that defines `encode/1` and
|
||||
`decode/1` returning an `{:ok, value}` tuple. Defaults to
|
||||
`:external_term_format`.
|
||||
|
||||
* `:log` - Log level to use when the cookie cannot be decoded.
|
||||
Defaults to `:debug`, can be set to false to disable it.
|
||||
|
||||
* `:rotating_options` - additional list of options to use when decrypting and
|
||||
verifying the cookie. These options are used only when the cookie could not
|
||||
be decoded using primary options and are fetched on init so they cannot be
|
||||
changed in runtime. Defaults to `[]`.
|
||||
|
||||
## Examples
|
||||
|
||||
plug Plug.Session, store: :cookie,
|
||||
key: "_my_app_session",
|
||||
encryption_salt: "cookie store encryption salt",
|
||||
signing_salt: "cookie store signing salt",
|
||||
log: :debug
|
||||
"""
|
||||
|
||||
require Logger
|
||||
@behaviour Plug.Session.Store
|
||||
|
||||
alias Plug.Crypto.KeyGenerator
|
||||
alias Plug.Crypto.MessageVerifier
|
||||
alias Plug.Crypto.MessageEncryptor
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
build_opts(opts)
|
||||
|> build_rotating_opts(opts[:rotating_options])
|
||||
|> Map.delete(:secret_key_base)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def get(conn, raw_cookie, opts) do
|
||||
opts = Map.put(opts, :secret_key_base, conn.secret_key_base)
|
||||
|
||||
[opts | opts.rotating_options]
|
||||
|> Enum.find_value(:error, &read_raw_cookie(raw_cookie, &1))
|
||||
|> decode(opts.serializer, opts.log)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def put(conn, _sid, term, opts) do
|
||||
%{serializer: serializer, key_opts: key_opts, signing_salt: signing_salt} = opts
|
||||
binary = encode(term, serializer)
|
||||
|
||||
case opts do
|
||||
%{encryption_salt: nil} ->
|
||||
MessageVerifier.sign(binary, derive(conn.secret_key_base, signing_salt, key_opts))
|
||||
|
||||
%{encryption_salt: encryption_salt} ->
|
||||
MessageEncryptor.encrypt(
|
||||
binary,
|
||||
derive(conn.secret_key_base, encryption_salt, key_opts),
|
||||
derive(conn.secret_key_base, signing_salt, key_opts)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def delete(_conn, _sid, _opts) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp encode(term, :external_term_format) do
|
||||
:erlang.term_to_binary(term)
|
||||
end
|
||||
|
||||
defp encode(term, serializer) do
|
||||
{:ok, binary} = serializer.encode(term)
|
||||
binary
|
||||
end
|
||||
|
||||
defp decode({:ok, binary}, :external_term_format, log) do
|
||||
{:term,
|
||||
try do
|
||||
Plug.Crypto.non_executable_binary_to_term(binary)
|
||||
rescue
|
||||
e ->
|
||||
Logger.log(
|
||||
log,
|
||||
"Plug.Session could not decode incoming session cookie. Reason: " <>
|
||||
Exception.message(e)
|
||||
)
|
||||
|
||||
%{}
|
||||
end}
|
||||
end
|
||||
|
||||
defp decode({:ok, binary}, serializer, _log) do
|
||||
case serializer.decode(binary) do
|
||||
{:ok, term} -> {:custom, term}
|
||||
_ -> {:custom, %{}}
|
||||
end
|
||||
end
|
||||
|
||||
defp decode(:error, _serializer, false) do
|
||||
{nil, %{}}
|
||||
end
|
||||
|
||||
defp decode(:error, _serializer, log) do
|
||||
Logger.log(
|
||||
log,
|
||||
"Plug.Session could not verify incoming session cookie. " <>
|
||||
"This may happen when the session settings change or a stale cookie is sent."
|
||||
)
|
||||
|
||||
{nil, %{}}
|
||||
end
|
||||
|
||||
defp prederive(secret_key_base, value, key_opts)
|
||||
when is_binary(secret_key_base) and is_binary(value) do
|
||||
{:prederived, derive(secret_key_base, value, Keyword.delete(key_opts, :cache))}
|
||||
end
|
||||
|
||||
defp prederive(_secret_key_base, value, _key_opts) do
|
||||
value
|
||||
end
|
||||
|
||||
defp derive(_secret_key_base, {:prederived, value}, _key_opts) do
|
||||
value
|
||||
end
|
||||
|
||||
defp derive(secret_key_base, {module, function, args}, key_opts) do
|
||||
derive(secret_key_base, apply(module, function, args), key_opts)
|
||||
end
|
||||
|
||||
defp derive(secret_key_base, key, key_opts) do
|
||||
secret_key_base
|
||||
|> validate_secret_key_base()
|
||||
|> KeyGenerator.generate(key, key_opts)
|
||||
end
|
||||
|
||||
defp validate_secret_key_base(nil),
|
||||
do: raise(ArgumentError, "cookie store expects conn.secret_key_base to be set")
|
||||
|
||||
defp validate_secret_key_base(secret_key_base) when byte_size(secret_key_base) < 64,
|
||||
do: raise(ArgumentError, "cookie store expects conn.secret_key_base to be at least 64 bytes")
|
||||
|
||||
defp validate_secret_key_base(secret_key_base), do: secret_key_base
|
||||
|
||||
defp check_signing_salt(opts) do
|
||||
case opts[:signing_salt] do
|
||||
nil -> raise ArgumentError, "cookie store expects :signing_salt as option"
|
||||
salt -> salt
|
||||
end
|
||||
end
|
||||
|
||||
defp check_serializer(serializer) when is_atom(serializer), do: serializer
|
||||
|
||||
defp check_serializer(_),
|
||||
do: raise(ArgumentError, "cookie store expects :serializer option to be a module")
|
||||
|
||||
defp read_raw_cookie(raw_cookie, opts) do
|
||||
signing_salt = derive(opts.secret_key_base, opts.signing_salt, opts.key_opts)
|
||||
|
||||
case opts do
|
||||
%{encryption_salt: nil} ->
|
||||
MessageVerifier.verify(raw_cookie, signing_salt)
|
||||
|
||||
%{encryption_salt: _} ->
|
||||
encryption_salt = derive(opts.secret_key_base, opts.encryption_salt, opts.key_opts)
|
||||
|
||||
MessageEncryptor.decrypt(raw_cookie, encryption_salt, signing_salt)
|
||||
end
|
||||
|> case do
|
||||
:error -> nil
|
||||
result -> result
|
||||
end
|
||||
end
|
||||
|
||||
defp build_opts(opts) do
|
||||
encryption_salt = opts[:encryption_salt]
|
||||
signing_salt = check_signing_salt(opts)
|
||||
|
||||
iterations = Keyword.get(opts, :key_iterations, 1000)
|
||||
length = Keyword.get(opts, :key_length, 32)
|
||||
digest = Keyword.get(opts, :key_digest, :sha256)
|
||||
log = Keyword.get(opts, :log, :debug)
|
||||
secret_key_base = Keyword.get(opts, :secret_key_base)
|
||||
key_opts = [iterations: iterations, length: length, digest: digest, cache: Plug.Keys]
|
||||
|
||||
serializer = check_serializer(opts[:serializer] || :external_term_format)
|
||||
|
||||
%{
|
||||
secret_key_base: secret_key_base,
|
||||
encryption_salt: prederive(secret_key_base, encryption_salt, key_opts),
|
||||
signing_salt: prederive(secret_key_base, signing_salt, key_opts),
|
||||
key_opts: key_opts,
|
||||
serializer: serializer,
|
||||
log: log
|
||||
}
|
||||
end
|
||||
|
||||
defp build_rotating_opts(opts, rotating_opts) when is_list(rotating_opts) do
|
||||
Map.put(opts, :rotating_options, Enum.map(rotating_opts, &build_opts/1))
|
||||
end
|
||||
|
||||
defp build_rotating_opts(opts, _), do: Map.put(opts, :rotating_options, [])
|
||||
end
|
||||
98
phoenix/deps/plug/lib/plug/session/ets.ex
Normal file
98
phoenix/deps/plug/lib/plug/session/ets.ex
Normal file
@@ -0,0 +1,98 @@
|
||||
defmodule Plug.Session.ETS do
|
||||
@moduledoc """
|
||||
Stores the session in an in-memory ETS table.
|
||||
|
||||
This store does not create the ETS table; it expects that an
|
||||
existing named table with public properties is passed as an
|
||||
argument.
|
||||
|
||||
We don't recommend using this store in production as every
|
||||
session will be stored in ETS and never cleaned until you
|
||||
create a task responsible for cleaning up old entries.
|
||||
|
||||
Also, since the store is in-memory, it means sessions are
|
||||
not shared between servers. If you deploy to more than one
|
||||
machine, using this store is again not recommended.
|
||||
|
||||
This store, however, can be used as an example for creating
|
||||
custom storages, based on Redis, Memcached, or a database
|
||||
itself.
|
||||
|
||||
## Options
|
||||
|
||||
* `:table` - ETS table name (required)
|
||||
|
||||
For more information on ETS tables, visit the Erlang documentation at
|
||||
http://www.erlang.org/doc/man/ets.html.
|
||||
|
||||
## Storage
|
||||
|
||||
The data is stored in ETS in the following format:
|
||||
|
||||
{sid :: String.t, data :: map, timestamp :: :erlang.timestamp}
|
||||
|
||||
The timestamp is updated whenever there is a read or write to the
|
||||
table and it may be used to detect if a session is still active.
|
||||
|
||||
## Examples
|
||||
|
||||
# Create an ETS table when the application starts
|
||||
:ets.new(:session, [:named_table, :public, read_concurrency: true])
|
||||
|
||||
# Use the session plug with the table name
|
||||
plug Plug.Session, store: :ets, key: "sid", table: :session
|
||||
|
||||
"""
|
||||
|
||||
@behaviour Plug.Session.Store
|
||||
|
||||
@max_tries 100
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
Keyword.fetch!(opts, :table)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def get(_conn, sid, table) do
|
||||
case :ets.lookup(table, sid) do
|
||||
[{^sid, data, _timestamp}] ->
|
||||
:ets.update_element(table, sid, {3, now()})
|
||||
{sid, data}
|
||||
|
||||
[] ->
|
||||
{nil, %{}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def put(_conn, nil, data, table) do
|
||||
put_new(data, table)
|
||||
end
|
||||
|
||||
def put(_conn, sid, data, table) do
|
||||
:ets.insert(table, {sid, data, now()})
|
||||
sid
|
||||
end
|
||||
|
||||
@impl true
|
||||
def delete(_conn, sid, table) do
|
||||
:ets.delete(table, sid)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp put_new(data, table, counter \\ 0)
|
||||
when counter < @max_tries do
|
||||
sid = Base.encode64(:crypto.strong_rand_bytes(96))
|
||||
|
||||
if :ets.insert_new(table, {sid, data, now()}) do
|
||||
sid
|
||||
else
|
||||
put_new(data, table, counter + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp now() do
|
||||
:os.timestamp()
|
||||
end
|
||||
end
|
||||
71
phoenix/deps/plug/lib/plug/session/store.ex
Normal file
71
phoenix/deps/plug/lib/plug/session/store.ex
Normal file
@@ -0,0 +1,71 @@
|
||||
defmodule Plug.Session.Store do
|
||||
@moduledoc """
|
||||
Specification for session stores.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Gets the store name from an atom or a module.
|
||||
|
||||
iex> Plug.Session.Store.get(CustomStore)
|
||||
CustomStore
|
||||
|
||||
iex> Plug.Session.Store.get(:cookie)
|
||||
Plug.Session.COOKIE
|
||||
|
||||
"""
|
||||
def get(store) do
|
||||
case Atom.to_string(store) do
|
||||
"Elixir." <> _ -> store
|
||||
reference -> Module.concat(Plug.Session, String.upcase(reference))
|
||||
end
|
||||
end
|
||||
|
||||
@typedoc """
|
||||
The internal reference to the session in the store.
|
||||
"""
|
||||
@type sid :: term | nil
|
||||
|
||||
@typedoc """
|
||||
The cookie value that will be sent in cookie headers. This value should be
|
||||
base64 encoded to avoid security issues.
|
||||
"""
|
||||
@type cookie :: binary
|
||||
|
||||
@typedoc """
|
||||
The session contents, the final data to be stored after it has been built
|
||||
with `Plug.Conn.put_session/3` and the other session manipulating functions.
|
||||
"""
|
||||
@type session :: map
|
||||
|
||||
@doc """
|
||||
Initializes the store.
|
||||
|
||||
The options returned from this function will be given
|
||||
to `c:get/3`, `c:put/4` and `c:delete/3`.
|
||||
"""
|
||||
@callback init(opts :: Plug.opts()) :: Plug.opts()
|
||||
|
||||
@doc """
|
||||
Parses the given cookie.
|
||||
|
||||
Returns a session id and the session contents. The session id is any
|
||||
value that can be used to identify the session by the store.
|
||||
|
||||
The session id may be nil in case the cookie does not identify any
|
||||
value in the store. The session contents must be a map.
|
||||
"""
|
||||
@callback get(conn :: Plug.Conn.t(), cookie, opts :: Plug.opts()) :: {sid, session}
|
||||
|
||||
@doc """
|
||||
Stores the session associated with given session id.
|
||||
|
||||
If `nil` is given as id, a new session id should be
|
||||
generated and returned.
|
||||
"""
|
||||
@callback put(conn :: Plug.Conn.t(), sid, any, opts :: Plug.opts()) :: cookie
|
||||
|
||||
@doc """
|
||||
Removes the session associated with given session id from the store.
|
||||
"""
|
||||
@callback delete(conn :: Plug.Conn.t(), sid, opts :: Plug.opts()) :: :ok
|
||||
end
|
||||
480
phoenix/deps/plug/lib/plug/ssl.ex
Normal file
480
phoenix/deps/plug/lib/plug/ssl.ex
Normal file
@@ -0,0 +1,480 @@
|
||||
defmodule Plug.SSL do
|
||||
@moduledoc """
|
||||
A plug to force SSL connections and enable HSTS.
|
||||
|
||||
If the scheme of a request is `https`, it'll add a `strict-transport-security`
|
||||
header to enable HTTP Strict Transport Security by default.
|
||||
|
||||
Otherwise, the request will be redirected to a corresponding location
|
||||
with the `https` scheme by setting the `location` header of the response.
|
||||
The status code will be 301 if the method of `conn` is `GET` or `HEAD`,
|
||||
or 307 in other situations.
|
||||
|
||||
Besides being a Plug, this module also provides conveniences for configuring
|
||||
SSL. See `configure/1`.
|
||||
|
||||
## x-forwarded-*
|
||||
|
||||
If your Plug application is behind a proxy that handles HTTPS, you may
|
||||
need to tell Plug to parse the proper protocol from the `x-forwarded-*`
|
||||
header. This can be done using the `:rewrite_on` option:
|
||||
|
||||
plug Plug.SSL, rewrite_on: [:x_forwarded_host, :x_forwarded_port, :x_forwarded_proto]
|
||||
|
||||
Rewriting happens on all requests, before the SSL options are processed.
|
||||
For further details, refer to `Plug.RewriteOn`.
|
||||
|
||||
## Plug Options
|
||||
|
||||
* `:rewrite_on` - rewrites the given connection information based on the given headers
|
||||
* `:hsts` - a boolean on enabling HSTS or not, defaults to `true`
|
||||
* `:expires` - seconds to expires for HSTS, defaults to `31_536_000` (1 year)
|
||||
* `:preload` - a boolean to request inclusion on the HSTS preload list
|
||||
(for full set of required flags, see: [Chromium HSTS submission site](https://hstspreload.org)),
|
||||
defaults to `false`
|
||||
* `:subdomains` - a boolean on including subdomains or not in HSTS,
|
||||
defaults to `false`
|
||||
* `:exclude` - exclude certain request from redirecting to the `https` scheme.
|
||||
It defaults to `[hosts: ["localhost", "127.0.0.1"]]`. See the
|
||||
["Exclude option"](#module-exclude-option) section below
|
||||
* `:host` - a new host to redirect to if the request's scheme is `http`,
|
||||
defaults to `conn.host`. It may be set to a binary or a tuple
|
||||
`{module, function, args}` that will be invoked on demand
|
||||
* `:log` - The log level at which this plug should log its request info.
|
||||
Default is `:info`. Can be `false` to disable logging
|
||||
|
||||
## Port
|
||||
|
||||
It is not possible to directly configure the port in `Plug.SSL` because
|
||||
HSTS expects the port to be 443 for SSL. If you are not using HSTS and
|
||||
want to redirect to HTTPS on another port, you can sneak it alongside
|
||||
the host, for example: `host: "example.com:443"`.
|
||||
|
||||
## Exclude option
|
||||
|
||||
There are many situations where one may want to avoid `Plug.SSL` from
|
||||
redirecting, such as requests coming from `localhost` or `127.0.0.1`,
|
||||
or from health check endpoints.
|
||||
|
||||
This can be done via the `:exclude` option, which allows you to specify
|
||||
conditions to skip the redirect. As long as any of the conditions match,
|
||||
the route will be excluded, it must be one of:
|
||||
|
||||
* `[hosts: list_of_hosts, ...]` - skips redirection if the request
|
||||
matches any of the given hosts
|
||||
|
||||
* `[paths: list_of_paths, ...]` - skips redirection if the request
|
||||
matches any of the given paths
|
||||
|
||||
* `[conn: {mod, fun, args}, ...]` - calls the given `mod`, `fun`,
|
||||
and `args` with `Plug.Conn` prepended to the list of arguments.
|
||||
The plug will be excluded if the call returns `true`
|
||||
|
||||
The default value is `[hosts: ["localhost", "127.0.0.1"]]`. If you pass
|
||||
any additional value, you must explicitly preserve the above if you want
|
||||
the hosts to remain excluded.
|
||||
|
||||
For example, you may define it as:
|
||||
|
||||
plug Plug.SSL,
|
||||
exclude: [
|
||||
hosts: ["localhost", "127.0.0.1"],
|
||||
paths: ["/health"]
|
||||
]
|
||||
|
||||
"""
|
||||
@behaviour Plug
|
||||
|
||||
require Logger
|
||||
import Plug.Conn
|
||||
|
||||
@strong_tls_ciphers [
|
||||
# TLS 1.3 Ciphersuites
|
||||
~c"TLS_AES_256_GCM_SHA384",
|
||||
~c"TLS_CHACHA20_POLY1305_SHA256",
|
||||
~c"TLS_AES_128_GCM_SHA256"
|
||||
]
|
||||
|
||||
@compatible_tls_ciphers [
|
||||
# TLS 1.3 Ciphersuites
|
||||
~c"TLS_AES_256_GCM_SHA384",
|
||||
~c"TLS_CHACHA20_POLY1305_SHA256",
|
||||
~c"TLS_AES_128_GCM_SHA256",
|
||||
# TLS 1.2 Ciphersuites
|
||||
~c"ECDHE-ECDSA-AES256-GCM-SHA384",
|
||||
~c"ECDHE-RSA-AES256-GCM-SHA384",
|
||||
~c"ECDHE-ECDSA-CHACHA20-POLY1305",
|
||||
~c"ECDHE-RSA-CHACHA20-POLY1305",
|
||||
~c"ECDHE-ECDSA-AES128-GCM-SHA256",
|
||||
~c"ECDHE-RSA-AES128-GCM-SHA256",
|
||||
~c"DHE-RSA-AES256-GCM-SHA384",
|
||||
~c"DHE-RSA-AES128-GCM-SHA256"
|
||||
]
|
||||
|
||||
@eccs [
|
||||
:x25519,
|
||||
:secp256r1,
|
||||
:secp384r1,
|
||||
:secp521r1
|
||||
]
|
||||
|
||||
@doc """
|
||||
Configures and validates the options given to the `:ssl` application.
|
||||
|
||||
This function is often called internally by adapters, such as Cowboy,
|
||||
to validate and set reasonable defaults for SSL handling. Therefore
|
||||
Plug users are not expected to invoke it directly, rather you pass
|
||||
the relevant SSL options to your adapter which then invokes this.
|
||||
|
||||
## Options
|
||||
|
||||
This function accepts all options defined
|
||||
[in Erlang/OTP `:ssl` documentation](http://erlang.org/doc/man/ssl.html).
|
||||
|
||||
Besides the options from `:ssl`, this function adds on extra option:
|
||||
|
||||
* `:cipher_suite` - it may be `:strong` or `:compatible`,
|
||||
as outlined in the following section
|
||||
|
||||
Furthermore, it sets the following defaults:
|
||||
|
||||
* `secure_renegotiate: true` - to avoid certain types of man-in-the-middle attacks
|
||||
* `reuse_sessions: true` - for improved handshake performance of recurring connections
|
||||
|
||||
For a complete guide on HTTPS and best pratices, see [our Plug HTTPS Guide](https.html).
|
||||
|
||||
## Cipher Suites
|
||||
|
||||
To simplify configuration of TLS defaults, this function provides two preconfigured
|
||||
options: `cipher_suite: :strong` and `cipher_suite: :compatible`. The Ciphers
|
||||
chosen and related configuration come from the [Transport Layer Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html)
|
||||
|
||||
The **Strong** cipher suite supports TLSv1.3 as recommended by the Transport
|
||||
Layer Security Cheat Sheet. General purpose web applications should default to
|
||||
TLSv1.3 with ALL other protocols disabled.
|
||||
|
||||
The **Compatible** cipher suite supports TLSv1.2 and TLSv1.3. This
|
||||
suite provides strong security while maintaining compatibility with a wide
|
||||
range of modern clients.
|
||||
|
||||
Legacy protocols TLSv1.1 and TLSv1.0 are officially deprecated by
|
||||
[RFC 8996](https://www.rfc-editor.org/rfc/rfc8996.html) and are
|
||||
considered insecure.
|
||||
|
||||
[Test your ssl configuration](https://ssl-config.mozilla.org/)
|
||||
|
||||
**The cipher suites were last updated on 2025-AUG-28.**
|
||||
"""
|
||||
@spec configure([:ssl.tls_server_option()]) ::
|
||||
{:ok, [:ssl.tls_server_option()]} | {:error, String.t()}
|
||||
def configure(options) do
|
||||
options
|
||||
|> check_for_missing_keys()
|
||||
|> validate_ciphers()
|
||||
|> normalize_ssl_files()
|
||||
|> normalize_certs_keys_ssl_files()
|
||||
|> convert_to_charlist()
|
||||
|> configure_managed_tls()
|
||||
|> set_secure_defaults()
|
||||
catch
|
||||
{:configure, message} -> {:error, message}
|
||||
else
|
||||
options -> {:ok, options}
|
||||
end
|
||||
|
||||
defp check_for_missing_keys(options) do
|
||||
has_certs_keys? = List.keymember?(options, :certs_keys, 0)
|
||||
has_sni? = List.keymember?(options, :sni_hosts, 0) or List.keymember?(options, :sni_fun, 0)
|
||||
has_key? = List.keymember?(options, :key, 0) or List.keymember?(options, :keyfile, 0)
|
||||
has_cert? = List.keymember?(options, :cert, 0) or List.keymember?(options, :certfile, 0)
|
||||
|
||||
cond do
|
||||
has_sni? -> options
|
||||
not (has_key? or has_certs_keys?) -> fail("missing option :key/:keyfile/:certs_keys")
|
||||
not (has_cert? or has_certs_keys?) -> fail("missing option :cert/:certfile/:certs_keys")
|
||||
true -> options
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_ssl_files(options) do
|
||||
ssl_files = [:keyfile, :certfile, :cacertfile, :dhfile]
|
||||
Enum.reduce(ssl_files, options, &normalize_ssl_file(&1, &2, options[:otp_app]))
|
||||
end
|
||||
|
||||
defp normalize_certs_keys_ssl_files(options) do
|
||||
if certs_keys = options[:certs_keys] do
|
||||
ssl_files = [:keyfile, :certfile]
|
||||
|
||||
updated_certs_keys =
|
||||
Enum.map(certs_keys, fn cert_key ->
|
||||
Enum.reduce(
|
||||
ssl_files,
|
||||
Map.to_list(cert_key),
|
||||
&normalize_ssl_file(&1, &2, options[:otp_app])
|
||||
)
|
||||
|> Map.new()
|
||||
end)
|
||||
|
||||
List.keystore(options, :certs_keys, 0, {:certs_keys, updated_certs_keys})
|
||||
else
|
||||
options
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_ssl_file(key, options, otp_app) do
|
||||
value = options[key]
|
||||
|
||||
cond do
|
||||
is_nil(value) ->
|
||||
options
|
||||
|
||||
Path.type(value) == :absolute ->
|
||||
put_ssl_file(options, key, value)
|
||||
|
||||
true ->
|
||||
put_ssl_file(options, key, Path.expand(value, resolve_otp_app(otp_app)))
|
||||
end
|
||||
end
|
||||
|
||||
defp put_ssl_file(options, key, value) do
|
||||
value = to_charlist(value)
|
||||
|
||||
unless File.exists?(value) do
|
||||
message =
|
||||
"the file #{value} required by SSL's #{inspect(key)} either does not exist, " <>
|
||||
"or the application does not have permission to access it"
|
||||
|
||||
fail(message)
|
||||
end
|
||||
|
||||
List.keystore(options, key, 0, {key, value})
|
||||
end
|
||||
|
||||
defp resolve_otp_app(otp_app) do
|
||||
if otp_app do
|
||||
Application.app_dir(otp_app)
|
||||
else
|
||||
fail("the :otp_app option is required when setting relative SSL certfiles")
|
||||
end
|
||||
end
|
||||
|
||||
defp convert_to_charlist(options) do
|
||||
Enum.reduce([:password], options, fn key, acc ->
|
||||
if value = acc[key] do
|
||||
List.keystore(acc, key, 0, {key, to_charlist(value)})
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp set_secure_defaults(options) do
|
||||
versions = options[:versions] || :ssl.versions()[:supported]
|
||||
|
||||
if Enum.any?([:tlsv1, :"tlsv1.1", :"tlsv1.2"], &(&1 in versions)) do
|
||||
options
|
||||
|> keynew(:secure_renegotiate, 0, {:secure_renegotiate, true})
|
||||
|> keynew(:reuse_sessions, 0, {:reuse_sessions, true})
|
||||
else
|
||||
options
|
||||
|> List.keydelete(:secure_renegotiate, 0)
|
||||
|> List.keydelete(:reuse_sessions, 0)
|
||||
end
|
||||
end
|
||||
|
||||
defp configure_managed_tls(options) do
|
||||
{_, cipher_suite} = List.keyfind(options, :cipher_suite, 0, {:cipher_suite, nil})
|
||||
options = List.keydelete(options, :cipher_suite, 0)
|
||||
|
||||
case cipher_suite do
|
||||
:strong -> set_strong_tls_defaults(options)
|
||||
:compatible -> set_compatible_tls_defaults(options)
|
||||
nil -> options
|
||||
_ -> fail("unknown :cipher_suite named #{inspect(cipher_suite)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp set_managed_tls_defaults(options) do
|
||||
options
|
||||
|> keynew(:honor_cipher_order, 0, {:honor_cipher_order, true})
|
||||
|> keynew(:eccs, 0, {:eccs, @eccs})
|
||||
end
|
||||
|
||||
defp set_strong_tls_defaults(options) do
|
||||
options
|
||||
|> set_managed_tls_defaults
|
||||
|> keynew(:ciphers, 0, {:ciphers, @strong_tls_ciphers})
|
||||
|> keynew(:versions, 0, {:versions, [:"tlsv1.3"]})
|
||||
end
|
||||
|
||||
defp set_compatible_tls_defaults(options) do
|
||||
options
|
||||
|> set_managed_tls_defaults
|
||||
|> keynew(:ciphers, 0, {:ciphers, @compatible_tls_ciphers})
|
||||
|> keynew(:versions, 0, {:versions, [:"tlsv1.3", :"tlsv1.2"]})
|
||||
end
|
||||
|
||||
defp validate_ciphers(options) do
|
||||
options
|
||||
|> List.keyfind(:ciphers, 0, {:ciphers, []})
|
||||
|> elem(1)
|
||||
|> Enum.each(&validate_cipher/1)
|
||||
|
||||
options
|
||||
end
|
||||
|
||||
defp validate_cipher(cipher) do
|
||||
if is_binary(cipher) do
|
||||
message =
|
||||
"invalid cipher #{inspect(cipher)} in cipher list. " <>
|
||||
"Strings (double-quoted) are not allowed in ciphers. " <>
|
||||
"Ciphers must be either charlists (single-quoted) or tuples. " <>
|
||||
"See the ssl application docs for reference"
|
||||
|
||||
fail(message)
|
||||
end
|
||||
end
|
||||
|
||||
defp fail(message) when is_binary(message) do
|
||||
throw({:configure, message})
|
||||
end
|
||||
|
||||
defp keynew(list, key, position, value) do
|
||||
if List.keymember?(list, key, position), do: list, else: [value | list]
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
host = Keyword.get(opts, :host)
|
||||
|
||||
case host do
|
||||
{:system, _} ->
|
||||
IO.warn(
|
||||
"Using {:system, host} as your Plug.SSL host is deprecated. Pass nil or a string instead."
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
exclude =
|
||||
if exclude = Keyword.get(opts, :exclude) do
|
||||
validate_exclude!(exclude)
|
||||
else
|
||||
[hosts: ["localhost", "127.0.0.1"]]
|
||||
end
|
||||
|
||||
log = Keyword.get(opts, :log, :info)
|
||||
rewrite_on = Plug.RewriteOn.init(Keyword.get(opts, :rewrite_on))
|
||||
{hsts_header(opts), exclude, host, rewrite_on, log}
|
||||
end
|
||||
|
||||
defp validate_exclude!(exclude) when is_list(exclude) do
|
||||
Enum.map(exclude, fn
|
||||
# TODO: Deprecate me on Plug v1.20
|
||||
binary when is_binary(binary) ->
|
||||
{:hosts, [binary]}
|
||||
|
||||
{:hosts, hosts} when is_list(hosts) ->
|
||||
{:hosts, hosts}
|
||||
|
||||
{:paths, paths} when is_list(paths) ->
|
||||
{:paths, Enum.map(paths, &Plug.Router.Utils.split/1)}
|
||||
|
||||
{:conn, {mod, fun, args}} when is_atom(mod) and is_atom(fun) and is_list(args) ->
|
||||
{:conn, {mod, fun, args}}
|
||||
|
||||
other ->
|
||||
raise ArgumentError,
|
||||
"invalid entry in :exclude, expected host or path, got: #{inspect(other)}"
|
||||
end)
|
||||
end
|
||||
|
||||
defp validate_exclude!({m, f, a}) do
|
||||
IO.warn(
|
||||
"exclude: {mod, fun, args} is deprecated, " <>
|
||||
"please use exclude: [conn: {mod, fun, args}], which will receive the whole connection instead"
|
||||
)
|
||||
|
||||
{m, f, a}
|
||||
end
|
||||
|
||||
defp validate_exclude!(exclude) do
|
||||
raise ArgumentError, ":exclude must be a list, got: #{inspect(exclude)}"
|
||||
end
|
||||
|
||||
@impl true
|
||||
def call(conn, {hsts, exclude, host, rewrite_on, log_level}) do
|
||||
conn = Plug.RewriteOn.call(conn, rewrite_on)
|
||||
|
||||
cond do
|
||||
excluded?(conn, exclude) -> conn
|
||||
conn.scheme == :https -> put_hsts_header(conn, hsts)
|
||||
true -> redirect_to_https(conn, host, log_level)
|
||||
end
|
||||
end
|
||||
|
||||
defp excluded?(conn, list) when is_list(list) do
|
||||
Enum.any?(list, fn
|
||||
{:hosts, hosts} -> conn.host in hosts
|
||||
{:paths, paths} -> conn.path_info in paths
|
||||
{:conn, {mod, fun, args}} -> apply(mod, fun, [conn | args])
|
||||
end)
|
||||
end
|
||||
|
||||
defp excluded?(conn, {mod, fun, args}), do: apply(mod, fun, [conn.host | args])
|
||||
|
||||
# http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
|
||||
defp hsts_header(opts) do
|
||||
if Keyword.get(opts, :hsts, true) do
|
||||
expires = Keyword.get(opts, :expires, 31_536_000)
|
||||
preload = Keyword.get(opts, :preload, false)
|
||||
subdomains = Keyword.get(opts, :subdomains, false)
|
||||
|
||||
"max-age=#{expires}" <>
|
||||
if(preload, do: "; preload", else: "") <>
|
||||
if(subdomains, do: "; includeSubDomains", else: "")
|
||||
end
|
||||
end
|
||||
|
||||
defp put_hsts_header(conn, hsts_header) when is_binary(hsts_header) do
|
||||
put_resp_header(conn, "strict-transport-security", hsts_header)
|
||||
end
|
||||
|
||||
defp put_hsts_header(conn, nil), do: conn
|
||||
|
||||
defp redirect_to_https(%{host: host} = conn, custom_host, log_level) do
|
||||
status = if conn.method in ~w(HEAD GET), do: 301, else: 307
|
||||
|
||||
scheme_and_host = "https://" <> host(custom_host, host)
|
||||
location = scheme_and_host <> conn.request_path <> qs(conn.query_string)
|
||||
|
||||
log_level &&
|
||||
Logger.log(log_level, fn ->
|
||||
[
|
||||
"Plug.SSL is redirecting ",
|
||||
conn.method,
|
||||
?\s,
|
||||
conn.request_path,
|
||||
" to ",
|
||||
scheme_and_host,
|
||||
" with status ",
|
||||
Integer.to_string(status)
|
||||
]
|
||||
end)
|
||||
|
||||
conn
|
||||
|> put_resp_header("location", location)
|
||||
|> send_resp(status, "")
|
||||
|> halt
|
||||
end
|
||||
|
||||
defp host(nil, host), do: host
|
||||
defp host(host, _) when is_binary(host), do: host
|
||||
defp host({mod, fun, args}, host), do: host(apply(mod, fun, args), host)
|
||||
# TODO: Remove me once the deprecation is removed.
|
||||
defp host({:system, env}, host), do: host(System.get_env(env), host)
|
||||
|
||||
defp qs(""), do: ""
|
||||
defp qs(qs), do: "?" <> qs
|
||||
end
|
||||
473
phoenix/deps/plug/lib/plug/static.ex
Normal file
473
phoenix/deps/plug/lib/plug/static.ex
Normal file
@@ -0,0 +1,473 @@
|
||||
defmodule Plug.Static do
|
||||
@moduledoc """
|
||||
A plug for serving static assets.
|
||||
|
||||
It requires two options:
|
||||
|
||||
* `:at` - the request path to reach for static assets.
|
||||
It must be a string.
|
||||
|
||||
* `:from` - the file system path to read static assets from.
|
||||
It can be either: a string containing a file system path, an
|
||||
atom representing the application name (where assets will
|
||||
be served from `priv/static`), a tuple containing the
|
||||
application name and the directory to serve assets from (besides
|
||||
`priv/static`), or an MFA tuple.
|
||||
|
||||
The preferred form is to use `:from` with an atom or tuple, since
|
||||
it will make your application independent from the starting directory.
|
||||
For example, if you pass:
|
||||
|
||||
plug Plug.Static, from: "priv/app/path"
|
||||
|
||||
Plug.Static will be unable to serve assets if you build releases
|
||||
or if you change the current directory. Instead do:
|
||||
|
||||
plug Plug.Static, from: {:app_name, "priv/app/path"}
|
||||
|
||||
If a static asset cannot be found, `Plug.Static` simply forwards
|
||||
the connection to the rest of the pipeline.
|
||||
|
||||
## Cache mechanisms
|
||||
|
||||
`Plug.Static` uses etags for HTTP caching. This means browsers/clients
|
||||
should cache assets on the first request and validate the cache on
|
||||
following requests, not downloading the static asset once again if it
|
||||
has not changed. The cache-control for etags is specified by the
|
||||
`cache_control_for_etags` option and defaults to `"public"`.
|
||||
|
||||
However, `Plug.Static` also supports direct cache control by using
|
||||
versioned query strings. If the request query string starts with
|
||||
"?vsn=", `Plug.Static` assumes the application is versioning assets
|
||||
and does not set the `ETag` header, meaning the cache behaviour will
|
||||
be specified solely by the `cache_control_for_vsn_requests` config,
|
||||
which defaults to `"public, max-age=31536000"`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:encodings` - list of 2-ary tuples where first value is value of
|
||||
the `Accept-Encoding` header and second is extension of the file to
|
||||
be served if given encoding is accepted by client. Entries will be tested
|
||||
in order in list, so entries higher in list will be preferred. Defaults
|
||||
to: `[]`.
|
||||
|
||||
In addition to setting this value directly it supports 2 additional
|
||||
options for compatibility reasons:
|
||||
|
||||
+ `:brotli` - will append `{"br", ".br"}` to the encodings list.
|
||||
+ `:gzip` - will append `{"gzip", ".gz"}` to the encodings list.
|
||||
|
||||
Additional options will be added in the above order (Brotli takes
|
||||
preference over Gzip) to reflect older behaviour which was set due
|
||||
to fact that Brotli in general provides better compression ratio than
|
||||
Gzip.
|
||||
|
||||
* `:cache_control_for_etags` - sets the cache header for requests
|
||||
that use etags. Defaults to `"public"`.
|
||||
|
||||
* `:etag_generation` - specify a `{module, function, args}` to be used
|
||||
to generate an etag. The `path` of the resource will be passed to
|
||||
the function, as well as the `args`. If this option is not supplied,
|
||||
etags will be generated based off of file size and modification time.
|
||||
Note it is [recommended for the etag value to be quoted](https://tools.ietf.org/html/rfc7232#section-2.3),
|
||||
which Plug won't do automatically.
|
||||
|
||||
* `:cache_control_for_vsn_requests` - sets the cache header for
|
||||
requests starting with "?vsn=" in the query string. Defaults to
|
||||
`"public, max-age=31536000"`.
|
||||
|
||||
* `:only` - filters which requests to serve. This is useful to avoid
|
||||
file system access on every request when this plug is mounted
|
||||
at `"/"`. For example, if `only: ["images", "favicon.ico"]` is
|
||||
specified, only files in the "images" directory and the
|
||||
"favicon.ico" file will be served by `Plug.Static`.
|
||||
Note that `Plug.Static` matches these filters against request
|
||||
uri and not against the filesystem. When requesting
|
||||
a file with name containing non-ascii or special characters,
|
||||
you should use urlencoded form. For example, you should write
|
||||
`only: ["file%20name"]` instead of `only: ["file name"]`.
|
||||
Defaults to `nil` (no filtering).
|
||||
|
||||
* `:only_matching` - a relaxed version of `:only` that will
|
||||
serve any request as long as one of the given values matches the
|
||||
given path. For example, `only_matching: ["images", "favicon"]`
|
||||
will match any request that starts at "images" or "favicon",
|
||||
be it "/images/foo.png", "/images-high/foo.png", "/favicon.ico"
|
||||
or "/favicon-high.ico". Such matches are useful when serving
|
||||
digested files at the root. Defaults to `nil` (no filtering).
|
||||
|
||||
* `:raise_on_missing_only` - when `true`, raises an exception if a static
|
||||
file exists but does not match the `:only` list. This is useful in
|
||||
development to catch missing entries, especially for digested files.
|
||||
For example, if `favicon.ico` is in `:only` but the actual file is
|
||||
`favicon-deadbeef.ico`, this option will raise an error. Defaults to `false`.
|
||||
|
||||
* `:headers` - other headers to be set when serving static assets. Specify either
|
||||
an enum of key-value pairs or a `{module, function, args}` to return an enum. The
|
||||
`conn` will be passed to the function, as well as the `args`.
|
||||
|
||||
* `:content_types` - controls custom MIME type mapping.
|
||||
It can be a map with filename as key and content type as value to override
|
||||
the default type for matching filenames. For example:
|
||||
`content_types: %{"apple-app-site-association" => "application/json"}`.
|
||||
Alternatively, it can be the value `false` to opt out of setting the header at all. The latter
|
||||
can be used to set the header based on custom logic before calling this plug.
|
||||
Defaults to an empty map `%{}`.
|
||||
|
||||
## Examples
|
||||
|
||||
This plug can be mounted in a `Plug.Builder` pipeline as follows:
|
||||
|
||||
defmodule MyPlug do
|
||||
use Plug.Builder
|
||||
|
||||
plug Plug.Static,
|
||||
at: "/public",
|
||||
from: :my_app,
|
||||
only: ~w(images robots.txt)
|
||||
plug :not_found
|
||||
|
||||
def not_found(conn, _) do
|
||||
send_resp(conn, 404, "not found")
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
@allowed_methods ~w(GET HEAD)
|
||||
|
||||
import Plug.Conn
|
||||
alias Plug.Conn
|
||||
|
||||
# In this module, the `:prim_file` Erlang module along with the `:file_info`
|
||||
# record are used instead of the more common and Elixir-y `File` module and
|
||||
# `File.Stat` struct, respectively. The reason behind this is performance: all
|
||||
# the `File` operations pass through a single process in order to support node
|
||||
# operations that we simply don't need when serving assets.
|
||||
|
||||
require Record
|
||||
Record.defrecordp(:file_info, Record.extract(:file_info, from_lib: "kernel/include/file.hrl"))
|
||||
|
||||
defmodule InvalidPathError do
|
||||
defexception message: "invalid path for static asset", plug_status: 400
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
from =
|
||||
case Keyword.fetch!(opts, :from) do
|
||||
{_, _} = from -> from
|
||||
{_, _, _} = from -> from
|
||||
from when is_atom(from) -> {from, "priv/static"}
|
||||
from when is_binary(from) -> from
|
||||
_ -> raise ArgumentError, ":from must be an atom, a binary or a tuple"
|
||||
end
|
||||
|
||||
encodings =
|
||||
opts
|
||||
|> Keyword.get(:encodings, [])
|
||||
|> maybe_add("br", ".br", Keyword.get(opts, :brotli, false))
|
||||
|> maybe_add("gzip", ".gz", Keyword.get(opts, :gzip, false))
|
||||
|
||||
only_status =
|
||||
if Keyword.get(opts, :raise_on_missing_only, false) do
|
||||
:raise
|
||||
else
|
||||
:forbidden
|
||||
end
|
||||
|
||||
%{
|
||||
encodings: encodings,
|
||||
only_rules:
|
||||
{Keyword.get(opts, :only, []), Keyword.get(opts, :only_matching, []), only_status},
|
||||
qs_cache:
|
||||
Keyword.get(opts, :cache_control_for_vsn_requests, "public, max-age=31536000, immutable"),
|
||||
et_cache: Keyword.get(opts, :cache_control_for_etags, "public"),
|
||||
et_generation: Keyword.get(opts, :etag_generation, nil),
|
||||
headers: Keyword.get(opts, :headers, %{}),
|
||||
content_types: Keyword.get(opts, :content_types, %{}),
|
||||
from: from,
|
||||
at: opts |> Keyword.fetch!(:at) |> Plug.Router.Utils.split()
|
||||
}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def call(
|
||||
conn = %Conn{method: meth},
|
||||
%{at: at, only_rules: only_rules, from: from, encodings: encodings} = options
|
||||
)
|
||||
when meth in @allowed_methods do
|
||||
segments = subset(at, conn.path_info)
|
||||
|
||||
case path_status(only_rules, segments) do
|
||||
:forbidden ->
|
||||
conn
|
||||
|
||||
status ->
|
||||
segments = Enum.map(segments, &URI.decode/1)
|
||||
|
||||
if invalid_path?(segments) do
|
||||
raise InvalidPathError, "invalid path for static asset: #{conn.request_path}"
|
||||
end
|
||||
|
||||
path = path(from, segments)
|
||||
range = get_req_header(conn, "range")
|
||||
|
||||
case file_encoding(conn, path, range, encodings) do
|
||||
:error ->
|
||||
conn
|
||||
|
||||
triplet ->
|
||||
if status == :raise do
|
||||
raise InvalidPathError,
|
||||
"static file exists but is not in the :only list: #{Enum.join(segments, "/")}. " <>
|
||||
"Add it to the :only list or use :only_matching for prefix matching"
|
||||
end
|
||||
|
||||
serve_static(triplet, conn, segments, range, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _options) do
|
||||
conn
|
||||
end
|
||||
|
||||
defp path_status(_only_rules, []), do: :forbidden
|
||||
defp path_status({[], [], _}, _list), do: :allowed
|
||||
|
||||
defp path_status({full, prefix, status}, [h | _]) do
|
||||
if h in full or (prefix != [] and match?({0, _}, :binary.match(h, prefix))) do
|
||||
:allowed
|
||||
else
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_put_content_type(conn, false, _), do: conn
|
||||
|
||||
defp maybe_put_content_type(conn, types, filename) do
|
||||
content_type = Map.get(types, filename) || MIME.from_path(filename)
|
||||
put_resp_header(conn, "content-type", content_type)
|
||||
end
|
||||
|
||||
defp serve_static({content_encoding, file_info, path}, conn, segments, range, options) do
|
||||
%{
|
||||
qs_cache: qs_cache,
|
||||
et_cache: et_cache,
|
||||
et_generation: et_generation,
|
||||
headers: headers,
|
||||
content_types: types
|
||||
} = options
|
||||
|
||||
case put_cache_header(conn, qs_cache, et_cache, et_generation, file_info, path) do
|
||||
{:stale, conn} ->
|
||||
filename = List.last(segments)
|
||||
|
||||
conn
|
||||
|> maybe_put_content_type(types, filename)
|
||||
|> put_resp_header("accept-ranges", "bytes")
|
||||
|> maybe_add_encoding(content_encoding)
|
||||
|> merge_headers(headers)
|
||||
|> serve_range(file_info, path, range, options)
|
||||
|
||||
{:fresh, conn} ->
|
||||
conn
|
||||
|> maybe_add_vary(options)
|
||||
|> send_resp(304, "")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
defp serve_range(conn, file_info, path, [range], options) do
|
||||
file_info(size: file_size) = file_info
|
||||
|
||||
with %{"bytes" => bytes} <- Plug.Conn.Utils.params(range),
|
||||
{range_start, range_end} <- start_and_end(bytes, file_size) do
|
||||
send_range(conn, path, range_start, range_end, file_size, options)
|
||||
else
|
||||
_ -> send_entire_file(conn, path, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp serve_range(conn, _file_info, path, _range, options) do
|
||||
send_entire_file(conn, path, options)
|
||||
end
|
||||
|
||||
defp start_and_end("-" <> rest, file_size) do
|
||||
case Integer.parse(rest) do
|
||||
{last, ""} when last > 0 and last <= file_size -> {file_size - last, file_size - 1}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp start_and_end(range, file_size) do
|
||||
case Integer.parse(range) do
|
||||
{first, "-"} when first >= 0 ->
|
||||
{first, file_size - 1}
|
||||
|
||||
{first, "-" <> rest} when first >= 0 ->
|
||||
case Integer.parse(rest) do
|
||||
{last, ""} when last >= first -> {first, min(last, file_size - 1)}
|
||||
_ -> :error
|
||||
end
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp send_range(conn, path, 0, range_end, file_size, options) when range_end == file_size - 1 do
|
||||
send_entire_file(conn, path, options)
|
||||
end
|
||||
|
||||
defp send_range(conn, path, range_start, range_end, file_size, _options) do
|
||||
length = range_end - range_start + 1
|
||||
|
||||
conn
|
||||
|> put_resp_header("content-range", "bytes #{range_start}-#{range_end}/#{file_size}")
|
||||
|> send_file(206, path, range_start, length)
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp send_entire_file(conn, path, options) do
|
||||
conn
|
||||
|> maybe_add_vary(options)
|
||||
|> send_file(200, path)
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp maybe_add_encoding(conn, nil), do: conn
|
||||
defp maybe_add_encoding(conn, ce), do: put_resp_header(conn, "content-encoding", ce)
|
||||
|
||||
defp maybe_add_vary(conn, %{encodings: encodings}) do
|
||||
# If we serve gzip or brotli at any moment, we need to set the proper vary
|
||||
# header regardless of whether we are serving gzip content right now.
|
||||
# See: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
|
||||
if encodings != [] do
|
||||
update_in(conn.resp_headers, &[{"vary", "Accept-Encoding"} | &1])
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
defp put_cache_header(
|
||||
%Conn{query_string: "vsn=" <> _} = conn,
|
||||
qs_cache,
|
||||
_et_cache,
|
||||
_et_generation,
|
||||
_file_info,
|
||||
_path
|
||||
)
|
||||
when is_binary(qs_cache) do
|
||||
{:stale, put_resp_header(conn, "cache-control", qs_cache)}
|
||||
end
|
||||
|
||||
defp put_cache_header(conn, _qs_cache, et_cache, et_generation, file_info, path)
|
||||
when is_binary(et_cache) do
|
||||
etag = etag_for_path(file_info, et_generation, path)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_resp_header("cache-control", et_cache)
|
||||
|> put_resp_header("etag", etag)
|
||||
|
||||
if etag in get_req_header(conn, "if-none-match") do
|
||||
{:fresh, conn}
|
||||
else
|
||||
{:stale, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp put_cache_header(conn, _, _, _, _, _) do
|
||||
{:stale, conn}
|
||||
end
|
||||
|
||||
defp etag_for_path(file_info, et_generation, path) do
|
||||
case et_generation do
|
||||
{module, function, args} ->
|
||||
apply(module, function, [path | args])
|
||||
|
||||
nil ->
|
||||
file_info(size: size, mtime: mtime) = file_info
|
||||
<<?", {size, mtime} |> :erlang.phash2() |> Integer.to_string(16)::binary, ?">>
|
||||
end
|
||||
end
|
||||
|
||||
defp file_encoding(conn, path, [_range], _encodings) do
|
||||
# We do not support compression for range queries.
|
||||
file_encoding(conn, path, nil, [])
|
||||
end
|
||||
|
||||
defp file_encoding(conn, path, _range, encodings) do
|
||||
encoded =
|
||||
Enum.find_value(encodings, fn {encoding, ext} ->
|
||||
if file_info = accept_encoding?(conn, encoding) && regular_file_info(path <> ext) do
|
||||
{encoding, file_info, path <> ext}
|
||||
end
|
||||
end)
|
||||
|
||||
cond do
|
||||
not is_nil(encoded) ->
|
||||
encoded
|
||||
|
||||
file_info = regular_file_info(path) ->
|
||||
{nil, file_info, path}
|
||||
|
||||
true ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp regular_file_info(path) do
|
||||
case :prim_file.read_file_info(path) do
|
||||
{:ok, file_info(type: :regular) = file_info} ->
|
||||
file_info
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_encoding?(conn, encoding) do
|
||||
encoding? = &String.contains?(&1, [encoding, "*"])
|
||||
|
||||
Enum.any?(get_req_header(conn, "accept-encoding"), fn accept ->
|
||||
accept |> Plug.Conn.Utils.list() |> Enum.any?(encoding?)
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_add(list, key, value, true), do: list ++ [{key, value}]
|
||||
defp maybe_add(list, _key, _value, false), do: list
|
||||
|
||||
defp path({module, function, arguments}, segments)
|
||||
when is_atom(module) and is_atom(function) and is_list(arguments),
|
||||
do: Enum.join([apply(module, function, arguments) | segments], "/")
|
||||
|
||||
defp path({app, from}, segments) when is_atom(app) and is_binary(from),
|
||||
do: Enum.join([Application.app_dir(app), from | segments], "/")
|
||||
|
||||
defp path(from, segments),
|
||||
do: Enum.join([from | segments], "/")
|
||||
|
||||
defp subset([h | expected], [h | actual]), do: subset(expected, actual)
|
||||
defp subset([], actual), do: actual
|
||||
defp subset(_, _), do: []
|
||||
|
||||
defp invalid_path?(list) do
|
||||
invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"]))
|
||||
end
|
||||
|
||||
defp invalid_path?([h | _], _match) when h in [".", "..", ""], do: true
|
||||
defp invalid_path?([h | t], match), do: String.contains?(h, match) or invalid_path?(t)
|
||||
defp invalid_path?([], _match), do: false
|
||||
|
||||
defp merge_headers(conn, {module, function, args}) do
|
||||
merge_headers(conn, apply(module, function, [conn | args]))
|
||||
end
|
||||
|
||||
defp merge_headers(conn, headers) do
|
||||
merge_resp_headers(conn, headers)
|
||||
end
|
||||
end
|
||||
89
phoenix/deps/plug/lib/plug/telemetry.ex
Normal file
89
phoenix/deps/plug/lib/plug/telemetry.ex
Normal file
@@ -0,0 +1,89 @@
|
||||
defmodule Plug.Telemetry do
|
||||
@moduledoc """
|
||||
A plug to instrument the pipeline with `:telemetry` events.
|
||||
|
||||
When plugged, the event prefix is a required option:
|
||||
|
||||
plug Plug.Telemetry, event_prefix: [:my, :plug]
|
||||
|
||||
In the example above, two events will be emitted:
|
||||
|
||||
* `[:my, :plug, :start]` - emitted when the plug is invoked.
|
||||
The event carries the `system_time` as measurement. The metadata
|
||||
is the whole `Plug.Conn` under the `:conn` key and any leftover
|
||||
options given to the plug under `:options`.
|
||||
|
||||
* `[:my, :plug, :stop]` - emitted right before the response is sent.
|
||||
The event carries a single measurement, `:duration`, which is the
|
||||
monotonic time difference between the stop and start events.
|
||||
It has the same metadata as the start event, except the connection
|
||||
has been updated.
|
||||
|
||||
Note this plug measures the time between its invocation until a response
|
||||
is sent. The `:stop` event is not guaranteed to be emitted in all error
|
||||
cases, so this Plug cannot be used as a Telemetry span.
|
||||
|
||||
## Time unit
|
||||
|
||||
The `:duration` measurements are presented in the `:native` time unit.
|
||||
You can read more about it in the docs for `System.convert_time_unit/3`.
|
||||
|
||||
## Example
|
||||
|
||||
defmodule InstrumentedPlug do
|
||||
use Plug.Router
|
||||
|
||||
plug :match
|
||||
plug Plug.Telemetry, event_prefix: [:my, :plug]
|
||||
plug Plug.Parsers, parsers: [:urlencoded, :multipart]
|
||||
plug :dispatch
|
||||
|
||||
get "/" do
|
||||
send_resp(conn, 200, "Hello, world!")
|
||||
end
|
||||
end
|
||||
|
||||
In this example, the stop event's `duration` includes the time
|
||||
it takes to parse the request, dispatch it to the correct handler,
|
||||
and execute the handler. The events are not emitted for requests
|
||||
not matching any handlers, since the plug is placed after the match plug.
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
{event_prefix, opts} = Keyword.pop(opts, :event_prefix)
|
||||
|
||||
unless event_prefix do
|
||||
raise ArgumentError, ":event_prefix is required"
|
||||
end
|
||||
|
||||
ensure_valid_event_prefix!(event_prefix)
|
||||
start_event = event_prefix ++ [:start]
|
||||
stop_event = event_prefix ++ [:stop]
|
||||
{start_event, stop_event, opts}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def call(conn, {start_event, stop_event, opts}) do
|
||||
start_time = System.monotonic_time()
|
||||
metadata = %{conn: conn, options: opts}
|
||||
:telemetry.execute(start_event, %{system_time: System.system_time()}, metadata)
|
||||
|
||||
Plug.Conn.register_before_send(conn, fn conn ->
|
||||
duration = System.monotonic_time() - start_time
|
||||
:telemetry.execute(stop_event, %{duration: duration}, %{conn: conn, options: opts})
|
||||
conn
|
||||
end)
|
||||
end
|
||||
|
||||
defp ensure_valid_event_prefix!(event_prefix) do
|
||||
if is_list(event_prefix) && Enum.all?(event_prefix, &is_atom/1) do
|
||||
:ok
|
||||
else
|
||||
raise ArgumentError,
|
||||
"expected :event_prefix to be a list of atoms, got: #{inspect(event_prefix)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
1151
phoenix/deps/plug/lib/plug/templates/debugger.html.eex
Normal file
1151
phoenix/deps/plug/lib/plug/templates/debugger.html.eex
Normal file
File diff suppressed because it is too large
Load Diff
24
phoenix/deps/plug/lib/plug/templates/debugger.md.eex
Normal file
24
phoenix/deps/plug/lib/plug/templates/debugger.md.eex
Normal file
@@ -0,0 +1,24 @@
|
||||
# <%= @title %> at <%= method(@conn) %> <%= @conn.request_path %>
|
||||
|
||||
Exception:
|
||||
|
||||
<%= String.replace(@formatted, "\n", "\n ") %>
|
||||
|
||||
## Connection details
|
||||
|
||||
### Params
|
||||
|
||||
<%= inspect(@params) %>
|
||||
|
||||
### Request info
|
||||
|
||||
* URI: <%= url(@conn) %>
|
||||
* Query string: <%= @conn.query_string %>
|
||||
|
||||
<%= if @full_version do %>### Headers
|
||||
<%= for {key, value} <- Enum.sort(@conn.req_headers) do %>
|
||||
* <%= key %>: <%= value %><% end %>
|
||||
|
||||
<% end %>### Session
|
||||
|
||||
<%= inspect(@session) %>
|
||||
300
phoenix/deps/plug/lib/plug/test.ex
Normal file
300
phoenix/deps/plug/lib/plug/test.ex
Normal file
@@ -0,0 +1,300 @@
|
||||
defmodule Plug.Test do
|
||||
@moduledoc """
|
||||
Conveniences for testing plugs.
|
||||
|
||||
This module can be used in your test cases, like this:
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
import Plug.Test
|
||||
import Plug.Conn
|
||||
|
||||
Using this module will:
|
||||
|
||||
* import all the functions from this module
|
||||
* import all the functions from the `Plug.Conn` module
|
||||
|
||||
By default, Plug tests checks for invalid header keys, e.g. header keys which
|
||||
include uppercase letters, and raises a `Plug.Conn.InvalidHeaderError` when
|
||||
it finds one. To disable it, set `:validate_header_keys_during_test` to
|
||||
false on the app config.
|
||||
|
||||
config :plug, :validate_header_keys_during_test, false
|
||||
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@deprecated """
|
||||
Please use `import Plug.Test` and `import Plug.Conn` directly instead.
|
||||
"""
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
import Plug.Test
|
||||
import Plug.Conn
|
||||
end
|
||||
end
|
||||
|
||||
alias Plug.Conn
|
||||
@typep params :: binary | list | map | nil
|
||||
|
||||
@doc """
|
||||
Creates a test connection.
|
||||
|
||||
The request `method` and `path` are required arguments. `method` may be any
|
||||
value that implements `to_string/1` and it will be properly converted and
|
||||
normalized (e.g., `:get` or `"post"`).
|
||||
|
||||
The `path` is commonly the request path with optional query string but it may
|
||||
also be a complete URI. When a URI is given, the host and schema will be used
|
||||
as part of the request too.
|
||||
|
||||
The `params_or_body` field must be one of:
|
||||
|
||||
* `nil` - meaning there is no body;
|
||||
* a binary - containing a request body. For such cases, `:headers`
|
||||
must be given as option with a content-type;
|
||||
* a map or list - containing the parameters which will automatically
|
||||
set the content-type to multipart. The map or list may contain
|
||||
other lists or maps and all entries will be normalized to string
|
||||
keys;
|
||||
|
||||
## Examples
|
||||
|
||||
conn(:get, "/foo?bar=10")
|
||||
conn(:get, "/foo", %{bar: 10})
|
||||
conn(:post, "/")
|
||||
conn("patch", "/", "") |> put_req_header("content-type", "application/json")
|
||||
|
||||
"""
|
||||
@spec conn(String.Chars.t(), binary, params) :: Conn.t()
|
||||
def conn(method, path, params_or_body \\ nil) do
|
||||
Plug.Adapters.Test.Conn.conn(%Plug.Conn{}, method, path, params_or_body)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the sent response.
|
||||
|
||||
This function is useful when the code being invoked crashes and
|
||||
there is a need to verify a particular response was sent, even with
|
||||
the crash. It returns a tuple with `{status, headers, body}`.
|
||||
"""
|
||||
def sent_resp(%Conn{adapter: {Plug.Adapters.Test.Conn, %{ref: ref}}}) do
|
||||
case receive_resp(ref) do
|
||||
:no_resp ->
|
||||
raise "no sent response available for the given connection. " <>
|
||||
"Maybe the application did not send anything?"
|
||||
|
||||
response ->
|
||||
case receive_resp(ref) do
|
||||
:no_resp ->
|
||||
send(self(), {ref, response})
|
||||
response
|
||||
|
||||
_otherwise ->
|
||||
raise "a response for the given connection has been sent more than once"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_resp(ref) do
|
||||
receive do
|
||||
{^ref, response} -> response
|
||||
after
|
||||
0 -> :no_resp
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the informational requests that have been sent.
|
||||
|
||||
This function depends on gathering the messages sent by the test adapter when
|
||||
informational messages, such as an early hint, are sent. Calling this
|
||||
function will clear the informational request messages from the inbox for the
|
||||
process. To assert on multiple informs, the result of the function should be
|
||||
stored in a variable.
|
||||
|
||||
## Examples
|
||||
|
||||
conn = conn(:get, "/foo", "bar=10")
|
||||
informs = Plug.Test.sent_informs(conn)
|
||||
assert {"/static/application.css", [{"accept", "text/css"}]} in informs
|
||||
assert {"/static/application.js", [{"accept", "application/javascript"}]} in informs
|
||||
|
||||
"""
|
||||
def sent_informs(%Conn{adapter: {Plug.Adapters.Test.Conn, %{ref: ref}}}) do
|
||||
Enum.reverse(receive_informs(ref, []))
|
||||
end
|
||||
|
||||
defp receive_informs(ref, informs) do
|
||||
receive do
|
||||
{^ref, :inform, response} ->
|
||||
receive_informs(ref, [response | informs])
|
||||
after
|
||||
0 -> informs
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the upgrade requests that have been sent.
|
||||
|
||||
This function depends on gathering the messages sent by the test adapter when
|
||||
upgrade requests are sent. Calling this function will clear the upgrade request messages from the inbox for the
|
||||
process.
|
||||
|
||||
## Examples
|
||||
|
||||
conn = conn(:get, "/foo", "bar=10")
|
||||
upgrades = Plug.Test.send_upgrades(conn)
|
||||
assert {:websocket, [opt: :value]} in upgrades
|
||||
|
||||
"""
|
||||
def sent_upgrades(%Conn{adapter: {Plug.Adapters.Test.Conn, %{ref: ref}}}) do
|
||||
Enum.reverse(receive_upgrades(ref, []))
|
||||
end
|
||||
|
||||
defp receive_upgrades(ref, upgrades) do
|
||||
receive do
|
||||
{^ref, :upgrade, response} ->
|
||||
receive_upgrades(ref, [response | upgrades])
|
||||
after
|
||||
0 -> upgrades
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the assets that have been pushed.
|
||||
|
||||
This function depends on gathering the messages sent by the test adapter
|
||||
when assets are pushed. Calling this function will clear the pushed message
|
||||
from the inbox for the process. To assert on multiple pushes, the result
|
||||
of the function should be stored in a variable.
|
||||
|
||||
## Examples
|
||||
|
||||
conn = conn(:get, "/foo?bar=10")
|
||||
pushes = Plug.Test.sent_pushes(conn)
|
||||
assert {"/static/application.css", [{"accept", "text/css"}]} in pushes
|
||||
assert {"/static/application.js", [{"accept", "application/javascript"}]} in pushes
|
||||
|
||||
"""
|
||||
@deprecated "Most browsers and clients have removed push support"
|
||||
def sent_pushes(%Conn{adapter: {Plug.Adapters.Test.Conn, %{ref: ref}}}) do
|
||||
Enum.reverse(receive_pushes(ref, []))
|
||||
end
|
||||
|
||||
defp receive_pushes(ref, pushes) do
|
||||
receive do
|
||||
{^ref, :push, response} ->
|
||||
receive_pushes(ref, [response | pushes])
|
||||
after
|
||||
0 -> pushes
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the HTTP protocol.
|
||||
"""
|
||||
def put_http_protocol(conn, http_protocol) do
|
||||
update_in(conn.adapter, fn {adapter, payload} ->
|
||||
{adapter, Map.put(payload, :http_protocol, http_protocol)}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the peer data.
|
||||
"""
|
||||
def put_peer_data(conn, peer_data) do
|
||||
update_in(conn.adapter, fn {adapter, payload} ->
|
||||
{adapter, Map.put(payload, :peer_data, peer_data)}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the sock data.
|
||||
"""
|
||||
def put_sock_data(conn, sock_data) do
|
||||
update_in(conn.adapter, fn {adapter, payload} ->
|
||||
{adapter, Map.put(payload, :sock_data, sock_data)}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the ssl data.
|
||||
"""
|
||||
def put_ssl_data(conn, ssl_data) do
|
||||
update_in(conn.adapter, fn {adapter, payload} ->
|
||||
{adapter, Map.put(payload, :ssl_data, ssl_data)}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts a request cookie.
|
||||
"""
|
||||
@spec put_req_cookie(Conn.t(), binary, binary) :: Conn.t()
|
||||
def put_req_cookie(conn, key, value) when is_binary(key) and is_binary(value) do
|
||||
conn = delete_req_cookie(conn, key)
|
||||
%{conn | req_headers: [{"cookie", "#{key}=#{value}"} | conn.req_headers]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a request cookie.
|
||||
"""
|
||||
@spec delete_req_cookie(Conn.t(), binary) :: Conn.t()
|
||||
def delete_req_cookie(%Conn{req_cookies: %Plug.Conn.Unfetched{}} = conn, key)
|
||||
when is_binary(key) do
|
||||
key = "#{key}="
|
||||
size = byte_size(key)
|
||||
fun = &match?({"cookie", value} when binary_part(value, 0, size) == key, &1)
|
||||
%{conn | req_headers: Enum.reject(conn.req_headers, fun)}
|
||||
end
|
||||
|
||||
def delete_req_cookie(_conn, key) when is_binary(key) do
|
||||
raise ArgumentError, message: "cannot put/delete request cookies after cookies were fetched"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Moves cookies from a connection into a new connection for subsequent requests.
|
||||
|
||||
This function copies the cookie information in `old_conn` into `new_conn`,
|
||||
emulating multiple requests done by clients where cookies are always passed
|
||||
forward, and returns the new version of `new_conn`.
|
||||
"""
|
||||
@spec recycle_cookies(Conn.t(), Conn.t()) :: Conn.t()
|
||||
def recycle_cookies(new_conn, old_conn) do
|
||||
req_cookies = Plug.Conn.fetch_cookies(old_conn).req_cookies
|
||||
|
||||
resp_cookies =
|
||||
Enum.reduce(Plug.Conn.get_resp_cookies(old_conn), req_cookies, fn {key, opts}, acc ->
|
||||
if value = Map.get(opts, :value) do
|
||||
Map.put(acc, key, value)
|
||||
else
|
||||
Map.delete(acc, key)
|
||||
end
|
||||
end)
|
||||
|
||||
Enum.reduce(resp_cookies, new_conn, fn {key, value}, acc ->
|
||||
put_req_cookie(acc, key, value)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Initializes the session with the given contents.
|
||||
|
||||
If the session has already been initialized, the new contents will be merged
|
||||
with the previous ones.
|
||||
"""
|
||||
@spec init_test_session(Conn.t(), %{optional(String.t() | atom) => any} | keyword) :: Conn.t()
|
||||
def init_test_session(conn, session) do
|
||||
conn =
|
||||
if conn.private[:plug_session_fetch] do
|
||||
Conn.fetch_session(conn)
|
||||
else
|
||||
conn
|
||||
|> Conn.put_private(:plug_session, %{})
|
||||
|> Conn.put_private(:plug_session_fetch, :done)
|
||||
end
|
||||
|
||||
Enum.reduce(session, conn, fn {key, value}, conn ->
|
||||
Conn.put_session(conn, key, value)
|
||||
end)
|
||||
end
|
||||
end
|
||||
270
phoenix/deps/plug/lib/plug/upload.ex
Normal file
270
phoenix/deps/plug/lib/plug/upload.ex
Normal file
@@ -0,0 +1,270 @@
|
||||
defmodule Plug.UploadError do
|
||||
defexception [:message]
|
||||
end
|
||||
|
||||
defmodule Plug.Upload do
|
||||
@moduledoc """
|
||||
A server (a `GenServer` specifically) that manages uploaded files.
|
||||
|
||||
Uploaded files are stored in a temporary directory
|
||||
and removed from that directory after the process that
|
||||
requested the file dies.
|
||||
|
||||
During the request, files are represented with
|
||||
a `Plug.Upload` struct that contains three fields:
|
||||
|
||||
* `:path` - the path to the uploaded file on the filesystem
|
||||
* `:content_type` - the content type of the uploaded file
|
||||
* `:filename` - the filename of the uploaded file given in the request
|
||||
|
||||
**Note**: as mentioned in the documentation for `Plug.Parsers`, the `:plug`
|
||||
application has to be started in order to upload files and use the
|
||||
`Plug.Upload` module.
|
||||
|
||||
## Security
|
||||
|
||||
The `:content_type` and `:filename` fields in the `Plug.Upload` struct are
|
||||
client-controlled. These values should be validated, via file content
|
||||
inspection or similar, before being trusted.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
defstruct [:path, :content_type, :filename]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
path: Path.t(),
|
||||
filename: binary,
|
||||
content_type: binary | nil
|
||||
}
|
||||
|
||||
@dir_table __MODULE__.Dir
|
||||
@path_table __MODULE__.Path
|
||||
@max_attempts 10
|
||||
|
||||
@doc """
|
||||
Requests a random file to be created in the upload directory
|
||||
with the given prefix.
|
||||
"""
|
||||
@spec random_file(binary) ::
|
||||
{:ok, binary}
|
||||
| {:too_many_attempts, binary, pos_integer}
|
||||
| {:no_tmp, [binary]}
|
||||
def random_file(prefix) do
|
||||
case ensure_tmp() do
|
||||
{:ok, tmp} ->
|
||||
open_random_file(prefix, tmp, 0)
|
||||
|
||||
{:no_tmp, tmps} ->
|
||||
{:no_tmp, tmps}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the given upload file.
|
||||
|
||||
Uploads are automatically removed when the current process terminates,
|
||||
but you may invoke this to request the file to be removed sooner.
|
||||
"""
|
||||
@spec delete(t | binary) :: :ok | {:error, term}
|
||||
def delete(%__MODULE__{path: path}), do: delete(path)
|
||||
|
||||
def delete(path) when is_binary(path) do
|
||||
with :ok <- :file.delete(path, [:raw]) do
|
||||
:ets.delete_object(@path_table, {self(), path})
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Assign ownership of the given upload file to another process.
|
||||
|
||||
Useful if you want to do some work on an uploaded file in another process
|
||||
since it means that the file will survive the end of the request.
|
||||
"""
|
||||
@spec give_away(t | binary, pid, pid) :: :ok | {:error, :unknown_path}
|
||||
def give_away(upload, to_pid, from_pid \\ self())
|
||||
|
||||
def give_away(%__MODULE__{path: path}, to_pid, from_pid) do
|
||||
give_away(path, to_pid, from_pid)
|
||||
end
|
||||
|
||||
def give_away(path, to_pid, from_pid)
|
||||
when is_binary(path) and is_pid(to_pid) and is_pid(from_pid) do
|
||||
with [{^from_pid, _tmp}] <- :ets.lookup(@dir_table, from_pid),
|
||||
true <- path_owner?(from_pid, path) do
|
||||
case :ets.lookup(@dir_table, to_pid) do
|
||||
[{^to_pid, _tmp}] ->
|
||||
:ets.insert(@path_table, {to_pid, path})
|
||||
:ets.delete_object(@path_table, {from_pid, path})
|
||||
|
||||
:ok
|
||||
|
||||
[] ->
|
||||
server = plug_server(to_pid)
|
||||
{:ok, tmp} = generate_tmp_dir()
|
||||
:ok = GenServer.call(server, {:give_away, to_pid, tmp, path})
|
||||
:ets.delete_object(@path_table, {from_pid, path})
|
||||
:ok
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
{:error, :unknown_path}
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_tmp() do
|
||||
pid = self()
|
||||
|
||||
case :ets.lookup(@dir_table, pid) do
|
||||
[{^pid, tmp}] ->
|
||||
{:ok, tmp}
|
||||
|
||||
[] ->
|
||||
server = plug_server(pid)
|
||||
GenServer.cast(server, {:monitor, pid})
|
||||
|
||||
with {:ok, tmp} <- generate_tmp_dir() do
|
||||
true = :ets.insert_new(@dir_table, {pid, tmp})
|
||||
{:ok, tmp}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_tmp_dir() do
|
||||
{tmp_roots, suffix} = :persistent_term.get(__MODULE__)
|
||||
sec = System.system_time(:second) |> div(1024 * 1024)
|
||||
subdir = "/plug-" <> i(sec) <> "-" <> suffix
|
||||
|
||||
if tmp = Enum.find_value(tmp_roots, &make_tmp_dir(&1 <> subdir)) do
|
||||
{:ok, tmp}
|
||||
else
|
||||
{:no_tmp, tmp_roots}
|
||||
end
|
||||
end
|
||||
|
||||
defp make_tmp_dir(path) do
|
||||
case File.mkdir_p(path) do
|
||||
:ok -> path
|
||||
{:error, _} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp open_random_file(prefix, tmp, attempts) when attempts < @max_attempts do
|
||||
path = path(prefix, tmp)
|
||||
|
||||
case :file.write_file(path, "", [:write, :raw, :exclusive, :binary]) do
|
||||
:ok ->
|
||||
:ets.insert(@path_table, {self(), path})
|
||||
{:ok, path}
|
||||
|
||||
{:error, reason} when reason in [:eexist, :eacces] ->
|
||||
open_random_file(prefix, tmp, attempts + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp open_random_file(_prefix, tmp, attempts) do
|
||||
{:too_many_attempts, tmp, attempts}
|
||||
end
|
||||
|
||||
defp path(prefix, tmp) do
|
||||
sec = System.system_time(:second)
|
||||
rand = :rand.uniform(999_999_999_999)
|
||||
scheduler_id = :erlang.system_info(:scheduler_id)
|
||||
tmp <> "/" <> prefix <> "-" <> i(sec) <> "-" <> i(rand) <> "-" <> i(scheduler_id)
|
||||
end
|
||||
|
||||
defp path_owner?(pid, path) do
|
||||
owned_paths = :ets.lookup(@path_table, pid)
|
||||
Enum.any?(owned_paths, fn {_pid, p} -> p == path end)
|
||||
end
|
||||
|
||||
@compile {:inline, i: 1}
|
||||
defp i(integer), do: Integer.to_string(integer)
|
||||
|
||||
@doc """
|
||||
Requests a random file to be created in the upload directory
|
||||
with the given prefix. Raises on failure.
|
||||
"""
|
||||
@spec random_file!(binary) :: binary | no_return
|
||||
def random_file!(prefix) do
|
||||
case random_file(prefix) do
|
||||
{:ok, path} ->
|
||||
path
|
||||
|
||||
{:too_many_attempts, tmp, attempts} ->
|
||||
raise Plug.UploadError,
|
||||
"tried #{attempts} times to create an uploaded file at #{tmp} but failed. " <>
|
||||
"Set PLUG_TMPDIR to a directory with write permission"
|
||||
|
||||
{:no_tmp, _tmps} ->
|
||||
raise Plug.UploadError,
|
||||
"could not create a tmp directory to store uploads. " <>
|
||||
"Set PLUG_TMPDIR to a directory with write permission"
|
||||
end
|
||||
end
|
||||
|
||||
defp plug_server(pid) do
|
||||
PartitionSupervisor.whereis_name({__MODULE__, pid})
|
||||
rescue
|
||||
ArgumentError ->
|
||||
raise Plug.UploadError,
|
||||
"could not find process Plug.Upload. Have you started the :plug application?"
|
||||
end
|
||||
|
||||
@doc false
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, :ok)
|
||||
end
|
||||
|
||||
## Callbacks
|
||||
|
||||
@impl true
|
||||
def init(:ok) do
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:give_away, pid, tmp, path}, _from, state) do
|
||||
# Since we are writing in behalf of another process, we need to make sure
|
||||
# the monitor and writing to the tables happen within the same operation.
|
||||
Process.monitor(pid)
|
||||
:ets.insert_new(@dir_table, {pid, tmp})
|
||||
:ets.insert(@path_table, {pid, path})
|
||||
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:monitor, pid}, state) do
|
||||
Process.monitor(pid)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
|
||||
case :ets.lookup(@dir_table, pid) do
|
||||
[{pid, _tmp}] ->
|
||||
:ets.delete(@dir_table, pid)
|
||||
|
||||
@path_table
|
||||
|> :ets.lookup(pid)
|
||||
|> Enum.each(&delete_path/1)
|
||||
|
||||
:ets.delete(@path_table, pid)
|
||||
|
||||
[] ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp delete_path({_pid, path}) do
|
||||
:file.delete(path, [:raw])
|
||||
:ok
|
||||
end
|
||||
end
|
||||
34
phoenix/deps/plug/lib/plug/upload/supervisor.ex
Normal file
34
phoenix/deps/plug/lib/plug/upload/supervisor.ex
Normal file
@@ -0,0 +1,34 @@
|
||||
defmodule Plug.Upload.Supervisor do
|
||||
@moduledoc false
|
||||
use Supervisor
|
||||
|
||||
@temp_env_vars ~w(PLUG_TMPDIR TMPDIR TMP TEMP)s
|
||||
@dir_table Plug.Upload.Dir
|
||||
@path_table Plug.Upload.Path
|
||||
@otp_vsn System.otp_release() |> String.to_integer()
|
||||
@write_mode if @otp_vsn >= 25, do: :auto, else: true
|
||||
@ets_opts [:public, :named_table, read_concurrency: true, write_concurrency: @write_mode]
|
||||
|
||||
def start_link(args) do
|
||||
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_args) do
|
||||
# Initialize the upload system
|
||||
tmp = Enum.find_value(@temp_env_vars, "/tmp", &System.get_env/1) |> Path.expand()
|
||||
cwd = Path.join(File.cwd!(), "tmp")
|
||||
# Add a tiny random component to avoid clashes between nodes
|
||||
suffix = :crypto.strong_rand_bytes(3) |> Base.url_encode64()
|
||||
:persistent_term.put(Plug.Upload, {[tmp, cwd], suffix})
|
||||
:ets.new(@dir_table, [:set | @ets_opts])
|
||||
:ets.new(@path_table, [:duplicate_bag | @ets_opts])
|
||||
|
||||
children = [
|
||||
Plug.Upload.Terminator,
|
||||
{PartitionSupervisor, child_spec: Plug.Upload, name: Plug.Upload}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
27
phoenix/deps/plug/lib/plug/upload/terminator.ex
Normal file
27
phoenix/deps/plug/lib/plug/upload/terminator.ex
Normal file
@@ -0,0 +1,27 @@
|
||||
defmodule Plug.Upload.Terminator do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
|
||||
@path_table Plug.Upload.Path
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, :ok)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(:ok) do
|
||||
Process.flag(:trap_exit, true)
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate(_reason, _state) do
|
||||
folder = fn entry, :ok -> delete_path(entry) end
|
||||
:ets.foldl(folder, :ok, @path_table)
|
||||
end
|
||||
|
||||
defp delete_path({_pid, path}) do
|
||||
:file.delete(path, [:raw])
|
||||
:ok
|
||||
end
|
||||
end
|
||||
132
phoenix/deps/plug/mix.exs
Normal file
132
phoenix/deps/plug/mix.exs
Normal file
@@ -0,0 +1,132 @@
|
||||
defmodule Plug.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@version "1.19.1"
|
||||
@description "Compose web applications with functions"
|
||||
@xref_exclude [Plug.Cowboy, :ssl]
|
||||
@source_url "https://github.com/elixir-plug/plug"
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :plug,
|
||||
version: @version,
|
||||
elixir: "~> 1.14",
|
||||
deps: deps(),
|
||||
package: package(),
|
||||
description: @description,
|
||||
name: "Plug",
|
||||
xref: [exclude: @xref_exclude],
|
||||
consolidate_protocols: Mix.env() != :test,
|
||||
docs: [
|
||||
extras: [
|
||||
"CHANGELOG.md",
|
||||
"README.md",
|
||||
"guides/https.md"
|
||||
],
|
||||
main: "readme",
|
||||
groups_for_modules: groups_for_modules(),
|
||||
groups_for_extras: groups_for_extras(),
|
||||
source_ref: "v#{@version}",
|
||||
source_url: @source_url
|
||||
],
|
||||
test_ignore_filters: [&String.starts_with?(&1, "test/fixtures/")]
|
||||
]
|
||||
end
|
||||
|
||||
# Configuration for the OTP application
|
||||
def application do
|
||||
[
|
||||
extra_applications: extra_applications(Mix.env()),
|
||||
mod: {Plug.Application, []},
|
||||
env: [validate_header_keys_during_test: true]
|
||||
]
|
||||
end
|
||||
|
||||
defp extra_applications(:test), do: [:logger, :eex, :ssl]
|
||||
defp extra_applications(_), do: [:logger, :eex]
|
||||
|
||||
def deps do
|
||||
[
|
||||
{:mime, "~> 1.0 or ~> 2.0"},
|
||||
{:plug_crypto, plug_crypto_version()},
|
||||
{:telemetry, "~> 0.4.3 or ~> 1.0"},
|
||||
{:ex_doc, "~> 0.21", only: :docs}
|
||||
]
|
||||
end
|
||||
|
||||
if System.get_env("PLUG_CRYPTO_2_0", "true") == "true" do
|
||||
defp plug_crypto_version, do: "~> 1.1.1 or ~> 1.2 or ~> 2.0"
|
||||
else
|
||||
defp plug_crypto_version, do: "~> 1.1.1 or ~> 1.2"
|
||||
end
|
||||
|
||||
defp package do
|
||||
%{
|
||||
licenses: ["Apache-2.0"],
|
||||
maintainers: ["Gary Rennie", "José Valim"],
|
||||
links: %{
|
||||
"Changelog" => "#{@source_url}/blob/main/CHANGELOG.md",
|
||||
"GitHub" => @source_url
|
||||
},
|
||||
files: ["lib", "mix.exs", "README.md", "CHANGELOG.md", "LICENSE", "src", ".formatter.exs"]
|
||||
}
|
||||
end
|
||||
|
||||
defp groups_for_modules do
|
||||
# Ungrouped Modules
|
||||
#
|
||||
# Plug
|
||||
# Plug.Builder
|
||||
# Plug.Conn
|
||||
# Plug.HTML
|
||||
# Plug.Router
|
||||
# Plug.Test
|
||||
# Plug.Upload
|
||||
|
||||
[
|
||||
Plugs: [
|
||||
Plug.BasicAuth,
|
||||
Plug.CSRFProtection,
|
||||
Plug.Head,
|
||||
Plug.Logger,
|
||||
Plug.MethodOverride,
|
||||
Plug.Parsers,
|
||||
Plug.RequestId,
|
||||
Plug.RewriteOn,
|
||||
Plug.SSL,
|
||||
Plug.Session,
|
||||
Plug.Static,
|
||||
Plug.Telemetry
|
||||
],
|
||||
"Error handling": [
|
||||
Plug.Debugger,
|
||||
Plug.ErrorHandler,
|
||||
Plug.Exception
|
||||
],
|
||||
"Plug.Conn": [
|
||||
Plug.Conn.Adapter,
|
||||
Plug.Conn.Cookies,
|
||||
Plug.Conn.Query,
|
||||
Plug.Conn.Status,
|
||||
Plug.Conn.Unfetched,
|
||||
Plug.Conn.Utils
|
||||
],
|
||||
"Plug.Parsers": [
|
||||
Plug.Parsers.JSON,
|
||||
Plug.Parsers.MULTIPART,
|
||||
Plug.Parsers.URLENCODED
|
||||
],
|
||||
"Plug.Session": [
|
||||
Plug.Session.COOKIE,
|
||||
Plug.Session.ETS,
|
||||
Plug.Session.Store
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
defp groups_for_extras do
|
||||
[
|
||||
Guides: ~r/guides\/[^\/]+\.md/
|
||||
]
|
||||
end
|
||||
end
|
||||
471
phoenix/deps/plug/src/plug_multipart.erl
Normal file
471
phoenix/deps/plug/src/plug_multipart.erl
Normal file
@@ -0,0 +1,471 @@
|
||||
%% Copyright (c) 2014-2015, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(plug_multipart).
|
||||
|
||||
%% Parsing.
|
||||
-export([parse_headers/2]).
|
||||
-export([parse_body/2]).
|
||||
|
||||
%% Building.
|
||||
-export([boundary/0]).
|
||||
-export([first_part/2]).
|
||||
-export([part/2]).
|
||||
-export([close/1]).
|
||||
|
||||
%% Headers.
|
||||
-export([form_data/1]).
|
||||
-export([parse_content_disposition/1]).
|
||||
-export([parse_content_transfer_encoding/1]).
|
||||
-export([parse_content_type/1]).
|
||||
|
||||
-type headers() :: [{iodata(), iodata()}].
|
||||
-export_type([headers/0]).
|
||||
|
||||
-define(LC(C), case C of
|
||||
$A -> $a;
|
||||
$B -> $b;
|
||||
$C -> $c;
|
||||
$D -> $d;
|
||||
$E -> $e;
|
||||
$F -> $f;
|
||||
$G -> $g;
|
||||
$H -> $h;
|
||||
$I -> $i;
|
||||
$J -> $j;
|
||||
$K -> $k;
|
||||
$L -> $l;
|
||||
$M -> $m;
|
||||
$N -> $n;
|
||||
$O -> $o;
|
||||
$P -> $p;
|
||||
$Q -> $q;
|
||||
$R -> $r;
|
||||
$S -> $s;
|
||||
$T -> $t;
|
||||
$U -> $u;
|
||||
$V -> $v;
|
||||
$W -> $w;
|
||||
$X -> $x;
|
||||
$Y -> $y;
|
||||
$Z -> $z;
|
||||
_ -> C
|
||||
end).
|
||||
|
||||
%% LOWER(Bin)
|
||||
%%
|
||||
%% Lowercase the entire binary string in a binary comprehension.
|
||||
|
||||
-define(LOWER(Bin), << << ?LC(C) >> || << C >> <= Bin >>).
|
||||
|
||||
%% LOWERCASE(Function, Rest, Acc, ...)
|
||||
%%
|
||||
%% To be included at the end of a case block.
|
||||
%% Defined for up to 10 extra arguments.
|
||||
|
||||
-define(LOWER(Function, Rest, Acc), case C of
|
||||
$A -> Function(Rest, << Acc/binary, $a >>);
|
||||
$B -> Function(Rest, << Acc/binary, $b >>);
|
||||
$C -> Function(Rest, << Acc/binary, $c >>);
|
||||
$D -> Function(Rest, << Acc/binary, $d >>);
|
||||
$E -> Function(Rest, << Acc/binary, $e >>);
|
||||
$F -> Function(Rest, << Acc/binary, $f >>);
|
||||
$G -> Function(Rest, << Acc/binary, $g >>);
|
||||
$H -> Function(Rest, << Acc/binary, $h >>);
|
||||
$I -> Function(Rest, << Acc/binary, $i >>);
|
||||
$J -> Function(Rest, << Acc/binary, $j >>);
|
||||
$K -> Function(Rest, << Acc/binary, $k >>);
|
||||
$L -> Function(Rest, << Acc/binary, $l >>);
|
||||
$M -> Function(Rest, << Acc/binary, $m >>);
|
||||
$N -> Function(Rest, << Acc/binary, $n >>);
|
||||
$O -> Function(Rest, << Acc/binary, $o >>);
|
||||
$P -> Function(Rest, << Acc/binary, $p >>);
|
||||
$Q -> Function(Rest, << Acc/binary, $q >>);
|
||||
$R -> Function(Rest, << Acc/binary, $r >>);
|
||||
$S -> Function(Rest, << Acc/binary, $s >>);
|
||||
$T -> Function(Rest, << Acc/binary, $t >>);
|
||||
$U -> Function(Rest, << Acc/binary, $u >>);
|
||||
$V -> Function(Rest, << Acc/binary, $v >>);
|
||||
$W -> Function(Rest, << Acc/binary, $w >>);
|
||||
$X -> Function(Rest, << Acc/binary, $x >>);
|
||||
$Y -> Function(Rest, << Acc/binary, $y >>);
|
||||
$Z -> Function(Rest, << Acc/binary, $z >>);
|
||||
C -> Function(Rest, << Acc/binary, C >>)
|
||||
end).
|
||||
|
||||
-define(LOWER(Function, Rest, A0, Acc), case C of
|
||||
$A -> Function(Rest, A0, << Acc/binary, $a >>);
|
||||
$B -> Function(Rest, A0, << Acc/binary, $b >>);
|
||||
$C -> Function(Rest, A0, << Acc/binary, $c >>);
|
||||
$D -> Function(Rest, A0, << Acc/binary, $d >>);
|
||||
$E -> Function(Rest, A0, << Acc/binary, $e >>);
|
||||
$F -> Function(Rest, A0, << Acc/binary, $f >>);
|
||||
$G -> Function(Rest, A0, << Acc/binary, $g >>);
|
||||
$H -> Function(Rest, A0, << Acc/binary, $h >>);
|
||||
$I -> Function(Rest, A0, << Acc/binary, $i >>);
|
||||
$J -> Function(Rest, A0, << Acc/binary, $j >>);
|
||||
$K -> Function(Rest, A0, << Acc/binary, $k >>);
|
||||
$L -> Function(Rest, A0, << Acc/binary, $l >>);
|
||||
$M -> Function(Rest, A0, << Acc/binary, $m >>);
|
||||
$N -> Function(Rest, A0, << Acc/binary, $n >>);
|
||||
$O -> Function(Rest, A0, << Acc/binary, $o >>);
|
||||
$P -> Function(Rest, A0, << Acc/binary, $p >>);
|
||||
$Q -> Function(Rest, A0, << Acc/binary, $q >>);
|
||||
$R -> Function(Rest, A0, << Acc/binary, $r >>);
|
||||
$S -> Function(Rest, A0, << Acc/binary, $s >>);
|
||||
$T -> Function(Rest, A0, << Acc/binary, $t >>);
|
||||
$U -> Function(Rest, A0, << Acc/binary, $u >>);
|
||||
$V -> Function(Rest, A0, << Acc/binary, $v >>);
|
||||
$W -> Function(Rest, A0, << Acc/binary, $w >>);
|
||||
$X -> Function(Rest, A0, << Acc/binary, $x >>);
|
||||
$Y -> Function(Rest, A0, << Acc/binary, $y >>);
|
||||
$Z -> Function(Rest, A0, << Acc/binary, $z >>);
|
||||
C -> Function(Rest, A0, << Acc/binary, C >>)
|
||||
end).
|
||||
|
||||
%% Parsing.
|
||||
%%
|
||||
%% The multipart format is defined in RFC 2045.
|
||||
|
||||
%% @doc Parse the headers for the next multipart part.
|
||||
%%
|
||||
%% This function skips any preamble before the boundary.
|
||||
%% The preamble may be retrieved using parse_body/2.
|
||||
%%
|
||||
%% This function will accept input of any size, it is
|
||||
%% up to the caller to limit it if needed.
|
||||
|
||||
-spec parse_headers(binary(), binary())
|
||||
-> more | {more, binary()}
|
||||
| {ok, headers(), binary()}
|
||||
| {done, binary()}.
|
||||
%% If the stream starts with the boundary we can make a few assumptions
|
||||
%% and quickly figure out if we got the complete list of headers.
|
||||
parse_headers(<< "--", Stream/bits >>, Boundary) ->
|
||||
BoundarySize = byte_size(Boundary),
|
||||
case Stream of
|
||||
%% Last boundary. Return the epilogue.
|
||||
<< Boundary:BoundarySize/binary, "--", Stream2/bits >> ->
|
||||
{done, Stream2};
|
||||
<< Boundary:BoundarySize/binary, Stream2/bits >> ->
|
||||
%% We have all the headers only if there is a \r\n\r\n
|
||||
%% somewhere in the data after the boundary.
|
||||
case binary:match(Stream2, <<"\r\n\r\n">>) of
|
||||
nomatch ->
|
||||
more;
|
||||
_ ->
|
||||
before_parse_headers(Stream2)
|
||||
end;
|
||||
%% If there isn't enough to represent Boundary \r\n\r\n
|
||||
%% then we definitely don't have all the headers.
|
||||
_ when byte_size(Stream) < byte_size(Boundary) + 4 ->
|
||||
more;
|
||||
%% Otherwise we have preamble data to skip.
|
||||
%% We still got rid of the first two misleading bytes.
|
||||
_ ->
|
||||
skip_preamble(Stream, Boundary)
|
||||
end;
|
||||
%% Otherwise we have preamble data to skip.
|
||||
parse_headers(Stream, Boundary) ->
|
||||
skip_preamble(Stream, Boundary).
|
||||
|
||||
%% We need to find the boundary and a \r\n\r\n after that.
|
||||
%% Since the boundary isn't at the start, it must be right
|
||||
%% after a \r\n too.
|
||||
skip_preamble(Stream, Boundary) ->
|
||||
case binary:match(Stream, <<"\r\n--", Boundary/bits >>) of
|
||||
%% No boundary, need more data.
|
||||
nomatch ->
|
||||
%% We can safely skip the size of the stream
|
||||
%% minus the last 3 bytes which may be a partial boundary.
|
||||
SkipSize = byte_size(Stream) - 3,
|
||||
case SkipSize > 0 of
|
||||
false ->
|
||||
more;
|
||||
true ->
|
||||
<< _:SkipSize/binary, Stream2/bits >> = Stream,
|
||||
{more, Stream2}
|
||||
end;
|
||||
{Start, Length} ->
|
||||
Start2 = Start + Length,
|
||||
<< _:Start2/binary, Stream2/bits >> = Stream,
|
||||
case Stream2 of
|
||||
%% Last boundary. Return the epilogue.
|
||||
<< "--", Stream3/bits >> ->
|
||||
{done, Stream3};
|
||||
_ ->
|
||||
case binary:match(Stream, <<"\r\n\r\n">>) of
|
||||
%% We don't have the full headers.
|
||||
nomatch ->
|
||||
{more, Stream2};
|
||||
_ ->
|
||||
before_parse_headers(Stream2)
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
before_parse_headers(<< "\r\n\r\n", Stream/bits >>) ->
|
||||
%% This indicates that there are no headers, so we can abort immediately.
|
||||
{ok, [], Stream};
|
||||
before_parse_headers(<< "\r\n", Stream/bits >>) ->
|
||||
%% There is a line break right after the boundary, skip it.
|
||||
parse_hd_name(Stream, [], <<>>).
|
||||
|
||||
parse_hd_name(<< C, Rest/bits >>, H, SoFar) ->
|
||||
case C of
|
||||
$: -> parse_hd_before_value(Rest, H, SoFar);
|
||||
$\s -> parse_hd_name_ws(Rest, H, SoFar);
|
||||
$\t -> parse_hd_name_ws(Rest, H, SoFar);
|
||||
_ -> ?LOWER(parse_hd_name, Rest, H, SoFar)
|
||||
end.
|
||||
|
||||
parse_hd_name_ws(<< C, Rest/bits >>, H, Name) ->
|
||||
case C of
|
||||
$\s -> parse_hd_name_ws(Rest, H, Name);
|
||||
$\t -> parse_hd_name_ws(Rest, H, Name);
|
||||
$: -> parse_hd_before_value(Rest, H, Name)
|
||||
end.
|
||||
|
||||
parse_hd_before_value(<< $\s, Rest/bits >>, H, N) ->
|
||||
parse_hd_before_value(Rest, H, N);
|
||||
parse_hd_before_value(<< $\t, Rest/bits >>, H, N) ->
|
||||
parse_hd_before_value(Rest, H, N);
|
||||
parse_hd_before_value(Buffer, H, N) ->
|
||||
parse_hd_value(Buffer, H, N, <<>>).
|
||||
|
||||
parse_hd_value(<< $\r, Rest/bits >>, Headers, Name, SoFar) ->
|
||||
case Rest of
|
||||
<< "\n\r\n", Rest2/bits >> ->
|
||||
{ok, [{Name, SoFar}|Headers], Rest2};
|
||||
<< $\n, C, Rest2/bits >> when C =:= $\s; C =:= $\t ->
|
||||
parse_hd_value(Rest2, Headers, Name, SoFar);
|
||||
<< $\n, Rest2/bits >> ->
|
||||
parse_hd_name(Rest2, [{Name, SoFar}|Headers], <<>>)
|
||||
end;
|
||||
parse_hd_value(<< C, Rest/bits >>, H, N, SoFar) ->
|
||||
parse_hd_value(Rest, H, N, << SoFar/binary, C >>).
|
||||
|
||||
%% @doc Parse the body of the current multipart part.
|
||||
%%
|
||||
%% The body is everything until the next boundary.
|
||||
|
||||
-spec parse_body(binary(), binary())
|
||||
-> {ok, binary()} | {ok, binary(), binary()}
|
||||
| done | {done, binary()} | {done, binary(), binary()}.
|
||||
parse_body(Stream, Boundary) ->
|
||||
BoundarySize = byte_size(Boundary),
|
||||
case Stream of
|
||||
<< "--", Boundary:BoundarySize/binary, _/bits >> ->
|
||||
done;
|
||||
_ ->
|
||||
case binary:match(Stream, << "\r\n--", Boundary/bits >>) of
|
||||
%% No boundary, check for a possible partial at the end.
|
||||
%% Return more or less of the body depending on the result.
|
||||
nomatch ->
|
||||
StreamSize = byte_size(Stream),
|
||||
From = StreamSize - BoundarySize - 3,
|
||||
MatchOpts = if
|
||||
%% Binary too small to contain boundary, check it fully.
|
||||
From < 0 -> [];
|
||||
%% Optimize, only check the end of the binary.
|
||||
true -> [{scope, {From, StreamSize - From}}]
|
||||
end,
|
||||
case binary:match(Stream, <<"\r">>, MatchOpts) of
|
||||
nomatch ->
|
||||
{ok, Stream};
|
||||
{Pos, _} ->
|
||||
case Stream of
|
||||
<< Body:Pos/binary >> ->
|
||||
{ok, Body};
|
||||
<< Body:Pos/binary, Rest/bits >> ->
|
||||
{ok, Body, Rest}
|
||||
end
|
||||
end;
|
||||
%% Boundary found, this is the last chunk of the body.
|
||||
{Pos, _} ->
|
||||
case Stream of
|
||||
<< Body:Pos/binary, "\r\n" >> ->
|
||||
{done, Body};
|
||||
<< Body:Pos/binary, "\r\n", Rest/bits >> ->
|
||||
{done, Body, Rest};
|
||||
<< Body:Pos/binary, Rest/bits >> ->
|
||||
{done, Body, Rest}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
%% Building.
|
||||
|
||||
%% @doc Generate a new random boundary.
|
||||
%%
|
||||
%% The boundary generated has a low probability of ever appearing
|
||||
%% in the data.
|
||||
|
||||
-spec boundary() -> binary().
|
||||
boundary() ->
|
||||
base64:encode(crypto:strong_rand_bytes(48)).
|
||||
|
||||
%% @doc Return the first part's head.
|
||||
%%
|
||||
%% This works exactly like the part/2 function except there is
|
||||
%% no leading \r\n. It's not required to use this function,
|
||||
%% just makes the output a little smaller and prettier.
|
||||
|
||||
-spec first_part(binary(), headers()) -> iodata().
|
||||
first_part(Boundary, Headers) ->
|
||||
[<<"--">>, Boundary, <<"\r\n">>, headers_to_iolist(Headers, [])].
|
||||
|
||||
%% @doc Return a part's head.
|
||||
|
||||
-spec part(binary(), headers()) -> iodata().
|
||||
part(Boundary, Headers) ->
|
||||
[<<"\r\n--">>, Boundary, <<"\r\n">>, headers_to_iolist(Headers, [])].
|
||||
|
||||
headers_to_iolist([], Acc) ->
|
||||
lists:reverse([<<"\r\n">>|Acc]);
|
||||
headers_to_iolist([{N, V}|Tail], Acc) ->
|
||||
%% We don't want to create a sublist so we list the
|
||||
%% values in reverse order so that it gets reversed properly.
|
||||
headers_to_iolist(Tail, [<<"\r\n">>, V, <<": ">>, N|Acc]).
|
||||
|
||||
%% @doc Return the closing delimiter of the multipart message.
|
||||
|
||||
-spec close(binary()) -> iodata().
|
||||
close(Boundary) ->
|
||||
[<<"\r\n--">>, Boundary, <<"--">>].
|
||||
|
||||
%% Headers.
|
||||
|
||||
%% @doc Convenience function for extracting information from headers
|
||||
%% when parsing a multipart/form-data stream.
|
||||
|
||||
-spec form_data(headers())
|
||||
-> {data, binary()}
|
||||
| {file, binary(), binary(), binary(), binary()}.
|
||||
form_data(Headers) ->
|
||||
{_, DispositionBin} = lists:keyfind(<<"content-disposition">>, 1, Headers),
|
||||
{<<"form-data">>, Params} = parse_content_disposition(DispositionBin),
|
||||
{_, FieldName} = lists:keyfind(<<"name">>, 1, Params),
|
||||
case lists:keyfind(<<"filename">>, 1, Params) of
|
||||
false ->
|
||||
{data, FieldName};
|
||||
{_, Filename} ->
|
||||
Type = case lists:keyfind(<<"content-type">>, 1, Headers) of
|
||||
false -> <<"text/plain">>;
|
||||
{_, T} -> T
|
||||
end,
|
||||
%% Turns out this is unnecessary per RFC7578 4.7.
|
||||
TransferEncoding = case lists:keyfind(
|
||||
<<"content-transfer-encoding">>, 1, Headers) of
|
||||
false -> <<"7bit">>;
|
||||
{_, TE} -> TE
|
||||
end,
|
||||
{file, FieldName, Filename, Type, TransferEncoding}
|
||||
end.
|
||||
|
||||
%% @doc Parse an RFC 2183 content-disposition value.
|
||||
|
||||
-spec parse_content_disposition(binary())
|
||||
-> {binary(), [{binary(), binary()}]}.
|
||||
parse_content_disposition(Bin) ->
|
||||
parse_cd_type(Bin, <<>>).
|
||||
|
||||
parse_cd_type(<<>>, Acc) ->
|
||||
{Acc, []};
|
||||
parse_cd_type(<< C, Rest/bits >>, Acc) ->
|
||||
case C of
|
||||
$; -> {Acc, parse_before_param(Rest, [])};
|
||||
$\s -> {Acc, parse_before_param(Rest, [])};
|
||||
$\t -> {Acc, parse_before_param(Rest, [])};
|
||||
_ -> ?LOWER(parse_cd_type, Rest, Acc)
|
||||
end.
|
||||
|
||||
%% @doc Parse an RFC 2045 content-transfer-encoding header.
|
||||
|
||||
-spec parse_content_transfer_encoding(binary()) -> binary().
|
||||
parse_content_transfer_encoding(Bin) ->
|
||||
?LOWER(Bin).
|
||||
|
||||
%% @doc Parse an RFC 2045 content-type header.
|
||||
|
||||
-spec parse_content_type(binary())
|
||||
-> {binary(), binary(), [{binary(), binary()}]}.
|
||||
parse_content_type(Bin) ->
|
||||
parse_ct_type(Bin, <<>>).
|
||||
|
||||
parse_ct_type(<< C, Rest/bits >>, Acc) ->
|
||||
case C of
|
||||
$/ -> parse_ct_subtype(Rest, Acc, <<>>);
|
||||
_ -> ?LOWER(parse_ct_type, Rest, Acc)
|
||||
end.
|
||||
|
||||
parse_ct_subtype(<<>>, Type, Subtype) when Subtype =/= <<>> ->
|
||||
{Type, Subtype, []};
|
||||
parse_ct_subtype(<< C, Rest/bits >>, Type, Acc) ->
|
||||
case C of
|
||||
$; -> {Type, Acc, parse_before_param(Rest, [])};
|
||||
$\s -> {Type, Acc, parse_before_param(Rest, [])};
|
||||
$\t -> {Type, Acc, parse_before_param(Rest, [])};
|
||||
_ -> ?LOWER(parse_ct_subtype, Rest, Type, Acc)
|
||||
end.
|
||||
|
||||
%% @doc Parse RFC 2045 parameters.
|
||||
|
||||
parse_before_param(<<>>, Params) ->
|
||||
lists:reverse(Params);
|
||||
parse_before_param(<< C, Rest/bits >>, Params) ->
|
||||
case C of
|
||||
$; -> parse_before_param(Rest, Params);
|
||||
$\s -> parse_before_param(Rest, Params);
|
||||
$\t -> parse_before_param(Rest, Params);
|
||||
_ -> ?LOWER(parse_param_name, Rest, Params, <<>>)
|
||||
end.
|
||||
|
||||
parse_param_name(<<>>, Params, Acc) ->
|
||||
lists:reverse([{Acc, <<>>}|Params]);
|
||||
parse_param_name(<< C, Rest/bits >>, Params, Acc) ->
|
||||
case C of
|
||||
$= -> parse_param_value(Rest, Params, Acc);
|
||||
_ -> ?LOWER(parse_param_name, Rest, Params, Acc)
|
||||
end.
|
||||
|
||||
parse_param_value(<<>>, Params, Name) ->
|
||||
lists:reverse([{Name, <<>>}|Params]);
|
||||
parse_param_value(<< C, Rest/bits >>, Params, Name) ->
|
||||
case C of
|
||||
$" -> parse_param_quoted_value(Rest, Params, Name, <<>>);
|
||||
$; -> parse_before_param(Rest, [{Name, <<>>}|Params]);
|
||||
$\s -> parse_before_param(Rest, [{Name, <<>>}|Params]);
|
||||
$\t -> parse_before_param(Rest, [{Name, <<>>}|Params]);
|
||||
C -> parse_param_value(Rest, Params, Name, << C >>)
|
||||
end.
|
||||
|
||||
parse_param_value(<<>>, Params, Name, Acc) ->
|
||||
lists:reverse([{Name, Acc}|Params]);
|
||||
parse_param_value(<< C, Rest/bits >>, Params, Name, Acc) ->
|
||||
case C of
|
||||
$; -> parse_before_param(Rest, [{Name, Acc}|Params]);
|
||||
$\s -> parse_before_param(Rest, [{Name, Acc}|Params]);
|
||||
$\t -> parse_before_param(Rest, [{Name, Acc}|Params]);
|
||||
C -> parse_param_value(Rest, Params, Name, << Acc/binary, C >>)
|
||||
end.
|
||||
|
||||
%% We expect a final $" so no need to test for <<>>.
|
||||
parse_param_quoted_value(<< $\\, C, Rest/bits >>, Params, Name, Acc) ->
|
||||
parse_param_quoted_value(Rest, Params, Name, << Acc/binary, C >>);
|
||||
parse_param_quoted_value(<< $", Rest/bits >>, Params, Name, Acc) ->
|
||||
parse_before_param(Rest, [{Name, Acc}|Params]);
|
||||
parse_param_quoted_value(<< C, Rest/bits >>, Params, Name, Acc)
|
||||
when C =/= $\r ->
|
||||
parse_param_quoted_value(Rest, Params, Name, << Acc/binary, C >>).
|
||||
Reference in New Issue
Block a user