Merge branch 'release/2.1.2' into 'stable'

Release/2.1.2

See merge request pleroma/secteam/pleroma!17
This commit is contained in:
rinpatch 2020-09-17 19:09:10 +00:00
commit a0f5e8b27e
45 changed files with 321 additions and 76 deletions

View File

@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [2.1.2] - 2020-09-17
### Security
- Fix most MRF rules either crashing or not being applied to objects passed into the Common Pipeline (ChatMessage, Question, Answer, Audio, Event).
### Fixed
- Welcome Chat messages preventing user registration with MRF Simple Policy applied to the local instance.
- Mastodon API: the public timeline returning an error when the `reply_visibility` parameter is set to `self` for an unauthenticated user.
- Mastodon Streaming API: Handler crashes on authentication failures, resulting in error logs.
- Mastodon Streaming API: Error logs on client pings.
- Rich media: Log spam on failures. Now the error is only logged once per attempt.
### Changed
- Rich Media: A HEAD request is now done to the url, to ensure it has the appropriate content type and size before proceeding with a GET.
### Upgrade notes
1. Restart Pleroma
## [2.1.1] - 2020-09-08 ## [2.1.1] - 2020-09-08
### Security ### Security

View File

@ -744,7 +744,7 @@ defp restrict_replies(query, %{exclude_replies: true}) do
end end
defp restrict_replies(query, %{ defp restrict_replies(query, %{
reply_filtering_user: user, reply_filtering_user: %User{} = user,
reply_visibility: "self" reply_visibility: "self"
}) do }) do
from( from(
@ -760,7 +760,7 @@ defp restrict_replies(query, %{
end end
defp restrict_replies(query, %{ defp restrict_replies(query, %{
reply_filtering_user: user, reply_filtering_user: %User{} = user,
reply_visibility: "following" reply_visibility: "following"
}) do }) do
from( from(

View File

@ -5,16 +5,34 @@
defmodule Pleroma.Web.ActivityPub.MRF do defmodule Pleroma.Web.ActivityPub.MRF do
@callback filter(Map.t()) :: {:ok | :reject, Map.t()} @callback filter(Map.t()) :: {:ok | :reject, Map.t()}
def filter(policies, %{} = object) do def filter(policies, %{} = message) do
policies policies
|> Enum.reduce({:ok, object}, fn |> Enum.reduce({:ok, message}, fn
policy, {:ok, object} -> policy.filter(object) policy, {:ok, message} -> policy.filter(message)
_, error -> error _, error -> error
end) end)
end end
def filter(%{} = object), do: get_policies() |> filter(object) def filter(%{} = object), do: get_policies() |> filter(object)
def pipeline_filter(%{} = message, meta) do
object = meta[:object_data]
ap_id = message["object"]
if object && ap_id do
with {:ok, message} <- filter(Map.put(message, "object", object)) do
meta = Keyword.put(meta, :object_data, message["object"])
{:ok, Map.put(message, "object", ap_id), meta}
else
{err, message} -> {err, message, meta}
end
else
{err, message} = filter(message)
{err, message, meta}
end
end
def get_policies do def get_policies do
Pleroma.Config.get([:mrf, :policies], []) |> get_policies() Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
end end

View File

@ -20,9 +20,17 @@ defp string_matches?(string, pattern) do
String.match?(string, pattern) String.match?(string, pattern)
end end
defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = message) do defp object_payload(%{} = object) do
[object["content"], object["summary"], object["name"]]
|> Enum.filter(& &1)
|> Enum.join("\n")
end
defp check_reject(%{"object" => %{} = object} = message) do
payload = object_payload(object)
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern -> if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
string_matches?(content, pattern) or string_matches?(summary, pattern) string_matches?(payload, pattern)
end) do end) do
{:reject, "[KeywordPolicy] Matches with rejected keyword"} {:reject, "[KeywordPolicy] Matches with rejected keyword"}
else else
@ -30,12 +38,12 @@ defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} =
end end
end end
defp check_ftl_removal( defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do
%{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message payload = object_payload(object)
) do
if Pleroma.Constants.as_public() in to and if Pleroma.Constants.as_public() in to and
Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern -> Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
string_matches?(content, pattern) or string_matches?(summary, pattern) string_matches?(payload, pattern)
end) do end) do
to = List.delete(to, Pleroma.Constants.as_public()) to = List.delete(to, Pleroma.Constants.as_public())
cc = [Pleroma.Constants.as_public() | message["cc"] || []] cc = [Pleroma.Constants.as_public() | message["cc"] || []]
@ -51,35 +59,24 @@ defp check_ftl_removal(
end end
end end
defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do defp check_replace(%{"object" => %{} = object} = message) do
content = object =
if is_binary(content) do ["content", "name", "summary"]
content |> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)
else |> Enum.reduce(object, fn field, object ->
"" data =
end
summary =
if is_binary(summary) do
summary
else
""
end
{content, summary} =
Enum.reduce( Enum.reduce(
Pleroma.Config.get([:mrf_keyword, :replace]), Pleroma.Config.get([:mrf_keyword, :replace]),
{content, summary}, object[field],
fn {pattern, replacement}, {content_acc, summary_acc} -> fn {pat, repl}, acc -> String.replace(acc, pat, repl) end
{String.replace(content_acc, pattern, replacement),
String.replace(summary_acc, pattern, replacement)}
end
) )
{:ok, Map.put(object, field, data)
message end)
|> put_in(["object", "content"], content)
|> put_in(["object", "summary"], summary)} message = Map.put(message, "object", object)
{:ok, message}
end end
@impl true @impl true

View File

@ -66,7 +66,8 @@ defp check_media_nsfw(
"type" => "Create", "type" => "Create",
"object" => child_object "object" => child_object
} = object } = object
) do )
when is_map(child_object) do
media_nsfw = media_nsfw =
Config.get([:mrf_simple, :media_nsfw]) Config.get([:mrf_simple, :media_nsfw])
|> MRF.subdomains_regex() |> MRF.subdomains_regex()

