changed
CHANGELOG.md
|
@@ -1,5 +1,20 @@
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+ ## v1.16.0 (2024-05-18)
|
4
|
+
|
5
|
+ ### Enhancements
|
6
|
+
|
7
|
+ * Support x-forwarded-for in Plug.RewriteOn
|
8
|
+ * Support MFArgs in Plug.RewriteOn
|
9
|
+ * Add immutable directive to versioned requests in `Plug.Static`
|
10
|
+ * Support disabling MIME type handling in `Plug.Static`
|
11
|
+
|
12
|
+ ### Bug fixes
|
13
|
+
|
14
|
+ * Fix bug with discarded connection state in `Plug.Debugger`
|
15
|
+ * Parse media types with underscores in them
|
16
|
+ * Do not crash on `max_age` set to nil (for consistency)
|
17
|
+
|
3
18
|
## v1.15.3 (2024-01-16)
|
4
19
|
|
5
20
|
### Enhancements
|
changed
README.md
|
@@ -188,12 +188,13 @@ Now run `mix run --no-halt` and it will start your application with a web server
|
188
188
|
|
189
189
|
| Branch | Support |
|
190
190
|
|--------|--------------------------|
|
191
|
- | v1.14 | Bug fixes |
|
191
|
+ | v1.15 | Bug fixes |
|
192
|
+ | v1.14 | Security patches only |
|
192
193
|
| v1.13 | Security patches only |
|
193
194
|
| v1.12 | Security patches only |
|
194
195
|
| v1.11 | Security patches only |
|
195
196
|
| v1.10 | Security patches only |
|
196
|
- | v1.9 | Security patches only |
|
197
|
+ | v1.9 | Unsupported from 10/2023 |
|
197
198
|
| v1.8 | Unsupported from 01/2023 |
|
198
199
|
| v1.7 | Unsupported from 01/2022 |
|
199
200
|
| v1.6 | Unsupported from 01/2022 |
|
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.3">>}.
|
3
|
+ {<<"version">>,<<"1.16.0">>}.
|
4
4
|
{<<"description">>,<<"Compose web applications with functions">>}.
|
5
5
|
{<<"elixir">>,<<"~> 1.10">>}.
|
6
6
|
{<<"app">>,<<"plug">>}.
|
changed
lib/plug/basic_auth.ex
|
@@ -38,7 +38,7 @@ defmodule Plug.BasicAuth do
|
38
38
|
|
39
39
|
plug :auth
|
40
40
|
|
41
|
- defp auth(conn, opts) do
|
41
|
+ defp auth(conn, _opts) do
|
42
42
|
username = System.fetch_env!("AUTH_USERNAME")
|
43
43
|
password = System.fetch_env!("AUTH_PASSWORD")
|
44
44
|
Plug.BasicAuth.basic_auth(conn, username: username, password: password)
|
|
@@ -73,7 +73,8 @@ defmodule Plug.BasicAuth do
|
73
73
|
* The supplied `user` and `pass` may be empty strings;
|
74
74
|
|
75
75
|
* If you are comparing the username and password with existing strings,
|
76
|
- do not use `==/2`. Use `Plug.Crypto.secure_compare/2` instead.
|
76
|
+ do not use `==/2` or pattern matching. Use `Plug.Crypto.secure_compare/2`
|
77
|
+ instead.
|
77
78
|
|
78
79
|
"""
|
79
80
|
import Plug.Conn
|
changed
lib/plug/conn.ex
|
@@ -533,7 +533,7 @@ defmodule Plug.Conn do
|
533
533
|
`send_chunked/2`. It returns `{:ok, conn}` in case of success,
|
534
534
|
otherwise `{:error, reason}`.
|
535
535
|
|
536
|
- To stream data use `Enum.reduce_while/3` instead of `Enum.into/2`.
|
536
|
+ To stream data use `Enum.reduce_while/3` instead of `Enum.reduce/2`.
|
537
537
|
`Enum.reduce_while/3` allows aborting the execution if `chunk/2` fails to
|
538
538
|
deliver the chunk of data.
|
539
539
|
|
|
@@ -1644,7 +1644,8 @@ defmodule Plug.Conn do
|
1644
1644
|
end
|
1645
1645
|
|
1646
1646
|
defp max_age(opts) do
|
1647
|
- [keys: Plug.Keys, max_age: Keyword.get(opts, :max_age, 86400)]
|
1647
|
+ max_age = Keyword.get(opts, :max_age) || 86400
|
1648
|
+ [keys: Plug.Keys, max_age: max_age]
|
1648
1649
|
end
|
1649
1650
|
|
1650
1651
|
defp maybe_secure_cookie(cookie, :https), do: Map.put_new(cookie, :secure, true)
|
changed
lib/plug/conn/utils.ex
|
@@ -8,7 +8,7 @@ defmodule Plug.Conn.Utils do
|
8
8
|
@upper ?A..?Z
|
9
9
|
@lower ?a..?z
|
10
10
|
@alpha ?0..?9
|
11
|
- @other [?., ?-, ?+]
|
11
|
+ @other [?., ?-, ?+, ?_]
|
12
12
|
@space [?\s, ?\t]
|
13
13
|
@specials ~c|()<>@,;:\\"/[]?={}|
|
14
14
|
|
|
@@ -32,6 +32,9 @@ defmodule Plug.Conn.Utils do
|
32
32
|
iex> media_type "APPLICATION/vnd.ms-data+XML"
|
33
33
|
{:ok, "application", "vnd.ms-data+xml", %{}}
|
34
34
|
|
35
|
+ iex> media_type "application/media_control+xml"
|
36
|
+ {:ok, "application", "media_control+xml", %{}}
|
37
|
+
|
35
38
|
iex> media_type "text/*; q=1.0"
|
36
39
|
{:ok, "text", "*", %{"q" => "1.0"}}
|
changed
lib/plug/debugger.ex
|
@@ -231,7 +231,7 @@ defmodule Plug.Debugger do
|
231
231
|
|
232
232
|
@doc false
|
233
233
|
def run_action(%Plug.Conn{} = conn) do
|
234
|
- with %Plug.Conn{body_params: params} <- fetch_body_params(conn),
|
234
|
+ with %Plug.Conn{body_params: params} = conn <- fetch_body_params(conn),
|
235
235
|
{:ok, {module, function, args}} <-
|
236
236
|
Plug.Crypto.verify(conn.secret_key_base, @salt, params["encoded_handler"]) do
|
237
237
|
apply(module, function, args)
|
changed
lib/plug/rewrite_on.ex
|
@@ -10,10 +10,14 @@ defmodule Plug.RewriteOn do
|
10
10
|
|
11
11
|
The supported values are:
|
12
12
|
|
13
|
+ * `:x_forwarded_for` - to override the remote ip based on on the "x-forwarded-for" header
|
13
14
|
* `:x_forwarded_host` - to override the host based on on the "x-forwarded-host" header
|
14
15
|
* `:x_forwarded_port` - to override the port based on on the "x-forwarded-port" header
|
15
16
|
* `:x_forwarded_proto` - to override the protocol based on on the "x-forwarded-proto" header
|
16
17
|
|
18
|
+ A tuple representing a Module-Function-Args can also be given as argument
|
19
|
+ instead of a list.
|
20
|
+
|
17
21
|
Since rewriting the scheme based on `x-forwarded-*` headers can open up
|
18
22
|
security vulnerabilities, only use this plug if:
|
19
23
|
|
|
@@ -26,9 +30,16 @@ defmodule Plug.RewriteOn do
|
26
30
|
import Plug.Conn, only: [get_req_header: 2]
|
27
31
|
|
28
32
|
@impl true
|
33
|
+ def init(header) when is_tuple(header), do: header
|
29
34
|
def init(header), do: List.wrap(header)
|
30
35
|
|
31
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
|
+
|
32
43
|
def call(conn, [:x_forwarded_proto | rewrite_on]) do
|
33
44
|
conn
|
34
45
|
|> put_scheme(get_req_header(conn, "x-forwarded-proto"))
|
|
@@ -55,6 +66,10 @@ defmodule Plug.RewriteOn do
|
55
66
|
conn
|
56
67
|
end
|
57
68
|
|
69
|
+ def call(conn, {mod, fun, args}) do
|
70
|
+ call(conn, apply(mod, fun, args))
|
71
|
+ end
|
72
|
+
|
58
73
|
defp put_scheme(%{scheme: :http, port: 80} = conn, ["https"]),
|
59
74
|
do: %{conn | scheme: :https, port: 443}
|
60
75
|
|
|
@@ -84,4 +99,14 @@ defmodule Plug.RewriteOn do
|
84
99
|
_ -> conn
|
85
100
|
end
|
86
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
|
87
112
|
end
|
changed
lib/plug/static.ex
|
@@ -100,9 +100,13 @@ defmodule Plug.Static do
|
100
100
|
an enum of key-value pairs or a `{module, function, args}` to return an enum. The
|
101
101
|
`conn` will be passed to the function, as well as the `args`.
|
102
102
|
|
103
|
- * `:content_types` - custom MIME type mapping. As a map with filename as key
|
104
|
- and content type as value. For example:
|
103
|
+ * `:content_types` - controls custom MIME type mapping.
|
104
|
+ It can be a map with filename as key and content type as value to override
|
105
|
+ the default type for matching filenames. For example:
|
105
106
|
`content_types: %{"apple-app-site-association" => "application/json"}`.
|
107
|
+ Alternatively, it can be the value `false` to opt out of setting the header at all. The latter
|
108
|
+ can be used to set the header based on custom logic before calling this plug.
|
109
|
+ Defaults to an empty map `%{}`.
|
106
110
|
|
107
111
|
## Examples
|
108
112
|
|
|
@@ -163,7 +167,7 @@ defmodule Plug.Static do
|
163
167
|
%{
|
164
168
|
encodings: encodings,
|
165
169
|
only_rules: {Keyword.get(opts, :only, []), Keyword.get(opts, :only_matching, [])},
|
166
|
- qs_cache: Keyword.get(opts, :cache_control_for_vsn_requests, "public, max-age=31536000"),
|
170
|
+ qs_cache: Keyword.get(opts, :cache_control_for_vsn_requests, "public, max-age=31536000, immutable"),
|
167
171
|
et_cache: Keyword.get(opts, :cache_control_for_etags, "public"),
|
168
172
|
et_generation: Keyword.get(opts, :etag_generation, nil),
|
169
173
|
headers: Keyword.get(opts, :headers, %{}),
|
|
@@ -218,6 +222,13 @@ defmodule Plug.Static do
|
218
222
|
h in full or (prefix != [] and match?({0, _}, :binary.match(h, prefix)))
|
219
223
|
end
|
220
224
|
|
225
|
+ defp maybe_put_content_type(conn, false, _), do: conn
|
226
|
+
|
227
|
+ defp maybe_put_content_type(conn, types, filename) do
|
228
|
+ content_type = Map.get(types, filename) || MIME.from_path(filename)
|
229
|
+ put_resp_header(conn, "content-type", content_type)
|
230
|
+ end
|
231
|
+
|
221
232
|
defp serve_static({content_encoding, file_info, path}, conn, segments, range, options) do
|
222
233
|
%{
|
223
234
|
qs_cache: qs_cache,
|
|
@@ -230,10 +241,9 @@ defmodule Plug.Static do
|
230
241
|
case put_cache_header(conn, qs_cache, et_cache, et_generation, file_info, path) do
|
231
242
|
{:stale, conn} ->
|
232
243
|
filename = List.last(segments)
|
233
|
- content_type = Map.get(types, filename) || MIME.from_path(filename)
|
234
244
|
|
235
245
|
conn
|
236
|
- |> put_resp_header("content-type", content_type)
|
246
|
+ |> maybe_put_content_type(types, filename)
|
237
247
|
|> put_resp_header("accept-ranges", "bytes")
|
238
248
|
|> maybe_add_encoding(content_encoding)
|
239
249
|
|> merge_headers(headers)
|
changed
mix.exs
|
@@ -1,7 +1,7 @@
|
1
1
|
defmodule Plug.MixProject do
|
2
2
|
use Mix.Project
|
3
3
|
|
4
|
- @version "1.15.3"
|
4
|
+ @version "1.16.0"
|
5
5
|
@description "Compose web applications with functions"
|
6
6
|
@xref_exclude [Plug.Cowboy, :ssl]
|