changed
CHANGELOG.md
|
@@ -1,6 +1,16 @@
|
1
1
|
# Changelog
|
2
2
|
|
3
|
- ## v1.15.0 (2023-10-06)
|
3
|
+ ## v1.15.2 (2023-11-14)
|
4
|
+
|
5
|
+ ### Enhancements
|
6
|
+
|
7
|
+ * Add `:assign_as` option to `Plug.RequestId`
|
8
|
+ * Improve performance of `Plug.RequestId`
|
9
|
+ * Avoid clashes between Plug nodes
|
10
|
+ * Add specs to `Plug.BasicAuth`
|
11
|
+ * Fix a bug with non-string `_method` body parameters in `Plug.MethodOverride`
|
12
|
+
|
13
|
+ ## v1.15.1 (2023-10-06)
|
4
14
|
|
5
15
|
### Enhancements
|
6
16
|
|
|
@@ -12,7 +22,7 @@
|
12
22
|
|
13
23
|
* Add `Plug.Conn.get_session/3` for default value
|
14
24
|
* Allow `Plug.SSL.configure/1` to accept all :ssl options
|
15
|
- * Optimize query decoding by 15% to 45% - this removes the previously deprecated `:limit` MFA and `:include_unnamed_parts_at` from MULTIPART
|
25
|
+ * Optimize query decoding by 15% to 45% - this removes the previously deprecated `:limit` MFA and `:include_unnamed_parts_at` from MULTIPART. This may be backwards incompatible for applications that were relying on ambiguous arguments, such as `user[][key]=1&user[][key]=2`, which has unspecified parsing behaviour
|
16
26
|
|
17
27
|
## v1.14.2 (2023-03-23)
|
changed
hex_metadata.config
|
@@ -1,6 +1,6 @@
|
1
1
|
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/elixir-plug/plug">>}]}.
|
2
2
|
{<<"name">>,<<"plug">>}.
|
3
|
- {<<"version">>,<<"1.15.1">>}.
|
3
|
+ {<<"version">>,<<"1.15.2">>}.
|
4
4
|
{<<"description">>,<<"Compose web applications with functions">>}.
|
5
5
|
{<<"elixir">>,<<"~> 1.10">>}.
|
6
6
|
{<<"app">>,<<"plug">>}.
|
changed
lib/plug/basic_auth.ex
|
@@ -92,7 +92,9 @@ defmodule Plug.BasicAuth do
|
92
92
|
strings with only alphanumeric characters and space
|
93
93
|
|
94
94
|
"""
|
95
|
- def basic_auth(conn, options \\ []) do
|
95
|
+ @spec basic_auth(Plug.Conn.t(), [auth_option]) :: Plug.Conn.t()
|
96
|
+ when auth_option: {:username, String.t()} | {:password, String.t()} | {:realm, String.t()}
|
97
|
+ def basic_auth(%Plug.Conn{} = conn, options \\ []) when is_list(options) do
|
96
98
|
username = Keyword.fetch!(options, :username)
|
97
99
|
password = Keyword.fetch!(options, :password)
|
98
100
|
|
|
@@ -116,7 +118,8 @@ defmodule Plug.BasicAuth do
|
116
118
|
|
117
119
|
See the module docs for examples.
|
118
120
|
"""
|
119
|
- def parse_basic_auth(conn) do
|
121
|
+ @spec parse_basic_auth(Plug.Conn.t()) :: {user :: String.t(), password :: String.t()} | :error
|
122
|
+ def parse_basic_auth(%Plug.Conn{} = conn) do
|
120
123
|
with ["Basic " <> encoded_user_and_pass] <- get_req_header(conn, "authorization"),
|
121
124
|
{:ok, decoded_user_and_pass} <- Base.decode64(encoded_user_and_pass),
|
122
125
|
[user, pass] <- :binary.split(decoded_user_and_pass, ":") do
|
|
@@ -134,6 +137,7 @@ defmodule Plug.BasicAuth do
|
134
137
|
put_req_header(conn, "authorization", encode_basic_auth("hello", "world"))
|
135
138
|
|
136
139
|
"""
|
140
|
+ @spec encode_basic_auth(String.t(), String.t()) :: String.t()
|
137
141
|
def encode_basic_auth(user, pass) when is_binary(user) and is_binary(pass) do
|
138
142
|
"Basic " <> Base.encode64("#{user}:#{pass}")
|
139
143
|
end
|
|
@@ -150,8 +154,11 @@ defmodule Plug.BasicAuth do
|
150
154
|
* `:realm` - the authentication realm. The value is not fully
|
151
155
|
sanitized, so do not accept user input as the realm and use
|
152
156
|
strings with only alphanumeric characters and space
|
157
|
+
|
153
158
|
"""
|
154
|
- def request_basic_auth(conn, options \\ []) when is_list(options) do
|
159
|
+ @spec request_basic_auth(Plug.Conn.t(), [option]) :: Plug.Conn.t()
|
160
|
+ when option: {:realm, String.t()}
|
161
|
+ def request_basic_auth(%Plug.Conn{} = conn, options \\ []) when is_list(options) do
|
155
162
|
realm = Keyword.get(options, :realm, "Application")
|
156
163
|
escaped_realm = String.replace(realm, "\"", "")
|
changed
lib/plug/conn/query.ex
|
@@ -24,6 +24,20 @@ defmodule Plug.Conn.Query do
|
24
24
|
iex> decode("foo[]=bar&foo[]=baz")["foo"]
|
25
25
|
["bar", "baz"]
|
26
26
|
|
27
|
+ > #### Nesting inside lists {: .error}
|
28
|
+ >
|
29
|
+ > Nesting inside lists is ambiguous and unspecified behaviour.
|
30
|
+ > Therefore, you should not rely on the decoding behaviour of
|
31
|
+ > `user[][foo]=1&user[][bar]=2`.
|
32
|
+ >
|
33
|
+ > As an alternative, you can explicitly specify the keys:
|
34
|
+ >
|
35
|
+ > # If foo and bar belong to the same entry
|
36
|
+ > user[0][foo]=1&user[0][bar]=2
|
37
|
+ >
|
38
|
+ > # If foo and bar are different entries
|
39
|
+ > user[0][foo]=1&user[1][bar]=2
|
40
|
+
|
27
41
|
Keys without values are treated as empty strings,
|
28
42
|
according to https://url.spec.whatwg.org/#application/x-www-form-urlencoded:
|
changed
lib/plug/conn/utils.ex
|
@@ -52,7 +52,7 @@ defmodule Plug.Conn.Utils do
|
52
52
|
|
53
53
|
"""
|
54
54
|
@spec media_type(binary) :: {:ok, type :: binary, subtype :: binary, params} | :error
|
55
|
- def media_type(binary) do
|
55
|
+ def media_type(binary) when is_binary(binary) do
|
56
56
|
case strip_spaces(binary) do
|
57
57
|
"*/*" <> t -> mt_params(t, "*", "*")
|
58
58
|
t -> mt_first(t, "")
|
changed
lib/plug/method_override.ex
|
@@ -9,14 +9,33 @@ defmodule Plug.MethodOverride do
|
9
9
|
* `PATCH`
|
10
10
|
* `DELETE`
|
11
11
|
|
12
|
- This plug expects the body parameters to be already parsed and
|
13
|
- fetched. Those can be fetched with `Plug.Parsers`.
|
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`.
|
14
20
|
|
15
21
|
This plug doesn't accept any options.
|
16
22
|
|
17
|
- ## Examples
|
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]
|
18
36
|
|
19
37
|
plug Plug.MethodOverride
|
38
|
+
|
20
39
|
"""
|
21
40
|
|
22
41
|
@behaviour Plug
|
|
@@ -39,12 +58,12 @@ defmodule Plug.MethodOverride do
|
39
58
|
end
|
40
59
|
|
41
60
|
defp override_method(conn, body_params) do
|
42
|
- method = String.upcase(body_params["_method"] || "", :ascii)
|
43
|
-
|
44
|
- if method in @allowed_methods do
|
45
|
- %{conn | method: method}
|
61
|
+ with method when is_binary(method) <- body_params["_method"] || "",
|
62
|
+ method = String.upcase(method, :ascii),
|
63
|
+ true <- method in @allowed_methods do
|
64
|
+ %Plug.Conn{conn | method: method}
|
46
65
|
else
|
47
|
- conn
|
66
|
+ _ -> conn
|
48
67
|
end
|
49
68
|
end
|
50
69
|
end
|
changed
lib/plug/request_id.ex
|
@@ -1,26 +1,33 @@
|
1
1
|
defmodule Plug.RequestId do
|
2
2
|
@moduledoc """
|
3
|
- A plug for generating a unique request id for each request.
|
3
|
+ A plug for generating a unique request ID for each request.
|
4
4
|
|
5
|
- The generated request id will be in the format "uq8hs30oafhj5vve8ji5pmp7mtopc08f".
|
5
|
+ The generated request ID will be in the format:
|
6
6
|
|
7
|
- If a request id already exists as the "x-request-id" HTTP request header,
|
8
|
- then that value will be used assuming it is between 20 and 200 characters.
|
9
|
- If it is not, a new request id will be generated.
|
7
|
+ ```
|
8
|
+ uq8hs30oafhj5vve8ji5pmp7mtopc08f
|
9
|
+ ```
|
10
10
|
|
11
|
- The request id is added to the Logger metadata as `:request_id` and the response as
|
12
|
- the "x-request-id" HTTP header. To see the request id in your log output,
|
13
|
- configure your logger backends to include the `:request_id` metadata:
|
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 backends to include the `:request_id`
|
18
|
+ metadata. For example:
|
14
19
|
|
15
20
|
config :logger, :console, metadata: [:request_id]
|
16
21
|
|
17
|
- It is recommended to include this metadata configuration in your production
|
22
|
+ We recommend to include this metadata configuration in your production
|
18
23
|
configuration file.
|
19
24
|
|
20
|
- You can also access the `request_id` programmatically by calling
|
21
|
- `Logger.metadata[:request_id]`. Do not access it via the request header, as
|
22
|
- the request header value has not been validated and it may not always be
|
23
|
- present.
|
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
|
24
31
|
|
25
32
|
To use this plug, just plug it into the desired module:
|
26
33
|
|
|
@@ -29,11 +36,17 @@ defmodule Plug.RequestId do
|
29
36
|
## Options
|
30
37
|
|
31
38
|
* `:http_header` - The name of the HTTP *request* header to check for
|
32
|
- existing request ids. This is also the HTTP *response* header that will be
|
33
|
- set with the request id. Default value is "x-request-id"
|
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"`.
|
34
41
|
|
35
42
|
plug Plug.RequestId, http_header: "custom-request-id"
|
36
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
|
+
|
37
50
|
"""
|
38
51
|
|
39
52
|
require Logger
|
|
@@ -42,28 +55,29 @@ defmodule Plug.RequestId do
|
42
55
|
|
43
56
|
@impl true
|
44
57
|
def init(opts) do
|
45
|
- Keyword.get(opts, :http_header, "x-request-id")
|
58
|
+ {
|
59
|
+ Keyword.get(opts, :http_header, "x-request-id"),
|
60
|
+ Keyword.get(opts, :assign_as)
|
61
|
+ }
|
46
62
|
end
|
47
63
|
|
48
64
|
@impl true
|
49
|
- def call(conn, req_id_header) do
|
50
|
- conn
|
51
|
- |> get_request_id(req_id_header)
|
52
|
- |> set_request_id(req_id_header)
|
65
|
+ def call(conn, {header, assign_as}) do
|
66
|
+ request_id = get_request_id(conn, header)
|
67
|
+
|
68
|
+ Logger.metadata(request_id: request_id)
|
69
|
+ conn = if assign_as, do: Conn.assign(conn, assign_as, request_id), else: conn
|
70
|
+
|
71
|
+ Conn.put_resp_header(conn, header, request_id)
|
53
72
|
end
|
54
73
|
|
55
74
|
defp get_request_id(conn, header) do
|
56
75
|
case Conn.get_req_header(conn, header) do
|
57
|
- [] -> {conn, generate_request_id()}
|
58
|
- [val | _] -> if valid_request_id?(val), do: {conn, val}, else: {conn, generate_request_id()}
|
76
|
+ [] -> generate_request_id()
|
77
|
+ [val | _] -> if valid_request_id?(val), do: val, else: generate_request_id()
|
59
78
|
end
|
60
79
|
end
|
61
80
|
|
62
|
- defp set_request_id({conn, request_id}, header) do
|
63
|
- Logger.metadata(request_id: request_id)
|
64
|
- Conn.put_resp_header(conn, header, request_id)
|
65
|
- end
|
66
|
-
|
67
81
|
defp generate_request_id do
|
68
82
|
binary = <<
|
69
83
|
System.system_time(:nanosecond)::64,
|
changed
lib/plug/session/cookie.ex
|
@@ -59,7 +59,6 @@ defmodule Plug.Session.COOKIE do
|
59
59
|
key: "_my_app_session",
|
60
60
|
encryption_salt: "cookie store encryption salt",
|
61
61
|
signing_salt: "cookie store signing salt",
|
62
|
- key_length: 64,
|
63
62
|
log: :debug
|
64
63
|
"""
|
changed
lib/plug/upload.ex
|
@@ -116,9 +116,9 @@ defmodule Plug.Upload do
|
116
116
|
end
|
117
117
|
|
118
118
|
defp generate_tmp_dir() do
|
119
|
- tmp_roots = :persistent_term.get(__MODULE__)
|
119
|
+ {tmp_roots, suffix} = :persistent_term.get(__MODULE__)
|
120
120
|
{mega, _, _} = :os.timestamp()
|
121
|
- subdir = "/plug-" <> i(mega)
|
121
|
+ subdir = "/plug-" <> i(mega) <> "-" <> suffix
|
122
122
|
|
123
123
|
if tmp = Enum.find_value(tmp_roots, &make_tmp_dir(&1 <> subdir)) do
|
124
124
|
{:ok, tmp}
|
|
@@ -206,7 +206,9 @@ defmodule Plug.Upload do
|
206
206
|
Process.flag(:trap_exit, true)
|
207
207
|
tmp = Enum.find_value(@temp_env_vars, "/tmp", &System.get_env/1) |> Path.expand()
|
208
208
|
cwd = Path.join(File.cwd!(), "tmp")
|
209
|
- :persistent_term.put(__MODULE__, [tmp, cwd])
|
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})
|
210
212
|
|
211
213
|
:ets.new(@dir_table, [:named_table, :public, :set])
|
212
214
|
:ets.new(@path_table, [:named_table, :public, :duplicate_bag])
|
changed
mix.exs
|
@@ -1,7 +1,7 @@
|
1
1
|
defmodule Plug.MixProject do
|
2
2
|
use Mix.Project
|
3
3
|
|
4
|
- @version "1.15.1"
|
4
|
+ @version "1.15.2"
|
5
5
|
@description "Compose web applications with functions"
|
6
6
|
@xref_exclude [Plug.Cowboy, :ssl]
|