View File

@ -28,8 +28,7 @@ def filter(%{"actor" => actor} = message) do
}" }"
) )
subchain MRF.filter(subchain, message)
|> MRF.filter(message)
else else
_e -> {:ok, message} _e -> {:ok, message}
end end

View File

@ -26,13 +26,17 @@ def common_pipeline(object, meta) do
{:error, e} -> {:error, e} ->
{:error, e} {:error, e}
{:reject, e} ->
{:reject, e}
end end
end end
def do_common_pipeline(object, meta) do def do_common_pipeline(object, meta) do
with {_, {:ok, validated_object, meta}} <- with {_, {:ok, validated_object, meta}} <-
{:validate_object, ObjectValidator.validate(object, meta)}, {:validate_object, ObjectValidator.validate(object, meta)},
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)}, {_, {:ok, mrfd_object, meta}} <-
{:mrf_object, MRF.pipeline_filter(validated_object, meta)},
{_, {:ok, activity, meta}} <- {_, {:ok, activity, meta}} <-
{:persist_object, ActivityPub.persist(mrfd_object, meta)}, {:persist_object, ActivityPub.persist(mrfd_object, meta)},
{_, {:ok, activity, meta}} <- {_, {:ok, activity, meta}} <-
@ -40,7 +44,7 @@ def do_common_pipeline(object, meta) do
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do {_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta} {:ok, activity, meta}
else else
{:mrf_object, {:reject, _}} -> {:ok, nil, meta} {:mrf_object, {:reject, message, _}} -> {:reject, message}
e -> {:error, e} e -> {:error, e}
end end
end end

View File

@ -311,7 +311,7 @@ def fix_url(object), do: object
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji = emoji =
tags tags
|> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end) |> Enum.filter(fn data -> is_map(data) and data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping -> |> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":") name = String.trim(data["name"], ":")

View File

@ -184,7 +184,8 @@ def post_chat_message_operation do
"application/json", "application/json",
ChatMessage ChatMessage
), ),
400 => Operation.response("Bad Request", "application/json", ApiError) 400 => Operation.response("Bad Request", "application/json", ApiError),
422 => Operation.response("MRF Rejection", "application/json", ApiError)
}, },
security: [ security: [
%{ %{

View File

@ -55,7 +55,7 @@ def create_operation do
"application/json", "application/json",
%Schema{oneOf: [Status, ScheduledStatus]} %Schema{oneOf: [Status, ScheduledStatus]}
), ),
422 => Operation.response("Bad Request", "application/json", ApiError) 422 => Operation.response("Bad Request / MRF Rejection", "application/json", ApiError)
} }
} }
end end

