fix(deps): adapt hackney and mdex behavior

This commit is contained in:
Lain Soykaf 2026-07-02 12:48:52 +04:00
commit dc348e8be8
No known key found for this signature in database
3 changed files with 89 additions and 26 deletions

View file

@ -131,12 +131,16 @@ defmodule Pleroma.Formatter do
breaks = Map.get(opts, :breaks, false) breaks = Map.get(opts, :breaks, false)
text = normalize_markdown_quirks(text) text = normalize_markdown_quirks(text)
text MDEx.new(
|> MDEx.to_html!( markdown: text,
extension: [autolink: true, strikethrough: true], extension: [autolink: true, strikethrough: true],
parse: [smart: false], parse: [smart: false],
render: [hardbreaks: breaks, unsafe: true] render: [hardbreaks: breaks, unsafe: true]
) )
|> MDEx.Document.run()
|> MDEx.Document.update_nodes(MDEx.HtmlBlock, &strip_dangerous_raw_html/1)
|> MDEx.Document.update_nodes(MDEx.HtmlInline, &strip_dangerous_raw_html/1)
|> MDEx.to_html!()
|> compact_markdown_html(text) |> compact_markdown_html(text)
end end
@ -155,20 +159,33 @@ defmodule Pleroma.Formatter do
end end
defp unwrap_raw_html_paragraph(html, markdown) do defp unwrap_raw_html_paragraph(html, markdown) do
if raw_html_only?(markdown) do if inline_raw_html_only?(markdown) do
String.replace(html, ~r/\A<p>(.*)<\/p>\z/s, "\\1") String.replace(html, ~r/\A<p>(.*)<\/p>\z/s, "\\1")
else else
html html
end end
end end
defp raw_html_only?(markdown) do defp inline_raw_html_only?(markdown) do
markdown = String.trim(markdown) markdown = String.trim(markdown)
String.match?(markdown, ~r/\A(?:<!--|<\/?[[:alpha:]][[:alnum:]:-]*(?:\s|>|\/>))/) && String.match?(
markdown,
~r/\A(?:<!--|<\/?(?:a|abbr|acronym|b|bdi|bdo|big|br|cite|code|data|del|dfn|em|i|img|ins|kbd|mark|q|rb|rp|rt|rtc|ruby|s|samp|small|span|strong|sub|sup|time|tt|u|var|wbr)(?:\s|>|\/>))/i
) &&
String.ends_with?(markdown, ">") String.ends_with?(markdown, ">")
end end
defp strip_dangerous_raw_html(%{literal: literal} = node) do
literal =
literal
|> String.replace(~r/<(script|style)\b[^>]*>.*?<\/\1>/is, "")
|> String.replace(~r/<(script|style)\b[^>]*>.*\z/is, "")
|> String.replace(~r/<\/(script|style)\s*>/i, "")
%{node | literal: literal}
end
def html_escape({text, mentions, hashtags}, type) do def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags} {html_escape(text, type), mentions, hashtags}
end end

View file

