├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── guides └── https.md ├── lib ├── plug.ex └── plug │ ├── adapters │ ├── cowboy.ex │ └── test │ │ └── conn.ex │ ├── application.ex │ ├── basic_auth.ex │ ├── builder.ex │ ├── conn.ex │ ├── conn │ ├── adapter.ex │ ├── cookies.ex │ ├── query.ex │ ├── status.ex │ ├── unfetched.ex │ ├── utils.ex │ └── wrapper_error.ex │ ├── csrf_protection.ex │ ├── debugger.ex │ ├── error_handler.ex │ ├── exceptions.ex │ ├── head.ex │ ├── html.ex │ ├── logger.ex │ ├── method_override.ex │ ├── mime.ex │ ├── parsers.ex │ ├── parsers │ ├── json.ex │ ├── multipart.ex │ └── urlencoded.ex │ ├── request_id.ex │ ├── rewrite_on.ex │ ├── router.ex │ ├── router │ └── utils.ex │ ├── session.ex │ ├── session │ ├── cookie.ex │ ├── ets.ex │ └── store.ex │ ├── ssl.ex │ ├── static.ex │ ├── telemetry.ex │ ├── templates │ ├── debugger.html.eex │ └── debugger.md.eex │ ├── test.ex │ └── upload.ex ├── mix.exs ├── mix.lock ├── src └── plug_multipart.erl └── test ├── fixtures ├── file-deadbeef.txt ├── manifest-file ├── plug_cowboy.exs ├── ssl │ ├── README.md │ ├── ca.cer │ ├── ca.key │ ├── client.cer │ ├── client.key │ ├── client.req │ ├── server.cer │ ├── server.key │ └── server.key.enc ├── static with spaces.txt ├── static.txt ├── static.txt.br ├── static.txt.gz ├── static.txt.zst └── static │ └── file.txt ├── plug ├── adapters │ └── test │ │ └── conn_test.exs ├── basic_auth_test.exs ├── builder_test.exs ├── conn │ ├── adapter_test.exs │ ├── cookies_test.exs │ ├── query_test.exs │ ├── status_test.exs │ ├── utils_test.exs │ └── wrapper_error_test.exs ├── conn_test.exs ├── csrf_protection_test.exs ├── debugger_test.exs ├── error_handler_test.exs ├── head_test.exs ├── html_test.exs ├── logger_test.exs ├── method_override_test.exs ├── parsers │ └── json_test.exs ├── parsers_test.exs ├── request_id_test.exs ├── rewrite_on_test.exs ├── router │ └── utils_test.exs ├── router_test.exs ├── session │ ├── cookie_test.exs │ └── ets_test.exs ├── session_test.exs ├── ssl_test.exs ├── static_test.exs ├── telemetry_test.exs └── upload_test.exs ├── plug_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" and to export configuration. 2 | export_locals_without_parens = [ 3 | plug: 1, 4 | plug: 2, 5 | forward: 2, 6 | forward: 3, 7 | forward: 4, 8 | match: 2, 9 | match: 3, 10 | get: 2, 11 | get: 3, 12 | head: 2, 13 | head: 3, 14 | post: 2, 15 | post: 3, 16 | put: 2, 17 | put: 3, 18 | patch: 2, 19 | patch: 3, 20 | delete: 2, 21 | delete: 3, 22 | options: 2, 23 | options: 3 24 | ] 25 | 26 | [ 27 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 28 | locals_without_parens: export_locals_without_parens, 29 | export: [locals_without_parens: export_locals_without_parens] 30 | ] 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-22.04 12 | env: 13 | MIX_ENV: test 14 | PLUG_CRYPTO_2_0: "${{ matrix.PLUG_CRYPTO_2_0 }}" 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | include: 19 | # Earliest-supported Elixir/Erlang pair. 20 | - elixir: "1.12" 21 | otp: "24.3" 22 | PLUG_CRYPTO_2_0: "false" 23 | 24 | # Latest-supported Elixir/Erlang pair. 25 | - elixir: "1.18" 26 | otp: "27.2" 27 | lint: lint 28 | PLUG_CRYPTO_2_0: "true" 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Install Erlang and Elixir 34 | uses: erlef/setup-beam@v1 35 | with: 36 | otp-version: ${{ matrix.otp }} 37 | elixir-version: ${{ matrix.elixir }} 38 | 39 | - name: Install dependencies 40 | run: mix deps.get 41 | 42 | - name: Ensure mix.lock is up to date 43 | run: mix deps.get --check-locked 44 | if: ${{ matrix.lint }} 45 | 46 | - name: Ensure that files are formatted 47 | run: mix format --check-formatted 48 | if: ${{ matrix.lint }} 49 | 50 | - name: Check for unused dependencies 51 | run: mix deps.unlock --check-unused 52 | if: ${{ matrix.lint }} 53 | 54 | - name: Run tests 55 | run: mix test 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /docs 5 | /doc 6 | erl_crash.dump 7 | *.ez 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Plataformatec. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, :console, 4 | format: "$time $metadata[$level] $message\n", 5 | colors: [enabled: false], 6 | metadata: [:request_id] 7 | 8 | if Mix.env() == :test do 9 | config :plug, :statuses, %{ 10 | 418 => "Totally not a teapot", 11 | 998 => "Not An RFC Status Code" 12 | } 13 | end 14 | -------------------------------------------------------------------------------- /lib/plug.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug do 2 | @moduledoc """ 3 | The plug specification. 4 | 5 | ## Types of plugs 6 | 7 | There are two kind of plugs: function plugs and module plugs. 8 | 9 | ### Function plugs 10 | 11 | A function plug is by definition any function that receives a connection 12 | and a set of options and returns a connection. Function plugs must have 13 | the following type signature: 14 | 15 | (Plug.Conn.t, Plug.opts) :: Plug.Conn.t 16 | 17 | ### Module plugs 18 | 19 | A module plug is an extension of the function plug. It is a module that must 20 | export: 21 | 22 | * a `c:call/2` function with the signature defined above 23 | * an `c:init/1` function which takes a set of options and initializes it. 24 | 25 | The result returned by `c:init/1` is passed as second argument to `c:call/2`. Note 26 | that `c:init/1` may be called during compilation and as such it must not return 27 | pids, ports or values that are specific to the runtime. 28 | 29 | The API expected by a module plug is defined as a behaviour by the 30 | `Plug` module (this module). 31 | 32 | ## Examples 33 | 34 | Here's an example of a function plug: 35 | 36 | def json_header_plug(conn, _opts) do 37 | Plug.Conn.put_resp_content_type(conn, "application/json") 38 | end 39 | 40 | Here's an example of a module plug: 41 | 42 | defmodule JSONHeaderPlug do 43 | @behaviour Plug 44 | 45 | import Plug.Conn 46 | 47 | def init(opts) do 48 | opts 49 | end 50 | 51 | def call(conn, _opts) do 52 | put_resp_content_type(conn, "application/json") 53 | end 54 | end 55 | 56 | ## The Plug pipeline 57 | 58 | The `Plug.Builder` module provides conveniences for building plug pipelines. 59 | """ 60 | 61 | @type opts :: 62 | binary 63 | | tuple 64 | | atom 65 | | integer 66 | | float 67 | | [opts] 68 | | %{optional(opts) => opts} 69 | | MapSet.t() 70 | 71 | @callback init(opts) :: opts 72 | @callback call(conn :: Plug.Conn.t(), opts) :: Plug.Conn.t() 73 | 74 | require Logger 75 | 76 | @doc """ 77 | Run a series of plugs at runtime. 78 | 79 | The plugs given here can be either a tuple, representing a module plug 80 | and their options, or a simple function that receives a connection and 81 | returns a connection. 82 | 83 | If any plug halts, the connection won't invoke the remaining plugs. If the 84 | given connection was already halted, none of the plugs are invoked either. 85 | 86 | While `Plug.Builder` is designed to operate at compile-time, the `run` function 87 | serves as a straightforward alternative for runtime executions. 88 | 89 | ## Examples 90 | 91 | Plug.run(conn, [{Plug.Head, []}, &IO.inspect/1]) 92 | 93 | ## Options 94 | 95 | * `:log_on_halt` - a log level to be used if a plug halts 96 | 97 | """ 98 | @spec run(Plug.Conn.t(), [{module, opts} | (Plug.Conn.t() -> Plug.Conn.t())], Keyword.t()) :: 99 | Plug.Conn.t() 100 | def run(conn, plugs, opts \\ []) 101 | 102 | def run(%Plug.Conn{halted: true} = conn, _plugs, _opts), 103 | do: conn 104 | 105 | def run(%Plug.Conn{} = conn, plugs, opts), 106 | do: do_run(conn, plugs, Keyword.get(opts, :log_on_halt)) 107 | 108 | defp do_run(conn, [{mod, opts} | plugs], level) when is_atom(mod) do 109 | case mod.call(conn, mod.init(opts)) do 110 | %Plug.Conn{halted: true} = conn -> 111 | level && Logger.log(level, "Plug halted in #{inspect(mod)}.call/2") 112 | conn 113 | 114 | %Plug.Conn{} = conn -> 115 | do_run(conn, plugs, level) 116 | 117 | other -> 118 | raise "expected #{inspect(mod)} to return Plug.Conn, got: #{inspect(other)}" 119 | end 120 | end 121 | 122 | defp do_run(conn, [fun | plugs], level) when is_function(fun, 1) do 123 | case fun.(conn) do 124 | %Plug.Conn{halted: true} = conn -> 125 | level && Logger.log(level, "Plug halted in #{inspect(fun)}") 126 | conn 127 | 128 | %Plug.Conn{} = conn -> 129 | do_run(conn, plugs, level) 130 | 131 | other -> 132 | raise "expected #{inspect(fun)} to return Plug.Conn, got: #{inspect(other)}" 133 | end 134 | end 135 | 136 | defp do_run(conn, [], _level), do: conn 137 | 138 | @doc """ 139 | Forwards requests to another plug while setting the connection to a trailing subpath of the request. 140 | 141 | The `path_info` on the forwarded connection will only include the request path trailing segments 142 | supplied to the `forward` function. The `conn.script_name` attribute retains the correct base path, 143 | e.g., url generation. 144 | 145 | ## Example 146 | 147 | defmodule Router do 148 | @behaviour Plug 149 | 150 | def init(opts), do: opts 151 | 152 | def call(conn, opts) do 153 | case conn do 154 | # Match subdomain 155 | %{host: "admin." <> _} -> 156 | AdminRouter.call(conn, opts) 157 | 158 | # Match path on localhost 159 | %{host: "localhost", path_info: ["admin" | rest]} -> 160 | Plug.forward(conn, rest, AdminRouter, opts) 161 | 162 | _ -> 163 | MainRouter.call(conn, opts) 164 | end 165 | end 166 | end 167 | 168 | """ 169 | @spec forward(Plug.Conn.t(), [String.t()], atom, Plug.opts()) :: Plug.Conn.t() 170 | def forward(%Plug.Conn{path_info: path, script_name: script} = conn, new_path, target, opts) do 171 | {base, split_path} = Enum.split(path, length(path) - length(new_path)) 172 | 173 | conn = do_forward(target, %{conn | path_info: split_path, script_name: script ++ base}, opts) 174 | %{conn | path_info: path, script_name: script} 175 | end 176 | 177 | defp do_forward({mod, fun}, conn, opts), do: apply(mod, fun, [conn, opts]) 178 | defp do_forward(mod, conn, opts), do: mod.call(conn, opts) 179 | end 180 | -------------------------------------------------------------------------------- /lib/plug/adapters/cowboy.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Adapters.Cowboy do 2 | @moduledoc false 3 | 4 | @doc false 5 | @deprecated "Use Plug.Cowboy.http/3 instead" 6 | def http(plug, opts, cowboy_options \\ []) do 7 | unless using_plug_cowboy?(), do: warn_and_raise() 8 | Plug.Cowboy.http(plug, opts, cowboy_options) 9 | end 10 | 11 | @doc false 12 | @deprecated "Use Plug.Cowboy.https/3 instead" 13 | def https(plug, opts, cowboy_options \\ []) do 14 | unless using_plug_cowboy?(), do: warn_and_raise() 15 | Plug.Cowboy.https(plug, opts, cowboy_options) 16 | end 17 | 18 | @doc false 19 | @deprecated "Use Plug.Cowboy.shutdown/1 instead" 20 | def shutdown(ref) do 21 | unless using_plug_cowboy?(), do: warn_and_raise() 22 | Plug.Cowboy.shutdown(ref) 23 | end 24 | 25 | @doc false 26 | @deprecated "Use Plug.Cowboy.child_spec/4 instead" 27 | def child_spec(scheme, plug, opts, cowboy_options \\ []) do 28 | unless using_plug_cowboy?(), do: warn_and_raise() 29 | Plug.Cowboy.child_spec(scheme, plug, opts, cowboy_options) 30 | end 31 | 32 | @doc false 33 | @deprecated "Use Plug.Cowboy.child_spec/1 instead" 34 | def child_spec(opts) do 35 | unless using_plug_cowboy?(), do: warn_and_raise() 36 | Plug.Cowboy.child_spec(opts) 37 | end 38 | 39 | defp using_plug_cowboy?() do 40 | Code.ensure_loaded?(Plug.Cowboy) 41 | end 42 | 43 | defp warn_and_raise() do 44 | error = """ 45 | please add the following dependency to your mix.exs: 46 | {:plug_cowboy, "~> 1.0"} 47 | This dependency is required by Plug.Adapters.Cowboy 48 | which you may be using directly or indirectly. 49 | Note you no longer need to depend on :cowboy directly. 50 | """ 51 | 52 | IO.warn(error, []) 53 | :erlang.raise(:exit, "plug_cowboy dependency missing", []) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/plug/adapters/test/conn.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Adapters.Test.Conn do 2 | @behaviour Plug.Conn.Adapter 3 | @moduledoc false 4 | 5 | ## Test helpers 6 | 7 | def conn(%Plug.Conn{} = conn, method, uri, body_or_params) do 8 | maybe_flush() 9 | uri = URI.parse(uri) 10 | 11 | if is_binary(uri.path) and not String.starts_with?(uri.path, "/") do 12 | # TODO: Convert to an error 13 | IO.warn("the URI path used in plug tests must start with \"/\", got: #{inspect(uri.path)}") 14 | end 15 | 16 | method = method |> to_string |> String.upcase() 17 | query = uri.query || "" 18 | owner = self() 19 | 20 | {params, {body, body_params}, {query, query_params}, req_headers} = 21 | body_or_params(body_or_params, query, conn.req_headers, method) 22 | 23 | state = %{ 24 | method: method, 25 | params: params, 26 | req_body: body, 27 | chunks: nil, 28 | ref: make_ref(), 29 | owner: owner, 30 | http_protocol: get_from_adapter(conn, :get_http_protocol, :"HTTP/1.1"), 31 | peer_data: 32 | get_from_adapter(conn, :get_peer_data, %{ 33 | address: {127, 0, 0, 1}, 34 | port: 111_317, 35 | ssl_cert: nil 36 | }), 37 | sock_data: 38 | get_from_adapter(conn, :get_sock_data, %{ 39 | address: {127, 0, 0, 1}, 40 | port: 111_318 41 | }), 42 | ssl_data: get_from_adapter(conn, :get_ssl_data, nil) 43 | } 44 | 45 | conn_port = if conn.port != 0, do: conn.port, else: 80 46 | 47 | %{ 48 | conn 49 | | adapter: {__MODULE__, state}, 50 | host: uri.host || conn.host || "www.example.com", 51 | method: method, 52 | owner: owner, 53 | path_info: split_path(uri.path), 54 | port: uri.port || conn_port, 55 | remote_ip: conn.remote_ip || {127, 0, 0, 1}, 56 | req_headers: req_headers, 57 | request_path: uri.path, 58 | query_string: query, 59 | query_params: query_params || %Plug.Conn.Unfetched{aspect: :query_params}, 60 | body_params: body_params || %Plug.Conn.Unfetched{aspect: :body_params}, 61 | params: params || %Plug.Conn.Unfetched{aspect: :params}, 62 | scheme: (uri.scheme || "http") |> String.downcase() |> String.to_atom() 63 | } 64 | end 65 | 66 | ## Connection adapter 67 | 68 | def send_resp(%{method: "HEAD"} = state, status, headers, _body) do 69 | do_send(state, status, headers, "") 70 | end 71 | 72 | def send_resp(state, status, headers, body) do 73 | do_send(state, status, headers, IO.iodata_to_binary(body)) 74 | end 75 | 76 | def send_file(%{method: "HEAD"} = state, status, headers, _path, _offset, _length) do 77 | do_send(state, status, headers, "") 78 | end 79 | 80 | def send_file(state, status, headers, path, offset, length) do 81 | %File.Stat{type: :regular, size: size} = File.stat!(path) 82 | 83 | length = 84 | cond do 85 | length == :all -> size 86 | is_integer(length) -> length 87 | end 88 | 89 | {:ok, data} = 90 | File.open!(path, [:read, :binary], fn device -> 91 | :file.pread(device, offset, length) 92 | end) 93 | 94 | do_send(state, status, headers, data) 95 | end 96 | 97 | def send_chunked(state, _status, _headers), do: {:ok, "", %{state | chunks: ""}} 98 | 99 | def chunk(%{method: "HEAD"} = state, _body), do: {:ok, "", state} 100 | 101 | def chunk(%{chunks: chunks} = state, body) do 102 | body = chunks <> IO.iodata_to_binary(body) 103 | {:ok, body, %{state | chunks: body}} 104 | end 105 | 106 | defp do_send(%{owner: owner, ref: ref} = state, status, headers, body) do 107 | send(owner, {ref, {status, headers, body}}) 108 | {:ok, body, state} 109 | end 110 | 111 | def read_req_body(%{req_body: body} = state, opts \\ []) do 112 | size = min(byte_size(body), Keyword.get(opts, :length, 8_000_000)) 113 | data = :binary.part(body, 0, size) 114 | rest = :binary.part(body, size, byte_size(body) - size) 115 | 116 | tag = 117 | case rest do 118 | "" -> :ok 119 | _ -> :more 120 | end 121 | 122 | {tag, data, %{state | req_body: rest}} 123 | end 124 | 125 | def inform(%{owner: owner, ref: ref}, status, headers) do 126 | send(owner, {ref, :inform, {status, headers}}) 127 | :ok 128 | end 129 | 130 | def upgrade(%{owner: owner, ref: ref}, :not_supported = protocol, opts) do 131 | send(owner, {ref, :upgrade, {protocol, opts}}) 132 | {:error, :not_supported} 133 | end 134 | 135 | def upgrade(%{owner: owner, ref: ref} = state, protocol, opts) do 136 | send(owner, {ref, :upgrade, {protocol, opts}}) 137 | {:ok, state} 138 | end 139 | 140 | def push(%{owner: owner, ref: ref}, path, headers) do 141 | send(owner, {ref, :push, {path, headers}}) 142 | :ok 143 | end 144 | 145 | def get_peer_data(payload) do 146 | Map.fetch!(payload, :peer_data) 147 | end 148 | 149 | def get_sock_data(payload) do 150 | Map.fetch!(payload, :sock_data) 151 | end 152 | 153 | def get_ssl_data(payload) do 154 | Map.fetch!(payload, :ssl_data) 155 | end 156 | 157 | def get_http_protocol(payload) do 158 | Map.fetch!(payload, :http_protocol) 159 | end 160 | 161 | ## Private helpers 162 | 163 | defp get_from_adapter(conn, op, default) do 164 | case conn.adapter do 165 | {Plug.MissingAdapter, _} -> default 166 | {adapter, payload} -> apply(adapter, op, [payload]) 167 | end 168 | end 169 | 170 | defp body_or_params(nil, query, headers, _method), do: {nil, {"", nil}, {query, nil}, headers} 171 | 172 | defp body_or_params(body, query, headers, _method) when is_binary(body) do 173 | {nil, {body, nil}, {query, nil}, headers} 174 | end 175 | 176 | defp body_or_params(params, query, headers, method) when is_list(params) do 177 | body_or_params(Enum.into(params, %{}), query, headers, method) 178 | end 179 | 180 | defp body_or_params(params, query, headers, method) 181 | when is_map(params) and method in ["GET", "HEAD"] do 182 | params = stringify_params(params, &to_string/1) 183 | 184 | from_query = Plug.Conn.Query.decode(query) 185 | params = Map.merge(from_query, params) 186 | 187 | query = 188 | params 189 | |> Map.merge(from_query) 190 | |> Plug.Conn.Query.encode() 191 | 192 | {params, {"", nil}, {query, params}, headers} 193 | end 194 | 195 | defp body_or_params(params, query, headers, _method) when is_map(params) do 196 | content_type_header = {"content-type", "multipart/mixed; boundary=plug_conn_test"} 197 | content_type = List.keyfind(headers, "content-type", 0, content_type_header) 198 | headers = List.keystore(headers, "content-type", 0, content_type) 199 | 200 | body_params = stringify_params(params, & &1) 201 | query_params = Plug.Conn.Query.decode(query) 202 | params = Map.merge(query_params, body_params) 203 | 204 | {params, {"--plug_conn_test--", body_params}, {query, query_params}, headers} 205 | end 206 | 207 | defp stringify_params([{_, _} | _] = params, value_fun), 208 | do: Enum.into(params, %{}, &stringify_kv(&1, value_fun)) 209 | 210 | defp stringify_params([_ | _] = params, value_fun), 211 | do: Enum.map(params, &stringify_params(&1, value_fun)) 212 | 213 | defp stringify_params(%{__struct__: mod} = struct, _value_fun) when is_atom(mod), do: struct 214 | defp stringify_params(fun, _value_fun) when is_function(fun), do: fun 215 | 216 | defp stringify_params(%{} = params, value_fun), 217 | do: Enum.into(params, %{}, &stringify_kv(&1, value_fun)) 218 | 219 | defp stringify_params(other, value_fun), do: value_fun.(other) 220 | 221 | defp stringify_kv({k, v}, value_fun), do: {to_string(k), stringify_params(v, value_fun)} 222 | 223 | defp split_path(nil), do: [] 224 | 225 | defp split_path(path) do 226 | segments = :binary.split(path, "/", [:global]) 227 | for segment <- segments, segment != "", do: segment 228 | end 229 | 230 | @already_sent {:plug_conn, :sent} 231 | 232 | defp maybe_flush() do 233 | receive do 234 | @already_sent -> :ok 235 | after 236 | 0 -> :ok 237 | end 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /lib/plug/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_, _) do 6 | # While Plug.Crypto provides its own cache, Plug ship its own too, 7 | # both to keep storages separate and for backwards compatibility. 8 | Plug.Keys = :ets.new(Plug.Keys, [:named_table, :public, read_concurrency: true]) 9 | 10 | children = [ 11 | Plug.Upload 12 | ] 13 | 14 | Supervisor.start_link(children, name: __MODULE__, strategy: :one_for_one) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/plug/basic_auth.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.BasicAuth do 2 | @moduledoc """ 3 | Functionality for providing Basic HTTP authentication. 4 | 5 | It is recommended to only use this module in production 6 | if SSL is enabled and enforced. See `Plug.SSL` for more 7 | information. 8 | 9 | ## Compile-time usage 10 | 11 | If you have a single username and password, you can use 12 | the `basic_auth/2` plug: 13 | 14 | import Plug.BasicAuth 15 | plug :basic_auth, username: "hello", password: "secret" 16 | 17 | Or if you would rather put those in a config file: 18 | 19 | # lib/your_app.ex 20 | import Plug.BasicAuth 21 | plug :basic_auth, Application.compile_env(:my_app, :basic_auth) 22 | 23 | # config/config.exs 24 | config :my_app, :basic_auth, username: "hello", password: "secret" 25 | 26 | Once the user first accesses the page, the request will be denied 27 | with reason 401 and the request is halted. The browser will then 28 | prompt the user for username and password. If they match, then the 29 | request succeeds. 30 | 31 | Both approaches shown above rely on static configuration. Let's see 32 | alternatives. 33 | 34 | ## Runtime-time usage 35 | 36 | As any other Plug, we can use the `basic_auth` at runtime by simply 37 | wrapping it in a function: 38 | 39 | plug :auth 40 | 41 | defp auth(conn, _opts) do 42 | username = System.fetch_env!("AUTH_USERNAME") 43 | password = System.fetch_env!("AUTH_PASSWORD") 44 | Plug.BasicAuth.basic_auth(conn, username: username, password: password) 45 | end 46 | 47 | This approach is useful when both username and password are specified 48 | upfront and available at runtime. However, you may also want to compute 49 | a different password for each different user. In those cases, we can use 50 | the low-level API. 51 | 52 | ## Low-level usage 53 | 54 | If you want to provide your own authentication logic on top of Basic HTTP 55 | auth, you can use the low-level functions. As an example, we define `:auth` 56 | plug that extracts username and password from the request headers, compares 57 | them against the database, and either assigns a `:current_user` on success 58 | or responds with an error on failure. 59 | 60 | plug :auth 61 | 62 | defp auth(conn, _opts) do 63 | with {user, pass} <- Plug.BasicAuth.parse_basic_auth(conn), 64 | %User{} = user <- MyApp.Accounts.find_by_username_and_password(user, pass) do 65 | assign(conn, :current_user, user) 66 | else 67 | _ -> conn |> Plug.BasicAuth.request_basic_auth() |> halt() 68 | end 69 | end 70 | 71 | Keep in mind that: 72 | 73 | * The supplied `user` and `pass` may be empty strings; 74 | 75 | * If you are comparing the username and password with existing strings, 76 | do not use `==/2` or pattern matching. Use `Plug.Crypto.secure_compare/2` 77 | instead. 78 | 79 | """ 80 | import Plug.Conn 81 | 82 | @doc """ 83 | Higher level usage of Basic HTTP auth. 84 | 85 | See the module docs for examples. 86 | 87 | ## Options 88 | 89 | * `:username` - the expected username 90 | * `:password` - the expected password 91 | * `:realm` - the authentication realm. The value is not fully 92 | sanitized, so do not accept user input as the realm and use 93 | strings with only alphanumeric characters and space 94 | 95 | """ 96 | @spec basic_auth(Plug.Conn.t(), [auth_option]) :: Plug.Conn.t() 97 | when auth_option: {:username, String.t()} | {:password, String.t()} | {:realm, String.t()} 98 | def basic_auth(%Plug.Conn{} = conn, options \\ []) when is_list(options) do 99 | username = Keyword.fetch!(options, :username) 100 | password = Keyword.fetch!(options, :password) 101 | 102 | with {request_username, request_password} <- parse_basic_auth(conn), 103 | valid_username? = Plug.Crypto.secure_compare(username, request_username), 104 | valid_password? = Plug.Crypto.secure_compare(password, request_password), 105 | true <- valid_username? and valid_password? do 106 | conn 107 | else 108 | _ -> conn |> request_basic_auth(options) |> halt() 109 | end 110 | end 111 | 112 | @doc """ 113 | Parses the request username and password from Basic HTTP auth. 114 | 115 | It returns either `{user, pass}` or `:error`. Note the username 116 | and password may be empty strings. When comparing the username 117 | and password with the expected values, be sure to use 118 | `Plug.Crypto.secure_compare/2`. 119 | 120 | See the module docs for examples. 121 | """ 122 | @spec parse_basic_auth(Plug.Conn.t()) :: {user :: String.t(), password :: String.t()} | :error 123 | def parse_basic_auth(%Plug.Conn{} = conn) do 124 | with ["Basic " <> encoded_user_and_pass] <- get_req_header(conn, "authorization"), 125 | {:ok, decoded_user_and_pass} <- Base.decode64(encoded_user_and_pass), 126 | [user, pass] <- :binary.split(decoded_user_and_pass, ":") do 127 | {user, pass} 128 | else 129 | _ -> :error 130 | end 131 | end 132 | 133 | @doc """ 134 | Encodes a basic authentication header. 135 | 136 | This can be used during tests: 137 | 138 | put_req_header(conn, "authorization", encode_basic_auth("hello", "world")) 139 | 140 | """ 141 | @spec encode_basic_auth(String.t(), String.t()) :: String.t() 142 | def encode_basic_auth(user, pass) when is_binary(user) and is_binary(pass) do 143 | "Basic " <> Base.encode64("#{user}:#{pass}") 144 | end 145 | 146 | @doc """ 147 | Requests basic authentication from the client. 148 | 149 | It sets the response to status 401 with "Unauthorized" as body. 150 | The response is not sent though (nor the connection is halted), 151 | allowing developers to further customize it. 152 | 153 | ## Options 154 | 155 | * `:realm` - the authentication realm. The value is not fully 156 | sanitized, so do not accept user input as the realm and use 157 | strings with only alphanumeric characters and space 158 | 159 | """ 160 | @spec request_basic_auth(Plug.Conn.t(), [option]) :: Plug.Conn.t() 161 | when option: {:realm, String.t()} 162 | def request_basic_auth(%Plug.Conn{} = conn, options \\ []) when is_list(options) do 163 | realm = Keyword.get(options, :realm, "Application") 164 | escaped_realm = String.replace(realm, "\"", "") 165 | 166 | conn 167 | |> put_resp_header("www-authenticate", "Basic realm=\"#{escaped_realm}\"") 168 | |> resp(401, "Unauthorized") 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /lib/plug/conn/adapter.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.Adapter do 2 | @moduledoc """ 3 | Specification of the connection adapter API implemented by webservers. 4 | """ 5 | alias Plug.Conn 6 | 7 | @type http_protocol :: :"HTTP/1" | :"HTTP/1.1" | :"HTTP/2" | atom 8 | @type payload :: term 9 | @type peer_data :: %{ 10 | address: :inet.ip_address(), 11 | port: :inet.port_number(), 12 | ssl_cert: binary | nil 13 | } 14 | @type sock_data :: %{ 15 | address: :inet.ip_address(), 16 | port: :inet.port_number() 17 | } 18 | @type ssl_data :: :ssl.connection_info() | nil 19 | 20 | @doc """ 21 | Function used by adapters to create a new connection. 22 | """ 23 | def conn(adapter, method, uri, remote_ip, req_headers) do 24 | %URI{path: path, host: host, port: port, query: qs, scheme: scheme} = uri 25 | 26 | %Plug.Conn{ 27 | adapter: adapter, 28 | host: host, 29 | method: method, 30 | owner: self(), 31 | path_info: split_path(path), 32 | port: port, 33 | remote_ip: remote_ip, 34 | query_string: qs || "", 35 | req_headers: req_headers, 36 | request_path: path, 37 | scheme: String.to_atom(scheme) 38 | } 39 | end 40 | 41 | defp split_path(path) do 42 | segments = :binary.split(path, "/", [:global]) 43 | for segment <- segments, segment != "", do: segment 44 | end 45 | 46 | @doc """ 47 | Sends the given status, headers and body as a response 48 | back to the client. 49 | 50 | If the request has method `"HEAD"`, the adapter should 51 | not send the response to the client. 52 | 53 | Webservers are advised to return `nil` as the sent_body, 54 | as the body can no longer be manipulated. However, the 55 | test implementation returns the actual body so it can 56 | be used during testing. 57 | 58 | Webservers must send a `{:plug_conn, :sent}` message to the 59 | process that called `Plug.Conn.Adapter.conn/5`. 60 | """ 61 | @callback send_resp( 62 | payload, 63 | status :: Conn.status(), 64 | headers :: Conn.headers(), 65 | body :: Conn.body() 66 | ) :: 67 | {:ok, sent_body :: binary | nil, payload} 68 | 69 | @doc """ 70 | Sends the given status, headers and file as a response 71 | back to the client. 72 | 73 | If the request has method `"HEAD"`, the adapter should 74 | not send the response to the client. 75 | 76 | Webservers are advised to return `nil` as the sent_body, 77 | as the body can no longer be manipulated. However, the 78 | test implementation returns the actual body so it can 79 | be used during testing. 80 | 81 | Webservers must send a `{:plug_conn, :sent}` message to the 82 | process that called `Plug.Conn.Adapter.conn/5`. 83 | """ 84 | @callback send_file( 85 | payload, 86 | status :: Conn.status(), 87 | headers :: Conn.headers(), 88 | file :: binary, 89 | offset :: integer, 90 | length :: integer | :all 91 | ) :: {:ok, sent_body :: binary | nil, payload} 92 | 93 | @doc """ 94 | Sends the given status, headers as the beginning of 95 | a chunked response to the client. 96 | 97 | Webservers are advised to return `nil` as the sent_body, 98 | since this function does not actually produce a body. 99 | However, the test implementation returns an empty binary 100 | as the body in order to be consistent with the built-up 101 | body returned by subsequent calls to the test implementation's 102 | `chunk/2` function 103 | 104 | Webservers must send a `{:plug_conn, :sent}` message to the 105 | process that called `Plug.Conn.Adapter.conn/5`. 106 | """ 107 | @callback send_chunked(payload, status :: Conn.status(), headers :: Conn.headers()) :: 108 | {:ok, sent_body :: binary | nil, payload} 109 | 110 | @doc """ 111 | Sends a chunk in the chunked response. 112 | 113 | If the request has method `"HEAD"`, the adapter should 114 | not send the response to the client. 115 | 116 | Webservers are advised to return `nil` as the sent_body, 117 | since the complete sent body depends on the sum of all 118 | calls to this function. However, the test implementation 119 | tracks the overall body and payload so it can be used 120 | during testing. 121 | """ 122 | @callback chunk(payload, body :: Conn.body()) :: 123 | :ok | {:ok, sent_body :: binary | nil, payload} | {:error, term} 124 | 125 | @doc """ 126 | Reads the request body. 127 | 128 | Read the docs in `Plug.Conn.read_body/2` for the supported 129 | options and expected behaviour. 130 | """ 131 | @callback read_req_body(payload, options :: Keyword.t()) :: 132 | {:ok, data :: binary, payload} 133 | | {:more, data :: binary, payload} 134 | | {:error, term} 135 | 136 | @doc """ 137 | Push a resource to the client. 138 | 139 | If the adapter does not support server push then `{:error, :not_supported}` 140 | should be returned. 141 | 142 | This callback no longer needs to be implemented, as browsers no longer support server push. 143 | """ 144 | @callback push(payload, path :: String.t(), headers :: Keyword.t()) :: :ok | {:error, term} 145 | 146 | @doc """ 147 | Send an informational response to the client. 148 | 149 | If the adapter does not support inform, then `{:error, :not_supported}` 150 | should be returned. 151 | """ 152 | @callback inform(payload, status :: Conn.status(), headers :: Keyword.t()) :: 153 | :ok | {:ok, payload()} | {:error, term()} 154 | 155 | @doc """ 156 | Attempt to upgrade the connection with the client. 157 | 158 | If the adapter does not support the indicated upgrade, then `{:error, :not_supported}` should be 159 | be returned. 160 | 161 | If the adapter supports the indicated upgrade but is unable to proceed with it (due to 162 | a negotiation error, invalid opts being passed to this function, or some other reason), then an 163 | arbitrary error may be returned. Note that an adapter does not need to process the actual 164 | upgrade within this function; it is a wholly supported failure mode for an adapter to attempt 165 | the upgrade process later in the connection lifecycle and fail at that point. 166 | """ 167 | @callback upgrade(payload, protocol :: atom, opts :: term) :: {:ok, payload} | {:error, term} 168 | 169 | @doc """ 170 | Returns peer information such as the address, port and ssl cert. 171 | """ 172 | @callback get_peer_data(payload) :: peer_data() 173 | 174 | @doc """ 175 | Returns sock (local-side) information such as the address and port. 176 | """ 177 | @callback get_sock_data(payload) :: sock_data() 178 | 179 | @doc """ 180 | Returns details of the negotiated SSL connection, if present. If the connection is not SSL, 181 | returns nil 182 | """ 183 | @callback get_ssl_data(payload) :: ssl_data() 184 | 185 | @doc """ 186 | Returns the HTTP protocol and its version. 187 | """ 188 | @callback get_http_protocol(payload) :: http_protocol 189 | 190 | @optional_callbacks push: 3, get_sock_data: 1, get_ssl_data: 1 191 | end 192 | -------------------------------------------------------------------------------- /lib/plug/conn/cookies.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.Cookies do 2 | @moduledoc """ 3 | Conveniences for encoding and decoding cookies. 4 | """ 5 | 6 | @doc """ 7 | Decodes the given cookies as given in either a request or response header. 8 | 9 | If a cookie is invalid, it is automatically discarded from the result. 10 | 11 | ## Examples 12 | 13 | iex> decode("key1=value1;key2=value2") 14 | %{"key1" => "value1", "key2" => "value2"} 15 | 16 | """ 17 | def decode(cookie) when is_binary(cookie) do 18 | Map.new(decode_kv(cookie, [])) 19 | end 20 | 21 | defp decode_kv("", acc), do: acc 22 | defp decode_kv(<>, acc) when h in [?\s, ?\t], do: decode_kv(t, acc) 23 | defp decode_kv(kv, acc) when is_binary(kv), do: decode_key(kv, "", acc) 24 | 25 | defp decode_key(<>, _key, acc) when h in [?\s, ?\t, ?\r, ?\n, ?\v, ?\f], 26 | do: skip_until_cc(t, acc) 27 | 28 | defp decode_key(<>, _key, acc), do: decode_kv(t, acc) 29 | defp decode_key(<>, "", acc), do: skip_until_cc(t, acc) 30 | defp decode_key(<>, key, acc), do: decode_value(t, "", 0, key, acc) 31 | defp decode_key(<>, key, acc), do: decode_key(t, <>, acc) 32 | defp decode_key(<<>>, _key, acc), do: acc 33 | 34 | defp decode_value(<>, value, spaces, key, acc), 35 | do: decode_kv(t, [{key, trim_spaces(value, spaces)} | acc]) 36 | 37 | defp decode_value(<>, value, spaces, key, acc), 38 | do: decode_value(t, <>, spaces + 1, key, acc) 39 | 40 | defp decode_value(<>, _value, _spaces, _key, acc) 41 | when h in [?\t, ?\r, ?\n, ?\v, ?\f], 42 | do: skip_until_cc(t, acc) 43 | 44 | defp decode_value(<>, value, _spaces, key, acc), 45 | do: decode_value(t, <>, 0, key, acc) 46 | 47 | defp decode_value(<<>>, value, spaces, key, acc), 48 | do: [{key, trim_spaces(value, spaces)} | acc] 49 | 50 | defp skip_until_cc(<>, acc), do: decode_kv(t, acc) 51 | defp skip_until_cc(<<_, t::binary>>, acc), do: skip_until_cc(t, acc) 52 | defp skip_until_cc(<<>>, acc), do: acc 53 | 54 | defp trim_spaces(value, 0), do: value 55 | defp trim_spaces(value, spaces), do: binary_part(value, 0, byte_size(value) - spaces) 56 | 57 | @doc """ 58 | Encodes the given cookies as expected in a response header. 59 | 60 | ## Examples 61 | 62 | iex> encode("key1", %{value: "value1"}) 63 | "key1=value1; path=/; HttpOnly" 64 | 65 | iex> encode("key1", %{value: "value1", secure: true, path: "/example", http_only: false}) 66 | "key1=value1; path=/example; secure" 67 | """ 68 | def encode(key, opts \\ %{}) when is_map(opts) do 69 | value = Map.get(opts, :value) 70 | path = Map.get(opts, :path, "/") 71 | 72 | IO.iodata_to_binary([ 73 | "#{key}=#{value}; path=#{path}", 74 | emit_if(opts[:domain], &["; domain=", &1]), 75 | emit_if(opts[:max_age], &encode_max_age(&1, opts)), 76 | emit_if(Map.get(opts, :secure, false), "; secure"), 77 | emit_if(Map.get(opts, :http_only, true), "; HttpOnly"), 78 | emit_if(Map.get(opts, :same_site, nil), &encode_same_site/1), 79 | emit_if(opts[:extra], &["; ", &1]) 80 | ]) 81 | end 82 | 83 | defp encode_max_age(max_age, opts) do 84 | time = Map.get(opts, :universal_time) || :calendar.universal_time() 85 | time = add_seconds(time, max_age) 86 | ["; expires=", rfc2822(time), "; max-age=", Integer.to_string(max_age)] 87 | end 88 | 89 | defp encode_same_site(value) when is_binary(value), do: "; SameSite=#{value}" 90 | 91 | defp emit_if(value, fun_or_string) do 92 | cond do 93 | !value -> 94 | [] 95 | 96 | is_function(fun_or_string) -> 97 | fun_or_string.(value) 98 | 99 | is_binary(fun_or_string) -> 100 | fun_or_string 101 | end 102 | end 103 | 104 | defp pad(number) when number in 0..9, do: <> 105 | defp pad(number), do: Integer.to_string(number) 106 | 107 | defp rfc2822({{year, month, day} = date, {hour, minute, second}}) do 108 | # Sat, 17 Apr 2010 14:00:00 GMT 109 | [ 110 | weekday_name(:calendar.day_of_the_week(date)), 111 | ?,, 112 | ?\s, 113 | pad(day), 114 | ?\s, 115 | month_name(month), 116 | ?\s, 117 | Integer.to_string(year), 118 | ?\s, 119 | pad(hour), 120 | ?:, 121 | pad(minute), 122 | ?:, 123 | pad(second), 124 | " GMT" 125 | ] 126 | end 127 | 128 | defp weekday_name(1), do: "Mon" 129 | defp weekday_name(2), do: "Tue" 130 | defp weekday_name(3), do: "Wed" 131 | defp weekday_name(4), do: "Thu" 132 | defp weekday_name(5), do: "Fri" 133 | defp weekday_name(6), do: "Sat" 134 | defp weekday_name(7), do: "Sun" 135 | 136 | defp month_name(1), do: "Jan" 137 | defp month_name(2), do: "Feb" 138 | defp month_name(3), do: "Mar" 139 | defp month_name(4), do: "Apr" 140 | defp month_name(5), do: "May" 141 | defp month_name(6), do: "Jun" 142 | defp month_name(7), do: "Jul" 143 | defp month_name(8), do: "Aug" 144 | defp month_name(9), do: "Sep" 145 | defp month_name(10), do: "Oct" 146 | defp month_name(11), do: "Nov" 147 | defp month_name(12), do: "Dec" 148 | 149 | defp add_seconds(time, seconds_to_add) do 150 | time_seconds = :calendar.datetime_to_gregorian_seconds(time) 151 | :calendar.gregorian_seconds_to_datetime(time_seconds + seconds_to_add) 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/plug/conn/status.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.Status do 2 | @moduledoc """ 3 | Conveniences for working with status codes. 4 | """ 5 | 6 | custom_statuses = Application.compile_env(:plug, :statuses, %{}) 7 | 8 | statuses = %{ 9 | 100 => "Continue", 10 | 101 => "Switching Protocols", 11 | 102 => "Processing", 12 | 103 => "Early Hints", 13 | 200 => "OK", 14 | 201 => "Created", 15 | 202 => "Accepted", 16 | 203 => "Non-Authoritative Information", 17 | 204 => "No Content", 18 | 205 => "Reset Content", 19 | 206 => "Partial Content", 20 | 207 => "Multi-Status", 21 | 208 => "Already Reported", 22 | 226 => "IM Used", 23 | 300 => "Multiple Choices", 24 | 301 => "Moved Permanently", 25 | 302 => "Found", 26 | 303 => "See Other", 27 | 304 => "Not Modified", 28 | 305 => "Use Proxy", 29 | 306 => "Switch Proxy", 30 | 307 => "Temporary Redirect", 31 | 308 => "Permanent Redirect", 32 | 400 => "Bad Request", 33 | 401 => "Unauthorized", 34 | 402 => "Payment Required", 35 | 403 => "Forbidden", 36 | 404 => "Not Found", 37 | 405 => "Method Not Allowed", 38 | 406 => "Not Acceptable", 39 | 407 => "Proxy Authentication Required", 40 | 408 => "Request Timeout", 41 | 409 => "Conflict", 42 | 410 => "Gone", 43 | 411 => "Length Required", 44 | 412 => "Precondition Failed", 45 | 413 => "Request Entity Too Large", 46 | 414 => "Request-URI Too Long", 47 | 415 => "Unsupported Media Type", 48 | 416 => "Requested Range Not Satisfiable", 49 | 417 => "Expectation Failed", 50 | 418 => "I'm a teapot", 51 | 421 => "Misdirected Request", 52 | 422 => "Unprocessable Entity", 53 | 423 => "Locked", 54 | 424 => "Failed Dependency", 55 | 425 => "Too Early", 56 | 426 => "Upgrade Required", 57 | 428 => "Precondition Required", 58 | 429 => "Too Many Requests", 59 | 431 => "Request Header Fields Too Large", 60 | 451 => "Unavailable For Legal Reasons", 61 | 500 => "Internal Server Error", 62 | 501 => "Not Implemented", 63 | 502 => "Bad Gateway", 64 | 503 => "Service Unavailable", 65 | 504 => "Gateway Timeout", 66 | 505 => "HTTP Version Not Supported", 67 | 506 => "Variant Also Negotiates", 68 | 507 => "Insufficient Storage", 69 | 508 => "Loop Detected", 70 | 510 => "Not Extended", 71 | 511 => "Network Authentication Required" 72 | } 73 | 74 | reason_phrase_to_atom = fn reason_phrase -> 75 | reason_phrase 76 | |> String.downcase() 77 | |> String.replace("'", "") 78 | |> String.replace(~r/[^a-z0-9]/, "_") 79 | |> String.to_atom() 80 | end 81 | 82 | status_map_to_doc = fn statuses -> 83 | statuses 84 | |> Enum.sort_by(&elem(&1, 0)) 85 | |> Enum.map(fn {code, reason_phrase} -> 86 | atom = reason_phrase_to_atom.(reason_phrase) 87 | " * `#{inspect(atom)}` - #{code}\n" 88 | end) 89 | end 90 | 91 | custom_status_doc = 92 | if custom_statuses != %{} do 93 | """ 94 | ## Custom status codes 95 | 96 | #{status_map_to_doc.(custom_statuses)} 97 | """ 98 | end 99 | 100 | @doc """ 101 | Returns the status code given an integer or a known atom. 102 | 103 | ## Known status codes 104 | 105 | The following status codes can be given as atoms with their 106 | respective value shown next: 107 | 108 | #{status_map_to_doc.(statuses)} 109 | #{custom_status_doc} 110 | """ 111 | @spec code(integer | atom) :: integer 112 | def code(integer_or_atom) 113 | 114 | def code(integer) when integer in 100..999 do 115 | integer 116 | end 117 | 118 | for {code, reason_phrase} <- statuses do 119 | atom = reason_phrase_to_atom.(reason_phrase) 120 | def code(unquote(atom)), do: unquote(code) 121 | end 122 | 123 | # This ensures that both the default and custom statuses will work 124 | for {code, reason_phrase} <- custom_statuses do 125 | atom = reason_phrase_to_atom.(reason_phrase) 126 | def code(unquote(atom)), do: unquote(code) 127 | end 128 | 129 | @doc """ 130 | Returns the atom for given integer. 131 | 132 | See `code/1` for the mapping. 133 | """ 134 | @spec reason_atom(integer) :: atom 135 | def reason_atom(code) 136 | 137 | for {code, reason_phrase} <- Map.merge(statuses, custom_statuses) do 138 | atom = reason_phrase_to_atom.(reason_phrase) 139 | def reason_atom(unquote(code)), do: unquote(atom) 140 | end 141 | 142 | def reason_atom(code) do 143 | raise ArgumentError, "unknown status code #{inspect(code)}" 144 | end 145 | 146 | @spec reason_phrase(integer) :: String.t() 147 | def reason_phrase(integer) 148 | 149 | for {code, phrase} <- Map.merge(statuses, custom_statuses) do 150 | def reason_phrase(unquote(code)), do: unquote(phrase) 151 | end 152 | 153 | def reason_phrase(code) do 154 | raise ArgumentError, """ 155 | unknown status code #{inspect(code)} 156 | 157 | Custom codes can be defined in the configuration for the :plug application, 158 | under the :statuses key (which contains a map of status codes as keys and 159 | reason phrases as values). For example: 160 | 161 | config :plug, :statuses, %{998 => "Not An RFC Status Code"} 162 | 163 | After defining the config for custom statuses, Plug must be recompiled for 164 | the changes to take place using: 165 | 166 | MIX_ENV=dev mix deps.clean plug --build 167 | 168 | Doing this will allow the use of the integer status code 998 as 169 | well as the atom :not_an_rfc_status_code in many Plug functions. 170 | For example: 171 | 172 | put_status(conn, :not_an_rfc_status_code) 173 | """ 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /lib/plug/conn/unfetched.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.Unfetched do 2 | @moduledoc """ 3 | A struct used as default on unfetched fields. 4 | 5 | The `:aspect` key of the struct specifies what field is still unfetched. 6 | 7 | ## Examples 8 | 9 | unfetched = %Plug.Conn.Unfetched{aspect: :cookies} 10 | 11 | """ 12 | 13 | defstruct [:aspect] 14 | @type t :: %__MODULE__{aspect: atom()} 15 | 16 | @behaviour Access 17 | 18 | def fetch(%{aspect: aspect}, key) do 19 | raise_unfetched(__ENV__.function, aspect, key) 20 | end 21 | 22 | def get(%{aspect: aspect}, key, _value) do 23 | raise_unfetched(__ENV__.function, aspect, key) 24 | end 25 | 26 | def get_and_update(%{aspect: aspect}, key, _fun) do 27 | raise_unfetched(__ENV__.function, aspect, key) 28 | end 29 | 30 | def pop(%{aspect: aspect}, key) do 31 | raise_unfetched(__ENV__.function, aspect, key) 32 | end 33 | 34 | defp raise_unfetched({access, _}, aspect, key) do 35 | raise ArgumentError, 36 | "cannot #{access} key #{inspect(key)} from conn.#{aspect} " <> 37 | "because they were not fetched" <> hint(aspect) 38 | end 39 | 40 | defp hint(aspect) when aspect in [:cookies, :query_params], 41 | do: ". Call Plug.Conn.fetch_#{aspect}/2, either as a plug or directly, to fetch it" 42 | 43 | defp hint(aspect) when aspect in [:params, :body_params], 44 | do: ". Configure and invoke Plug.Parsers to set params based on the request" 45 | 46 | defp hint(_), do: "" 47 | end 48 | -------------------------------------------------------------------------------- /lib/plug/conn/wrapper_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.WrapperError do 2 | @moduledoc """ 3 | Wraps the connection in an error which is meant 4 | to be handled upper in the stack. 5 | 6 | Used by both `Plug.Debugger` and `Plug.ErrorHandler`. 7 | """ 8 | defexception [:conn, :kind, :reason, :stack] 9 | 10 | def message(%{kind: kind, reason: reason, stack: stack}) do 11 | Exception.format_banner(kind, reason, stack) 12 | end 13 | 14 | @doc """ 15 | Reraises an error or a wrapped one. 16 | """ 17 | def reraise(%__MODULE__{stack: stack} = reason) do 18 | :erlang.raise(:error, reason, stack) 19 | end 20 | 21 | @deprecated "Use reraise/1 or reraise/4 instead" 22 | def reraise(conn, kind, reason) do 23 | reraise(conn, kind, reason, []) 24 | end 25 | 26 | def reraise(_conn, :error, %__MODULE__{stack: stack} = reason, _stack) do 27 | :erlang.raise(:error, reason, stack) 28 | end 29 | 30 | def reraise(conn, :error, reason, stack) do 31 | wrapper = %__MODULE__{conn: conn, kind: :error, reason: reason, stack: stack} 32 | :erlang.raise(:error, wrapper, stack) 33 | end 34 | 35 | def reraise(_conn, kind, reason, stack) do 36 | :erlang.raise(kind, reason, stack) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/plug/error_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.ErrorHandler do 2 | @moduledoc """ 3 | A module to be used in your existing plugs in order to provide 4 | error handling. 5 | 6 | defmodule AppRouter do 7 | use Plug.Router 8 | use Plug.ErrorHandler 9 | 10 | plug :match 11 | plug :dispatch 12 | 13 | get "/hello" do 14 | send_resp(conn, 200, "world") 15 | end 16 | 17 | @impl Plug.ErrorHandler 18 | def handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do 19 | send_resp(conn, conn.status, "Something went wrong") 20 | end 21 | end 22 | 23 | Once this module is used, a callback named `handle_errors/2` should 24 | be defined in your plug. This callback will receive the connection 25 | already updated with a proper status code for the given exception. 26 | The second argument is a map containing: 27 | 28 | * the exception kind (`:throw`, `:error` or `:exit`), 29 | * the reason (an exception for errors or a term for others) 30 | * the stacktrace 31 | 32 | After the callback is invoked, the error is re-raised. 33 | 34 | It is advised to do as little work as possible when handling errors 35 | and avoid accessing data like parameters and session, as the parsing 36 | of those is what could have led the error to trigger in the first place. 37 | 38 | Also notice that these pages are going to be shown in production. If 39 | you are looking for error handling to help during development, consider 40 | using `Plug.Debugger`. 41 | 42 | **Note:** If this module is used with `Plug.Debugger`, it must be used 43 | after `Plug.Debugger`. 44 | """ 45 | 46 | @doc """ 47 | Handle errors from plugs. 48 | 49 | Called when an exception is raised during the processing of a plug. 50 | """ 51 | @callback handle_errors(Plug.Conn.t(), %{ 52 | kind: :error | :throw | :exit, 53 | reason: Exception.t() | term(), 54 | stack: Exception.stacktrace() 55 | }) :: no_return() 56 | 57 | @doc false 58 | defmacro __using__(_) do 59 | quote location: :keep do 60 | @before_compile Plug.ErrorHandler 61 | 62 | @behaviour Plug.ErrorHandler 63 | 64 | @impl Plug.ErrorHandler 65 | def handle_errors(conn, assigns) do 66 | Plug.Conn.send_resp(conn, conn.status, "Something went wrong") 67 | end 68 | 69 | defoverridable handle_errors: 2 70 | end 71 | end 72 | 73 | @doc false 74 | defmacro __before_compile__(_env) do 75 | quote location: :keep do 76 | defoverridable call: 2 77 | 78 | def call(conn, opts) do 79 | try do 80 | super(conn, opts) 81 | rescue 82 | e in Plug.Conn.WrapperError -> 83 | %{conn: conn, kind: kind, reason: reason, stack: stack} = e 84 | Plug.ErrorHandler.__catch__(conn, kind, e, reason, stack, &handle_errors/2) 85 | catch 86 | kind, reason -> 87 | Plug.ErrorHandler.__catch__( 88 | conn, 89 | kind, 90 | reason, 91 | reason, 92 | __STACKTRACE__, 93 | &handle_errors/2 94 | ) 95 | end 96 | end 97 | end 98 | end 99 | 100 | @already_sent {:plug_conn, :sent} 101 | 102 | @doc false 103 | def __catch__(conn, kind, reason, wrapped_reason, stack, handle_errors) do 104 | receive do 105 | @already_sent -> 106 | send(self(), @already_sent) 107 | after 108 | 0 -> 109 | normalized_reason = Exception.normalize(kind, wrapped_reason, stack) 110 | 111 | conn 112 | |> Plug.Conn.put_status(status(kind, normalized_reason)) 113 | |> handle_errors.(%{kind: kind, reason: normalized_reason, stack: stack}) 114 | end 115 | 116 | :erlang.raise(kind, reason, stack) 117 | end 118 | 119 | defp status(:error, error), do: Plug.Exception.status(error) 120 | defp status(:throw, _throw), do: 500 121 | defp status(:exit, _exit), do: 500 122 | end 123 | -------------------------------------------------------------------------------- /lib/plug/exceptions.ex: -------------------------------------------------------------------------------- 1 | # This file defines the Plug.Exception protocol and 2 | # the exceptions that implement such protocol. 3 | 4 | defprotocol Plug.Exception do 5 | @moduledoc """ 6 | A protocol that extends exceptions to be status-code aware. 7 | 8 | By default, it looks for an implementation of the protocol, 9 | otherwise checks if the exception has the `:plug_status` field 10 | or simply returns 500. 11 | """ 12 | 13 | @fallback_to_any true 14 | 15 | @type action :: %{label: String.t(), handler: {module(), atom(), list()}} 16 | 17 | @doc """ 18 | Receives an exception and returns its HTTP status code. 19 | """ 20 | @spec status(t) :: Plug.Conn.status() 21 | def status(exception) 22 | 23 | @doc """ 24 | Receives an exception and returns the possible actions that could be triggered for that error. 25 | Should return a list of actions in the following structure: 26 | 27 | %{ 28 | label: "Text that will be displayed in the button", 29 | handler: {Module, :function, [args]} 30 | } 31 | 32 | Where: 33 | 34 | * `label` a string/binary that names this action 35 | * `handler` a MFArgs that will be executed when this action is triggered 36 | 37 | It will be rendered in the `Plug.Debugger` generated error page as buttons showing the `label` 38 | that upon pressing executes the MFArgs defined in the `handler`. 39 | 40 | ## Examples 41 | 42 | defimpl Plug.Exception, for: ActionableExample do 43 | def actions(_), do: [%{label: "Print HI", handler: {IO, :puts, ["Hi!"]}}] 44 | end 45 | """ 46 | @spec actions(t) :: [action()] 47 | def actions(exception) 48 | end 49 | 50 | defimpl Plug.Exception, for: Any do 51 | def status(%{plug_status: status}), do: Plug.Conn.Status.code(status) 52 | def status(_), do: 500 53 | def actions(_exception), do: [] 54 | end 55 | 56 | defmodule Plug.BadRequestError do 57 | @moduledoc """ 58 | The request will not be processed due to a client error. 59 | """ 60 | 61 | defexception message: "could not process the request due to client error", plug_status: 400 62 | end 63 | 64 | defmodule Plug.TimeoutError do 65 | @moduledoc """ 66 | Timeout while waiting for the request. 67 | """ 68 | 69 | defexception message: "timeout while waiting for request data", plug_status: 408 70 | end 71 | -------------------------------------------------------------------------------- /lib/plug/head.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Head do 2 | @moduledoc """ 3 | A Plug to convert `HEAD` requests to `GET` requests. 4 | 5 | ## Examples 6 | 7 | plug Plug.Head 8 | """ 9 | 10 | @behaviour Plug 11 | 12 | alias Plug.Conn 13 | 14 | @impl true 15 | def init([]), do: [] 16 | 17 | @impl true 18 | def call(%Conn{method: "HEAD"} = conn, []), do: %{conn | method: "GET"} 19 | def call(conn, []), do: conn 20 | end 21 | -------------------------------------------------------------------------------- /lib/plug/html.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.HTML do 2 | @moduledoc """ 3 | Conveniences for generating HTML. 4 | """ 5 | 6 | @doc ~S""" 7 | Escapes the given HTML to string. 8 | 9 | iex> Plug.HTML.html_escape("foo") 10 | "foo" 11 | 12 | iex> Plug.HTML.html_escape("") 13 | "<foo>" 14 | 15 | iex> Plug.HTML.html_escape("quotes: \" & \'") 16 | "quotes: " & '" 17 | """ 18 | @spec html_escape(String.t()) :: String.t() 19 | def html_escape(data) when is_binary(data) do 20 | IO.iodata_to_binary(to_iodata(data, 0, data, [])) 21 | end 22 | 23 | @doc ~S""" 24 | Escapes the given HTML to iodata. 25 | 26 | iex> Plug.HTML.html_escape_to_iodata("foo") 27 | "foo" 28 | 29 | iex> Plug.HTML.html_escape_to_iodata("") 30 | [[[] | "<"], "foo" | ">"] 31 | 32 | iex> Plug.HTML.html_escape_to_iodata("quotes: \" & \'") 33 | [[[[], "quotes: " | """], " " | "&"], " " | "'"] 34 | 35 | """ 36 | @spec html_escape_to_iodata(String.t()) :: iodata 37 | def html_escape_to_iodata(data) when is_binary(data) do 38 | to_iodata(data, 0, data, []) 39 | end 40 | 41 | escapes = [ 42 | {?<, "<"}, 43 | {?>, ">"}, 44 | {?&, "&"}, 45 | {?", """}, 46 | {?', "'"} 47 | ] 48 | 49 | for {match, insert} <- escapes do 50 | defp to_iodata(<>, skip, original, acc) do 51 | to_iodata(rest, skip + 1, original, [acc | unquote(insert)]) 52 | end 53 | end 54 | 55 | defp to_iodata(<<_char, rest::bits>>, skip, original, acc) do 56 | to_iodata(rest, skip, original, acc, 1) 57 | end 58 | 59 | defp to_iodata(<<>>, _skip, _original, acc) do 60 | acc 61 | end 62 | 63 | for {match, insert} <- escapes do 64 | defp to_iodata(<>, skip, original, acc, len) do 65 | part = binary_part(original, skip, len) 66 | to_iodata(rest, skip + len + 1, original, [acc, part | unquote(insert)]) 67 | end 68 | end 69 | 70 | defp to_iodata(<<_char, rest::bits>>, skip, original, acc, len) do 71 | to_iodata(rest, skip, original, acc, len + 1) 72 | end 73 | 74 | defp to_iodata(<<>>, 0, original, _acc, _len) do 75 | original 76 | end 77 | 78 | defp to_iodata(<<>>, skip, original, acc, len) do 79 | [acc | binary_part(original, skip, len)] 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/plug/logger.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Logger do 2 | @moduledoc """ 3 | A plug for logging basic request information in the format: 4 | 5 | GET /index.html 6 | Sent 200 in 572ms 7 | 8 | To use it, just plug it into the desired module. 9 | 10 | plug Plug.Logger, log: :debug 11 | 12 | ## Options 13 | 14 | * `:log` - The log level at which this plug should log its request info. 15 | Default is `:info`. 16 | The [list of supported levels](https://hexdocs.pm/logger/Logger.html#module-levels) 17 | is available in the `Logger` documentation. 18 | 19 | """ 20 | 21 | require Logger 22 | alias Plug.Conn 23 | @behaviour Plug 24 | 25 | @impl true 26 | def init(opts) do 27 | Keyword.get(opts, :log, :info) 28 | end 29 | 30 | @impl true 31 | def call(conn, level) do 32 | Logger.log(level, fn -> 33 | [conn.method, ?\s, conn.request_path] 34 | end) 35 | 36 | start = System.monotonic_time() 37 | 38 | Conn.register_before_send(conn, fn conn -> 39 | Logger.log(level, fn -> 40 | stop = System.monotonic_time() 41 | diff = System.convert_time_unit(stop - start, :native, :microsecond) 42 | status = Integer.to_string(conn.status) 43 | 44 | [connection_type(conn), ?\s, status, " in ", formatted_diff(diff)] 45 | end) 46 | 47 | conn 48 | end) 49 | end 50 | 51 | defp formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"] 52 | defp formatted_diff(diff), do: [Integer.to_string(diff), "µs"] 53 | 54 | defp connection_type(%{state: :set_chunked}), do: "Chunked" 55 | defp connection_type(_), do: "Sent" 56 | end 57 | -------------------------------------------------------------------------------- /lib/plug/method_override.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.MethodOverride do 2 | @moduledoc """ 3 | This plug overrides the request's `POST` method with the method defined in 4 | the `_method` request parameter. 5 | 6 | The `POST` method can be overridden only by these HTTP methods: 7 | 8 | * `PUT` 9 | * `PATCH` 10 | * `DELETE` 11 | 12 | This plug only replaces the request method if the `_method` request 13 | parameter is a string. If the `_method` request parameter is not a string, 14 | the request method is not changed. 15 | 16 | > #### Parse Body Parameters First {: .info} 17 | > 18 | > This plug expects the body parameters to be **already fetched and 19 | > parsed**. Those can be fetched with `Plug.Parsers`. 20 | 21 | This plug doesn't accept any options. 22 | 23 | To recap, here are all the conditions that the request must meet in order 24 | for this plug to replace the `:method` field in the `Plug.Conn`: 25 | 26 | 1. The conn's request `:method` must be `POST`. 27 | 1. The conn's `:body_params` must have been fetched already (for example, 28 | with `Plug.Parsers`). 29 | 1. The conn's `:body_params` must have a `_method` field that is a string 30 | and whose value is `"PUT"`, `"PATCH"`, or `"DELETE"` (case insensitive). 31 | 32 | ## Usage 33 | 34 | # You'll need to fetch and parse parameters first, for example: 35 | # plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json] 36 | 37 | plug Plug.MethodOverride 38 | 39 | """ 40 | 41 | @behaviour Plug 42 | 43 | @allowed_methods ~w(DELETE PUT PATCH) 44 | 45 | @impl true 46 | def init([]), do: [] 47 | 48 | @impl true 49 | def call(%Plug.Conn{method: "POST", body_params: body_params} = conn, []), 50 | do: override_method(conn, body_params) 51 | 52 | def call(%Plug.Conn{} = conn, []), do: conn 53 | 54 | defp override_method(conn, %Plug.Conn.Unfetched{}) do 55 | # Just skip it because maybe it is a content-type that 56 | # we could not parse as parameters (for example, text/gps) 57 | conn 58 | end 59 | 60 | defp override_method(%Plug.Conn{} = conn, body_params) do 61 | with method when is_binary(method) <- body_params["_method"] || "", 62 | method = String.upcase(method, :ascii), 63 | true <- method in @allowed_methods do 64 | %{conn | method: method} 65 | else 66 | _ -> conn 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/plug/mime.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.MIME do 2 | @moduledoc false 3 | 4 | if Application.compile_env(:plug, :mimes) do 5 | IO.puts(:stderr, """ 6 | warning: you have set the :mimes configuration for the :plug 7 | application but it is no longer supported. Instead of: 8 | 9 | config :plug, :mimes, %{...} 10 | 11 | You must write: 12 | 13 | config :mime, :types, %{...} 14 | 15 | After adding the configuration, MIME needs to be recompiled. 16 | If you are using mix, it can be done with: 17 | 18 | $ mix deps.clean mime --build 19 | $ mix deps.get 20 | 21 | """) 22 | end 23 | 24 | @deprecated "Use MIME.extensions(type) != [] instead" 25 | def valid?(type) do 26 | IO.puts( 27 | :stderr, 28 | "Plug.MIME.valid?/1 is deprecated, please use MIME.extensions(type) != [] instead\n" <> 29 | Exception.format_stacktrace() 30 | ) 31 | 32 | MIME.extensions(type) != [] 33 | end 34 | 35 | @deprecated "Use MIME.extensions/1 instead" 36 | def extensions(type) do 37 | IO.puts( 38 | :stderr, 39 | "Plug.MIME.extensions/1 is deprecated, please use MIME.extensions/1 instead\n" <> 40 | Exception.format_stacktrace() 41 | ) 42 | 43 | MIME.extensions(type) 44 | end 45 | 46 | @deprecated "Use MIME.type/1 instead" 47 | def type(file_extension) do 48 | IO.puts( 49 | :stderr, 50 | "Plug.MIME.type/1 is deprecated, please use MIME.type/1 instead\n" <> 51 | Exception.format_stacktrace() 52 | ) 53 | 54 | MIME.type(file_extension) 55 | end 56 | 57 | @deprecated "Use MIME.from_path/1 instead" 58 | def path(path) do 59 | IO.puts( 60 | :stderr, 61 | "Plug.MIME.path/1 is deprecated, please use MIME.from_path/1 instead\n" <> 62 | Exception.format_stacktrace() 63 | ) 64 | 65 | MIME.from_path(path) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/plug/parsers/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Parsers.JSON do 2 | @moduledoc """ 3 | Parses JSON request body. 4 | 5 | JSON documents that aren't maps (arrays, strings, numbers, etc) are parsed 6 | into a `"_json"` key to allow proper param merging. 7 | 8 | An empty request body is parsed as an empty map. 9 | 10 | ## Options 11 | 12 | All options supported by `Plug.Conn.read_body/2` are also supported here. 13 | They are repeated here for convenience: 14 | 15 | * `:length` - sets the maximum number of bytes to read from the request, 16 | defaults to 8_000_000 bytes 17 | * `:read_length` - sets the amount of bytes to read at one time from the 18 | underlying socket to fill the chunk, defaults to 1_000_000 bytes 19 | * `:read_timeout` - sets the timeout for each socket read, defaults to 20 | 15_000ms 21 | 22 | So by default, `Plug.Parsers` will read 1_000_000 bytes at a time from the 23 | socket with an overall limit of 8_000_000 bytes. 24 | 25 | The option `:nest_all_json`, when true, specifies all parsed JSON (including maps) 26 | are parsed into a `"_json"` key. 27 | """ 28 | 29 | @behaviour Plug.Parsers 30 | 31 | @impl true 32 | def init(opts) do 33 | {decoder, opts} = Keyword.pop(opts, :json_decoder) 34 | {body_reader, opts} = Keyword.pop(opts, :body_reader, {Plug.Conn, :read_body, []}) 35 | decoder = validate_decoder!(decoder) 36 | {body_reader, decoder, opts} 37 | end 38 | 39 | defp validate_decoder!(nil) do 40 | raise ArgumentError, "JSON parser expects a :json_decoder option" 41 | end 42 | 43 | defp validate_decoder!({module, fun, args} = mfa) 44 | when is_atom(module) and is_atom(fun) and is_list(args) do 45 | arity = length(args) + 1 46 | 47 | if Code.ensure_compiled(module) != {:module, module} do 48 | raise ArgumentError, 49 | "invalid :json_decoder option. The module #{inspect(module)} is not " <> 50 | "loaded and could not be found" 51 | end 52 | 53 | if not function_exported?(module, fun, arity) do 54 | raise ArgumentError, 55 | "invalid :json_decoder option. The module #{inspect(module)} must " <> 56 | "implement #{fun}/#{arity}" 57 | end 58 | 59 | mfa 60 | end 61 | 62 | defp validate_decoder!(decoder) when is_atom(decoder) do 63 | validate_decoder!({decoder, :decode!, []}) 64 | end 65 | 66 | defp validate_decoder!(decoder) do 67 | raise ArgumentError, 68 | "the :json_decoder option expects a module, or a three-element " <> 69 | "tuple in the form of {module, function, extra_args}, got: #{inspect(decoder)}" 70 | end 71 | 72 | @impl true 73 | def parse(conn, "application", subtype, _headers, {{mod, fun, args}, decoder, opts}) do 74 | if subtype == "json" or String.ends_with?(subtype, "+json") do 75 | apply(mod, fun, [conn, opts | args]) |> decode(decoder, opts) 76 | else 77 | {:next, conn} 78 | end 79 | end 80 | 81 | def parse(conn, _type, _subtype, _headers, _opts) do 82 | {:next, conn} 83 | end 84 | 85 | defp decode({:ok, "", conn}, _decoder, _opts) do 86 | {:ok, %{}, conn} 87 | end 88 | 89 | defp decode({:ok, body, conn}, {module, fun, args}, opts) do 90 | nest_all = Keyword.get(opts, :nest_all_json, false) 91 | 92 | try do 93 | apply(module, fun, [body | args]) 94 | rescue 95 | e -> raise Plug.Parsers.ParseError, exception: e 96 | else 97 | terms when is_map(terms) and not nest_all -> 98 | {:ok, terms, conn} 99 | 100 | terms -> 101 | {:ok, %{"_json" => terms}, conn} 102 | end 103 | end 104 | 105 | defp decode({:more, _, conn}, _decoder, _opts) do 106 | {:error, :too_large, conn} 107 | end 108 | 109 | defp decode({:error, :timeout}, _decoder, _opts) do 110 | raise Plug.TimeoutError 111 | end 112 | 113 | defp decode({:error, _}, _decoder, _opts) do 114 | raise Plug.BadRequestError 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/plug/parsers/urlencoded.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Parsers.URLENCODED do 2 | @moduledoc """ 3 | Parses urlencoded request body. 4 | 5 | ## Options 6 | 7 | All options supported by `Plug.Conn.read_body/2` are also supported here. 8 | They are repeated here for convenience: 9 | 10 | * `:length` - sets the maximum number of bytes to read from the request, 11 | defaults to 1_000_000 bytes 12 | * `:read_length` - sets the amount of bytes to read at one time from the 13 | underlying socket to fill the chunk, defaults to 1_000_000 bytes 14 | * `:read_timeout` - sets the timeout for each socket read, defaults to 15 | 15_000ms 16 | 17 | So by default, `Plug.Parsers` will read 1_000_000 bytes at a time from the 18 | socket with an overall limit of 8_000_000 bytes. 19 | """ 20 | 21 | @behaviour Plug.Parsers 22 | 23 | @impl true 24 | def init(opts) do 25 | opts = Keyword.put_new(opts, :length, 1_000_000) 26 | Keyword.pop(opts, :body_reader, {Plug.Conn, :read_body, []}) 27 | end 28 | 29 | @impl true 30 | def parse(conn, "application", "x-www-form-urlencoded", _headers, {{mod, fun, args}, opts}) do 31 | case apply(mod, fun, [conn, opts | args]) do 32 | {:ok, body, conn} -> 33 | validate_utf8 = Keyword.get(opts, :validate_utf8, true) 34 | 35 | {:ok, 36 | Plug.Conn.Query.decode( 37 | body, 38 | %{}, 39 | Plug.Parsers.BadEncodingError, 40 | validate_utf8 41 | ), conn} 42 | 43 | {:more, _data, conn} -> 44 | {:error, :too_large, conn} 45 | 46 | {:error, :timeout} -> 47 | raise Plug.TimeoutError 48 | 49 | {:error, _} -> 50 | raise Plug.BadRequestError 51 | end 52 | end 53 | 54 | def parse(conn, _type, _subtype, _headers, _opts) do 55 | {:next, conn} 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/plug/request_id.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.RequestId do 2 | @moduledoc """ 3 | A plug for generating a unique request ID for each request. 4 | 5 | The generated request ID will be in the format: 6 | 7 | ``` 8 | GEBMr97eLMHtGWsAAAVj 9 | ``` 10 | 11 | If a request ID already exists in a configured HTTP request header (see options below), 12 | then this plug will use that value, *assuming it is between 20 and 200 characters*. 13 | If such header is not present, this plug will generate a new request ID. 14 | 15 | The request ID is added to the `Logger` metadata as `:request_id`, and to the 16 | response as the configured HTTP response header (see options below). To see the 17 | request ID in your log output, configure your logger formatter to include the `:request_id` 18 | metadata. For example: 19 | 20 | config :logger, :default_formatter, metadata: [:request_id] 21 | 22 | We recommend to include this metadata configuration in your production 23 | configuration file. 24 | 25 | > #### Programmatic access to the request ID {: .tip} 26 | > 27 | > To access the request ID programmatically, use the `:assign_as` option (see below) 28 | > to assign the request ID to a key in `conn.assigns`, and then fetch it from there. 29 | 30 | ## Usage 31 | 32 | To use this plug, just plug it into the desired module: 33 | 34 | plug Plug.RequestId 35 | 36 | ## Options 37 | 38 | * `:http_header` - The name of the HTTP *request* header to check for 39 | existing request IDs. This is also the HTTP *response* header that will be 40 | set with the request id. Default value is `"x-request-id"`. 41 | 42 | plug Plug.RequestId, http_header: "custom-request-id" 43 | 44 | * `:assign_as` - The name of the key that will be used to store the 45 | discovered or generated request id in `conn.assigns`. If not provided, 46 | the request id will not be stored. *Available since v1.16.0*. 47 | 48 | plug Plug.RequestId, assign_as: :plug_request_id 49 | 50 | * `:logger_metadata_key` - The name of the key that will be used to store the 51 | discovered or generated request id in `Logger` metadata. If not provided, 52 | the request ID Logger metadata will be stored as `:request_id`. *Available 53 | since v1.18.0*. 54 | 55 | plug Plug.RequestId, logger_metadata_key: :my_request_id 56 | 57 | """ 58 | 59 | require Logger 60 | alias Plug.Conn 61 | @behaviour Plug 62 | 63 | @impl true 64 | def init(opts) do 65 | { 66 | Keyword.get(opts, :http_header, "x-request-id"), 67 | Keyword.get(opts, :assign_as), 68 | Keyword.get(opts, :logger_metadata_key, :request_id) 69 | } 70 | end 71 | 72 | @impl true 73 | def call(conn, {header, assign_as, logger_metadata_key}) do 74 | request_id = get_request_id(conn, header) 75 | 76 | Logger.metadata([{logger_metadata_key, request_id}]) 77 | conn = if assign_as, do: Conn.assign(conn, assign_as, request_id), else: conn 78 | 79 | Conn.put_resp_header(conn, header, request_id) 80 | end 81 | 82 | defp get_request_id(conn, header) do 83 | case Conn.get_req_header(conn, header) do 84 | [] -> generate_request_id() 85 | [val | _] -> if valid_request_id?(val), do: val, else: generate_request_id() 86 | end 87 | end 88 | 89 | defp generate_request_id do 90 | binary = << 91 | System.system_time(:nanosecond)::64, 92 | :erlang.phash2({node(), self()}, 16_777_216)::24, 93 | :erlang.unique_integer()::32 94 | >> 95 | 96 | Base.url_encode64(binary) 97 | end 98 | 99 | defp valid_request_id?(s), do: byte_size(s) in 20..200 100 | end 101 | -------------------------------------------------------------------------------- /lib/plug/rewrite_on.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.RewriteOn do 2 | @moduledoc """ 3 | A plug to rewrite the request's host/port/protocol from `x-forwarded-*` headers. 4 | 5 | If your Plug application is behind a proxy that handles HTTPS, you may 6 | need to tell Plug to parse the proper protocol from the `x-forwarded-*` 7 | header. 8 | 9 | plug Plug.RewriteOn, [:x_forwarded_host, :x_forwarded_port, :x_forwarded_proto] 10 | 11 | The supported values are: 12 | 13 | * `:x_forwarded_for` - to override the remote ip based on the "x-forwarded-for" header 14 | * `:x_forwarded_host` - to override the host based on the "x-forwarded-host" header 15 | * `:x_forwarded_port` - to override the port based on the "x-forwarded-port" header 16 | * `:x_forwarded_proto` - to override the protocol based on the "x-forwarded-proto" header 17 | 18 | A tuple representing a Module-Function-Args can also be given as argument 19 | instead of a list. 20 | 21 | Since rewriting the scheme based on `x-forwarded-*` headers can open up 22 | security vulnerabilities, only use this plug if: 23 | 24 | * your app is behind a proxy 25 | * your proxy strips the given `x-forwarded-*` headers from all incoming requests 26 | * your proxy sets the `x-forwarded-*` headers and sends it to Plug 27 | """ 28 | @behaviour Plug 29 | 30 | import Plug.Conn, only: [get_req_header: 2] 31 | 32 | @impl true 33 | def init(header) when is_tuple(header), do: header 34 | def init(header), do: List.wrap(header) 35 | 36 | @impl true 37 | def call(conn, [:x_forwarded_for | rewrite_on]) do 38 | conn 39 | |> put_remote_ip(get_req_header(conn, "x-forwarded-for")) 40 | |> call(rewrite_on) 41 | end 42 | 43 | def call(conn, [:x_forwarded_proto | rewrite_on]) do 44 | conn 45 | |> put_scheme(get_req_header(conn, "x-forwarded-proto")) 46 | |> call(rewrite_on) 47 | end 48 | 49 | def call(conn, [:x_forwarded_port | rewrite_on]) do 50 | conn 51 | |> put_port(get_req_header(conn, "x-forwarded-port")) 52 | |> call(rewrite_on) 53 | end 54 | 55 | def call(conn, [:x_forwarded_host | rewrite_on]) do 56 | conn 57 | |> put_host(get_req_header(conn, "x-forwarded-host")) 58 | |> call(rewrite_on) 59 | end 60 | 61 | def call(_conn, [other | _rewrite_on]) do 62 | raise "unknown rewrite: #{inspect(other)}" 63 | end 64 | 65 | def call(conn, []) do 66 | conn 67 | end 68 | 69 | def call(conn, {mod, fun, args}) do 70 | call(conn, apply(mod, fun, args)) 71 | end 72 | 73 | defp put_scheme(%{scheme: :http, port: 80} = conn, ["https"]), 74 | do: %{conn | scheme: :https, port: 443} 75 | 76 | defp put_scheme(conn, ["https"]), 77 | do: %{conn | scheme: :https} 78 | 79 | defp put_scheme(%{scheme: :https, port: 443} = conn, ["http"]), 80 | do: %{conn | scheme: :http, port: 80} 81 | 82 | defp put_scheme(conn, ["http"]), 83 | do: %{conn | scheme: :http} 84 | 85 | defp put_scheme(conn, _scheme), 86 | do: conn 87 | 88 | defp put_host(conn, [proper_host]), 89 | do: %{conn | host: proper_host} 90 | 91 | defp put_host(conn, _), 92 | do: conn 93 | 94 | defp put_port(conn, headers) do 95 | with [header] <- headers, 96 | {port, ""} <- Integer.parse(header) do 97 | %{conn | port: port} 98 | else 99 | _ -> conn 100 | end 101 | end 102 | 103 | defp put_remote_ip(conn, headers) do 104 | with [header] <- headers, 105 | [client | _] <- :binary.split(header, ","), 106 | {:ok, remote_ip} <- :inet.parse_address(String.to_charlist(client)) do 107 | %{conn | remote_ip: remote_ip} 108 | else 109 | _ -> conn 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/plug/router/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Router.InvalidSpecError do 2 | defexception message: "invalid route specification" 3 | end 4 | 5 | defmodule Plug.Router.MalformedURIError do 6 | defexception message: "malformed URI", plug_status: 400 7 | end 8 | 9 | defmodule Plug.Router.Utils do 10 | @moduledoc false 11 | 12 | @doc """ 13 | Decodes path information for dispatching. 14 | """ 15 | def decode_path_info!(conn) do 16 | # TODO: Remove rescue as this can't fail from Elixir v1.13 17 | try do 18 | Enum.map(conn.path_info, &URI.decode/1) 19 | rescue 20 | e in ArgumentError -> 21 | reason = %Plug.Router.MalformedURIError{message: e.message} 22 | Plug.Conn.WrapperError.reraise(conn, :error, reason, __STACKTRACE__) 23 | end 24 | end 25 | 26 | @doc """ 27 | Converts a given method to its connection representation. 28 | 29 | The request method is stored in the `Plug.Conn` struct as an uppercase string 30 | (like `"GET"` or `"POST"`). This function converts `method` to that 31 | representation. 32 | 33 | ## Examples 34 | 35 | iex> Plug.Router.Utils.normalize_method(:get) 36 | "GET" 37 | 38 | """ 39 | def normalize_method(method) do 40 | method |> to_string |> String.upcase() 41 | end 42 | 43 | @doc ~S""" 44 | Builds the pattern that will be used to match against the request's host 45 | (provided via the `:host`) option. 46 | 47 | If `host` is `nil`, a wildcard match (`_`) will be returned. If `host` ends 48 | with a dot, a match like `"host." <> _` will be returned. 49 | 50 | ## Examples 51 | 52 | iex> Plug.Router.Utils.build_host_match(nil) 53 | {:_, [], Plug.Router.Utils} 54 | 55 | iex> Plug.Router.Utils.build_host_match("foo.com") 56 | "foo.com" 57 | 58 | iex> "api." |> Plug.Router.Utils.build_host_match() |> Macro.to_string() 59 | "\"api.\" <> _" 60 | 61 | """ 62 | def build_host_match(host) do 63 | cond do 64 | is_nil(host) -> quote do: _ 65 | String.last(host) == "." -> quote do: unquote(host) <> _ 66 | is_binary(host) -> host 67 | end 68 | end 69 | 70 | @doc """ 71 | Generates a representation that will only match routes 72 | according to the given `spec`. 73 | 74 | If a non-binary spec is given, it is assumed to be 75 | custom match arguments and they are simply returned. 76 | 77 | ## Examples 78 | 79 | iex> Plug.Router.Utils.build_path_match("/foo/:id") 80 | {[:id], ["foo", {:id, [], nil}]} 81 | 82 | """ 83 | def build_path_match(path, context \\ nil) when is_binary(path) do 84 | case build_path_clause(path, true, context) do 85 | {params, match, true, _post_match} -> 86 | {Enum.map(params, &String.to_atom(&1)), match} 87 | 88 | {_, _, _, _} -> 89 | raise Plug.Router.InvalidSpecError, 90 | "invalid dynamic path. Only letters, numbers, and underscore are allowed after : in " <> 91 | inspect(path) 92 | end 93 | end 94 | 95 | @doc """ 96 | Builds a list of path param names and var match pairs. 97 | 98 | This is used to build parameter maps from existing variables. 99 | Excludes variables with underscore. 100 | 101 | ## Examples 102 | 103 | iex> Plug.Router.Utils.build_path_params_match(["id"]) 104 | [{"id", {:id, [], nil}}] 105 | iex> Plug.Router.Utils.build_path_params_match(["_id"]) 106 | [] 107 | 108 | iex> Plug.Router.Utils.build_path_params_match([:id]) 109 | [{"id", {:id, [], nil}}] 110 | iex> Plug.Router.Utils.build_path_params_match([:_id]) 111 | [] 112 | 113 | """ 114 | def build_path_params_match(params, context \\ nil) 115 | 116 | def build_path_params_match([param | _] = params, context) when is_binary(param) do 117 | params 118 | |> Enum.reject(&match?("_" <> _, &1)) 119 | |> Enum.map(&{&1, Macro.var(String.to_atom(&1), context)}) 120 | end 121 | 122 | def build_path_params_match([param | _] = params, context) when is_atom(param) do 123 | params 124 | |> Enum.map(&{Atom.to_string(&1), Macro.var(&1, context)}) 125 | |> Enum.reject(&match?({"_" <> _var, _macro}, &1)) 126 | end 127 | 128 | def build_path_params_match([], _context) do 129 | [] 130 | end 131 | 132 | @doc """ 133 | Builds a clause with match, guards, and post matches, 134 | including the known parameters. 135 | """ 136 | def build_path_clause(path, guard, context \\ nil) when is_binary(path) do 137 | compiled = :binary.compile_pattern([":", "*"]) 138 | 139 | {params, match, guards, post_match} = 140 | path 141 | |> split() 142 | |> build_path_clause([], [], [], [], context, compiled) 143 | 144 | if guard != true and guards != [] do 145 | raise ArgumentError, "cannot use \"when\" guards in route when using suffix matches" 146 | end 147 | 148 | params = params |> Enum.uniq() |> Enum.reverse() 149 | guards = Enum.reduce(guards, guard, "e(do: unquote(&1) and unquote(&2))) 150 | {params, match, guards, post_match} 151 | end 152 | 153 | defp build_path_clause([segment | rest], params, match, guards, post_match, context, compiled) do 154 | case :binary.matches(segment, compiled) do 155 | [] -> 156 | build_path_clause(rest, params, [segment | match], guards, post_match, context, compiled) 157 | 158 | [{prefix_size, _}] -> 159 | suffix_size = byte_size(segment) - prefix_size - 1 160 | <> = segment 161 | {param, suffix} = parse_suffix(suffix) 162 | params = [param | params] 163 | var = Macro.var(String.to_atom(param), context) 164 | 165 | case char do 166 | ?* when suffix != "" -> 167 | raise Plug.Router.InvalidSpecError, 168 | "globs (*var) cannot be followed by suffixes, got: #{inspect(segment)}" 169 | 170 | ?* when rest != [] -> 171 | raise Plug.Router.InvalidSpecError, 172 | "globs (*var) must always be in the last path, got glob in: #{inspect(segment)}" 173 | 174 | ?* -> 175 | submatch = 176 | if prefix != "" do 177 | IO.warn(""" 178 | doing a prefix match with globs is deprecated, invalid segment #{inspect(segment)}. 179 | 180 | You can either replace by a single segment match: 181 | 182 | /foo/bar-:var 183 | 184 | Or by mixing single segment match with globs: 185 | 186 | /foo/bar-:var/*rest 187 | """) 188 | 189 | quote do: [unquote(prefix) <> _ | _] = unquote(var) 190 | else 191 | var 192 | end 193 | 194 | match = 195 | case match do 196 | [] -> 197 | submatch 198 | 199 | [last | match] -> 200 | Enum.reverse([quote(do: unquote(last) | unquote(submatch)) | match]) 201 | end 202 | 203 | {params, match, guards, post_match} 204 | 205 | ?: -> 206 | match = 207 | if prefix == "", 208 | do: [var | match], 209 | else: [quote(do: unquote(prefix) <> unquote(var)) | match] 210 | 211 | {post_match, guards} = 212 | if suffix == "" do 213 | {post_match, guards} 214 | else 215 | guard = 216 | quote do 217 | binary_part( 218 | unquote(var), 219 | byte_size(unquote(var)) - unquote(byte_size(suffix)), 220 | unquote(byte_size(suffix)) 221 | ) == unquote(suffix) 222 | end 223 | 224 | trim = 225 | quote do 226 | unquote(var) = String.trim_trailing(unquote(var), unquote(suffix)) 227 | end 228 | 229 | {[trim | post_match], [guard | guards]} 230 | end 231 | 232 | build_path_clause(rest, params, match, guards, post_match, context, compiled) 233 | end 234 | 235 | [_ | _] -> 236 | raise Plug.Router.InvalidSpecError, 237 | "only one dynamic entry (:var or *glob) per path segment is allowed, got: " <> 238 | inspect(segment) 239 | end 240 | end 241 | 242 | defp build_path_clause([], params, match, guards, post_match, _context, _compiled) do 243 | {params, Enum.reverse(match), guards, post_match} 244 | end 245 | 246 | defp parse_suffix(<>) when h in ?a..?z or h == ?_, 247 | do: parse_suffix(t, <>) 248 | 249 | defp parse_suffix(suffix) do 250 | raise Plug.Router.InvalidSpecError, 251 | "invalid dynamic path. The characters : and * must be immediately followed by " <> 252 | "lowercase letters or underscore, got: :#{suffix}" 253 | end 254 | 255 | defp parse_suffix(<>, acc) 256 | when h in ?a..?z or h in ?A..?Z or h in ?0..?9 or h == ?_, 257 | do: parse_suffix(t, <>) 258 | 259 | defp parse_suffix(rest, acc), 260 | do: {acc, rest} 261 | 262 | @doc """ 263 | Splits the given path into several segments. 264 | It ignores both leading and trailing slashes in the path. 265 | 266 | ## Examples 267 | 268 | iex> Plug.Router.Utils.split("/foo/bar") 269 | ["foo", "bar"] 270 | 271 | iex> Plug.Router.Utils.split("/:id/*") 272 | [":id", "*"] 273 | 274 | iex> Plug.Router.Utils.split("/foo//*_bar") 275 | ["foo", "*_bar"] 276 | 277 | """ 278 | def split(bin) do 279 | for segment <- String.split(bin, "/"), segment != "", do: segment 280 | end 281 | 282 | @deprecated "Use Plug.forward/4 instead" 283 | defdelegate forward(conn, new_path, target, opts), to: Plug 284 | end 285 | -------------------------------------------------------------------------------- /lib/plug/session.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Session do 2 | @moduledoc """ 3 | A plug to handle session cookies and session stores. 4 | 5 | The session is accessed via functions on `Plug.Conn`. Cookies and 6 | session have to be fetched with `Plug.Conn.fetch_session/1` before the 7 | session can be accessed. 8 | 9 | The session is also lazy. Once configured, a cookie header with the 10 | session will only be sent to the client if something is written to the 11 | session in the first place. 12 | 13 | When using `Plug.Session`, also consider using `Plug.CSRFProtection` 14 | to avoid Cross Site Request Forgery attacks. 15 | 16 | ## Session stores 17 | 18 | See `Plug.Session.Store` for the specification session stores are required to 19 | implement. 20 | 21 | Plug ships with the following session stores: 22 | 23 | * `Plug.Session.ETS` 24 | * `Plug.Session.COOKIE` 25 | 26 | ## Options 27 | 28 | * `:store` - session store module (required); 29 | * `:key` - session cookie key (required); 30 | * `:domain` - see `Plug.Conn.put_resp_cookie/4`; 31 | * `:max_age` - see `Plug.Conn.put_resp_cookie/4`; 32 | * `:path` - see `Plug.Conn.put_resp_cookie/4`; 33 | * `:secure` - see `Plug.Conn.put_resp_cookie/4`; 34 | * `:http_only` - see `Plug.Conn.put_resp_cookie/4`; 35 | * `:same_site` - see `Plug.Conn.put_resp_cookie/4`; 36 | * `:extra` - see `Plug.Conn.put_resp_cookie/4`; 37 | 38 | Additional options can be given to the session store, see the store's 39 | documentation for the options it accepts. 40 | 41 | ## Examples 42 | 43 | plug Plug.Session, store: :ets, key: "_my_app_session", table: :session 44 | """ 45 | 46 | alias Plug.Conn 47 | @behaviour Plug 48 | 49 | @cookie_opts [:domain, :max_age, :path, :secure, :http_only, :extra, :same_site] 50 | 51 | @impl true 52 | def init(opts) do 53 | store = Plug.Session.Store.get(Keyword.fetch!(opts, :store)) 54 | key = Keyword.fetch!(opts, :key) 55 | cookie_opts = Keyword.take(opts, @cookie_opts) 56 | store_opts = Keyword.drop(opts, [:store, :key] ++ @cookie_opts) 57 | store_config = store.init(store_opts) 58 | 59 | %{ 60 | store: store, 61 | store_config: store_config, 62 | key: key, 63 | cookie_opts: cookie_opts 64 | } 65 | end 66 | 67 | @impl true 68 | def call(conn, config) do 69 | Conn.put_private(conn, :plug_session_fetch, fetch_session(config)) 70 | end 71 | 72 | defp fetch_session(config) do 73 | %{store: store, store_config: store_config, key: key} = config 74 | 75 | fn conn -> 76 | {sid, session} = 77 | if cookie = Plug.Conn.get_cookies(conn)[key] do 78 | store.get(conn, cookie, store_config) 79 | else 80 | {nil, %{}} 81 | end 82 | 83 | session = Map.merge(session, Map.get(conn.private, :plug_session, %{})) 84 | 85 | conn 86 | |> Conn.put_private(:plug_session, session) 87 | |> Conn.put_private(:plug_session_fetch, :done) 88 | |> Conn.register_before_send(before_send(sid, config)) 89 | end 90 | end 91 | 92 | defp before_send(sid, config) do 93 | fn conn -> 94 | case Map.get(conn.private, :plug_session_info) do 95 | :write -> 96 | value = put_session(sid, conn, config) 97 | put_cookie(value, conn, config) 98 | 99 | :drop -> 100 | drop_session(sid, conn, config) 101 | 102 | :renew -> 103 | renew_session(sid, conn, config) 104 | 105 | :ignore -> 106 | conn 107 | 108 | nil -> 109 | conn 110 | end 111 | end 112 | end 113 | 114 | defp drop_session(sid, conn, config) do 115 | if sid do 116 | delete_session(sid, conn, config) 117 | delete_cookie(conn, config) 118 | else 119 | conn 120 | end 121 | end 122 | 123 | defp renew_session(sid, conn, config) do 124 | if sid, do: delete_session(sid, conn, config) 125 | value = put_session(nil, conn, config) 126 | put_cookie(value, conn, config) 127 | end 128 | 129 | defp put_session(sid, conn, %{store: store, store_config: store_config}), 130 | do: store.put(conn, sid, conn.private[:plug_session], store_config) 131 | 132 | defp delete_session(sid, conn, %{store: store, store_config: store_config}), 133 | do: store.delete(conn, sid, store_config) 134 | 135 | defp put_cookie(value, conn, %{cookie_opts: cookie_opts, key: key}), 136 | do: Conn.put_resp_cookie(conn, key, value, cookie_opts) 137 | 138 | defp delete_cookie(conn, %{cookie_opts: cookie_opts, key: key}), 139 | do: Conn.delete_resp_cookie(conn, key, cookie_opts) 140 | end 141 | -------------------------------------------------------------------------------- /lib/plug/session/cookie.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Session.COOKIE do 2 | @moduledoc """ 3 | Stores the session in a cookie. 4 | 5 | This cookie store is based on `Plug.Crypto.MessageVerifier` 6 | and `Plug.Crypto.MessageEncryptor` which encrypts and signs 7 | each cookie to ensure they can't be read nor tampered with. 8 | 9 | Since this store uses crypto features, it requires you to 10 | set the `:secret_key_base` field in your connection. This 11 | can be easily achieved with a plug: 12 | 13 | plug :put_secret_key_base 14 | 15 | def put_secret_key_base(conn, _) do 16 | put_in conn.secret_key_base, "-- LONG STRING WITH AT LEAST 64 BYTES --" 17 | end 18 | 19 | ## Options 20 | 21 | * `:secret_key_base` - the secret key base to built the cookie 22 | signing/encryption on top of. If one is given on initialization, 23 | the cookie store can precompute all relevant values at compilation 24 | time. Otherwise, the value is taken from `conn.secret_key_base` 25 | and cached. 26 | 27 | * `:encryption_salt` - a salt used with `conn.secret_key_base` to generate 28 | a key for encrypting/decrypting a cookie, can be either a binary or 29 | an MFA returning a binary; 30 | 31 | * `:signing_salt` - a salt used with `conn.secret_key_base` to generate a 32 | key for signing/verifying a cookie, can be either a binary or 33 | an MFA returning a binary; 34 | 35 | * `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator` 36 | when generating the encryption and signing keys. Defaults to 1000; 37 | 38 | * `:key_length` - option passed to `Plug.Crypto.KeyGenerator` 39 | when generating the encryption and signing keys. Defaults to 32; 40 | 41 | * `:key_digest` - option passed to `Plug.Crypto.KeyGenerator` 42 | when generating the encryption and signing keys. Defaults to `:sha256`; 43 | 44 | * `:serializer` - cookie serializer module that defines `encode/1` and 45 | `decode/1` returning an `{:ok, value}` tuple. Defaults to 46 | `:external_term_format`. 47 | 48 | * `:log` - Log level to use when the cookie cannot be decoded. 49 | Defaults to `:debug`, can be set to false to disable it. 50 | 51 | * `:rotating_options` - additional list of options to use when decrypting and 52 | verifying the cookie. These options are used only when the cookie could not 53 | be decoded using primary options and are fetched on init so they cannot be 54 | changed in runtime. Defaults to `[]`. 55 | 56 | ## Examples 57 | 58 | plug Plug.Session, store: :cookie, 59 | key: "_my_app_session", 60 | encryption_salt: "cookie store encryption salt", 61 | signing_salt: "cookie store signing salt", 62 | log: :debug 63 | """ 64 | 65 | require Logger 66 | @behaviour Plug.Session.Store 67 | 68 | alias Plug.Crypto.KeyGenerator 69 | alias Plug.Crypto.MessageVerifier 70 | alias Plug.Crypto.MessageEncryptor 71 | 72 | @impl true 73 | def init(opts) do 74 | build_opts(opts) 75 | |> build_rotating_opts(opts[:rotating_options]) 76 | |> Map.delete(:secret_key_base) 77 | end 78 | 79 | @impl true 80 | def get(conn, raw_cookie, opts) do 81 | opts = Map.put(opts, :secret_key_base, conn.secret_key_base) 82 | 83 | [opts | opts.rotating_options] 84 | |> Enum.find_value(:error, &read_raw_cookie(raw_cookie, &1)) 85 | |> decode(opts.serializer, opts.log) 86 | end 87 | 88 | @impl true 89 | def put(conn, _sid, term, opts) do 90 | %{serializer: serializer, key_opts: key_opts, signing_salt: signing_salt} = opts 91 | binary = encode(term, serializer) 92 | 93 | case opts do 94 | %{encryption_salt: nil} -> 95 | MessageVerifier.sign(binary, derive(conn.secret_key_base, signing_salt, key_opts)) 96 | 97 | %{encryption_salt: encryption_salt} -> 98 | MessageEncryptor.encrypt( 99 | binary, 100 | derive(conn.secret_key_base, encryption_salt, key_opts), 101 | derive(conn.secret_key_base, signing_salt, key_opts) 102 | ) 103 | end 104 | end 105 | 106 | @impl true 107 | def delete(_conn, _sid, _opts) do 108 | :ok 109 | end 110 | 111 | defp encode(term, :external_term_format) do 112 | :erlang.term_to_binary(term) 113 | end 114 | 115 | defp encode(term, serializer) do 116 | {:ok, binary} = serializer.encode(term) 117 | binary 118 | end 119 | 120 | defp decode({:ok, binary}, :external_term_format, log) do 121 | {:term, 122 | try do 123 | Plug.Crypto.non_executable_binary_to_term(binary) 124 | rescue 125 | e -> 126 | Logger.log( 127 | log, 128 | "Plug.Session could not decode incoming session cookie. Reason: " <> 129 | Exception.message(e) 130 | ) 131 | 132 | %{} 133 | end} 134 | end 135 | 136 | defp decode({:ok, binary}, serializer, _log) do 137 | case serializer.decode(binary) do 138 | {:ok, term} -> {:custom, term} 139 | _ -> {:custom, %{}} 140 | end 141 | end 142 | 143 | defp decode(:error, _serializer, false) do 144 | {nil, %{}} 145 | end 146 | 147 | defp decode(:error, _serializer, log) do 148 | Logger.log( 149 | log, 150 | "Plug.Session could not verify incoming session cookie. " <> 151 | "This may happen when the session settings change or a stale cookie is sent." 152 | ) 153 | 154 | {nil, %{}} 155 | end 156 | 157 | defp prederive(secret_key_base, value, key_opts) 158 | when is_binary(secret_key_base) and is_binary(value) do 159 | {:prederived, derive(secret_key_base, value, Keyword.delete(key_opts, :cache))} 160 | end 161 | 162 | defp prederive(_secret_key_base, value, _key_opts) do 163 | value 164 | end 165 | 166 | defp derive(_secret_key_base, {:prederived, value}, _key_opts) do 167 | value 168 | end 169 | 170 | defp derive(secret_key_base, {module, function, args}, key_opts) do 171 | derive(secret_key_base, apply(module, function, args), key_opts) 172 | end 173 | 174 | defp derive(secret_key_base, key, key_opts) do 175 | secret_key_base 176 | |> validate_secret_key_base() 177 | |> KeyGenerator.generate(key, key_opts) 178 | end 179 | 180 | defp validate_secret_key_base(nil), 181 | do: raise(ArgumentError, "cookie store expects conn.secret_key_base to be set") 182 | 183 | defp validate_secret_key_base(secret_key_base) when byte_size(secret_key_base) < 64, 184 | do: raise(ArgumentError, "cookie store expects conn.secret_key_base to be at least 64 bytes") 185 | 186 | defp validate_secret_key_base(secret_key_base), do: secret_key_base 187 | 188 | defp check_signing_salt(opts) do 189 | case opts[:signing_salt] do 190 | nil -> raise ArgumentError, "cookie store expects :signing_salt as option" 191 | salt -> salt 192 | end 193 | end 194 | 195 | defp check_serializer(serializer) when is_atom(serializer), do: serializer 196 | 197 | defp check_serializer(_), 198 | do: raise(ArgumentError, "cookie store expects :serializer option to be a module") 199 | 200 | defp read_raw_cookie(raw_cookie, opts) do 201 | signing_salt = derive(opts.secret_key_base, opts.signing_salt, opts.key_opts) 202 | 203 | case opts do 204 | %{encryption_salt: nil} -> 205 | MessageVerifier.verify(raw_cookie, signing_salt) 206 | 207 | %{encryption_salt: _} -> 208 | encryption_salt = derive(opts.secret_key_base, opts.encryption_salt, opts.key_opts) 209 | 210 | MessageEncryptor.decrypt(raw_cookie, encryption_salt, signing_salt) 211 | end 212 | |> case do 213 | :error -> nil 214 | result -> result 215 | end 216 | end 217 | 218 | defp build_opts(opts) do 219 | encryption_salt = opts[:encryption_salt] 220 | signing_salt = check_signing_salt(opts) 221 | 222 | iterations = Keyword.get(opts, :key_iterations, 1000) 223 | length = Keyword.get(opts, :key_length, 32) 224 | digest = Keyword.get(opts, :key_digest, :sha256) 225 | log = Keyword.get(opts, :log, :debug) 226 | secret_key_base = Keyword.get(opts, :secret_key_base) 227 | key_opts = [iterations: iterations, length: length, digest: digest, cache: Plug.Keys] 228 | 229 | serializer = check_serializer(opts[:serializer] || :external_term_format) 230 | 231 | %{ 232 | secret_key_base: secret_key_base, 233 | encryption_salt: prederive(secret_key_base, encryption_salt, key_opts), 234 | signing_salt: prederive(secret_key_base, signing_salt, key_opts), 235 | key_opts: key_opts, 236 | serializer: serializer, 237 | log: log 238 | } 239 | end 240 | 241 | defp build_rotating_opts(opts, rotating_opts) when is_list(rotating_opts) do 242 | Map.put(opts, :rotating_options, Enum.map(rotating_opts, &build_opts/1)) 243 | end 244 | 245 | defp build_rotating_opts(opts, _), do: Map.put(opts, :rotating_options, []) 246 | end 247 | -------------------------------------------------------------------------------- /lib/plug/session/ets.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Session.ETS do 2 | @moduledoc """ 3 | Stores the session in an in-memory ETS table. 4 | 5 | This store does not create the ETS table; it expects that an 6 | existing named table with public properties is passed as an 7 | argument. 8 | 9 | We don't recommend using this store in production as every 10 | session will be stored in ETS and never cleaned until you 11 | create a task responsible for cleaning up old entries. 12 | 13 | Also, since the store is in-memory, it means sessions are 14 | not shared between servers. If you deploy to more than one 15 | machine, using this store is again not recommended. 16 | 17 | This store, however, can be used as an example for creating 18 | custom storages, based on Redis, Memcached, or a database 19 | itself. 20 | 21 | ## Options 22 | 23 | * `:table` - ETS table name (required) 24 | 25 | For more information on ETS tables, visit the Erlang documentation at 26 | http://www.erlang.org/doc/man/ets.html. 27 | 28 | ## Storage 29 | 30 | The data is stored in ETS in the following format: 31 | 32 | {sid :: String.t, data :: map, timestamp :: :erlang.timestamp} 33 | 34 | The timestamp is updated whenever there is a read or write to the 35 | table and it may be used to detect if a session is still active. 36 | 37 | ## Examples 38 | 39 | # Create an ETS table when the application starts 40 | :ets.new(:session, [:named_table, :public, read_concurrency: true]) 41 | 42 | # Use the session plug with the table name 43 | plug Plug.Session, store: :ets, key: "sid", table: :session 44 | 45 | """ 46 | 47 | @behaviour Plug.Session.Store 48 | 49 | @max_tries 100 50 | 51 | @impl true 52 | def init(opts) do 53 | Keyword.fetch!(opts, :table) 54 | end 55 | 56 | @impl true 57 | def get(_conn, sid, table) do 58 | case :ets.lookup(table, sid) do 59 | [{^sid, data, _timestamp}] -> 60 | :ets.update_element(table, sid, {3, now()}) 61 | {sid, data} 62 | 63 | [] -> 64 | {nil, %{}} 65 | end 66 | end 67 | 68 | @impl true 69 | def put(_conn, nil, data, table) do 70 | put_new(data, table) 71 | end 72 | 73 | def put(_conn, sid, data, table) do 74 | :ets.insert(table, {sid, data, now()}) 75 | sid 76 | end 77 | 78 | @impl true 79 | def delete(_conn, sid, table) do 80 | :ets.delete(table, sid) 81 | :ok 82 | end 83 | 84 | defp put_new(data, table, counter \\ 0) 85 | when counter < @max_tries do 86 | sid = Base.encode64(:crypto.strong_rand_bytes(96)) 87 | 88 | if :ets.insert_new(table, {sid, data, now()}) do 89 | sid 90 | else 91 | put_new(data, table, counter + 1) 92 | end 93 | end 94 | 95 | defp now() do 96 | :os.timestamp() 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/plug/session/store.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Session.Store do 2 | @moduledoc """ 3 | Specification for session stores. 4 | """ 5 | 6 | @doc """ 7 | Gets the store name from an atom or a module. 8 | 9 | iex> Plug.Session.Store.get(CustomStore) 10 | CustomStore 11 | 12 | iex> Plug.Session.Store.get(:cookie) 13 | Plug.Session.COOKIE 14 | 15 | """ 16 | def get(store) do 17 | case Atom.to_string(store) do 18 | "Elixir." <> _ -> store 19 | reference -> Module.concat(Plug.Session, String.upcase(reference)) 20 | end 21 | end 22 | 23 | @typedoc """ 24 | The internal reference to the session in the store. 25 | """ 26 | @type sid :: term | nil 27 | 28 | @typedoc """ 29 | The cookie value that will be sent in cookie headers. This value should be 30 | base64 encoded to avoid security issues. 31 | """ 32 | @type cookie :: binary 33 | 34 | @typedoc """ 35 | The session contents, the final data to be stored after it has been built 36 | with `Plug.Conn.put_session/3` and the other session manipulating functions. 37 | """ 38 | @type session :: map 39 | 40 | @doc """ 41 | Initializes the store. 42 | 43 | The options returned from this function will be given 44 | to `c:get/3`, `c:put/4` and `c:delete/3`. 45 | """ 46 | @callback init(opts :: Plug.opts()) :: Plug.opts() 47 | 48 | @doc """ 49 | Parses the given cookie. 50 | 51 | Returns a session id and the session contents. The session id is any 52 | value that can be used to identify the session by the store. 53 | 54 | The session id may be nil in case the cookie does not identify any 55 | value in the store. The session contents must be a map. 56 | """ 57 | @callback get(conn :: Plug.Conn.t(), cookie, opts :: Plug.opts()) :: {sid, session} 58 | 59 | @doc """ 60 | Stores the session associated with given session id. 61 | 62 | If `nil` is given as id, a new session id should be 63 | generated and returned. 64 | """ 65 | @callback put(conn :: Plug.Conn.t(), sid, any, opts :: Plug.opts()) :: cookie 66 | 67 | @doc """ 68 | Removes the session associated with given session id from the store. 69 | """ 70 | @callback delete(conn :: Plug.Conn.t(), sid, opts :: Plug.opts()) :: :ok 71 | end 72 | -------------------------------------------------------------------------------- /lib/plug/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Telemetry do 2 | @moduledoc """ 3 | A plug to instrument the pipeline with `:telemetry` events. 4 | 5 | When plugged, the event prefix is a required option: 6 | 7 | plug Plug.Telemetry, event_prefix: [:my, :plug] 8 | 9 | In the example above, two events will be emitted: 10 | 11 | * `[:my, :plug, :start]` - emitted when the plug is invoked. 12 | The event carries the `system_time` as measurement. The metadata 13 | is the whole `Plug.Conn` under the `:conn` key and any leftover 14 | options given to the plug under `:options`. 15 | 16 | * `[:my, :plug, :stop]` - emitted right before the response is sent. 17 | The event carries a single measurement, `:duration`, which is the 18 | monotonic time difference between the stop and start events. 19 | It has the same metadata as the start event, except the connection 20 | has been updated. 21 | 22 | Note this plug measures the time between its invocation until a response 23 | is sent. The `:stop` event is not guaranteed to be emitted in all error 24 | cases, so this Plug cannot be used as a Telemetry span. 25 | 26 | ## Time unit 27 | 28 | The `:duration` measurements are presented in the `:native` time unit. 29 | You can read more about it in the docs for `System.convert_time_unit/3`. 30 | 31 | ## Example 32 | 33 | defmodule InstrumentedPlug do 34 | use Plug.Router 35 | 36 | plug :match 37 | plug Plug.Telemetry, event_prefix: [:my, :plug] 38 | plug Plug.Parsers, parsers: [:urlencoded, :multipart] 39 | plug :dispatch 40 | 41 | get "/" do 42 | send_resp(conn, 200, "Hello, world!") 43 | end 44 | end 45 | 46 | In this example, the stop event's `duration` includes the time 47 | it takes to parse the request, dispatch it to the correct handler, 48 | and execute the handler. The events are not emitted for requests 49 | not matching any handlers, since the plug is placed after the match plug. 50 | """ 51 | 52 | @behaviour Plug 53 | 54 | @impl true 55 | def init(opts) do 56 | {event_prefix, opts} = Keyword.pop(opts, :event_prefix) 57 | 58 | unless event_prefix do 59 | raise ArgumentError, ":event_prefix is required" 60 | end 61 | 62 | ensure_valid_event_prefix!(event_prefix) 63 | start_event = event_prefix ++ [:start] 64 | stop_event = event_prefix ++ [:stop] 65 | {start_event, stop_event, opts} 66 | end 67 | 68 | @impl true 69 | def call(conn, {start_event, stop_event, opts}) do 70 | start_time = System.monotonic_time() 71 | metadata = %{conn: conn, options: opts} 72 | :telemetry.execute(start_event, %{system_time: System.system_time()}, metadata) 73 | 74 | Plug.Conn.register_before_send(conn, fn conn -> 75 | duration = System.monotonic_time() - start_time 76 | :telemetry.execute(stop_event, %{duration: duration}, %{conn: conn, options: opts}) 77 | conn 78 | end) 79 | end 80 | 81 | defp ensure_valid_event_prefix!(event_prefix) do 82 | if is_list(event_prefix) && Enum.all?(event_prefix, &is_atom/1) do 83 | :ok 84 | else 85 | raise ArgumentError, 86 | "expected :event_prefix to be a list of atoms, got: #{inspect(event_prefix)}" 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/plug/templates/debugger.md.eex: -------------------------------------------------------------------------------- 1 | # <%= @title %> at <%= method(@conn) %> <%= @conn.request_path %> 2 | 3 | Exception: 4 | 5 | <%= String.replace(@formatted, "\n", "\n ") %> 6 | 7 | Code: 8 | <%= for frame <- @frames do %> 9 | `<%= h frame.file %>` 10 | <%= if (snippet = frame.snippet) && snippet != [] do %> 11 | <%= for {index, line, highlight} <- snippet do %><%= if highlight do %><%= h index %>> <% else %><%= h index %> <% end %><%= h String.trim_trailing(line) %> 12 | <% end %><% else %> 13 | No code available. 14 | <% end %><%= if frame.args do %> 15 | Called with <%= length(frame.args) %> arguments 16 | 17 | <%= for arg <- frame.args do %>* `<%= h inspect arg %>` 18 | <% end %><% end %><%= if frame.clauses do %><% {min, max, clauses} = frame.clauses %> 19 | Attempted function clauses (showing <%= min %> out of <%= max %>) 20 | 21 | <%= for clause <- clauses do %> <%= clause %> 22 | <% end %> 23 | <% end %><% end %> 24 | 25 | ## Connection details 26 | 27 | ### Params 28 | 29 | <%= inspect(@params) %> 30 | 31 | ### Request info 32 | 33 | * URI: <%= url(@conn) %> 34 | * Query string: <%= @conn.query_string %> 35 | 36 | ### Headers 37 | <%= for {key, value} <- Enum.sort(@conn.req_headers) do %> 38 | * <%= key %>: <%= value %><% end %> 39 | 40 | ### Session 41 | 42 | <%= inspect(@session) %> 43 | -------------------------------------------------------------------------------- /lib/plug/upload.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.UploadError do 2 | defexception [:message] 3 | end 4 | 5 | defmodule Plug.Upload do 6 | @moduledoc """ 7 | A server (a `GenServer` specifically) that manages uploaded files. 8 | 9 | Uploaded files are stored in a temporary directory 10 | and removed from that directory after the process that 11 | requested the file dies. 12 | 13 | During the request, files are represented with 14 | a `Plug.Upload` struct that contains three fields: 15 | 16 | * `:path` - the path to the uploaded file on the filesystem 17 | * `:content_type` - the content type of the uploaded file 18 | * `:filename` - the filename of the uploaded file given in the request 19 | 20 | **Note**: as mentioned in the documentation for `Plug.Parsers`, the `:plug` 21 | application has to be started in order to upload files and use the 22 | `Plug.Upload` module. 23 | 24 | ## Security 25 | 26 | The `:content_type` and `:filename` fields in the `Plug.Upload` struct are 27 | client-controlled. These values should be validated, via file content 28 | inspection or similar, before being trusted. 29 | """ 30 | 31 | use GenServer 32 | defstruct [:path, :content_type, :filename] 33 | 34 | @type t :: %__MODULE__{ 35 | path: Path.t(), 36 | filename: binary, 37 | content_type: binary | nil 38 | } 39 | 40 | @dir_table __MODULE__.Dir 41 | @path_table __MODULE__.Path 42 | @max_attempts 10 43 | @temp_env_vars ~w(PLUG_TMPDIR TMPDIR TMP TEMP)s 44 | 45 | @doc """ 46 | Requests a random file to be created in the upload directory 47 | with the given prefix. 48 | """ 49 | @spec random_file(binary) :: 50 | {:ok, binary} 51 | | {:too_many_attempts, binary, pos_integer} 52 | | {:no_tmp, [binary]} 53 | def random_file(prefix) do 54 | case ensure_tmp() do 55 | {:ok, tmp} -> 56 | open_random_file(prefix, tmp, 0) 57 | 58 | {:no_tmp, tmps} -> 59 | {:no_tmp, tmps} 60 | end 61 | end 62 | 63 | @doc """ 64 | Assign ownership of the given upload file to another process. 65 | 66 | Useful if you want to do some work on an uploaded file in another process 67 | since it means that the file will survive the end of the request. 68 | """ 69 | @spec give_away(t | binary, pid, pid) :: :ok | {:error, :unknown_path} 70 | def give_away(upload, to_pid, from_pid \\ self()) 71 | 72 | def give_away(%__MODULE__{path: path}, to_pid, from_pid) do 73 | give_away(path, to_pid, from_pid) 74 | end 75 | 76 | def give_away(path, to_pid, from_pid) 77 | when is_binary(path) and is_pid(to_pid) and is_pid(from_pid) do 78 | with [{^from_pid, _tmp}] <- :ets.lookup(@dir_table, from_pid), 79 | true <- path_owner?(from_pid, path) do 80 | case :ets.lookup(@dir_table, to_pid) do 81 | [{^to_pid, _tmp}] -> 82 | :ets.insert(@path_table, {to_pid, path}) 83 | :ets.delete_object(@path_table, {from_pid, path}) 84 | 85 | :ok 86 | 87 | [] -> 88 | server = plug_server() 89 | {:ok, tmp} = generate_tmp_dir() 90 | :ok = GenServer.call(server, {:give_away, to_pid, tmp, path}) 91 | :ets.delete_object(@path_table, {from_pid, path}) 92 | :ok 93 | end 94 | else 95 | _ -> 96 | {:error, :unknown_path} 97 | end 98 | end 99 | 100 | defp ensure_tmp() do 101 | pid = self() 102 | 103 | case :ets.lookup(@dir_table, pid) do 104 | [{^pid, tmp}] -> 105 | {:ok, tmp} 106 | 107 | [] -> 108 | server = plug_server() 109 | GenServer.cast(server, {:monitor, pid}) 110 | 111 | with {:ok, tmp} <- generate_tmp_dir() do 112 | true = :ets.insert_new(@dir_table, {pid, tmp}) 113 | {:ok, tmp} 114 | end 115 | end 116 | end 117 | 118 | defp generate_tmp_dir() do 119 | {tmp_roots, suffix} = :persistent_term.get(__MODULE__) 120 | {mega, _, _} = :os.timestamp() 121 | subdir = "/plug-" <> i(mega) <> "-" <> suffix 122 | 123 | if tmp = Enum.find_value(tmp_roots, &make_tmp_dir(&1 <> subdir)) do 124 | {:ok, tmp} 125 | else 126 | {:no_tmp, tmp_roots} 127 | end 128 | end 129 | 130 | defp make_tmp_dir(path) do 131 | case File.mkdir_p(path) do 132 | :ok -> path 133 | {:error, _} -> nil 134 | end 135 | end 136 | 137 | defp open_random_file(prefix, tmp, attempts) when attempts < @max_attempts do 138 | path = path(prefix, tmp) 139 | 140 | case :file.write_file(path, "", [:write, :raw, :exclusive, :binary]) do 141 | :ok -> 142 | :ets.insert(@path_table, {self(), path}) 143 | {:ok, path} 144 | 145 | {:error, reason} when reason in [:eexist, :eacces] -> 146 | open_random_file(prefix, tmp, attempts + 1) 147 | end 148 | end 149 | 150 | defp open_random_file(_prefix, tmp, attempts) do 151 | {:too_many_attempts, tmp, attempts} 152 | end 153 | 154 | defp path(prefix, tmp) do 155 | sec = :os.system_time(:second) 156 | rand = :rand.uniform(999_999_999_999) 157 | scheduler_id = :erlang.system_info(:scheduler_id) 158 | tmp <> "/" <> prefix <> "-" <> i(sec) <> "-" <> i(rand) <> "-" <> i(scheduler_id) 159 | end 160 | 161 | defp path_owner?(pid, path) do 162 | owned_paths = :ets.lookup(@path_table, pid) 163 | Enum.any?(owned_paths, fn {_pid, p} -> p == path end) 164 | end 165 | 166 | @compile {:inline, i: 1} 167 | defp i(integer), do: Integer.to_string(integer) 168 | 169 | @doc """ 170 | Requests a random file to be created in the upload directory 171 | with the given prefix. Raises on failure. 172 | """ 173 | @spec random_file!(binary) :: binary | no_return 174 | def random_file!(prefix) do 175 | case random_file(prefix) do 176 | {:ok, path} -> 177 | path 178 | 179 | {:too_many_attempts, tmp, attempts} -> 180 | raise Plug.UploadError, 181 | "tried #{attempts} times to create an uploaded file at #{tmp} but failed. " <> 182 | "Set PLUG_TMPDIR to a directory with write permission" 183 | 184 | {:no_tmp, _tmps} -> 185 | raise Plug.UploadError, 186 | "could not create a tmp directory to store uploads. " <> 187 | "Set PLUG_TMPDIR to a directory with write permission" 188 | end 189 | end 190 | 191 | defp plug_server do 192 | Process.whereis(__MODULE__) || 193 | raise Plug.UploadError, 194 | "could not find process Plug.Upload. Have you started the :plug application?" 195 | end 196 | 197 | @doc false 198 | def start_link(_) do 199 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 200 | end 201 | 202 | ## Callbacks 203 | 204 | @impl true 205 | def init(:ok) do 206 | Process.flag(:trap_exit, true) 207 | tmp = Enum.find_value(@temp_env_vars, "/tmp", &System.get_env/1) |> Path.expand() 208 | cwd = Path.join(File.cwd!(), "tmp") 209 | # Add a tiny random component to avoid clashes between nodes 210 | suffix = :crypto.strong_rand_bytes(3) |> Base.url_encode64() 211 | :persistent_term.put(__MODULE__, {[tmp, cwd], suffix}) 212 | 213 | :ets.new(@dir_table, [:named_table, :public, :set]) 214 | :ets.new(@path_table, [:named_table, :public, :duplicate_bag]) 215 | {:ok, %{}} 216 | end 217 | 218 | @impl true 219 | def handle_call({:give_away, pid, tmp, path}, _from, state) do 220 | # Since we are writing in behalf of another process, we need to make sure 221 | # the monitor and writing to the tables happen within the same operation. 222 | Process.monitor(pid) 223 | :ets.insert_new(@dir_table, {pid, tmp}) 224 | :ets.insert(@path_table, {pid, path}) 225 | 226 | {:reply, :ok, state} 227 | end 228 | 229 | @impl true 230 | def handle_cast({:monitor, pid}, state) do 231 | Process.monitor(pid) 232 | {:noreply, state} 233 | end 234 | 235 | @impl true 236 | def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do 237 | case :ets.lookup(@dir_table, pid) do 238 | [{pid, _tmp}] -> 239 | :ets.delete(@dir_table, pid) 240 | 241 | @path_table 242 | |> :ets.lookup(pid) 243 | |> Enum.each(&delete_path/1) 244 | 245 | :ets.delete(@path_table, pid) 246 | 247 | [] -> 248 | :ok 249 | end 250 | 251 | {:noreply, state} 252 | end 253 | 254 | def handle_info(_msg, state) do 255 | {:noreply, state} 256 | end 257 | 258 | @impl true 259 | def terminate(_reason, _state) do 260 | folder = fn entry, :ok -> delete_path(entry) end 261 | :ets.foldl(folder, :ok, @path_table) 262 | end 263 | 264 | defp delete_path({_pid, path}) do 265 | :file.delete(path) 266 | :ok 267 | end 268 | end 269 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.MixProject do 2 | use Mix.Project 3 | 4 | @version "1.18.0" 5 | @description "Compose web applications with functions" 6 | @xref_exclude [Plug.Cowboy, :ssl] 7 | @source_url "https://github.com/elixir-plug/plug" 8 | 9 | def project do 10 | [ 11 | app: :plug, 12 | version: @version, 13 | elixir: "~> 1.10", 14 | deps: deps(), 15 | package: package(), 16 | description: @description, 17 | name: "Plug", 18 | xref: [exclude: @xref_exclude], 19 | consolidate_protocols: Mix.env() != :test, 20 | docs: [ 21 | extras: [ 22 | "CHANGELOG.md", 23 | "README.md", 24 | "guides/https.md" 25 | ], 26 | main: "readme", 27 | groups_for_modules: groups_for_modules(), 28 | groups_for_extras: groups_for_extras(), 29 | source_ref: "v#{@version}", 30 | source_url: @source_url 31 | ], 32 | test_ignore_filters: [&String.starts_with?(&1, "test/fixtures/")] 33 | ] 34 | end 35 | 36 | # Configuration for the OTP application 37 | def application do 38 | [ 39 | extra_applications: extra_applications(Mix.env()), 40 | mod: {Plug.Application, []}, 41 | env: [validate_header_keys_during_test: true] 42 | ] 43 | end 44 | 45 | defp extra_applications(:test), do: [:logger, :eex, :ssl] 46 | defp extra_applications(_), do: [:logger, :eex] 47 | 48 | def deps do 49 | [ 50 | {:mime, "~> 1.0 or ~> 2.0"}, 51 | {:plug_crypto, plug_crypto_version()}, 52 | {:telemetry, "~> 0.4.3 or ~> 1.0"}, 53 | {:ex_doc, "~> 0.21", only: :docs} 54 | ] 55 | end 56 | 57 | if System.get_env("PLUG_CRYPTO_2_0", "true") == "true" do 58 | defp plug_crypto_version, do: "~> 1.1.1 or ~> 1.2 or ~> 2.0" 59 | else 60 | defp plug_crypto_version, do: "~> 1.1.1 or ~> 1.2" 61 | end 62 | 63 | defp package do 64 | %{ 65 | licenses: ["Apache-2.0"], 66 | maintainers: ["Gary Rennie", "José Valim"], 67 | links: %{ 68 | "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md", 69 | "GitHub" => @source_url 70 | }, 71 | files: ["lib", "mix.exs", "README.md", "CHANGELOG.md", "LICENSE", "src", ".formatter.exs"] 72 | } 73 | end 74 | 75 | defp groups_for_modules do 76 | # Ungrouped Modules 77 | # 78 | # Plug 79 | # Plug.Builder 80 | # Plug.Conn 81 | # Plug.HTML 82 | # Plug.Router 83 | # Plug.Test 84 | # Plug.Upload 85 | 86 | [ 87 | Plugs: [ 88 | Plug.BasicAuth, 89 | Plug.CSRFProtection, 90 | Plug.Head, 91 | Plug.Logger, 92 | Plug.MethodOverride, 93 | Plug.Parsers, 94 | Plug.RequestId, 95 | Plug.RewriteOn, 96 | Plug.SSL, 97 | Plug.Session, 98 | Plug.Static, 99 | Plug.Telemetry 100 | ], 101 | "Error handling": [ 102 | Plug.Debugger, 103 | Plug.ErrorHandler, 104 | Plug.Exception 105 | ], 106 | "Plug.Conn": [ 107 | Plug.Conn.Adapter, 108 | Plug.Conn.Cookies, 109 | Plug.Conn.Query, 110 | Plug.Conn.Status, 111 | Plug.Conn.Unfetched, 112 | Plug.Conn.Utils 113 | ], 114 | "Plug.Parsers": [ 115 | Plug.Parsers.JSON, 116 | Plug.Parsers.MULTIPART, 117 | Plug.Parsers.URLENCODED 118 | ], 119 | "Plug.Session": [ 120 | Plug.Session.COOKIE, 121 | Plug.Session.ETS, 122 | Plug.Session.Store 123 | ] 124 | ] 125 | end 126 | 127 | defp groups_for_extras do 128 | [ 129 | Guides: ~r/guides\/[^\/]+\.md/ 130 | ] 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 3 | "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, 4 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 6 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 7 | "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, 8 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 9 | "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, 10 | "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/file-deadbeef.txt: -------------------------------------------------------------------------------- 1 | HELLO 2 | -------------------------------------------------------------------------------- /test/fixtures/manifest-file: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /test/fixtures/plug_cowboy.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Cowboy do 2 | def http(_, _, _) do 3 | {:ok, :http} 4 | end 5 | 6 | def https(_, _, _) do 7 | {:ok, :https} 8 | end 9 | 10 | def shutdown(_) do 11 | {:ok, :shutdown} 12 | end 13 | 14 | def child_spec(_, _, _, _) do 15 | {:ok, :child_spec} 16 | end 17 | 18 | def child_spec(_) do 19 | {:ok, :child_spec} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/ssl/README.md: -------------------------------------------------------------------------------- 1 | How to generate keys for use with tests 2 | 3 | # Generate Certificate Authority Certificates 4 | 5 | - `openssl genrsa -out ca-key.pem 1024` 6 | - `openssl req -new -x509 -sha256 -days 730 -key ca-key.pem -out ca.pem` 7 | - Set CN to `Elixir CA` 8 | - Set password to `cowboy` 9 | 10 | # Generate Server Certificates 11 | 12 | - `openssl genrsa -out server-key.pem 1024` 13 | - `openssl req -new -key server-key.pem -sha256 -out server.csr` 14 | - Set CN to `localhost` 15 | - `openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -set_serial 1 -out server.pem` 16 | - `openssl rsa -des -in server.key -out server.key.enc` 17 | - Set password to `cowboy` 18 | 19 | # Generate Client Certificates 20 | 21 | - `openssl genrsa -out client-key.pem 1024` 22 | - `openssl req -new -key client-key.pem -out client.csr` 23 | - Set CN to `client` 24 | - `openssl x509 -req -days 3650 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -set_serial 2 -out client.pem` 25 | -------------------------------------------------------------------------------- /test/fixtures/ssl/ca.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBwTCCASoCCQD0ZyX4UUIwwzANBgkqhkiG9w0BAQsFADAlMQ8wDQYDVQQKDAZl 3 | bGl4aXIxEjAQBgNVBAMMCUVsaXhpciBDQTAeFw0xODA1MTYyMzEyNTlaFw0yODA1 4 | MTMyMzEyNTlaMCUxDzANBgNVBAoMBmVsaXhpcjESMBAGA1UEAwwJRWxpeGlyIENB 5 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5AM3nfiR9R04Al4B+MAoDQj2q 6 | I6CHZlMmQKFp+4QzCIykJYVH+zhGnmrx8Z/nEGQV02tutUuDgjhQNoaSnfoeiA4x 7 | m1Xus/xNsOmo7AShxthXA0egJSr6b/fo+ISxTBukvFw85RALFplNkkHWpPRxJQLd 8 | GUpdD0FP9PSFFHtFKQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBALXnkcjA5x9ZgLP8 9 | SrTnHSLgvCZ0ym3EtLJIbaOBpovRDxyEoGW72CMbqw+kEb1K5kmWIyMKRqtEWtcd 10 | RubFa/g9HUHNCtB4HdtA7to3YBEso+vCsr9aUiD/sfO2fC08sjIb1Mikao3D6781 11 | 1/xinNG7UX4HjRqP7kEBadWPul9R 12 | -----END CERTIFICATE----- 13 | -------------------------------------------------------------------------------- /test/fixtures/ssl/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQI6EOCNuVUwXgCAggA 3 | MB0GCWCGSAFlAwQBKgQQhxwJNp2q2J3aWnp32pvXZwSCAoCIb5jT4MBHA5UqODrX 4 | CQ2yMScj3QBoEkHLEppH0ejYQAuv+fILVia41Dmm9LomFmHHUPJqiYJoU4pWDOuP 5 | e21qproWIOn6m0rUjfWrgdpJy9zhrtHhJu6D9c0t8n4V2PLxZziIfaFrCSXCoby1 6 | IP+ncpfBIfg7i/db7qz86wYEK7EDAYBkc6G4YYq9VvTzc0IuegOxJ3maCNtC7OJv 7 | h1Cm+RNr39m1x7XL6T2OGccOoJNfcweX7a4HUDiBvIg+bx/3RWeSsz/2W7ebnmw+ 8 | p3+RByy33WWNAMWoWNDDTFS9fLsT+9CDWzXRunm9m1Csc+XPeye89A+F3v9QeUt3 9 | +aUfuVtZN7jZZ2kaBIihe8gzR9ffMcjksZiDaLy5VfO9pAdPDxcWq0NyMeMV3ZTI 10 | udcWZWm3HpHXGsXRrL2eNQRbpdbIuTm+hoUmMq2hCq9v+5UsvS5E/1RndZFByhQg 11 | UHK1hjWffhu8H1V8Q+ZQJ00JhdCUv4r7rl3AhQW0e1FwrQpR7xmyiD7O1+bi5aE+ 12 | CEikNlTSutQNeHuTwcj3AMECCxYZEA1LWHqjfYbqAeEUlJut+JMA31OiHWiDya+m 13 | QyMTb93Y36RPjjqf6WeFdODLYsOOOnm6FVWlqa2IKBiFzOmO5B27FRDrcBnkyBWI 14 | 1dyTkWNSoq+pw9WsjmKEHFRYWv3FDDq1+m3tumjyf8AK/3D0GyKebZ4DEPejz3LS 15 | yzj9Vp32sQQwoPaDSripBQ22ZPEqpD9qddTrvWlhTqMd5tFUHPa20vS2epINbic+ 16 | Qdjjm1cfp5KHAibWNBaTl4GK6wxiwO1EP4gxGo5G+fMUcKo6Qjh4lr7gv7XN3dSY 17 | nFdW 18 | -----END ENCRYPTED PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /test/fixtures/ssl/client.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBvTCCASYCAWUwDQYJKoZIhvcNAQEFBQAwJTEPMA0GA1UECgwGZWxpeGlyMRIw 3 | EAYDVQQDDAlFbGl4aXIgQ0EwHhcNMTgwNTE2MjMzNTQ1WhcNMTkwNTE2MjMzNTQ1 4 | WjApMRYwFAYDVQQKDA1lbGl4aXIgY2xpZW50MQ8wDQYDVQQDDAZjbGllbnQwgZ8w 5 | DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALCpViTQGay9oU6xmbnuts16ELTagvo4 6 | c3j1/tTMj6EGIp8NqHg8see8mWRmXt5V0ZxJcy0CBJxjohwNZ8SK8ttHNydyLTiU 7 | 5NWw4G1Gs3HSPasWbqFP49f/i80JS7KvScKSaLjSGr6JG5cEpuBOSpcACfpjmlSz 8 | 8FHGJupsN90ZAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEATVPhWYoZXW37Axa9sZhU 9 | /XqNMpujchxFxJeNWx5puivSXZuMDtcOGe64ek9Z7KMzLkAj1+eotf0FKsblTLiE 10 | NYZfRzJYs9CF9I9VppiXhc2AW3eY43pMHFFt9RKK79rEipeYG58yf0KZEM9iHHtU 11 | Gogf2ZIicCRnXUHj/bbqQXY= 12 | -----END CERTIFICATE----- 13 | -------------------------------------------------------------------------------- /test/fixtures/ssl/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQCwqVYk0BmsvaFOsZm57rbNehC02oL6OHN49f7UzI+hBiKfDah4 3 | PLHnvJlkZl7eVdGcSXMtAgScY6IcDWfEivLbRzcnci04lOTVsOBtRrNx0j2rFm6h 4 | T+PX/4vNCUuyr0nCkmi40hq+iRuXBKbgTkqXAAn6Y5pUs/BRxibqbDfdGQIDAQAB 5 | AoGAVKs0IDyksYfJMeAo31YrwttH+oXn1GkN3uF3myHXjMNWAIkZP1dHpNtdYSM3 6 | QLQ82/zP+LhI4XNXFL7QBwDZV7ck7/tVQVOJlBmhWhCjeEUk62dJi21ADXYoGRzZ 7 | D515paqP40Pk/1BopPDFT+D5R0CAzTe9rQOUoNrbi6I0p4kCQQDaSNclincmqeE5 8 | R/7jelkakfXSyb/0cLbehfB+bzxqUcwGZaDXjwJzLY2gizTJTScWq1CrICKZ27Ow 9 | FHAjFkTjAkEAzy9s1uMKH+HsohAUZHMJnj+dXa9CB31oxKa8zC0QfAWEDNruN+55 10 | OcZqVctNk7+0hblgtMDmu/bRC8AsGBBy0wJBAM7rj69Rk+N91DeFjRS8TS0Hwfyg 11 | LSudkWxdkX15Gs86XOqPein8sfjW7NOMQmy0i2JM4bpmSwaIosw+g5JvMLsCQCCy 12 | pGO5izSC7FybWwyLVz5BXe2WJj6WXT2D7xHuHsbj+/YnaycqnLkwhkGqB0FFJRFh 13 | s1BzjTam+lD3cD4QAn8CQQCzKu+LtJn+qJe8xqCGdoP1V9S8yhjvnKeXHG69xaAY 14 | eLItuoA5mPj3FcGWcboGkdCpRycxrxPePboGcPTrDZTW 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/fixtures/ssl/client.req: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBaDCB0gIBADApMRYwFAYDVQQKDA1lbGl4aXIgY2xpZW50MQ8wDQYDVQQDDAZj 3 | bGllbnQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALCpViTQGay9oU6xmbnu 4 | ts16ELTagvo4c3j1/tTMj6EGIp8NqHg8see8mWRmXt5V0ZxJcy0CBJxjohwNZ8SK 5 | 8ttHNydyLTiU5NWw4G1Gs3HSPasWbqFP49f/i80JS7KvScKSaLjSGr6JG5cEpuBO 6 | SpcACfpjmlSz8FHGJupsN90ZAgMBAAGgADANBgkqhkiG9w0BAQsFAAOBgQBVqJps 7 | akmtv7JuoijY3nnkykoBTp0sl37HexkZLjpGUsvgI0McC9W10q9cPUlYdLRGjWLK 8 | 8EhhP1CPbUVBXLvmlhZGMi1FH/Y/A7bpwVWGt1PrmgmCAyHoMCuvxi8TEQ3Ff5+u 9 | X+OfCkg0BY3MgkPF0ZWXg3NdflR2rtVyAX/UyQ== 10 | -----END CERTIFICATE REQUEST----- 11 | -------------------------------------------------------------------------------- /test/fixtures/ssl/server.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBuTCCASICAWQwDQYJKoZIhvcNAQELBQAwJTEPMA0GA1UECgwGZWxpeGlyMRIw 3 | EAYDVQQDDAlFbGl4aXIgQ0EwHhcNMTgwNTE2MjMzNTE2WhcNMjIwNTE1MjMzNTE2 4 | WjAlMQ8wDQYDVQQKDAZlbGl4aXIxEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkq 5 | hkiG9w0BAQEFAAOBjQAwgYkCgYEAtHkXh7tBjQpOjczHqjNTD0OLzoXZAdDqhK/A 6 | KaDHElLZvXhCRr1VOp3ExyCTGKeVf0meSeA9VHxi7Y3XF7Lg6zDDrcFt1B1pVN2w 7 | /Yds350qhmqNbhM5Yvv3mwXKRMPMo+gBNhkajmDa43OI/EfciJALLnpJ3pdfe6r0 8 | bUfo2tMCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCDuqi74PY5ScADQ9enRiBtvy0y 9 | Fc1sxyovw/jk4Ez6Rr0Fe4UYq6VqdIqYqqsDgKNbT/+SXsGlnLfT3LxFF0ya4SdG 10 | 0CtiqrSMd1OqLYyV2//LGZuD4l40MUlLNuzxS27taVgf+gskwJDgsvVgA83+sMIt 11 | Ol2zFzx2cK3Y8JkyzQ== 12 | -----END CERTIFICATE----- 13 | -------------------------------------------------------------------------------- /test/fixtures/ssl/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQC0eReHu0GNCk6NzMeqM1MPQ4vOhdkB0OqEr8ApoMcSUtm9eEJG 3 | vVU6ncTHIJMYp5V/SZ5J4D1UfGLtjdcXsuDrMMOtwW3UHWlU3bD9h2zfnSqGao1u 4 | Ezli+/ebBcpEw8yj6AE2GRqOYNrjc4j8R9yIkAsueknel197qvRtR+ja0wIDAQAB 5 | AoGAKq2AHP7xT3MihHHqvZsJh1CH3TzVxpIrA1m0baOxr+mbyCyKL8RSRVxNznEr 6 | l+b5eXJlVj8LAdGwa1Dhjp8khNOI8p8D3w7YSzA8NT5xQmAitrx5hyUU7jSGFA5f 7 | JXUFHAwTXS84V1asHBYCl/mpqa0M2jceRKdI7CaYPaFqHeECQQDtSFiOmyK4JlBA 8 | JknlUqMaYiRFWvtihfZZo3L6VRlGMcG6QMQiVecF2DE7ST8VSS3FoMOy1exSTSOb 9 | vAClAlLrAkEAwrWKY0//z/r/c/VziBgsskVx+7LmeSHYHGTdAuoWxiTTjpdtEJxO 10 | kdPFk7+5B92IVhXbEhuC6Ty0xJWd3c8NuQJBAJ5XxvDzScoFl0wXwPxNlxZGI9o8 11 | isEGkIzk7Bdtrn4POi5mhfw7wv09di0QBg7YVLkrPS0cYKXTYE3Oucdjs50CQCyO 12 | SHXDd8GLKWvKrj5lccz1sUisvqrXgNG2jxC8qqt6/+JwamfTrPMX5+2QgPH40tsI 13 | M1Joc0OWPbOCnVaFrBkCQQCfa8YkpK1h7eDeykXEUBd3/qAw2jt6SHDwP+ayrLsw 14 | b1N0Sdtwy1O+WDl3Ek4/FZFtyOrpVhrV6UF71gJCJc8b 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/fixtures/ssl/server.key.enc: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-CBC,7BE0022A5FFE51C8 4 | 5 | dDwwY34TagfuUymdhwWN6grJkleFWqP0glsiEkvvNzKVbjghDWqNHbh1gx5SQ2os 6 | q/XMMX7Z3U49BFao+9EBCO/nurjQXvF0RC1NiTnWqKrdGWoabRJSAsPtOWGWxkDm 7 | ZsykQcrln23dLV+WepPm5UXrkojb2hE0qfj97W1o0fVfpS4JzZ3bVgvpfUzDC8Gt 8 | T0C/NGGv5b5PC+euvve41oX9rHD711TpsZ2Yspt3TLVcjvRRzPkn52dTh6Sl6HmG 9 | xL1M6b1tEre4RbBphs2wGHv/tiK6PHd6+yF9npzpqWPyK6fxuzGO5C/JLXQ75iaE 10 | VyeWmFZL09bDMAJ5AmJUsgWiUMHeirkTw4CVhJyYfzZJjJG1EIDpZ1iYW8r3Lo/F 11 | 4FXcQpsZwF1SivDMyyA4p3OzHYoViRr9WT0dQf+wJ+sZbVnt5wtBoo7a4IVfKsxD 12 | 377F7n4B6Lamka7TSK3T5eHcYS6pmdpnQyNUA1kdXOVilMmKDP7zTcrAdx2EKCV5 13 | 0Iy4PMOtTGGQX2a4PNxRE9BPduWZ4qg95GnGFJQPm68sCormg2k6RwRNUIrGDlA0 14 | r1r6Zn9xZMd9XHBQ6YroMKeCZ/q8WvDKoj35rP/62SVBmnh6H+lxHtMh4xb1/Bci 15 | y9oW/YDScwDPG9rzMePQ69II2G/ehAqmDGCyNw8W3rxul36KLAftd4Og9f6v0bqg 16 | +F/80yzMWcrsnBq2Aa/KLXeWGhOK1RNNHAfZCD8Nh13zwjeNN5cWOKhn1SkcNHE+ 17 | Fvrb4e46Ko7y+A1KDzFCUfAOg/qeDPjZuVgMrWG/TFoDJJFR53yCNg== 18 | -----END RSA PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /test/fixtures/static with spaces.txt: -------------------------------------------------------------------------------- 1 | SPACES -------------------------------------------------------------------------------- /test/fixtures/static.txt: -------------------------------------------------------------------------------- 1 | HELLO -------------------------------------------------------------------------------- /test/fixtures/static.txt.br: -------------------------------------------------------------------------------- 1 | BROTLIED HELLO -------------------------------------------------------------------------------- /test/fixtures/static.txt.gz: -------------------------------------------------------------------------------- 1 | GZIPPED HELLO -------------------------------------------------------------------------------- /test/fixtures/static.txt.zst: -------------------------------------------------------------------------------- 1 | ZSTANDARDED HELLO -------------------------------------------------------------------------------- /test/fixtures/static/file.txt: -------------------------------------------------------------------------------- 1 | GOOD BYE 2 | -------------------------------------------------------------------------------- /test/plug/adapters/test/conn_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Adapters.Test.ConnTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Plug.Test 5 | 6 | test "read_req_body/2" do 7 | conn = conn(:get, "/", "abcdefghij") 8 | {adapter, state} = conn.adapter 9 | 10 | assert {:more, "abcde", state} = adapter.read_req_body(state, length: 5) 11 | assert {:more, "f", state} = adapter.read_req_body(state, length: 1) 12 | assert {:more, "gh", state} = adapter.read_req_body(state, length: 2) 13 | assert {:ok, "ij", state} = adapter.read_req_body(state, length: 5) 14 | assert {:ok, "", _state} = adapter.read_req_body(state, length: 5) 15 | end 16 | 17 | test "custom params" do 18 | conn = conn(:head, "/posts", page: 2) 19 | assert conn.body_params == %Plug.Conn.Unfetched{aspect: :body_params} 20 | assert conn.query_string == "page=2" 21 | assert conn.query_params == conn.params 22 | assert conn.params == %{"page" => "2"} 23 | assert conn.req_headers == [] 24 | 25 | conn = conn(:get, "/", a: [b: 0, c: 5], d: [%{e: "f"}]) 26 | assert conn.body_params == %Plug.Conn.Unfetched{aspect: :body_params} 27 | assert conn.query_string == "a[b]=0&a[c]=5&d[][e]=f" 28 | assert conn.query_params == conn.params 29 | assert conn.params == %{"a" => %{"b" => "0", "c" => "5"}, "d" => [%{"e" => "f"}]} 30 | 31 | conn = conn(:get, "/?foo=bar", %{foo: "baz"}) 32 | assert conn.body_params == %Plug.Conn.Unfetched{aspect: :body_params} 33 | assert conn.query_string == "foo=bar" 34 | assert conn.query_params == conn.params 35 | assert conn.params == %{"foo" => "baz"} 36 | 37 | conn = conn(:get, "/?foo=bar", %{biz: "baz"}) 38 | assert conn.body_params == %Plug.Conn.Unfetched{aspect: :body_params} 39 | assert conn.query_string == "biz=baz&foo=bar" 40 | assert conn.query_params == conn.params 41 | assert conn.params == %{"foo" => "bar", "biz" => "baz"} 42 | 43 | conn = conn(:get, "/?f=g", a: "b", c: [d: "e"]) 44 | assert conn.body_params == %Plug.Conn.Unfetched{aspect: :body_params} 45 | assert conn.query_string == "a=b&c[d]=e&f=g" 46 | assert conn.query_params == conn.params 47 | assert conn.params == %{"a" => "b", "c" => %{"d" => "e"}, "f" => "g"} 48 | 49 | conn = conn(:get, "/", %{}) 50 | assert conn.body_params == %Plug.Conn.Unfetched{aspect: :body_params} 51 | assert conn.query_string == "" 52 | assert conn.query_params == conn.params 53 | assert conn.params == %{} 54 | 55 | conn = conn(:post, "/?foo=bar", %{foo: "baz", answer: 42}) 56 | assert conn.body_params == %{"foo" => "baz", "answer" => 42} 57 | assert conn.query_string == "foo=bar" 58 | assert conn.query_params == %{"foo" => "bar"} 59 | assert conn.params == %{"foo" => "baz", "answer" => 42} 60 | 61 | conn = conn(:post, "/?foo=bar", %{biz: "baz"}) 62 | assert conn.body_params == %{"biz" => "baz"} 63 | assert conn.query_string == "foo=bar" 64 | assert conn.query_params == %{"foo" => "bar"} 65 | assert conn.params == %{"foo" => "bar", "biz" => "baz"} 66 | 67 | conn = conn(:post, "/", %{foo: &length/1}) 68 | assert %{"foo" => value} = conn.body_params 69 | assert is_function(value) 70 | assert conn.query_string == "" 71 | assert conn.query_params == %{} 72 | assert conn.params == %{"foo" => &length/1} 73 | 74 | conn = conn(:post, "/", %{foo: %{__struct__: :mod}}) 75 | assert %{"foo" => value} = conn.body_params 76 | assert is_struct(value) 77 | assert conn.query_string == "" 78 | assert conn.query_params == %{} 79 | assert conn.params == %{"foo" => %{__struct__: :mod}} 80 | 81 | conn = conn(:post, "/", %{}) 82 | assert conn.body_params == %{} 83 | assert conn.query_string == "" 84 | assert conn.query_params == %{} 85 | assert conn.params == %{} 86 | end 87 | 88 | test "no body or params" do 89 | conn = conn(:get, "/") 90 | {adapter, state} = conn.adapter 91 | assert conn.req_headers == [] 92 | assert {:ok, "", _state} = adapter.read_req_body(state, length: 10) 93 | end 94 | 95 | test "no path" do 96 | conn = conn(:get, "http://www.elixir-lang.org") 97 | assert conn.path_info == [] 98 | end 99 | 100 | test "custom params sets no content-type for GET/HEAD requests" do 101 | conn = conn(:head, "/") 102 | assert conn.req_headers == [] 103 | conn = conn(:get, "/") 104 | assert conn.req_headers == [] 105 | conn = conn(:get, "/", foo: "bar") 106 | assert conn.req_headers == [] 107 | end 108 | 109 | test "custom params sets content-type to multipart/mixed when content-type is not set" do 110 | conn = conn(:post, "/", foo: "bar") 111 | assert conn.req_headers == [{"content-type", "multipart/mixed; boundary=plug_conn_test"}] 112 | end 113 | 114 | test "custom params does not change content-type when set" do 115 | conn = 116 | conn(:get, "/", foo: "bar") 117 | |> Plug.Conn.put_req_header("content-type", "application/vnd.api+json") 118 | |> Plug.Adapters.Test.Conn.conn(:get, "/", foo: "bar") 119 | 120 | assert conn.req_headers == [{"content-type", "application/vnd.api+json"}] 121 | end 122 | 123 | test "use existing conn.host if exists" do 124 | conn_with_host = conn(:get, "http://www.elixir-lang.org/") 125 | assert conn_with_host.host == "www.elixir-lang.org" 126 | 127 | child_conn = Plug.Adapters.Test.Conn.conn(conn_with_host, :get, "/getting-started/", nil) 128 | assert child_conn.host == "www.elixir-lang.org" 129 | end 130 | 131 | test "inform adds to the informational responses to the list" do 132 | conn = 133 | conn(:get, "/") 134 | |> Plug.Conn.inform(:early_hints, [{"link", "; rel=preload; as=style"}]) 135 | |> Plug.Conn.inform(:early_hints, [{"link", "; rel=preload; as=script"}]) 136 | 137 | informational_requests = Plug.Test.sent_informs(conn) 138 | 139 | assert {103, [{"link", "; rel=preload; as=style"}]} in informational_requests 140 | assert {103, [{"link", "; rel=preload; as=script"}]} in informational_requests 141 | end 142 | 143 | test "upgrade the supported upgrade request to the list" do 144 | conn = 145 | conn(:get, "/") 146 | |> Plug.Conn.upgrade_adapter(:supported, opt: :supported_value) 147 | 148 | upgrade_requests = Plug.Test.sent_upgrades(conn) 149 | 150 | assert {:supported, [opt: :supported_value]} in upgrade_requests 151 | end 152 | 153 | test "full URL overrides existing conn.host" do 154 | conn_with_host = conn(:get, "http://www.elixir-lang.org/") 155 | assert conn_with_host.host == "www.elixir-lang.org" 156 | 157 | child_conn = 158 | Plug.Adapters.Test.Conn.conn(conn_with_host, :get, "http://www.example.org/", nil) 159 | 160 | assert child_conn.host == "www.example.org" 161 | end 162 | 163 | test "use existing conn.remote_ip if exists" do 164 | conn_with_remote_ip = %{conn(:get, "/") | remote_ip: {151, 236, 219, 228}} 165 | child_conn = Plug.Adapters.Test.Conn.conn(conn_with_remote_ip, :get, "/", foo: "bar") 166 | assert child_conn.remote_ip == {151, 236, 219, 228} 167 | end 168 | 169 | test "use existing conn.port if exists" do 170 | conn_with_port = %{conn(:get, "/") | port: 4200} 171 | child_conn = Plug.Adapters.Test.Conn.conn(conn_with_port, :get, "/", foo: "bar") 172 | assert child_conn.port == 4200 173 | end 174 | 175 | test "conn/4 writes message to stderr when URI path does not start with forward slash" do 176 | assert ExUnit.CaptureIO.capture_io(:stderr, fn -> 177 | Plug.Adapters.Test.Conn.conn(%Plug.Conn{}, :get, "foo", []) 178 | end) =~ 179 | ~s(the URI path used in plug tests must start with "/", got: "foo") 180 | end 181 | 182 | test "use custom peer data" do 183 | peer_data = %{address: {127, 0, 0, 1}, port: 111_317} 184 | conn = conn(:get, "/") |> put_peer_data(peer_data) 185 | assert peer_data == Plug.Conn.get_peer_data(conn) 186 | end 187 | 188 | test "use custom sock data" do 189 | sock_data = %{address: {127, 0, 0, 1}, port: 111_318} 190 | conn = conn(:get, "/") |> put_sock_data(sock_data) 191 | assert sock_data == Plug.Conn.get_sock_data(conn) 192 | end 193 | 194 | test "use custom ssl data" do 195 | ssl_data = %{address: {127, 0, 0, 1}, port: 111_317} 196 | conn = conn(:get, "/") |> put_ssl_data(ssl_data) 197 | assert ssl_data == Plug.Conn.get_ssl_data(conn) 198 | end 199 | 200 | test "push/3 sends message including path and headers" do 201 | ref = make_ref() 202 | 203 | Plug.Adapters.Test.Conn.push(%{owner: self(), ref: ref}, "/", []) 204 | 205 | assert_receive {^ref, :push, {"/", []}} 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /test/plug/basic_auth_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.BasicAuthTest do 2 | use ExUnit.Case, async: true 3 | import Plug.Test 4 | import Plug.Conn 5 | 6 | import Plug.BasicAuth 7 | 8 | describe "basic_auth" do 9 | test "authenticates valid user and password" do 10 | conn = 11 | conn(:get, "/") 12 | |> put_req_header("authorization", encode_basic_auth("hello", "world")) 13 | |> basic_auth(username: "hello", password: "world") 14 | 15 | refute conn.status 16 | refute conn.halted 17 | end 18 | 19 | test "raises key error when no options are given" do 20 | assert_raise KeyError, fn -> 21 | conn(:get, "/") 22 | |> put_req_header("authorization", encode_basic_auth("hello", "world")) 23 | |> basic_auth() 24 | end 25 | end 26 | 27 | test "refutes invalid user and password" do 28 | for {user, pass} <- [{"hello", "wrong"}, {"wrong", "hello"}] do 29 | conn = 30 | conn(:get, "/") 31 | |> put_req_header("authorization", encode_basic_auth(user, pass)) 32 | |> basic_auth(username: "hello", password: "world") 33 | 34 | assert conn.halted 35 | assert conn.status == 401 36 | assert conn.resp_body == "Unauthorized" 37 | assert get_resp_header(conn, "www-authenticate") == ["Basic realm=\"Application\""] 38 | end 39 | end 40 | end 41 | 42 | describe "encode_basic_auth" do 43 | test "encodes the given user and password" do 44 | assert encode_basic_auth("hello", "world") == "Basic aGVsbG86d29ybGQ=" 45 | end 46 | end 47 | 48 | describe "parse_basic_auth" do 49 | test "returns :error with no authentication header" do 50 | assert conn(:get, "/") 51 | |> parse_basic_auth() == :error 52 | end 53 | 54 | test "returns :error with another authentication header" do 55 | assert conn(:get, "/") 56 | |> put_req_header("authorization", "Token abcdef") 57 | |> parse_basic_auth() == :error 58 | end 59 | 60 | test "returns :error with invalid base64 token" do 61 | assert conn(:get, "/") 62 | |> put_req_header("authorization", "Basic abcdef") 63 | |> parse_basic_auth() == :error 64 | end 65 | 66 | test "returns :error with only username or password in token" do 67 | assert conn(:get, "/") 68 | |> put_req_header("authorization", "Basic #{Base.encode64("hello")}") 69 | |> parse_basic_auth() == :error 70 | end 71 | 72 | test "returns username and password" do 73 | assert conn(:get, "/") 74 | |> put_req_header("authorization", encode_basic_auth("hello", "world")) 75 | |> parse_basic_auth() == {"hello", "world"} 76 | 77 | assert conn(:get, "/") 78 | |> put_req_header("authorization", encode_basic_auth("hello", "long:world")) 79 | |> parse_basic_auth() == {"hello", "long:world"} 80 | end 81 | end 82 | 83 | describe "request_basic_auth" do 84 | test "sets www-authenticate header with realm information" do 85 | assert conn(:get, "/") 86 | |> request_basic_auth() 87 | |> get_resp_header("www-authenticate") == ["Basic realm=\"Application\""] 88 | 89 | assert conn(:get, "/") 90 | |> request_basic_auth(realm: ~S|"tricky"|) 91 | |> get_resp_header("www-authenticate") == ["Basic realm=\"tricky\""] 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /test/plug/builder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.BuilderTest do 2 | defmodule Module do 3 | import Plug.Conn 4 | 5 | def init(val) do 6 | {:init, val} 7 | end 8 | 9 | def call(conn, opts) do 10 | stack = [{:call, opts} | conn.assigns[:stack]] 11 | assign(conn, :stack, stack) 12 | end 13 | end 14 | 15 | defmodule Sample do 16 | use Plug.Builder, copy_opts_to_assign: :stack 17 | 18 | plug :fun, :step1 19 | plug Module, :step2 20 | plug Module, :step3 21 | 22 | def fun(conn, opts) do 23 | stack = [{:fun, opts} | conn.assigns[:stack]] 24 | assign(conn, :stack, stack) 25 | end 26 | end 27 | 28 | defmodule Overridable do 29 | use Plug.Builder 30 | 31 | def call(conn, opts) do 32 | try do 33 | super(conn, opts) 34 | catch 35 | :throw, {:not_found, conn} -> assign(conn, :not_found, :caught) 36 | end 37 | end 38 | 39 | plug :boom 40 | 41 | def boom(conn, _opts) do 42 | conn = assign(conn, :entered_stack, true) 43 | throw({:not_found, conn}) 44 | end 45 | end 46 | 47 | defmodule Halter do 48 | use Plug.Builder 49 | 50 | plug :step, :first 51 | plug :step, :second 52 | plug :authorize 53 | plug :step, :end_of_chain_reached 54 | 55 | def step(conn, step), do: assign(conn, step, true) 56 | 57 | def authorize(conn, _) do 58 | conn 59 | |> assign(:authorize_reached, true) 60 | |> halt 61 | end 62 | end 63 | 64 | defmodule FaultyModulePlug do 65 | defmodule FaultyPlug do 66 | def init([]), do: [] 67 | 68 | # Doesn't return a Plug.Conn 69 | def call(_conn, _opts), do: "foo" 70 | end 71 | 72 | use Plug.Builder 73 | plug FaultyPlug 74 | end 75 | 76 | defmodule FaultyFunctionPlug do 77 | use Plug.Builder 78 | plug :faulty_function 79 | 80 | # Doesn't return a Plug.Conn 81 | def faulty_function(_conn, _opts), do: "foo" 82 | end 83 | 84 | use ExUnit.Case, async: true 85 | import Plug.Test 86 | import Plug.Conn 87 | 88 | test "exports the init/1 function" do 89 | assert Sample.init(:ok) == :ok 90 | end 91 | 92 | test "builds plug stack in the order" do 93 | conn = conn(:get, "/") 94 | 95 | assert Sample.call(conn, []).assigns[:stack] == [ 96 | call: {:init, :step3}, 97 | call: {:init, :step2}, 98 | fun: :step1 99 | ] 100 | 101 | assert Sample.call(conn, [:initial]).assigns[:stack] == [ 102 | {:call, {:init, :step3}}, 103 | {:call, {:init, :step2}}, 104 | {:fun, :step1}, 105 | :initial 106 | ] 107 | end 108 | 109 | test "allows call/2 to be overridden with super" do 110 | conn = Overridable.call(conn(:get, "/"), []) 111 | assert conn.assigns[:not_found] == :caught 112 | assert conn.assigns[:entered_stack] == true 113 | end 114 | 115 | test "halt/2 halts the plug stack" do 116 | conn = Halter.call(conn(:get, "/"), []) 117 | assert conn.halted 118 | assert conn.assigns[:first] 119 | assert conn.assigns[:second] 120 | assert conn.assigns[:authorize_reached] 121 | refute conn.assigns[:end_of_chain_reached] 122 | end 123 | 124 | test "an exception is raised if a plug doesn't return a connection" do 125 | assert_raise RuntimeError, fn -> 126 | FaultyModulePlug.call(conn(:get, "/"), []) 127 | end 128 | 129 | assert_raise RuntimeError, fn -> 130 | FaultyFunctionPlug.call(conn(:get, "/"), []) 131 | end 132 | end 133 | 134 | test "an exception is raised at compile time if a plug with no call/2 function is plugged" do 135 | assert_raise ArgumentError, fn -> 136 | defmodule BadPlug do 137 | defmodule Bad do 138 | def init(opts), do: opts 139 | end 140 | 141 | use Plug.Builder 142 | plug Bad 143 | end 144 | end 145 | end 146 | 147 | test "compile and runtime init modes" do 148 | {:ok, _agent} = Agent.start_link(fn -> :compile end, name: :plug_init) 149 | 150 | defmodule Assigner do 151 | use Plug.Builder 152 | 153 | def init(agent), do: {:init, Agent.get(agent, & &1)} 154 | def call(conn, opts), do: Plug.Conn.assign(conn, :opts, opts) 155 | end 156 | 157 | defmodule CompileInit do 158 | use Plug.Builder 159 | 160 | var = :plug_init 161 | plug Assigner, var 162 | end 163 | 164 | defmodule RuntimeInit do 165 | use Plug.Builder, init_mode: :runtime 166 | 167 | var = :plug_init 168 | plug Assigner, var 169 | end 170 | 171 | :ok = Agent.update(:plug_init, fn :compile -> :runtime end) 172 | 173 | assert CompileInit.call(%Plug.Conn{}, :plug_init).assigns.opts == {:init, :compile} 174 | assert RuntimeInit.call(%Plug.Conn{}, :plug_init).assigns.opts == {:init, :runtime} 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /test/plug/conn/adapter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.AdapterTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "conn/5" do 5 | conn = 6 | Plug.Conn.Adapter.conn( 7 | {__MODULE__, :meta}, 8 | "POST", 9 | URI.parse("https://example.com/bar//baz?bat"), 10 | {127, 0, 0, 1}, 11 | [{"foo", "bar"}] 12 | ) 13 | 14 | assert conn.adapter == {__MODULE__, :meta} 15 | assert conn.method == "POST" 16 | assert conn.host == "example.com" 17 | assert conn.scheme == :https 18 | assert conn.request_path == "/bar//baz" 19 | assert conn.query_string == "bat" 20 | assert conn.path_info == ["bar", "baz"] 21 | assert conn.remote_ip == {127, 0, 0, 1} 22 | assert conn.req_headers == [{"foo", "bar"}] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/plug/conn/cookies_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.CookiesTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Plug.Conn.Cookies 5 | doctest Plug.Conn.Cookies 6 | 7 | test "decode cookies" do 8 | assert decode("key1=value1, key2=value2") == %{"key1" => "value1, key2=value2"} 9 | assert decode("key1=value1; key2=value2") == %{"key1" => "value1", "key2" => "value2"} 10 | 11 | assert decode("$key1=value1, key2=value2; $key3=value3") == %{ 12 | "$key1" => "value1, key2=value2", 13 | "$key3" => "value3" 14 | } 15 | 16 | assert decode("key space=value, key=value space") == %{} 17 | assert decode(" key1=value1 , key2=value2 ") == %{"key1" => "value1 , key2=value2"} 18 | assert decode("") == %{} 19 | assert decode("=") == %{} 20 | assert decode("=;") == %{} 21 | assert decode("key, =, value") == %{} 22 | assert decode("key=") == %{"key" => ""} 23 | assert decode("key1=;;key2=") == %{"key1" => "", "key2" => ""} 24 | 25 | for whitespace <- ["\s", "\t", "\r", "\n", "\v", "\f"] do 26 | assert decode("#{whitespace}=value") == %{} 27 | assert decode("#{whitespace}=#{whitespace}") == %{} 28 | 29 | if whitespace == "\s" do 30 | assert decode("key=#{whitespace}") == %{"key" => ""} 31 | else 32 | assert decode("key=#{whitespace}") == %{} 33 | end 34 | end 35 | end 36 | 37 | test "decodes encoded cookie" do 38 | start = {{2012, 9, 29}, {15, 32, 10}} 39 | cookie = encode("foo", %{value: "bar", max_age: 60, universal_time: start}) 40 | 41 | assert decode(cookie) == %{ 42 | "foo" => "bar", 43 | "expires" => "Sat, 29 Sep 2012 15:33:10 GMT", 44 | "max-age" => "60", 45 | "path" => "/" 46 | } 47 | end 48 | 49 | test "encodes the cookie" do 50 | assert encode("foo", %{value: "bar"}) == "foo=bar; path=/; HttpOnly" 51 | assert encode("foo", %{}) == "foo=; path=/; HttpOnly" 52 | assert encode("foo") == "foo=; path=/; HttpOnly" 53 | end 54 | 55 | test "encodes with :path option" do 56 | assert encode("foo", %{value: "bar", path: "/baz"}) == "foo=bar; path=/baz; HttpOnly" 57 | end 58 | 59 | test "encodes with :domain option" do 60 | assert encode("foo", %{value: "bar", domain: "google.com"}) == 61 | "foo=bar; path=/; domain=google.com; HttpOnly" 62 | end 63 | 64 | test "encodes with :secure option" do 65 | assert encode("foo", %{value: "bar", secure: true}) == "foo=bar; path=/; secure; HttpOnly" 66 | end 67 | 68 | test "encodes without :same_site option if not set" do 69 | assert encode("foo", %{value: "bar"}) == "foo=bar; path=/; HttpOnly" 70 | end 71 | 72 | test "encodes with :same_site option :lax" do 73 | assert encode("foo", %{value: "bar", same_site: "Lax"}) == 74 | "foo=bar; path=/; HttpOnly; SameSite=Lax" 75 | end 76 | 77 | test "encodes with :same_site option :strict" do 78 | assert encode("foo", %{value: "bar", same_site: "Strict"}) == 79 | "foo=bar; path=/; HttpOnly; SameSite=Strict" 80 | end 81 | 82 | test "encodes with :same_site option :none" do 83 | assert encode("foo", %{value: "bar", same_site: "None"}) == 84 | "foo=bar; path=/; HttpOnly; SameSite=None" 85 | end 86 | 87 | test "encodes with :http_only option, which defaults to true" do 88 | assert encode("foo", %{value: "bar", http_only: false}) == "foo=bar; path=/" 89 | end 90 | 91 | test "encodes with :max_age" do 92 | assert encode("foo", %{ 93 | value: "bar", 94 | max_age: 60, 95 | universal_time: {{2012, 1, 7}, {15, 32, 10}} 96 | }) == 97 | "foo=bar; path=/; expires=Sat, 07 Jan 2012 15:33:10 GMT; max-age=60; HttpOnly" 98 | 99 | assert encode("foo", %{ 100 | value: "bar", 101 | max_age: 60, 102 | universal_time: {{2012, 2, 7}, {15, 32, 10}} 103 | }) == 104 | "foo=bar; path=/; expires=Tue, 07 Feb 2012 15:33:10 GMT; max-age=60; HttpOnly" 105 | 106 | assert encode("foo", %{ 107 | value: "bar", 108 | max_age: 60, 109 | universal_time: {{2012, 3, 7}, {15, 32, 10}} 110 | }) == 111 | "foo=bar; path=/; expires=Wed, 07 Mar 2012 15:33:10 GMT; max-age=60; HttpOnly" 112 | 113 | assert encode("foo", %{ 114 | value: "bar", 115 | max_age: 60, 116 | universal_time: {{2012, 4, 7}, {15, 32, 10}} 117 | }) == 118 | "foo=bar; path=/; expires=Sat, 07 Apr 2012 15:33:10 GMT; max-age=60; HttpOnly" 119 | 120 | assert encode("foo", %{ 121 | value: "bar", 122 | max_age: 60, 123 | universal_time: {{2012, 5, 7}, {15, 32, 10}} 124 | }) == 125 | "foo=bar; path=/; expires=Mon, 07 May 2012 15:33:10 GMT; max-age=60; HttpOnly" 126 | 127 | assert encode("foo", %{ 128 | value: "bar", 129 | max_age: 60, 130 | universal_time: {{2012, 6, 7}, {15, 32, 10}} 131 | }) == 132 | "foo=bar; path=/; expires=Thu, 07 Jun 2012 15:33:10 GMT; max-age=60; HttpOnly" 133 | 134 | assert encode("foo", %{ 135 | value: "bar", 136 | max_age: 60, 137 | universal_time: {{2012, 7, 7}, {15, 32, 10}} 138 | }) == 139 | "foo=bar; path=/; expires=Sat, 07 Jul 2012 15:33:10 GMT; max-age=60; HttpOnly" 140 | 141 | assert encode("foo", %{ 142 | value: "bar", 143 | max_age: 60, 144 | universal_time: {{2012, 8, 7}, {15, 32, 10}} 145 | }) == 146 | "foo=bar; path=/; expires=Tue, 07 Aug 2012 15:33:10 GMT; max-age=60; HttpOnly" 147 | 148 | assert encode("foo", %{ 149 | value: "bar", 150 | max_age: 60, 151 | universal_time: {{2012, 9, 7}, {15, 32, 10}} 152 | }) == 153 | "foo=bar; path=/; expires=Fri, 07 Sep 2012 15:33:10 GMT; max-age=60; HttpOnly" 154 | 155 | assert encode("foo", %{ 156 | value: "bar", 157 | max_age: 60, 158 | universal_time: {{2012, 10, 7}, {15, 32, 10}} 159 | }) == 160 | "foo=bar; path=/; expires=Sun, 07 Oct 2012 15:33:10 GMT; max-age=60; HttpOnly" 161 | 162 | assert encode("foo", %{ 163 | value: "bar", 164 | max_age: 60, 165 | universal_time: {{2012, 11, 7}, {15, 32, 10}} 166 | }) == 167 | "foo=bar; path=/; expires=Wed, 07 Nov 2012 15:33:10 GMT; max-age=60; HttpOnly" 168 | 169 | assert encode("foo", %{ 170 | value: "bar", 171 | max_age: 60, 172 | universal_time: {{2012, 12, 7}, {15, 32, 10}} 173 | }) == 174 | "foo=bar; path=/; expires=Fri, 07 Dec 2012 15:33:10 GMT; max-age=60; HttpOnly" 175 | end 176 | 177 | test "encodes with :extra option" do 178 | assert encode("foo", %{value: "bar", extra: "SameSite=Lax"}) == 179 | "foo=bar; path=/; HttpOnly; SameSite=Lax" 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /test/plug/conn/query_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.QueryTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Plug.Conn.Query, only: [decode: 1, encode: 1, encode: 2] 5 | doctest Plug.Conn.Query 6 | 7 | describe "decode" do 8 | test "queries" do 9 | params = decode("foo=bar&baz=bat") 10 | assert params["foo"] == "bar" 11 | assert params["baz"] == "bat" 12 | 13 | params = decode("users[name]=hello&users[age]=17&users[address][street]=foo") 14 | assert params["users"]["name"] == "hello" 15 | assert params["users"]["age"] == "17" 16 | assert params["users"]["address"] == %{"street" => "foo"} 17 | 18 | params = decode("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F") 19 | assert params["my weird field"] == "q1!2\"'w$5&7/z8)?" 20 | 21 | assert decode("=")[""] == "" 22 | assert decode("key")["key"] == "" 23 | assert decode("key=")["key"] == "" 24 | assert decode("=value")[""] == "value" 25 | 26 | assert decode("foo[]")["foo"] == [""] 27 | assert decode("foo[]=")["foo"] == [""] 28 | assert decode("foo[]=bar&foo[]=baz")["foo"] == ["bar", "baz"] 29 | assert decode("foo[]=bar&foo[]=baz")["foo"] == ["bar", "baz"] 30 | 31 | params = decode("foo[]=bar&foo[]=baz&bat[]=1&bat[]=2") 32 | assert params["foo"] == ["bar", "baz"] 33 | assert params["bat"] == ["1", "2"] 34 | 35 | assert decode("x[y][z]=1")["x"]["y"]["z"] == "1" 36 | assert decode("x[y][z][]=1")["x"]["y"]["z"] == ["1"] 37 | assert decode("x[y][z]=1&x[y][z]=2")["x"]["y"]["z"] == "2" 38 | assert decode("x[y][z][]=1&x[y][z][]=2")["x"]["y"]["z"] == ["1", "2"] 39 | 40 | assert Enum.at(decode("x[y][][z]=1")["x"]["y"], 0)["z"] == "1" 41 | assert Enum.at(decode("x[y][][z][]=1")["x"]["y"], 0)["z"] |> Enum.at(0) == "1" 42 | end 43 | 44 | test "nested lists" do 45 | assert decode("x[][][]=1") == %{"x" => [[["1"]]]} 46 | end 47 | 48 | test "empty pairs" do 49 | assert decode("&x=1&&y=2&") == %{"x" => "1", "y" => "2"} 50 | end 51 | 52 | test "last always wins on bad queries" do 53 | assert decode("x[]=1&x[y]=1")["x"]["y"] == "1" 54 | assert decode("x[y][][w]=2&x[y]=1")["x"]["y"] == "1" 55 | assert decode("x=1&x[y]=1")["x"]["y"] == "1" 56 | assert decode("x[y][0][w]=2&x[y]=1")["x"]["y"] == "1" 57 | end 58 | 59 | test "raises exception on bad www-form" do 60 | assert_raise Plug.Conn.InvalidQueryError, fn -> 61 | decode("_utf8=%R2%9P%93") 62 | end 63 | end 64 | end 65 | 66 | describe "encode" do 67 | test "data" do 68 | assert encode(%{foo: "bar", baz: "bat"}) in ["baz=bat&foo=bar", "foo=bar&baz=bat"] 69 | 70 | assert encode(%{foo: nil}) == "foo=" 71 | assert encode(%{foo: "bå®"}) == "foo=b%C3%A5%C2%AE" 72 | assert encode(%{foo: 1337}) == "foo=1337" 73 | assert encode(%{foo: ["bar", "baz"]}) == "foo[]=bar&foo[]=baz" 74 | 75 | assert encode(%{users: %{name: "hello", age: 17}}) in [ 76 | "users[name]=hello&users[age]=17", 77 | "users[age]=17&users[name]=hello" 78 | ] 79 | 80 | assert encode(%{users: [name: "hello", age: 17]}) == "users[name]=hello&users[age]=17" 81 | 82 | assert encode(%{users: [name: "hello", age: 17, name: "goodbye"]}) == 83 | "users[name]=hello&users[age]=17" 84 | 85 | assert encode(%{"my weird field": "q1!2\"'w$5&7/z8)?"}) == 86 | "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F" 87 | 88 | assert encode(%{foo: %{"my weird field": "q1!2\"'w$5&7/z8)?"}}) == 89 | "foo[my+weird+field]=q1%212%22%27w%245%267%2Fz8%29%3F" 90 | 91 | assert encode(%{}) == "" 92 | assert encode([]) == "" 93 | 94 | assert encode(%{foo: [""]}) == "foo[]=" 95 | 96 | assert encode(%{foo: ["bar", "baz"], bat: [1, 2]}) in [ 97 | "bat[]=1&bat[]=2&foo[]=bar&foo[]=baz", 98 | "foo[]=bar&foo[]=baz&bat[]=1&bat[]=2" 99 | ] 100 | 101 | assert encode(%{x: %{y: %{z: 1}}}) == "x[y][z]=1" 102 | assert encode(%{x: %{y: %{z: [1]}}}) == "x[y][z][]=1" 103 | assert encode(%{x: %{y: %{z: [1, 2]}}}) == "x[y][z][]=1&x[y][z][]=2" 104 | assert encode(%{x: %{y: [%{z: 1}]}}) == "x[y][][z]=1" 105 | assert encode(%{x: %{y: [%{z: [1]}]}}) == "x[y][][z][]=1" 106 | end 107 | 108 | test "nested lists" do 109 | assert encode(%{"x" => [[[1]]]}) == "x[][][]=1" 110 | end 111 | 112 | test "with custom encoder" do 113 | encoder = &(&1 |> to_string |> String.duplicate(2)) 114 | 115 | assert encode([foo: "bar", baz: "bat"], encoder) == "foo=barbar&baz=batbat" 116 | assert encode([foo: ["bar", "baz"]], encoder) == "foo[]=barbar&foo[]=bazbaz" 117 | assert encode([foo: URI.parse("/bar")], encoder) == "foo=%2Fbar%2Fbar" 118 | end 119 | 120 | test "ignores empty maps or lists" do 121 | assert encode(filter: %{}, foo: "bar", baz: "bat") == "foo=bar&baz=bat" 122 | assert encode(filter: [], foo: "bar", baz: "bat") == "foo=bar&baz=bat" 123 | end 124 | 125 | test "raises when there's a map with 0 or >1 elems in a list" do 126 | message = ~r/cannot encode maps inside lists/ 127 | 128 | assert_raise ArgumentError, message, fn -> 129 | encode(%{foo: [%{a: 1, b: 2}]}) 130 | end 131 | 132 | assert_raise ArgumentError, message, fn -> 133 | encode(%{foo: [%{valid: :map}, %{}]}) 134 | end 135 | end 136 | end 137 | 138 | describe "decode_pair" do 139 | test "simple queries" do 140 | params = decode_pair([{"foo", "bar"}, {"baz", "bat"}]) 141 | assert params["foo"] == "bar" 142 | assert params["baz"] == "bat" 143 | end 144 | 145 | test "decode_pair one-level nested query" do 146 | params = decode_pair([{"users[name]", "hello"}]) 147 | assert params["users"]["name"] == "hello" 148 | 149 | params = decode_pair([{"users[name]", "hello"}, {"users[age]", "17"}]) 150 | assert params["users"]["name"] == "hello" 151 | assert params["users"]["age"] == "17" 152 | end 153 | 154 | test "query no override" do 155 | params = decode_pair([{"foo", "bar"}, {"foo", "baz"}]) 156 | assert params["foo"] == "baz" 157 | 158 | params = decode_pair([{"users[name]", "bar"}, {"users[name]", "baz"}]) 159 | assert params["users"]["name"] == "baz" 160 | end 161 | 162 | test "many-levels nested query" do 163 | params = decode_pair([{"users[name]", "hello"}]) 164 | assert params["users"]["name"] == "hello" 165 | 166 | params = 167 | decode_pair([ 168 | {"users[name]", "hello"}, 169 | {"users[age]", "17"}, 170 | {"users[address][street]", "Mourato"} 171 | ]) 172 | 173 | assert params["users"]["name"] == "hello" 174 | assert params["users"]["age"] == "17" 175 | assert params["users"]["address"]["street"] == "Mourato" 176 | end 177 | 178 | test "list query" do 179 | params = decode_pair([{"foo[]", "bar"}, {"foo[]", "baz"}]) 180 | assert params["foo"] == ["bar", "baz"] 181 | end 182 | 183 | defp decode_pair(pairs) do 184 | pairs 185 | |> Enum.reduce(Plug.Conn.Query.decode_init(), &Plug.Conn.Query.decode_each/2) 186 | |> Plug.Conn.Query.decode_done() 187 | end 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /test/plug/conn/status_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.StatusTest do 2 | use ExUnit.Case 3 | 4 | alias Plug.Conn.Status 5 | 6 | test "code/1 when given a numeric status code, returns the same numeric status code" do 7 | assert Status.code(200) == 200 8 | assert Status.code(203) == 203 9 | assert Status.code(404) == 404 10 | end 11 | 12 | test "code for built-in statuses the numeric code" do 13 | assert Status.code(:ok) == 200 14 | assert Status.code(:non_authoritative_information) == 203 15 | assert Status.code(:not_found) == 404 16 | end 17 | 18 | test "code for custom status return the numeric code" do 19 | assert Status.code(:not_an_rfc_status_code) == 998 20 | end 21 | 22 | test "code with both a built_in and custom code return the numeric code" do 23 | assert Status.code(:im_a_teapot) == 418 24 | assert Status.code(:totally_not_a_teapot) == 418 25 | end 26 | 27 | test "reason_atom returns the atom for built-in statuses" do 28 | assert Status.reason_atom(200) == :ok 29 | assert Status.reason_atom(203) == :non_authoritative_information 30 | assert Status.reason_atom(404) == :not_found 31 | end 32 | 33 | test "reason_atom returns the atom for custom statuses" do 34 | assert Status.reason_atom(998) == :not_an_rfc_status_code 35 | end 36 | 37 | test "reason_atom with both a built_in and custom status always returns the custom atom" do 38 | assert Status.reason_atom(418) == :totally_not_a_teapot 39 | end 40 | 41 | test "reason_atom with an unknown code raises an error" do 42 | assert_raise(ArgumentError, "unknown status code 999", fn -> 43 | Status.reason_atom(999) 44 | end) 45 | end 46 | 47 | test "reason_phrase returns the phrase for built_in statuses" do 48 | assert Status.reason_phrase(200) == "OK" 49 | assert Status.reason_phrase(203) == "Non-Authoritative Information" 50 | assert Status.reason_phrase(404) == "Not Found" 51 | end 52 | 53 | test "reason_phrase for custom status return the phrase" do 54 | assert Status.reason_phrase(998) == "Not An RFC Status Code" 55 | end 56 | 57 | test "reason_phrase with both a built_in and custom status always returns the custom phrase" do 58 | assert Status.reason_phrase(418) == "Totally not a teapot" 59 | end 60 | 61 | test "reason_phrase with an unknown code raises an error" do 62 | assert_raise(ArgumentError, ~r/unknown status code 999\n\nCustom codes/, fn -> 63 | Status.reason_phrase(999) 64 | end) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/plug/conn/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.UtilsTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Plug.Conn.Utils 5 | doctest Plug.Conn.Utils 6 | end 7 | -------------------------------------------------------------------------------- /test/plug/conn/wrapper_error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Conn.WrapperErrorTest do 2 | use ExUnit.Case, async: true 3 | import Plug.Test 4 | 5 | test "reraise/3" do 6 | conn = conn(:get, "/") 7 | err = RuntimeError.exception("hello") 8 | 9 | {wrap, stacktrace} = 10 | catch_error_stacktrace(&Plug.Conn.WrapperError.reraise(conn, :error, err, &1)) 11 | 12 | assert wrap.conn == conn 13 | assert wrap.kind == :error 14 | assert wrap.reason == err 15 | assert wrap.stack == stacktrace 16 | assert catch_error(Plug.Conn.WrapperError.reraise(:whatever, :error, wrap, [])) == wrap 17 | end 18 | 19 | test "reraise/3 does not change exits or throws" do 20 | assert catch_throw(Plug.Conn.WrapperError.reraise(conn(:get, "/"), :throw, :oops, [])) == 21 | :oops 22 | 23 | assert catch_exit(Plug.Conn.WrapperError.reraise(conn(:get, "/"), :exit, :oops, [])) == :oops 24 | end 25 | 26 | defp catch_error_stacktrace(fun) do 27 | stack = 28 | try do 29 | raise "oops" 30 | rescue 31 | _ -> __STACKTRACE__ 32 | end 33 | 34 | try do 35 | fun.(stack) 36 | flunk("Expected to catch error, got nothing") 37 | catch 38 | :error, error -> 39 | {error, stack} 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/plug/error_handler_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.ErrorHandlerTest do 2 | use ExUnit.Case, async: true 3 | import Plug.Test 4 | import Plug.Conn 5 | 6 | defmodule ForbiddenError do 7 | defexception plug_status: 403, message: "oops" 8 | end 9 | 10 | defmodule NotFoundError do 11 | defexception plug_status: :not_found, message: "oops" 12 | end 13 | 14 | defmodule Router do 15 | use Plug.Router 16 | use Plug.ErrorHandler 17 | 18 | plug :match 19 | plug :dispatch 20 | 21 | def call(conn, opts) do 22 | if conn.path_info == ~w(boom) do 23 | raise "oops" 24 | else 25 | super(conn, opts) 26 | end 27 | end 28 | 29 | get "/send_and_boom" do 30 | send_resp(conn, 200, "oops") 31 | raise "oops" 32 | end 33 | 34 | get "/send_and_wrapped" do 35 | stack = 36 | try do 37 | raise "oops" 38 | rescue 39 | _ -> __STACKTRACE__ 40 | end 41 | 42 | raise Plug.Conn.WrapperError, 43 | conn: conn, 44 | kind: :error, 45 | stack: stack, 46 | reason: ForbiddenError.exception([]) 47 | end 48 | 49 | get "/status_as_atom" do 50 | raise NotFoundError 51 | send_resp(conn, 200, "ok") 52 | end 53 | end 54 | 55 | test "call/2 is overridden" do 56 | conn = conn(:get, "/boom") 57 | 58 | assert_raise RuntimeError, "oops", fn -> 59 | Router.call(conn, []) 60 | end 61 | 62 | assert_received {:plug_conn, :sent} 63 | assert {500, _headers, "Something went wrong"} = sent_resp(conn) 64 | end 65 | 66 | test "call/2 is overridden but is a no-op when response is already sent" do 67 | conn = conn(:get, "/send_and_boom") 68 | 69 | assert_raise Plug.Conn.WrapperError, "** (RuntimeError) oops", fn -> 70 | Router.call(conn, []) 71 | end 72 | 73 | assert_received {:plug_conn, :sent} 74 | assert {200, _headers, "oops"} = sent_resp(conn) 75 | end 76 | 77 | test "call/2 is overridden and does not unwrap wrapped errors" do 78 | conn = conn(:get, "/send_and_wrapped") 79 | 80 | assert_raise Plug.Conn.WrapperError, "** (Plug.ErrorHandlerTest.ForbiddenError) oops", fn -> 81 | Router.call(conn, []) 82 | end 83 | 84 | assert_received {:plug_conn, :sent} 85 | assert {403, _headers, "Something went wrong"} = sent_resp(conn) 86 | end 87 | 88 | test "call/2 supports statuses as atoms" do 89 | conn = conn(:get, "/status_as_atom") 90 | 91 | assert_raise Plug.Conn.WrapperError, "** (Plug.ErrorHandlerTest.NotFoundError) oops", fn -> 92 | Router.call(conn, []) 93 | end 94 | 95 | assert_received {:plug_conn, :sent} 96 | assert {404, _headers, "Something went wrong"} = sent_resp(conn) 97 | end 98 | 99 | test "define a behaviour with a default implementation" do 100 | assert ExUnit.CaptureIO.capture_io(:stderr, fn -> 101 | Code.eval_string(""" 102 | defmodule Plug.ErrorHandlerTest.BadImplRouter do 103 | use Plug.Router 104 | use Plug.ErrorHandler 105 | 106 | plug :match 107 | plug :dispatch 108 | 109 | match _, do: conn 110 | 111 | @impl Plug.ErrorHandler 112 | def handle_errors(_conn), do: :boom 113 | end 114 | """) 115 | end) =~ 116 | "got \"@impl Plug.ErrorHandler\" for function handle_errors/1 but this behaviour does not specify such callback." 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /test/plug/head_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.HeadTest do 2 | use ExUnit.Case, async: true 3 | import Plug.Test 4 | import Plug.Conn 5 | 6 | @opts Plug.Head.init([]) 7 | 8 | test "converts HEAD requests to GET requests" do 9 | conn = Plug.Head.call(conn(:head, "/"), @opts) 10 | assert conn.method == "GET" 11 | end 12 | 13 | test "HEAD responses have headers but do not have a body" do 14 | conn = 15 | conn(:head, "/") 16 | |> Plug.Head.call(@opts) 17 | |> put_resp_content_type("text/plain") 18 | |> send_resp(200, "Hello world") 19 | 20 | assert conn.status == 200 21 | assert get_resp_header(conn, "content-type") == ["text/plain; charset=utf-8"] 22 | assert conn.resp_body == "" 23 | end 24 | 25 | test "if the request is different from HEAD, conn must be returned as is" do 26 | conn = 27 | conn(:get, "/") 28 | |> Plug.Head.call(@opts) 29 | |> send_resp(200, "Hello world") 30 | 31 | assert conn.status == 200 32 | assert conn.method == "GET" 33 | assert conn.resp_body == "Hello world" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/plug/html_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.HTMlTest do 2 | use ExUnit.Case, async: true 3 | doctest Plug.HTML 4 | 5 | import Plug.HTML, only: [html_escape: 1] 6 | 7 | test "escapes HTML" do 8 | assert html_escape("