View File

@ -49,6 +49,9 @@ def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ [])
local: true local: true
)} do )} do
{:ok, activity} {:ok, activity}
else
{:common_pipeline, {:reject, _} = e} -> e
e -> e
end end
end end

View File

@ -37,12 +37,12 @@ def init(%{qs: qs} = req, state) do
else else
{:error, :bad_topic} -> {:error, :bad_topic} ->
Logger.debug("#{__MODULE__} bad topic #{inspect(req)}") Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
{:ok, req} = :cowboy_req.reply(404, req) req = :cowboy_req.reply(404, req)
{:ok, req, state} {:ok, req, state}
{:error, :unauthorized} -> {:error, :unauthorized} ->
Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}") Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}")
{:ok, req} = :cowboy_req.reply(401, req) req = :cowboy_req.reply(401, req)
{:ok, req, state} {:ok, req, state}
end end
end end
@ -64,7 +64,9 @@ def websocket_handle(:pong, state) do
{:ok, %{state | timer: timer()}} {:ok, %{state | timer: timer()}}
end end
# We never receive messages. # We only receive pings for now
def websocket_handle(:ping, state), do: {:ok, state}
def websocket_handle(frame, state) do def websocket_handle(frame, state) do
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}") Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state} {:ok, state}
@ -98,6 +100,10 @@ def websocket_info(:tick, state) do
{:reply, :ping, %{state | timer: nil, count: 0}, :hibernate} {:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}
end end
# State can be `[]` only in case we terminate before switching to websocket,
# we already log errors for these cases in `init/1`, so just do nothing here
def terminate(_reason, _req, []), do: :ok
def terminate(reason, _req, state) do def terminate(reason, _req, state) do
Logger.debug( Logger.debug(
"#{__MODULE__} terminating websocket connection for user #{ "#{__MODULE__} terminating websocket connection for user #{

View File

@ -145,7 +145,10 @@ def create_authorization(
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
"authorization" => %{"redirect_uri" => @oob_token_redirect_uri} "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
}) do }) do
render(conn, "oob_authorization_created.html", %{auth: auth}) # Enforcing the view to reuse the template when calling from other controllers
conn
|> put_view(OAuthView)
|> render("oob_authorization_created.html", %{auth: auth})
end end
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{

View File

@ -90,6 +90,16 @@ def post_chat_message(
conn conn
|> put_view(MessageReferenceView) |> put_view(MessageReferenceView)
|> render("show.json", chat_message_reference: cm_ref) |> render("show.json", chat_message_reference: cm_ref)
else
{:reject, message} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{error: message})
end end
end end

View File

@ -96,6 +96,50 @@ def rich_media_get(url) do
@rich_media_options @rich_media_options
end end
Pleroma.HTTP.get(url, headers, adapter: options) head_check =
case Pleroma.HTTP.head(url, headers, adapter: options) do
# If the HEAD request didn't reach the server for whatever reason,
# we assume the GET that comes right after won't either
{:error, _} = e ->
e
{:ok, %Tesla.Env{status: 200, headers: headers}} ->
with :ok <- check_content_type(headers),
:ok <- check_content_length(headers),
do: :ok
_ ->
:ok
end
with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, adapter: options)
end
defp check_content_type(headers) do
case List.keyfind(headers, "content-type", 0) do
{_, content_type} ->
case Plug.Conn.Utils.media_type(content_type) do
{:ok, "text", "html", _} -> :ok
_ -> {:error, {:content_type, content_type}}
end
_ ->
:ok
end
end
@max_body @rich_media_options[:max_body]
defp check_content_length(headers) do
case List.keyfind(headers, "content-length", 0) do
{_, maybe_content_length} ->
case Integer.parse(maybe_content_length) do
{content_length, ""} when content_length <= @max_body -> :ok
{_, ""} -> {:error, :body_too_large}
_ -> :ok
end
_ ->
:ok
end
end end
end end

View File