@ -35,10 +35,11 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
if opts[:follow_redirect] != false do if opts[:follow_redirect] != false do
{_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end) {_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end)
env = %{method: method, headers: headers, body: body, req_opts: req_opts} env = %{method: method, headers: headers, body: body, req_opts: req_opts}
res = :hackney.request(method, url, headers, body, req_opts) res = do_request(method, url, headers, body, req_opts)
case res do case res do
{:ok, code, resp_headers, _client} when code in @redirect_statuses -> {:ok, code, resp_headers, client} when code in @redirect_statuses ->
close(client)
redirect(url, resp_headers, env, @redirect_limit) redirect(url, resp_headers, env, @redirect_limit)
{:ok, code, resp_headers} when code in @redirect_statuses -> {:ok, code, resp_headers} when code in @redirect_statuses ->
@ -48,11 +49,22 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
res res
end end
else else
:hackney.request(method, url, headers, body, opts) do_request(method, url, headers, body, opts)
end end
end end
@impl true @impl true
def stream_body(%{ref: ref, recv_timeout: recv_timeout} = client) do
_ = :hackney.stream_next(ref)
case receive_response(ref, recv_timeout) do
{:ok, data} when is_binary(data) -> {:ok, data, client}
{:ok, :done} -> :done
{:ok, {:error, error}} -> {:error, error}
{:error, error} -> {:error, error}
end
end
def stream_body(ref) do def stream_body(ref) do
case :hackney.stream_body(ref) do case :hackney.stream_body(ref) do
:done -> :done :done -> :done
@ -62,8 +74,33 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
end end
@impl true @impl true
def close(%{ref: ref}), do: :hackney.close(ref)
def close(ref), do: :hackney.close(ref) def close(ref), do: :hackney.close(ref)
defp do_request(method, url, headers, body, opts) do
recv_timeout = Keyword.get(opts, :recv_timeout, :infinity)
req_opts =
opts
|> Keyword.put(:async, :once)
|> Keyword.put(:stream_to, self())
with {:ok, ref} <- :hackney.request(method, url, headers, body, req_opts),
{:ok, {:status, code, _reason}} <- receive_response(ref, recv_timeout),
{:ok, {:headers, resp_headers}} <- receive_response(ref, recv_timeout) do
{:ok, code, resp_headers, %{ref: ref, recv_timeout: recv_timeout}}
end
end
defp receive_response(ref, recv_timeout) do
receive do
{:hackney_response, ^ref, response} -> {:ok, response}
after
recv_timeout -> {:error, :timeout}
end
end
defp redirect(url, resp_headers, env, limit) when limit == 0 do defp redirect(url, resp_headers, env, limit) when limit == 0 do
new_url = absolute_redirect_url(url, resp_headers) new_url = absolute_redirect_url(url, resp_headers)
@ -71,17 +108,18 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
"#{__MODULE__}: Handling redirect #{url} -> #{new_url}; redirect limit was reached - returning response after final redirect" "#{__MODULE__}: Handling redirect #{url} -> #{new_url}; redirect limit was reached - returning response after final redirect"
) )
:hackney.request(env.method, new_url, env.headers, env.body, env.req_opts) do_request(env.method, new_url, env.headers, env.body, env.req_opts)
end end
defp redirect(url, resp_headers, env, limit) do defp redirect(url, resp_headers, env, limit) do
new_url = absolute_redirect_url(url, resp_headers) new_url = absolute_redirect_url(url, resp_headers)
Logger.debug("#{__MODULE__}: handling redirect #{url} -> #{new_url}; limit = #{limit}") Logger.debug("#{__MODULE__}: handling redirect #{url} -> #{new_url}; limit = #{limit}")
res = :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts) res = do_request(env.method, new_url, env.headers, env.body, env.req_opts)
case res do case res do
{:ok, code, new_resp_headers, _client} when code in @redirect_statuses -> {:ok, code, new_resp_headers, client} when code in @redirect_statuses ->
close(client)
redirect(new_url, new_resp_headers, env, limit - 1) redirect(new_url, new_resp_headers, env, limit - 1)
{:ok, code, new_resp_headers} when code in @redirect_statuses -> {:ok, code, new_resp_headers} when code in @redirect_statuses ->

View file

