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]