@ -31,6 +31,14 @@ defp get_cached_or_parse(url) do
{:ok, _data} = res -> {:ok, _data} = res ->
res res
{:error, :body_too_large} = e ->
e
{:error, {:content_type, _}} = e ->
e
# The TTL is not set for the errors above, since they are unlikely to change
# with time
{:error, _} = e -> {:error, _} = e ->
ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000) ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000)
Cachex.expire(:rich_media_cache, url, ttl) Cachex.expire(:rich_media_cache, url, ttl)

View File

@ -1,2 +1,2 @@
<h1>Successfully authorized</h1> <h1>Successfully authorized</h1>
<h2>Token code is <%= @auth.token %></h2> <h2>Token code is <br><%= @auth.token %></h2>

View File

@ -1,2 +1,2 @@
<h1>Authorization exists</h1> <h1>Authorization exists</h1>
<h2>Access token is <%= @token.token %></h2> <h2>Access token is <br><%= @token.token %></h2>

View File

@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do def project do
[ [
app: :pleroma, app: :pleroma,
version: version("2.1.1"), version: version("2.1.2"),
elixir: "~> 1.9", elixir: "~> 1.9",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(), compilers: [:phoenix, :gettext] ++ Mix.compilers(),

View File

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/app.77b1644622e3bae24b6b.css rel=stylesheet><link href=/static/fontello.1599568314856.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.90c4af83c1ae68f4cd95.js></script><script type=text/javascript src=/static/js/app.55d173dc5e39519aa518.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/app.77b1644622e3bae24b6b.css rel=stylesheet><link href=/static/fontello.1600365488745.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.90c4af83c1ae68f4cd95.js></script><script type=text/javascript src=/static/js/app.826c44232e0a76bbd9ba.js></script></body></html>

View File

@ -92,6 +92,8 @@
<glyph glyph-name="block" unicode="&#xe82a;" d="M732 359q0 90-48 164l-421-420q76-50 166-50 62 0 118 25t96 65 65 97 24 119z m-557-167l421 421q-75 50-167 50-83 0-153-40t-110-111-41-153q0-91 50-167z m682 167q0-88-34-168t-91-137-137-92-166-34-167 34-137 92-91 137-34 168 34 167 91 137 137 91 167 34 166-34 137-91 91-137 34-167z" horiz-adv-x="857.1" /> <glyph glyph-name="block" unicode="&#xe82a;" d="M732 359q0 90-48 164l-421-420q76-50 166-50 62 0 118 25t96 65 65 97 24 119z m-557-167l421 421q-75 50-167 50-83 0-153-40t-110-111-41-153q0-91 50-167z m682 167q0-88-34-168t-91-137-137-92-166-34-167 34-137 92-91 137-34 168 34 167 91 137 137 91 167 34 166-34 137-91 91-137 34-167z" horiz-adv-x="857.1" />
<glyph glyph-name="megaphone" unicode="&#xe82b;" d="M929 500q29 0 50-21t21-51-21-50-50-21v-214q0-29-22-50t-50-22q-233 194-453 212-32-10-51-36t-17-57 22-51q-11-19-13-37t4-32 19-31 26-28 35-28q-17-32-63-46t-94-7-73 31q-4 13-17 49t-18 53-12 50-9 56 2 55 12 62h-68q-36 0-63 26t-26 63v107q0 37 26 63t63 26h268q243 0 500 215 29 0 50-22t22-50v-214z m-72-337v532q-220-168-428-191v-151q210-23 428-190z" horiz-adv-x="1000" />
<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" /> <glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" /> <glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

View File

@ -405,6 +405,12 @@
"css": "block", "css": "block",
"code": 59434, "code": 59434,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "3e674995cacc2b09692c096ea7eb6165",
"css": "megaphone",
"code": 59435,
"src": "fontawesome"
} }
] ]
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -99,30 +99,30 @@ test "accepts valid tokens", state do
test "accepts the 'user' stream", %{token: token} = _state do test "accepts the 'user' stream", %{token: token} = _state do
assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
assert capture_log(fn -> capture_log(fn ->
assert {:error, {401, _}} = start_socket("?stream=user") assert {:error, {401, _}} = start_socket("?stream=user")
Process.sleep(30) Process.sleep(30)
end) =~ ":badarg" end)
end end
test "accepts the 'user:notification' stream", %{token: token} = _state do test "accepts the 'user:notification' stream", %{token: token} = _state do
assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}") assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
assert capture_log(fn -> capture_log(fn ->
assert {:error, {401, _}} = start_socket("?stream=user:notification") assert {:error, {401, _}} = start_socket("?stream=user:notification")
Process.sleep(30) Process.sleep(30)
end) =~ ":badarg" end)
end end
test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}]) assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
assert capture_log(fn -> capture_log(fn ->
assert {:error, {401, _}} = assert {:error, {401, _}} =
start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}]) start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
Process.sleep(30) Process.sleep(30)
end) =~ ":badarg" end)
end end
end end
end end