@ -19,7 +19,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
{:ok, tls_server: tls_server, proxy: proxy} {:ok, tls_server: tls_server, proxy: proxy}
end end
test "hackney follow_redirect crashes behind CONNECT proxy on relative redirects", %{ test "hackney follow_redirect handles relative redirects behind CONNECT proxy", %{
tls_server: tls_server, tls_server: tls_server,
proxy: proxy proxy: proxy
} do } do
@ -28,19 +28,15 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
opts = [ opts = [
pool: :media, pool: :media,
proxy: proxy.proxy_url, proxy: proxy.proxy_url,
insecure: true, ssl_options: tls_client_options(tls_server),
connect_timeout: 1_000, connect_timeout: 1_000,
recv_timeout: 1_000, recv_timeout: 1_000,
follow_redirect: true, follow_redirect: true,
force_redirect: true force_redirect: true,
with_body: true
] ]
{pid, ref} = spawn_monitor(fn -> :hackney.request(:get, url, [], <<>>, opts) end) assert {:ok, 200, _headers, "ok"} = :hackney.request(:get, url, [], <<>>, opts)
assert_receive {:DOWN, ^ref, :process, ^pid, reason}, 5_000
assert match?({%FunctionClauseError{}, _}, reason) or match?(%FunctionClauseError{}, reason) or
match?({:function_clause, _}, reason)
end end
test "redirects work via proxy when hackney follow_redirect is disabled", %{ test "redirects work via proxy when hackney follow_redirect is disabled", %{
@ -52,7 +48,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
adapter_opts = [ adapter_opts = [
pool: :media, pool: :media,
proxy: proxy.proxy_url, proxy: proxy.proxy_url,
insecure: true, ssl_options: tls_client_options(tls_server),
connect_timeout: 1_000, connect_timeout: 1_000,
recv_timeout: 1_000, recv_timeout: 1_000,
follow_redirect: false, follow_redirect: false,
@ -75,7 +71,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
opts = [ opts = [
pool: :media, pool: :media,
proxy: proxy.proxy_url, proxy: proxy.proxy_url,
insecure: true, ssl_options: tls_client_options(tls_server),
connect_timeout: 1_000, connect_timeout: 1_000,
recv_timeout: 1_000, recv_timeout: 1_000,
follow_redirect: true follow_redirect: true
@ -97,7 +93,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
opts = [ opts = [
pool: :media, pool: :media,
proxy: proxy.proxy_url, proxy: proxy.proxy_url,
insecure: true, ssl_options: tls_client_options(tls_server),
connect_timeout: 1_000, connect_timeout: 1_000,
recv_timeout: 1_000, recv_timeout: 1_000,
follow_redirect: true follow_redirect: true
@ -119,7 +115,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
opts = [ opts = [
pool: :media, pool: :media,
proxy: proxy.proxy_url, proxy: proxy.proxy_url,
insecure: true, ssl_options: tls_client_options(tls_server),
connect_timeout: 1_000, connect_timeout: 1_000,
recv_timeout: 1_000, recv_timeout: 1_000,
follow_redirect: true follow_redirect: true
@ -161,7 +157,13 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
accept_tls_loop(listener) accept_tls_loop(listener)
end) end)
{:ok, %{listener: listener, acceptor: acceptor, base_url: "https://127.0.0.1:#{port}"}} {:ok,
%{
listener: listener,
acceptor: acceptor,
certfile: certfile,
base_url: "https://127.0.0.1:#{port}"
}}
end end
defp stop_tls_redirect_server(%{listener: listener, acceptor: acceptor}) do defp stop_tls_redirect_server(%{listener: listener, acceptor: acceptor}) do
@ -210,11 +212,17 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
:ssl.close(ssl_socket) :ssl.close(ssl_socket)
else else
_ -> _ ->
_ = :gen_tcp.close(tcp_socket) _ = :ssl.close(tcp_socket)
:ok :ok
end end
end end
defp tls_client_options(_tls_server) do
[server_name_indication: ~c"localhost", verify_fun: {&accept_test_certificate/3, []}]
end
defp accept_test_certificate(_cert, _event, state), do: {:valid, state}
defp recv_ssl_headers(socket, acc \\ <<>>) do defp recv_ssl_headers(socket, acc \\ <<>>) do
case :ssl.recv(socket, 0, 1_000) do case :ssl.recv(socket, 0, 1_000) do
{:ok, data} -> {:ok, data} ->