This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

View 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

Binary file not shown.

View 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
View 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
View File

@@ -0,0 +1,372 @@
# Plug
[![Build Status](https://github.com/elixir-plug/plug/workflows/CI/badge.svg)](https://github.com/elixir-plug/plug/actions?query=workflow%3A%22CI%22)
[![hex.pm](https://img.shields.io/hexpm/v/plug.svg)](https://hex.pm/packages/plug)
[![hexdocs.pm](https://img.shields.io/badge/hex-docs-lightgreen.svg)](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

View 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">>]}.

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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(%{}, &params/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

View 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

View 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

File diff suppressed because one or more lines are too long

View 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

View 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

View 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

View 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>")
"&lt;foo&gt;"
iex> Plug.HTML.html_escape("quotes: \" & \'")
"quotes: &quot; &amp; &#39;"
"""
@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>")
[[[] | "&lt;"], "foo" | "&gt;"]
iex> Plug.HTML.html_escape_to_iodata("quotes: \" & \'")
[[[[], "quotes: " | "&quot;"], " " | "&amp;"], " " | "&#39;"]
"""
@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 = [
{?<, "&lt;"},
{?>, "&gt;"},
{?&, "&amp;"},
{?", "&quot;"},
{?', "&#39;"}
]
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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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, &quote(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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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) %>

View 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

View 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

View 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

View 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
View 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

View 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 >>).