View File

@ -1436,4 +1436,21 @@ def post(url, query, body, headers) do
inspect(headers) inspect(headers)
}"} }"}
end end
# Most of the rich media mocks are missing HEAD requests, so we just return 404.
@rich_media_mocks [
"https://example.com/ogp",
"https://example.com/ogp-missing-data",
"https://example.com/twitter-card"
]
def head(url, _query, _body, _headers) when url in @rich_media_mocks do
{:ok, %Tesla.Env{status: 404, body: ""}}
end
def head(url, query, body, headers) do
{:error,
"Mock response not implemented for HEAD #{inspect(url)}, #{query}, #{inspect(body)}, #{
inspect(headers)
}"}
end
end end

View File

@ -440,6 +440,45 @@ test "it sends a welcome chat message if it is set" do
assert activity.actor == welcome_user.ap_id assert activity.actor == welcome_user.ap_id
end end
setup do:
clear_config(:mrf_simple,
media_removal: [],
media_nsfw: [],
federated_timeline_removal: [],
report_removal: [],
reject: [],
followers_only: [],
accept: [],
avatar_removal: [],
banner_removal: [],
reject_deletes: []
)
setup do:
clear_config(:mrf,
policies: [
Pleroma.Web.ActivityPub.MRF.SimplePolicy
]
)
test "it sends a welcome chat message when Simple policy applied to local instance" do
Pleroma.Config.put([:mrf_simple, :media_nsfw], ["localhost"])
welcome_user = insert(:user)
Pleroma.Config.put([:welcome, :chat_message, :enabled], true)
Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname)
Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message")
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
activity = Repo.one(Pleroma.Activity)
assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity).data["content"] =~ "chat message"
assert activity.actor == welcome_user.ap_id
end
test "it sends a welcome email message if it is set" do test "it sends a welcome email message if it is set" do
welcome_user = insert(:user) welcome_user = insert(:user)
Pleroma.Config.put([:welcome, :email, :enabled], true) Pleroma.Config.put([:welcome, :email, :enabled], true)

View File

@ -1773,6 +1773,14 @@ test "public timeline with default reply_visibility `self`", %{users: %{u1: user
|> Enum.map(& &1.id) |> Enum.map(& &1.id)
assert activities_ids == [] assert activities_ids == []
activities_ids =
%{}
|> Map.put(:reply_visibility, "self")
|> Map.put(:reply_filtering_user, nil)
|> ActivityPub.fetch_public_activities()
assert activities_ids == []
end end
test "home timeline", %{users: %{u1: user}} do test "home timeline", %{users: %{u1: user}} do

View File

@ -26,7 +26,7 @@ test "when given an `object_data` in meta, Federation will receive a the origina
{ {
Pleroma.Web.ActivityPub.MRF, Pleroma.Web.ActivityPub.MRF,
[], [],
[filter: fn o -> {:ok, o} end] [pipeline_filter: fn o, m -> {:ok, o, m} end]
}, },
{ {
Pleroma.Web.ActivityPub.ActivityPub, Pleroma.Web.ActivityPub.ActivityPub,
@ -51,7 +51,7 @@ test "when given an `object_data` in meta, Federation will receive a the origina
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity)) assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta))
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
refute called(Pleroma.Web.Federator.publish(activity)) refute called(Pleroma.Web.Federator.publish(activity))
@ -68,7 +68,7 @@ test "it goes through validation, filtering, persisting, side effects and federa
{ {
Pleroma.Web.ActivityPub.MRF, Pleroma.Web.ActivityPub.MRF,
[], [],
[filter: fn o -> {:ok, o} end] [pipeline_filter: fn o, m -> {:ok, o, m} end]
}, },
{ {
Pleroma.Web.ActivityPub.ActivityPub, Pleroma.Web.ActivityPub.ActivityPub,
@ -93,7 +93,7 @@ test "it goes through validation, filtering, persisting, side effects and federa
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity)) assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta))
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
assert_called(Pleroma.Web.Federator.publish(activity)) assert_called(Pleroma.Web.Federator.publish(activity))
@ -109,7 +109,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
{ {
Pleroma.Web.ActivityPub.MRF, Pleroma.Web.ActivityPub.MRF,
[], [],
[filter: fn o -> {:ok, o} end] [pipeline_filter: fn o, m -> {:ok, o, m} end]
}, },
{ {
Pleroma.Web.ActivityPub.ActivityPub, Pleroma.Web.ActivityPub.ActivityPub,
@ -131,7 +131,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity)) assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta))
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
end end
@ -148,7 +148,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
{ {
Pleroma.Web.ActivityPub.MRF, Pleroma.Web.ActivityPub.MRF,
[], [],
[filter: fn o -> {:ok, o} end] [pipeline_filter: fn o, m -> {:ok, o, m} end]
}, },
{ {
Pleroma.Web.ActivityPub.ActivityPub, Pleroma.Web.ActivityPub.ActivityPub,
@ -170,7 +170,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity)) assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta))
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
end end

View File

@ -213,6 +213,17 @@ test "it reject messages over the local limit" do
assert message == :content_too_long assert message == :content_too_long
end end
test "it reject messages via MRF" do
clear_config([:mrf_keyword, :reject], ["GNO"])
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
author = insert(:user)
recipient = insert(:user)
assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
CommonAPI.post_chat_message(author, recipient, "GNO/Linux")
end
end end
describe "unblocking" do describe "unblocking" do

View File

@ -100,7 +100,7 @@ test "it fails if there is no content", %{conn: conn, user: user} do
|> post("/api/v1/pleroma/chats/#{chat.id}/messages") |> post("/api/v1/pleroma/chats/#{chat.id}/messages")
|> json_response_and_validate_schema(400) |> json_response_and_validate_schema(400)
assert result assert %{"error" => "no_content"} == result
end end
test "it works with an attachment", %{conn: conn, user: user} do test "it works with an attachment", %{conn: conn, user: user} do
@ -126,6 +126,23 @@ test "it works with an attachment", %{conn: conn, user: user} do
assert result["attachment"] assert result["attachment"]
end end
test "gets MRF reason when rejected", %{conn: conn, user: user} do
clear_config([:mrf_keyword, :reject], ["GNO"])
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
other_user = insert(:user)
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
result =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "GNO/Linux"})
|> json_response_and_validate_schema(422)
assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} == result
end
end end
describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do

View File

@ -56,6 +56,27 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
%{method: :get, url: "http://example.com/error"} -> %{method: :get, url: "http://example.com/error"} ->
{:error, :overload} {:error, :overload}
%{
method: :head,
url: "http://example.com/huge-page"
} ->
%Tesla.Env{
status: 200,
headers: [{"content-length", "2000001"}, {"content-type", "text/html"}]
}
%{
method: :head,
url: "http://example.com/pdf-file"
} ->
%Tesla.Env{
status: 200,
headers: [{"content-length", "1000000"}, {"content-type", "application/pdf"}]
}
%{method: :head} ->
%Tesla.Env{status: 404, body: "", headers: []}
end) end)
:ok :ok
@ -146,4 +167,12 @@ test "rejects invalid OGP data" do
test "returns error if getting page was not successful" do test "returns error if getting page was not successful" do
assert {:error, :overload} = Parser.parse("http://example.com/error") assert {:error, :overload} = Parser.parse("http://example.com/error")
end end
test "does a HEAD request to check if the body is too large" do
assert {:error, :body_too_large} = Parser.parse("http://example.com/huge-page")
end
test "does a HEAD request to check if the body is html" do
assert {:error, {:content_type, _}} = Parser.parse("http://example.com/pdf-file")
end
end end