Merge remote-tracking branch 'origin/develop' into shigusegubu-new

This commit is contained in:
Henry Jameson 2026-10-28 20:59:57 +02:00
commit 04b6aa2031
83 changed files with 1834 additions and 271 deletions

View file

@ -4,6 +4,13 @@ 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.11.1
### Security
- Update Bandit to version 1.14.0, which fixes HTTP/1.1, HTTP/2 and WebSocket denial-of-service and request smuggling vulnerabilities
- Update Hackney to version 4.9.1, which fixes unbounded response body buffering and decompression, CA pinning, redirect credential leaks and other vulnerabilities
## 2.11.0 ## 2.11.0
### Security ### Security

View file

@ -0,0 +1 @@
Reject remote actors whose inbox, shared inbox, followers, following or featured collection, or key id is on another host

View file

@ -0,0 +1 @@
Check OAuth token scopes on the ActivityPub C2S endpoints; posting to the outbox with a token now needs the `write` scope

View file

@ -0,0 +1 @@
Do not crash when rendering attachments with zero width or height

View file

@ -0,0 +1 @@
Do not deliver activities, including public ones and deletes, to instances rejected by SimplePolicy (or not on its accept list), and refuse their signed fetches in authorized fetch mode

View file

@ -0,0 +1 @@
Only accept fetched objects whose id is the URL they were fetched from; for other ids, refetch once from the canonical id on the same host

View file

@ -0,0 +1 @@
Treat filter phrases as literal text; a phrase like "(" no longer breaks timeline and notification queries, and phrases that start or end with punctuation (like "#tag") now match

View file

@ -0,0 +1 @@
Filters with whole_word set to false now also match inside words

View file

@ -0,0 +1 @@
Reject fetched objects whose author has no id instead of crashing

View file

@ -0,0 +1 @@
Add the Mastodon quote entity (`quote` with `state` and `quoted_status`) to statuses, so Mastodon 4.4+ clients show quote posts

View file

@ -0,0 +1 @@
Proxy media given as network-path URLs (//host/path), so remote HTML cannot load images directly

View file

@ -0,0 +1 @@
Do not notify tagged users who cannot see the post

View file

@ -0,0 +1 @@
Do not crash when creating notifications for posts without content while the user has filters

View file

@ -0,0 +1 @@
Support the Mastodon `profile` OAuth scope, which only gives the public account from verify_credentials

View file

@ -0,0 +1 @@
Require the read:security and write:security scopes to list and revoke OAuth tokens (HTTP Basic auth no longer works for these endpoints)

View file

@ -0,0 +1 @@
ObjectAgePolicy no longer makes old DMs and followers-only posts public

View file

@ -0,0 +1 @@
Do not let a refetched object (such as a poll) change its id or author

View file

View file

@ -0,0 +1 @@
Update the known user instead of failing when an actor is fetched from a display URL

View file

@ -0,0 +1 @@
Fix deactivated users being counted in followers collections and getting admin emails, because User.Query ignored the is_active key

View file

@ -177,8 +177,8 @@ To add configuration to your config file, you can copy it from the base config.
* `media_removal`: List of instances to strip media attachments from and the reason for doing so. * `media_removal`: List of instances to strip media attachments from and the reason for doing so.
* `media_nsfw`: List of instances to tag all media as NSFW (sensitive) from and the reason for doing so. * `media_nsfw`: List of instances to tag all media as NSFW (sensitive) from and the reason for doing so.
* `federated_timeline_removal`: List of instances to remove from the Federated Timeline (aka The Whole Known Network) and the reason for doing so. * `federated_timeline_removal`: List of instances to remove from the Federated Timeline (aka The Whole Known Network) and the reason for doing so.
* `reject`: List of instances to reject activities (except deletes) from and the reason for doing so. * `reject`: List of instances to reject activities (except deletes) from and the reason for doing so. Activities are also not delivered to these instances, and signed fetches from them are refused in `authorized_fetch_mode`.
* `accept`: List of instances to only accept activities (except deletes) from and the reason for doing so. * `accept`: List of instances to only accept activities (except deletes) from and the reason for doing so. If set, activities are also only delivered to these instances, and signed fetches from others are refused in `authorized_fetch_mode`.
* `followers_only`: Force posts from the given instances to be visible by followers only and the reason for doing so. * `followers_only`: Force posts from the given instances to be visible by followers only and the reason for doing so.
* `report_removal`: List of instances to reject reports from and the reason for doing so. * `report_removal`: List of instances to reject reports from and the reason for doing so.
* `avatar_removal`: List of instances to strip avatars from and the reason for doing so. * `avatar_removal`: List of instances to strip avatars from and the reason for doing so.

View file

@ -303,6 +303,10 @@ Additional fields are returned in the response:
- `id`: The primary key of this token in Pleroma's database. - `id`: The primary key of this token in Pleroma's database.
- `me` (user tokens only): The ActivityPub ID of the user who owns the token. - `me` (user tokens only): The ActivityPub ID of the user who owns the token.
### `profile` scope
As in Mastodon 4.3, a token with the `profile` scope can call `GET /api/v1/accounts/verify_credentials` and no other Mastodon API endpoint. Apps can use it to log users in with their fediverse account. Unless the token also has `read` or `read:accounts`, Pleroma returns only public data: unlike Mastodon, there is no `source.privacy`, no `follow_requests_count`, and none of the private `pleroma` fields such as `email`, `settings_store` and `chat_token`.
## Account Registration ## Account Registration
`POST /api/v1/accounts` `POST /api/v1/accounts`

View file

@ -780,6 +780,7 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
### Retrieve a list of active sessions for the user ### Retrieve a list of active sessions for the user
* Method: `GET` * Method: `GET`
* Authentication: required * Authentication: required
* OAuth scope: `read:security`
* Params: none * Params: none
* Response: JSON * Response: JSON
* Example response: * Example response:
@ -808,8 +809,9 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
### Revoke a user session by its ID ### Revoke a user session by its ID
* Method: `DELETE` * Method: `DELETE`
* Authentication: required * Authentication: required
* OAuth scope: `write:security`
* Params: none * Params: none
* Response: HTTP 200 on success, 500 on error * Response: HTTP 201 on success, 500 on error
## `/api/v1/pleroma/settings/:app` ## `/api/v1/pleroma/settings/:app`
### Gets settings for some application ### Gets settings for some application

View file

@ -33,7 +33,7 @@ defmodule Mix.Tasks.Pleroma.Email do
Pleroma.User.Query.build(%{ Pleroma.User.Query.build(%{
local: true, local: true,
is_active: true, deactivated: false,
is_confirmed: false, is_confirmed: false,
invisible: false invisible: false
}) })

View file

@ -362,7 +362,7 @@ defmodule Mix.Tasks.Pleroma.User do
Pleroma.User.Query.build(%{ Pleroma.User.Query.build(%{
local: true, local: true,
is_active: true, deactivated: false,
is_moderator: false, is_moderator: false,
is_admin: false, is_admin: false,
invisible: false invisible: false
@ -380,7 +380,7 @@ defmodule Mix.Tasks.Pleroma.User do
Pleroma.User.Query.build(%{ Pleroma.User.Query.build(%{
local: true, local: true,
is_active: true, deactivated: false,
is_moderator: false, is_moderator: false,
is_admin: false, is_admin: false,
invisible: false invisible: false

View file

@ -208,19 +208,30 @@ defmodule Pleroma.Filter do
end end
def compose_regex([_ | _] = filters, format) do def compose_regex([_ | _] = filters, format) do
boundary = if format == :postgres, do: "\\y", else: "\\b"
phrases = phrases =
filters filters
|> Enum.map(& &1.phrase) |> Enum.map(&phrase_regex(&1, boundary))
|> Enum.join("|") |> Enum.join("|")
case format do case format do
:postgres -> :postgres -> "(#{phrases})"
"\\y(#{phrases})\\y" :re -> Regex.compile!("(?:#{phrases})", "iu")
:re ->
~r/\b#{phrases}\b/i
end end
end end
def compose_regex(_, _), do: nil def compose_regex(_, _), do: nil
# Phrases are literal text. Like Mastodon, whole-word filters only get a
# word boundary where the phrase starts or ends with a word character.
defp phrase_regex(%__MODULE__{phrase: phrase, whole_word: false}, _boundary),
do: Regex.escape(phrase)
defp phrase_regex(%__MODULE__{phrase: phrase}, boundary) do
prefix = if String.match?(phrase, ~r/^\w/u), do: boundary, else: ""
suffix = if String.match?(phrase, ~r/\w$/u), do: boundary, else: ""
prefix <> Regex.escape(phrase) <> suffix
end
end end

View file

@ -79,8 +79,14 @@ defmodule Pleroma.HTTP do
adapter, adapter,
adapter_opts adapter_opts
) )
|> drop_partial_body()
end end
# Hackney 4.9 returns the bytes read before the peer closed. No caller wants
# them, and callers that cache errors would keep them.
defp drop_partial_body({:error, {:closed, body}}) when is_binary(body), do: {:error, :closed}
defp drop_partial_body(result), do: result
@spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()} @spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
def request(client, request), do: Tesla.request(client, request) def request(client, request), do: Tesla.request(client, request)

View file

@ -821,6 +821,9 @@ defmodule Pleroma.Notification do
object.data["actor"] == user.ap_id -> object.data["actor"] == user.ap_id ->
false false
not is_binary(object.data["content"]) ->
false
not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) -> not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
Regex.match?(regex, object.data["content"]) Regex.match?(regex, object.data["content"])

View file

@ -44,6 +44,9 @@ defmodule Pleroma.Object.Containment do
id id
end end
# remote data can carry any shape here; callers treat nil as "no actor"
def get_actor(_), do: nil
def get_object(%{"object" => id}) when is_binary(id) do def get_object(%{"object" => id}) when is_binary(id) do
id id
end end
@ -108,8 +111,46 @@ defmodule Pleroma.Object.Containment do
def contain_origin_from_id(_id, _data), do: :error def contain_origin_from_id(_id, _data), do: :error
@doc """
Checks that a fetched document's id is the URL it was fetched from
(after redirects).
Fragments, a trailing slash, the case of the host and differences
that our URL encoding removes before a request are ignored.
Documents without an id never match.
Based on Akkoma's `contain_id_to_fetch/2`.
"""
def contain_id_to_fetch(url, %{"id" => id}) when is_binary(url) and is_binary(id) do
if normalize_uri(url) == normalize_uri(id), do: :ok, else: :error
end
def contain_id_to_fetch(_url, _data), do: :error
defp normalize_uri(uri) do
uri = uri |> Pleroma.Utils.URIEncoding.encode_url() |> URI.parse()
%URI{
uri
| fragment: nil,
host: uri.host && String.downcase(uri.host, :ascii),
path: uri.path && String.replace_suffix(uri.path, "/", "")
}
|> URI.to_string()
end
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}), def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
do: contain_origin(id, object) do: contain_origin(id, object)
def contain_child(_), do: :ok def contain_child(_), do: :ok
@doc "Checks that two URIs are on the same host. URIs without a host never match."
def same_origin(id, other_id) when is_binary(id) and is_binary(other_id) do
case {URI.parse(id), URI.parse(other_id)} do
{%URI{host: host}, %URI{host: host}} when is_binary(host) and host != "" -> :ok
_ -> :error
end
end
def same_origin(_id, _other_id), do: :error
end end

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
@ -46,9 +47,14 @@ defmodule Pleroma.Object.Fetcher do
end end
end end
def refetch_object(%Object{data: %{"id" => id}} = object) do # The stored id is canonical, so the refetch must not redirect, and the
# remote must not swap in another object or author.
# Based on Akkoma 9061d148b and fee57eb37.
def refetch_object(%Object{data: %{"id" => id} = data} = object) do
with {:local, false} <- {:local, Object.local?(object)}, with {:local, false} <- {:local, Object.local?(object)},
{:ok, new_data} <- fetch_and_contain_remote_object_from_id(id), {:ok, new_data} <- fetch_and_contain(id, true),
{:id, true} <- {:id, new_data["id"] == id},
{:actor, true} <- {:actor, actor_of(new_data) == actor_of(data)},
{:ok, object} <- reinject_object(object, new_data) do {:ok, object} <- reinject_object(object, new_data) do
{:ok, object} {:ok, object}
else else
@ -57,6 +63,8 @@ defmodule Pleroma.Object.Fetcher do
end end
end end
defp actor_of(data), do: CommonFixes.fix_actor(data)["actor"]
@typep fetcher_errors :: @typep fetcher_errors ::
:error | :reject | :allowed_depth | :fetch | :containment | :transmogrifier :error | :reject | :allowed_depth | :fetch | :containment | :transmogrifier
@ -140,17 +148,39 @@ defmodule Pleroma.Object.Fetcher do
def fetch_and_contain_remote_object_from_id(%{"id" => id}), def fetch_and_contain_remote_object_from_id(%{"id" => id}),
do: fetch_and_contain_remote_object_from_id(id) do: fetch_and_contain_remote_object_from_id(id)
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do def fetch_and_contain_remote_object_from_id(id) when is_binary(id),
do: fetch_and_contain(id, false)
def fetch_and_contain_remote_object_from_id(_id),
do: {:error, "id must be a string"}
# The fetched document's id must be the URL it came from. Display URLs
# (e.g. Mastodon's /@user/123) serve the object under another id, so
# if the id differs we refetch once from that id, which has to be on the
# same host, must not redirect, and must then match exactly.
# Based on Akkoma 8684964c5 and 940792f8b.
defp fetch_and_contain(id, canonical?) do
Logger.debug("Fetching object #{id} via AP") Logger.debug("Fetching object #{id} via AP")
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")}, with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
{_, true} <- {:mrf, MRF.id_filter(id)}, {_, true} <- {:mrf, MRF.id_filter(id)},
{_, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)}, {_, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)},
{:ok, body} <- get_object(id), {:ok, final_url, body} <- get_object(id),
{_, true} <- {:redirect, not canonical? or same_url?(final_url, id)},
{:ok, data} <- safe_json_decode(body), {:ok, data} <- safe_json_decode(body),
:ok <- Containment.contain_origin_from_id(id, data) do :ok <- Containment.contain_origin_from_id(id, data),
{_, _, :ok} <- {:id, data, Containment.contain_id_to_fetch(final_url, data)} do
{:ok, data} {:ok, data}
else else
{:id, %{"id" => canonical_id}, :error} when is_binary(canonical_id) and not canonical? ->
fetch_and_contain(canonical_id, true)
{:id, _, :error} ->
{:error, :id_mismatch}
{:redirect, false} ->
{:error, :canonical_id_redirect}
{:scheme, _} -> {:scheme, _} ->
{:error, "Unsupported URI scheme"} {:error, "Unsupported URI scheme"}
@ -168,8 +198,7 @@ defmodule Pleroma.Object.Fetcher do
end end
end end
def fetch_and_contain_remote_object_from_id(_id), defp same_url?(url, other), do: Containment.contain_id_to_fetch(url, %{"id" => other}) == :ok
do: {:error, "id must be a string"}
defp check_crossdomain_redirect(final_host, _original_url) when is_nil(final_host) do defp check_crossdomain_redirect(final_host, _original_url) when is_nil(final_host) do
{:cross_domain_redirect, false} {:cross_domain_redirect, false}
@ -195,7 +224,7 @@ defmodule Pleroma.Object.Fetcher do
with {:cross_domain_redirect, false} <- check_crossdomain_redirect(remote_host, id), with {:cross_domain_redirect, false} <- check_crossdomain_redirect(remote_host, id),
{_, content_type} <- List.keyfind(headers, "content-type", 0), {_, content_type} <- List.keyfind(headers, "content-type", 0),
{:ok, _media_type} <- verify_content_type(content_type) do {:ok, _media_type} <- verify_content_type(content_type) do
{:ok, body} {:ok, if(final_url in [nil, ""], do: id, else: final_url), body}
else else
{:cross_domain_redirect, true} -> {:cross_domain_redirect, true} ->
{:error, {:cross_domain_redirect, true}} {:error, {:cross_domain_redirect, true}}
@ -209,7 +238,7 @@ defmodule Pleroma.Object.Fetcher do
case List.keyfind(headers, "content-type", 0) do case List.keyfind(headers, "content-type", 0) do
{_, content_type} -> {_, content_type} ->
case verify_content_type(content_type) do case verify_content_type(content_type) do
{:ok, _} -> {:ok, body} {:ok, _} -> {:ok, id, body}
error -> error error -> error
end end

View file

@ -7,16 +7,14 @@ defmodule Pleroma.Tesla.Middleware.MaxBody do
Enforces the `:max_body` adapter option for every adapter. Enforces the `:max_body` adapter option for every adapter.
`Pleroma.HTTP.Adapter.Gun` applies the limit as it accumulates a `:plain` `Pleroma.HTTP.Adapter.Gun` applies the limit as it accumulates a `:plain`
body, so an oversized response is abandoned mid-transfer. Hackney cannot: body, so an oversized response is abandoned mid-transfer. Since 4.9 Hackney
since 4.0 it ignores `with_body` and always hands back a fully read binary does the same with the `max_body` request option. Before that it ignored
(`hackney:request/5`), so `Tesla.Adapter.Hackney` never takes the branch that `with_body` and always handed back a fully read binary (`hackney:request/5`),
would honour `:max_body`. Without this middleware the option is silently so `Tesla.Adapter.Hackney` never took the branch that would honour
unenforced there. `:max_body`.
The check therefore bounds what callers are handed, and refuses up front when The check bounds what callers are handed, whatever the adapter does, and
`content-length` already announces too much. It cannot bound peak memory on refuses up front when `content-length` already announces too much.
the hackney path, because the adapter has read the body before any middleware
runs.
Streaming bodies are left alone: neither adapter limits those, and the caller Streaming bodies are left alone: neither adapter limits those, and the caller
that owns the stream is the one that has to drain and release it. that owns the stream is the one that has to drain and release it.

View file

@ -969,7 +969,7 @@ defmodule Pleroma.User do
candidates = Config.get([:instance, :autofollowed_nicknames]) candidates = Config.get([:instance, :autofollowed_nicknames])
autofollowed_users = autofollowed_users =
User.Query.build(%{nickname: candidates, local: true, is_active: true}) User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all() |> Repo.all()
follow_all(user, autofollowed_users) follow_all(user, autofollowed_users)
@ -1401,7 +1401,7 @@ defmodule Pleroma.User do
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) do def get_followers_query(%User{} = user, nil) do
User.Query.build(%{followers: user, is_active: true}) User.Query.build(%{followers: user, deactivated: false})
end end
def get_followers_query(%User{} = user, page) do def get_followers_query(%User{} = user, page) do
@ -1476,7 +1476,7 @@ defmodule Pleroma.User do
|> where([u], not u.hide_follows) |> where([u], not u.hide_follows)
|> select([u], u.id) |> select([u], u.id)
User.Query.build(%{is_active: true}) User.Query.build(%{deactivated: false})
|> where([u], u.id not in ^[user.id, current_user.id]) |> where([u], u.id not in ^[user.id, current_user.id])
|> join(:inner, [u], r in FollowingRelationship, |> join(:inner, [u], r in FollowingRelationship,
as: :followers_relationships, as: :followers_relationships,
@ -1611,7 +1611,7 @@ defmodule Pleroma.User do
@spec get_users_from_set([String.t()], keyword()) :: [User.t()] @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
def get_users_from_set(ap_ids, opts \\ []) do def get_users_from_set(ap_ids, opts \\ []) do
local_only = Keyword.get(opts, :local_only, true) local_only = Keyword.get(opts, :local_only, true)
criteria = %{ap_id: ap_ids, is_active: true} criteria = %{ap_id: ap_ids, deactivated: false}
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
User.Query.build(criteria) User.Query.build(criteria)
@ -1622,7 +1622,7 @@ defmodule Pleroma.User do
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
to = [actor | to] to = [actor | to]
query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true}) query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
query query
|> Repo.all() |> Repo.all()
@ -2473,7 +2473,7 @@ defmodule Pleroma.User do
@spec all_superusers() :: [User.t()] @spec all_superusers() :: [User.t()]
def all_superusers do def all_superusers do
User.Query.build(%{super_users: true, local: true, is_active: true}) User.Query.build(%{super_users: true, local: true, deactivated: false})
|> Repo.all() |> Repo.all()
end end

View file

@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Upload alias Pleroma.Upload
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
@ -1924,6 +1925,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def user_data_from_user_object(data, additional \\ []) do def user_data_from_user_object(data, additional \\ []) do
with {:ok, data} <- MRF.filter(data), with {:ok, data} <- MRF.filter(data),
{:ok, data, _} <- UserValidator.validate(data, []),
{:ok, data} <- object_to_user_data(data, additional) do {:ok, data} <- object_to_user_data(data, additional) do
{:ok, data} {:ok, data}
else else
@ -2053,11 +2055,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def enqueue_pin_fetches(_), do: nil def enqueue_pin_fetches(_), do: nil
def make_user_from_ap_id(ap_id, additional \\ []) do def make_user_from_ap_id(ap_id, additional \\ []) do
user = User.get_cached_by_ap_id(ap_id)
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
enqueue_pin_fetches(data) enqueue_pin_fetches(data)
# the fetched actor's id differs from ap_id when it was fetched from a display URL
user = User.get_cached_by_ap_id(data.ap_id)
if user do if user do
user user
|> User.remote_user_changeset(data) |> User.remote_user_changeset(data)

View file

@ -22,8 +22,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Web.ControllerHelper alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Web.Plugs.FederatingPlug alias Pleroma.Web.Plugs.FederatingPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger require Logger
@ -46,6 +48,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
when action in [:read_inbox, :update_outbox, :whoami, :upload_media] when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
) )
# Checks scopes only for OAuth tokens (signatures and Basic auth already skip the check).
plug(:check_token_scopes, %{scopes: ["read:accounts"]} when action == :whoami)
plug(:check_token_scopes, %{scopes: ["read:statuses"]} when action == :read_inbox)
plug(:check_token_scopes, %{scopes: ["write"]} when action == :update_outbox)
plug(:check_token_scopes, %{scopes: ["write:media"]} when action == :upload_media)
plug(
:check_token_scopes,
%{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
when action in [:outbox, :object_replies]
)
plug(
:check_token_scopes,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action in [:followers, :following]
)
plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media]) plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
plug( plug(
@ -57,6 +77,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
plug(:log_inbox_metadata when action in [:inbox]) plug(:log_inbox_metadata when action in [:inbox])
plug(:relay_active? when action in [:relay]) plug(:relay_active? when action in [:relay])
defp check_token_scopes(%{assigns: %{token: %Token{}}} = conn, opts),
do: OAuthScopesPlug.call(conn, opts)
defp check_token_scopes(conn, _), do: conn
defp relay_active?(conn, _) do defp relay_active?(conn, _) do
if Pleroma.Config.get([:instance, :allow_relay]) do if Pleroma.Config.get([:instance, :allow_relay]) do
conn conn

View file

@ -164,6 +164,39 @@ defmodule Pleroma.Web.ActivityPub.MRF do
Enum.any?(domains, fn domain -> Regex.match?(domain, host) end) Enum.any?(domains, fn domain -> Regex.match?(domain, host) end)
end end
@doc """
Returns a function that is false for a host if SimplePolicy is enabled and
rejects it, or has an accept list without it. Used for deliveries and signed
fetches, not only for incoming activities. Build it once and reuse it: it
compiles the instance lists.
"""
@spec instance_filter() :: (String.t() | nil -> boolean())
def instance_filter do
if Pleroma.Web.ActivityPub.MRF.SimplePolicy in get_policies() do
reject = simple_policy_domains(:reject)
accept = simple_policy_domains(:accept)
fn
host when is_binary(host) ->
not subdomain_match?(reject, host) and (accept == [] or subdomain_match?(accept, host))
_ ->
false
end
else
&is_binary/1
end
end
@spec instance_allowed?(String.t() | nil) :: boolean()
def instance_allowed?(host), do: instance_filter().(host)
defp simple_policy_domains(key) do
Pleroma.Config.get([:mrf_simple, key], [])
|> instance_list_from_tuples()
|> subdomains_regex()
end
@spec instance_list_from_tuples([{String.t(), String.t()}]) :: [String.t()] @spec instance_list_from_tuples([{String.t(), String.t()}]) :: [String.t()]
def instance_list_from_tuples(list) do def instance_list_from_tuples(list) do
Enum.map(list, fn {instance, _} -> instance end) Enum.map(list, fn {instance, _} -> instance end)

View file

@ -36,16 +36,34 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
end end
end end
@spec delete_and_count(list(), term()) :: {integer(), list()}
defp delete_and_count(list, element), do: delete_and_count(list, element, {0, [], list})
defp delete_and_count([], _element, {0, _nlist, olist}), do: {0, olist}
defp delete_and_count([], _element, {count, nlist, _olist}), do: {count, Enum.reverse(nlist)}
defp delete_and_count([h | r], h, {count, nlist, olist}),
do: delete_and_count(r, h, {count + 1, nlist, olist})
defp delete_and_count([h | r], element, {count, nlist, olist}),
do: delete_and_count(r, element, {count, [h | nlist], olist})
defp insert_if_needed(list, oldcount, element) do
if oldcount <= 0 || Enum.member?(list, element) do
list
else
[element | list]
end
end
defp check_delist(activity, actions) do defp check_delist(activity, actions) do
if :delist in actions do if :delist in actions do
with %User{} = user <- User.get_cached_by_ap_id(activity["actor"]) do with %User{} = user <- User.get_cached_by_ap_id(activity["actor"]) do
to = {pubcnt, to} = delete_and_count(activity["to"] || [], Pleroma.Constants.as_public())
List.delete(activity["to"] || [], Pleroma.Constants.as_public()) ++ {flwcnt, cc} = delete_and_count(activity["cc"] || [], user.follower_address)
[user.follower_address]
cc = cc = insert_if_needed(cc, pubcnt, Pleroma.Constants.as_public())
List.delete(activity["cc"] || [], user.follower_address) ++ to = insert_if_needed(to, flwcnt, user.follower_address)
[Pleroma.Constants.as_public()]
activity = activity =
activity activity
@ -67,8 +85,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
defp check_strip_followers(activity, actions) do defp check_strip_followers(activity, actions) do
if :strip_followers in actions do if :strip_followers in actions do
with %User{} = user <- User.get_cached_by_ap_id(activity["actor"]) do with %User{} = user <- User.get_cached_by_ap_id(activity["actor"]) do
to = List.delete(activity["to"] || [], user.follower_address) {_, to} = delete_and_count(activity["to"] || [], user.follower_address)
cc = List.delete(activity["cc"] || [], user.follower_address) {_, cc} = delete_and_count(activity["cc"] || [], user.follower_address)
activity = activity =
activity activity

View file

@ -333,12 +333,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
%{ %{
key: :reject, key: :reject,
description: description:
"List of instances to reject activities from (except deletes) and the reason for doing so" "List of instances to reject activities from (except deletes) and the reason for doing so. We also do not deliver to them, and refuse their signed fetches in authorized fetch mode"
}, },
%{ %{
key: :accept, key: :accept,
description: description:
"List of instances to only accept activities from (except deletes) and the reason for doing so" "List of instances to only accept activities from (except deletes) and the reason for doing so. If set, we also only deliver to them, and refuse signed fetches from others in authorized fetch mode"
}, },
%{ %{
key: :followers_only, key: :followers_only,

View file

@ -11,7 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating @behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
import Pleroma.Constants, only: [activity_types: 0, object_types: 0] import Pleroma.Constants, only: [activity_types: 0, actor_types: 0, object_types: 0]
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.EctoType.ActivityPub.ObjectValidators
@ -37,6 +37,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator
@impl true @impl true
def validate(object, meta) def validate(object, meta)
@ -226,7 +227,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end end
def validate(%{"type" => type} = object, meta) when type == "Update" do def validate(%{"type" => type} = object, meta) when type == "Update" do
with {:ok, object} <- with :ok <- validate_updated_actor(object, meta),
{:ok, object} <-
object object
|> UpdateValidator.cast_and_validate(meta) |> UpdateValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
@ -247,6 +249,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(o, m), do: {:error, {:validator_not_set, {o, m}}} def validate(o, m), do: {:error, {:validator_not_set, {o, m}}}
defp validate_updated_actor(%{"object" => %{"type" => type} = actor}, meta)
when type in actor_types() do
with {_, false} <- {:local, Access.get(meta, :local, false)},
{:ok, _, _} <- UserValidator.validate(actor, meta) do
:ok
else
{:local, true} -> :ok
error -> error
end
end
defp validate_updated_actor(_, _), do: :ok
def cast_and_apply_and_stringify_with_history(object) do def cast_and_apply_and_stringify_with_history(object) do
do_separate_with_history(object, fn object -> do_separate_with_history(object, fn object ->
with {:ok, object_data} <- cast_and_apply(object), with {:ok, object_data} <- cast_and_apply(object),

View file

@ -0,0 +1,53 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do
@moduledoc """
Checks that a remote actor document only points at its own host.
We deliver to the inbox and shared inbox, use the collections for
addressing and visibility checks, and map signature key ids back to
the actor. If an actor could point any of these at another host, it
could redirect our signed deliveries or claim another actor's
collections. Optional fields are only checked when present; null and
empty strings count as missing.
Based on Akkoma's UserValidator.
"""
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
alias Pleroma.Object.Containment
@collections ["inbox", "followers", "following", "featured"]
@impl true
def validate(%{"id" => id} = data, meta) when is_binary(id) do
fields =
Enum.map(@collections, &{&1, data[&1]}) ++
[
{"sharedInbox", get_in_map(data, ["endpoints", "sharedInbox"])},
{"publicKey", get_in_map(data, ["publicKey", "id"])}
]
case Enum.find(fields, fn {_, value} -> not same_origin?(id, value) end) do
nil -> {:ok, data, meta}
{field, _} -> {:error, "#{field} is not on the same host as the actor"}
end
end
def validate(_data, _meta), do: {:error, "Actor has no id"}
defp same_origin?(_id, value) when value in [nil, ""], do: true
defp same_origin?(id, %{"id" => value}), do: same_origin?(id, value)
defp same_origin?(id, value), do: Containment.same_origin(id, value) == :ok
defp get_in_map(data, [key | rest]) do
case data do
%{^key => value} when rest == [] -> value
%{^key => value} -> get_in_map(value, rest)
_ -> nil
end
end
end

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Publisher.Prepared alias Pleroma.Web.ActivityPub.Publisher.Prepared
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Workers.PublisherWorker alias Pleroma.Workers.PublisherWorker
@ -221,18 +222,25 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
end end
end end
def should_federate?(nil, _), do: false def should_federate?(inbox, public), do: federation_filter(public).(inbox)
def should_federate?(_, true), do: true
def should_federate?(inbox, _) do # Compiles the instance lists once, for filtering many inboxes.
%{host: host} = URI.parse(inbox) defp federation_filter(public) do
allowed? = MRF.instance_filter()
quarantined_instances = quarantined =
Config.get([:instance, :quarantined_instances], []) Config.get([:instance, :quarantined_instances], [])
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples() |> MRF.instance_list_from_tuples()
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex() |> MRF.subdomains_regex()
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host) fn
nil ->
false
inbox ->
%{host: host} = URI.parse(inbox)
allowed?.(host) and (public or not MRF.subdomain_match?(quarantined, host))
end
end end
@spec recipients(User.t(), Activity.t()) :: [[User.t()]] @spec recipients(User.t(), Activity.t()) :: [[User.t()]]
@ -312,7 +320,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity) def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
when is_list(bcc) and bcc != [] do when is_list(bcc) and bcc != [] do
public = public?(activity) should_federate? = activity |> public?() |> federation_filter()
[priority_recipients, recipients] = recipients(actor, activity) [priority_recipients, recipients] = recipients(actor, activity)
@ -324,7 +332,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
determine_inbox(activity, user) determine_inbox(activity, user)
end) end)
|> Enum.uniq() |> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end) |> Enum.filter(should_federate?)
|> Instances.filter_reachable() |> Instances.filter_reachable()
end) end)
@ -360,6 +368,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
Relay.publish(activity) Relay.publish(activity)
end end
should_federate? = federation_filter(public)
[priority_inboxes, inboxes] = [priority_inboxes, inboxes] =
recipients(actor, activity) recipients(actor, activity)
|> Enum.map(fn recipients -> |> Enum.map(fn recipients ->
@ -368,7 +378,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
determine_inbox(activity, user) determine_inbox(activity, user)
end) end)
|> Enum.uniq() |> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end) |> Enum.filter(should_federate?)
end) end)
inboxes = inboxes -- priority_inboxes inboxes = inboxes -- priority_inboxes

View file

@ -48,7 +48,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description: "Test to make sure that the user token works.", description: "Test to make sure that the user token works.",
summary: "Verify account credentials", summary: "Verify account credentials",
operationId: "AccountController.verify_credentials", operationId: "AccountController.verify_credentials",
security: [%{"oAuth" => ["read:accounts"]}], security: [%{"oAuth" => ["read:accounts"]}, %{"oAuth" => ["profile"]}],
responses: %{ responses: %{
200 => Operation.response("Account", "application/json", Account) 200 => Operation.response("Account", "application/json", Account)
} }

View file

@ -234,6 +234,29 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
type: :integer, type: :integer,
description: "How many statuses quoted this status." description: "How many statuses quoted this status."
}, },
quote: %Schema{
type: :object,
nullable: true,
description:
"The status being quoted, if any (Mastodon 4.4+). Quoted statuses only have `quoted_status_id`.",
properties: %{
state: %Schema{
type: :string,
enum: ["accepted", "unauthorized", "deleted"],
description: "Whether the quoted status can be shown"
},
quoted_status: %Schema{
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
nullable: true,
description: "The quoted status, if it can be shown"
},
quoted_status_id: %Schema{
type: :string,
nullable: true,
description: "ID of the quoted status, if it can be shown"
}
}
},
reblog: %Schema{ reblog: %Schema{
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}], allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
nullable: true, nullable: true,

View file

@ -435,13 +435,56 @@ defmodule Pleroma.Web.CommonAPI.Utils do
%{} %{}
end end
tagged_mentions = maybe_extract_mentions(object_data) tagged_mentions =
maybe_extract_mentions(object_data)
|> filter_tags_to_qualified(activity)
recipients ++ tagged_mentions recipients ++ tagged_mentions
end end
def maybe_notify_mentioned_recipients(recipients, _), do: recipients def maybe_notify_mentioned_recipients(recipients, _), do: recipients
# Filter tagged mentions to ensure they can actually access the status
defp filter_tags_to_qualified(tagged_mentions, %Activity{
recipients: recipients,
data: data,
actor: actor
}) do
recipients =
if recipients && recipients != [],
do: recipients,
else:
(data["to"] || []) ++ (data["cc"] || []) ++ (data["bto"] || []) ++ (data["bcc"] || [])
cond do
recipients == [] ->
[]
Pleroma.Constants.as_public() in recipients ->
tagged_mentions
true ->
author = User.get_cached_by_ap_id(actor)
to_followers = match?(%User{}, author) and author.follower_address in recipients
Enum.filter(tagged_mentions, fn tagged ->
cond do
tagged in recipients ->
true
to_followers ->
case User.get_cached_by_ap_id(tagged) do
%User{} = tagged_user -> User.following?(tagged_user, author)
_ -> false
end
true ->
false
end
end)
end
end
def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do
with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do
user user

View file

@ -24,6 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.Plugs.RateLimiter
alias Pleroma.Web.Registration alias Pleroma.Web.Registration
@ -49,8 +50,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:accounts"]} %{scopes: ["read:accounts", "profile"]} when action == :verify_credentials
when action in [:verify_credentials, :endorsements, :own_endorsements] )
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]} when action in [:endorsements, :own_endorsements]
) )
plug( plug(
@ -161,7 +166,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end end
@doc "GET /api/v1/accounts/verify_credentials" @doc "GET /api/v1/accounts/verify_credentials"
def verify_credentials(%{assigns: %{user: user}} = conn, _) do def verify_credentials(%{assigns: %{user: user, token: %Token{scopes: scopes}}} = conn, _) do
if OAuthScopesPlug.filter_descendants(["read:accounts"], scopes) == [] do
# The profile scope only identifies the user: show what anyone can see.
render(conn, "show.json", user: user, skip_visibility_check: true)
else
render_own_account(conn, user)
end
end
def verify_credentials(%{assigns: %{user: user}} = conn, _), do: render_own_account(conn, user)
defp render_own_account(conn, user) do
chat_token = Phoenix.Token.sign(conn, "user socket", user.id) chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
render(conn, "show.json", render(conn, "show.json",

View file

@ -455,6 +455,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
language: get_language(object), language: get_language(object),
emojis: build_emojis(object.data["emoji"]), emojis: build_emojis(object.data["emoji"]),
quotes_count: object.data["quotesCount"] || 0, quotes_count: object.data["quotesCount"] || 0,
quote: build_quote(object, quote_activity, quote_post, opts),
pleroma: %{ pleroma: %{
local: activity.local, local: activity.local,
conversation_id: get_context_id(activity), conversation_id: get_context_id(activity),
@ -675,7 +676,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def render("attachment_meta.json", %{ def render("attachment_meta.json", %{
attachment: %{"url" => [%{"width" => width, "height" => height} | _]} attachment: %{"url" => [%{"width" => width, "height" => height} | _]}
}) })
when is_integer(width) and is_integer(height) do when is_integer(width) and is_integer(height) and width > 0 and height > 0 do
%{ %{
original: %{ original: %{
width: width, width: width,
@ -691,7 +692,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
%{ancestors: ancestors, descendants: descendants} = %{ancestors: ancestors, descendants: descendants} =
activities activities
|> Enum.reverse() |> Enum.reverse()
|> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end) |> Enum.group_by(fn %{id: id} ->
if id < activity.id, do: :ancestors, else: :descendants
end)
|> Map.put_new(:ancestors, []) |> Map.put_new(:ancestors, [])
|> Map.put_new(:descendants, []) |> Map.put_new(:descendants, [])
@ -742,6 +745,26 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end end
end end
# Mastodon (4.4+) quote entity. Quoted statuses get a shallow quote that only
# has the id, as quotes are only rendered one level deep.
defp build_quote(%Object{data: %{"quoteUrl" => url}}, quote_activity, quote_post, opts)
when is_binary(url) and url != "" do
state =
cond do
is_nil(quote_activity) -> "deleted"
visible_for_user?(quote_activity, opts[:for]) -> "accepted"
true -> "unauthorized"
end
if opts[:show_quote] == false do
%{state: state, quoted_status_id: if(state == "accepted", do: to_string(quote_activity.id))}
else
%{state: state, quoted_status: if(state == "accepted", do: quote_post)}
end
end
defp build_quote(_object, _quote_activity, _quote_post, _opts), do: nil
def get_quote(activity, %{quoted_activities: quoted_activities}) do def get_quote(activity, %{quoted_activities: quoted_activities}) do
object = Object.normalize(activity, fetch: false) object = Object.normalize(activity, fetch: false)

View file

@ -41,6 +41,7 @@ defmodule Pleroma.Web.MediaProxy do
end end
def url(url) when is_nil(url) or url == "", do: nil def url(url) when is_nil(url) or url == "", do: nil
def url("//" <> _ = url), do: url("https:" <> url)
def url("/" <> _ = url), do: url def url("/" <> _ = url), do: url
def url(url) do def url(url) do
@ -56,7 +57,10 @@ defmodule Pleroma.Web.MediaProxy do
not local?(url) and not whitelisted?(url) not local?(url) and not whitelisted?(url)
end end
def preview_url(url, preview_params \\ []) do def preview_url(url, preview_params \\ [])
def preview_url("//" <> _ = url, pparams), do: preview_url("https:" <> url, pparams)
def preview_url(url, preview_params) do
if preview_enabled?() do if preview_enabled?() do
encode_preview_url(url, preview_params) encode_preview_url(url, preview_params)
else else

View file

@ -13,7 +13,8 @@ defmodule Pleroma.Web.OAuth.TokenController do
require Logger require Logger
plug(:skip_auth when action == :confirm_email) plug(:skip_auth when action == :confirm_email)
plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) plug(OAuthScopesPlug, %{scopes: ["read:security"]} when action == :oauth_tokens)
plug(OAuthScopesPlug, %{scopes: ["write:security"]} when action == :revoke_token)
action_fallback(:errors) action_fallback(:errors)

View file

@ -1,52 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PasswordController do
@moduledoc """
The module contains functions for password reset.
"""
use Pleroma.Web, :controller
require Logger
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.PasswordResetToken
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Registration
plug(Pleroma.Web.Plugs.RateLimiter, [name: :request] when action == :request)
@doc "POST /auth/password"
def request(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
Registration.password_reset(nickname_or_email)
json_response(conn, :no_content, "")
end
def reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
false <- PasswordResetToken.expired?(token),
%User{} = user <- User.get_cached_by_id(token.user_id) do
render(conn, "reset.html", %{
token: token,
user: user
})
else
_e -> render(conn, "invalid_token.html")
end
end
def do_reset(conn, %{"data" => data}) do
with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do
render(conn, "reset_success.html")
else
_e -> render(conn, "reset_failed.html")
end
end
end

View file

@ -1,59 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.TokenController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.PleromaAPI.TokenView
alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
plug(:skip_auth when action == :confirm_email)
plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
action_fallback(:errors)
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- User.get_cached_by_id(uid),
true <- user.local and !user.is_confirmed and user.confirmation_token == token,
{:ok, _} <- User.confirm(user) do
redirect(conn, to: "/")
end
end
def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
with oauth_tokens <- Token.get_user_tokens(user) do
conn
|> put_view(TokenView)
|> render("index.json", %{tokens: oauth_tokens})
end
end
def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
Token.delete_user_token(user, id)
json_reply(conn, 201, "")
end
defp errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
end
defp errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
defp json_reply(conn, status, json) do
conn
|> put_resp_content_type("application/json")
|> send_resp(status, json)
end
end

View file

@ -101,7 +101,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
conn.assigns[:actor_id] do conn.assigns[:actor_id] do
%{host: host} = URI.parse(conn.assigns.actor_id) %{host: host} = URI.parse(conn.assigns.actor_id)
if MRF.subdomain_match?(rejected_domains(), host) do if MRF.subdomain_match?(rejected_domains(), host) or not MRF.instance_allowed?(host) do
conn conn
|> put_status(:unauthorized) |> put_status(:unauthorized)
|> halt() |> halt()

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Workers.PublisherWorker do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
use Oban.Worker, queue: :federator_outgoing, max_attempts: 13 use Oban.Worker, queue: :federator_outgoing, max_attempts: 13
@ -32,26 +34,39 @@ defmodule Pleroma.Workers.PublisherWorker do
end end
end) end)
# Cancel / skip the job if this server believed to be unreachable now cond do
if not Instances.reachable?(params.inbox) do # Cancel / skip the job if this server believed to be unreachable now
{:cancel, :unreachable} not Instances.reachable?(params.inbox) ->
else {:cancel, :unreachable}
case federator().perform(:publish_one, params) do
{:ok, _} ->
:ok
{:error, _} = error -> # The instance may have been blocked after the job was queued
# Only mark as unreachable on final failure not federate?(params) ->
if job.attempt == job.max_attempts do {:cancel, :rejected}
Instances.set_unreachable(params.inbox)
end
error true ->
case federator().perform(:publish_one, params) do
{:ok, _} ->
:ok
error -> {:error, _} = error ->
# Unexpected error, may have been client side # Only mark as unreachable on final failure
error if job.attempt == job.max_attempts do
end Instances.set_unreachable(params.inbox)
end
error
error ->
# Unexpected error, may have been client side
error
end
end
end
defp federate?(%{inbox: inbox, activity_id: activity_id}) do
case Activity.get_by_id(activity_id) do
%Activity{} = activity -> Publisher.should_federate?(inbox, Visibility.public?(activity))
nil -> true
end end
end end

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do def project do
[ [
app: :pleroma, app: :pleroma,
version: version("2.11.0"), version: version("2.11.1"),
elixir: "~> 1.17", elixir: "~> 1.17",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(), compilers: Mix.compilers(),
@ -224,9 +224,7 @@ defmodule Pleroma.Mixfile do
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}, {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.9", only: :test}, {:mock, "~> 0.3.9", only: :test},
{:covertool, "~> 2.0", only: :test}, {:covertool, "~> 2.0", only: :test},
{:hackney, "~> 4.7.3", override: true}, {:hackney, "~> 4.9.1", override: true},
# Hackney 4.7.3 pins certifi ~> 2.17.0; 2.18.0 drops the distrusted emSign roots.
{:certifi, "~> 2.18.0", override: true},
{:mox, "~> 1.2", only: :test}, {:mox, "~> 1.2", only: :test},
{:websockex, "~> 0.4.3", only: :test}, {:websockex, "~> 0.4.3", only: :test},
{:benchee, "~> 1.4", only: :benchmark}, {:benchee, "~> 1.4", only: :benchmark},

View file

@ -1,7 +1,7 @@
%{ %{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
"argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"}, "argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"},
"bandit": {:hex, :bandit, "1.13.1", "1ce25a7beb750ed3fa2e905962f1f9a679cb585f1da0ae45637d06e385ce2eae", [:mix], [{:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.6", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "39028f0b5a21404661410a6c19275060a9822c9dcca70b7c3e4e4fa50b11da58"}, "bandit": {:hex, :bandit, "1.14.0", "555af34f126edb98d8b6b95bbeba4d5f4c3af21e37535d791f32db2365c532fb", [:mix], [{:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.6", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "2d07630e4bf77b55240881386f7629bcc2f87426a299860b2d31c4ad77a48746"},
"base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"}, "base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"}, "bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
@ -58,8 +58,8 @@
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"gun": {:hex, :gun, "2.7.0", "8dfc20ff1b70f3c78f07122f7d82ac6950c40d33a8a9b35da0134f49fb35b334", [:make, :rebar3], [{:cowlib, ">= 2.21.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "2a90d073069e72654b6c9a91fae365b746ff5ce385a4a5bbf71ef53a8bf95d82"}, "gun": {:hex, :gun, "2.7.0", "8dfc20ff1b70f3c78f07122f7d82ac6950c40d33a8a9b35da0134f49fb35b334", [:make, :rebar3], [{:cowlib, ">= 2.21.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "2a90d073069e72654b6c9a91fae365b746ff5ce385a4a5bbf71ef53a8bf95d82"},
"h2": {:hex, :h2, "0.11.0", "b8c9f7c86f400582c5cbed59f79c0efd6cf49ad6478c210bede91d2d15019b14", [:rebar3], [], "hexpm", "c61f24361d0dc76375eaf35c2bf0e01235d81b7685538df571db036467fac358"}, "h2": {:hex, :h2, "0.12.5", "03d492b4cce77466c4c4a2b3fd74b2bb16c1d78f9618bbfeb50ea493c005accf", [:rebar3], [], "hexpm", "a4524bf80cafb86e599710c1d5c9f63aca0284607cb4d99fb53ae63fd55aa03f"},
"hackney": {:hex, :hackney, "4.7.3", "42feec1a4986000411cb5d389bbc9fb61d9dff84c5eef42b11fc40ea327e6f9a", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.11.0", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.5", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.4", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "3f460e01d5a1abba39c32d06e9a733bb76e35d1ab87a83868b6bb29d646fe25f"}, "hackney": {:hex, :hackney, "4.9.1", "1c1c3d75ae0f9c35df40f56ca63ba2f8c9986d988d67ae8ebe26f96697e81485", [:rebar3], [{:certifi, "~> 2.18.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.12.5", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.5", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:quic, "~> 2.1.2", [hex: :quic, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.8", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "093e1830e523c2b1972d570dcee83d000d41f7b39e7eb3c6b5240cf0f64a1973"},
"hpax": {:hex, :hpax, "1.1.0", "782931867cc23217c68fb5f68fe1a11f5e7544c7fda82c8a7019a5df5a4a1cdf", [:mix], [], "hexpm", "0b8d0f05832f55571d65ac720f79bf8994138ffbb133209dc4685eae0ad456a8"}, "hpax": {:hex, :hpax, "1.1.0", "782931867cc23217c68fb5f68fe1a11f5e7544c7fda82c8a7019a5df5a4a1cdf", [:mix], [], "hexpm", "0b8d0f05832f55571d65ac720f79bf8994138ffbb133209dc4685eae0ad456a8"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"http_signatures": {:hex, :http_signatures, "0.1.3", "19f26aee35b4684e37efdce3ac4638605e6e41a04368186bd39d2b6138a60ea9", [:mix], [{:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20313a65516db88006f85b090f6f76cc5b04e9609b45943657e6781eb91174f4"}, "http_signatures": {:hex, :http_signatures, "0.1.3", "19f26aee35b4684e37efdce3ac4638605e6e41a04368186bd39d2b6138a60ea9", [:mix], [{:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20313a65516db88006f85b090f6f76cc5b04e9609b45943657e6781eb91174f4"},
@ -130,7 +130,7 @@
"prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]}, "prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]},
"prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"},
"quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"}, "quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"},
"quic": {:hex, :quic, "1.8.0", "7b074fc6f8a13b4a81e4e11254bf061db61c5c07b4366d568a79a38c5e8bb5f3", [:rebar3], [], "hexpm", "6b1f8f08a45412ac503bc6771efb8edc696bb14079bc48d2ca960b92933da8a4"}, "quic": {:hex, :quic, "2.1.3", "c66861354d39b0f2b8ac0d7cde56d90db0168ac7b3ffa11d1cb8ab37237d1233", [:rebar3], [], "hexpm", "1520df50fc63093c0a4d209cb97056e132d06f0b8fd54c5cf933ddc0c0ea6fe2"},
"ranch": {:hex, :ranch, "2.2.1", "fc2bb0e800efbeab8ff82eb325450b9c08653b14c3f69e8b09bbea304973105d", [:make, :rebar3], [], "hexpm", "55f05cce20ec2da1d90de5d5981afb93dbfc01325fc7e933189aa5c62f037c24"}, "ranch": {:hex, :ranch, "2.2.1", "fc2bb0e800efbeab8ff82eb325450b9c08653b14c3f69e8b09bbea304973105d", [:make, :rebar3], [], "hexpm", "55f05cce20ec2da1d90de5d5981afb93dbfc01325fc7e933189aa5c62f037c24"},
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
"remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"},
@ -158,7 +158,7 @@
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
"webtransport": {:hex, :webtransport, "0.4.4", "714180b340fbd30a7d666f3f593a612186ff1ba444566b3e8c9ce37fb2950638", [:rebar3], [{:h2, "~> 0.11.0", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 1.8.0", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "80a6641e0f6628938ceee4c9d7a4f6d48a6089890da1ae3288ca8dab13a6005e"}, "webtransport": {:hex, :webtransport, "0.4.8", "3c340d7ea381e3f639ece90378d85fb46b1da4eece17bba3223051e833166501", [:rebar3], [{:h2, "~> 0.12", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 2.0", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "08477a2195ccbf944da0b78f85dc4e6ee4498790e3dd7fe84ee7a245938e1b26"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"}, "yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"},
} }

View file

@ -33,6 +33,11 @@ msgstr "Write all using admin API"
msgid "follow" msgid "follow"
msgstr "Read and write user relationships" msgstr "Read and write user relationships"
#, elixir-autogen, elixir-format
#: lib/pleroma/web/api_spec/scopes/translator.ex:5
msgid "profile"
msgstr "Read only your public profile, to log in to other services"
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/pleroma/web/api_spec/scopes/translator.ex:5 #: lib/pleroma/web/api_spec/scopes/translator.ex:5
msgid "push" msgid "push"

View file

@ -220,6 +220,11 @@ msgstr ""
msgid "read:mutes" msgid "read:mutes"
msgstr "" msgstr ""
#: lib/pleroma/web/api_spec/scopes/translator.ex:5
#, elixir-autogen, elixir-format
msgid "profile"
msgstr ""
#: lib/pleroma/web/api_spec/scopes/translator.ex:5 #: lib/pleroma/web/api_spec/scopes/translator.ex:5
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "push" msgid "push"

View file

@ -17,6 +17,10 @@ defmodule Pleroma.HTML.Transform.MediaProxy do
{"src", media_url} {"src", media_url}
end end
def scrub_attribute(:img, {"src", "//" <> target}) do
scrub_attribute(:img, {"src", "https://" <> target})
end
def scrub_attribute(_tag, attribute), do: attribute def scrub_attribute(_tag, attribute), do: attribute
def scrub({:img, attributes, children}) do def scrub({:img, attributes, children}) do

View file

@ -70,7 +70,7 @@
"preferredUsername": "jk.nipponalba.scot", "preferredUsername": "jk.nipponalba.scot",
"summary": "", "summary": "",
"publicKey": { "publicKey": {
"id": "jk.nipponalba.scot", "id": "https://fed.brid.gy/jk.nipponalba.scot#key",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdarxwzxnNbJ2hneWOYHkYJowk\npyigQtxlUd0VjgSQHwxU9kWqfbrHBVADyTtcqi/4dAzQd3UnCI1TPNnn4LPZY9PW\noiWd3Zl1/EfLFxO7LU9GS7fcSLQkyj5JNhSlN3I8QPudZbybrgRDVZYooDe1D+52\n5KLGqC2ajrIVOiDRTQIDAQAB\n-----END PUBLIC KEY-----" "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdarxwzxnNbJ2hneWOYHkYJowk\npyigQtxlUd0VjgSQHwxU9kWqfbrHBVADyTtcqi/4dAzQd3UnCI1TPNnn4LPZY9PW\noiWd3Zl1/EfLFxO7LU9GS7fcSLQkyj5JNhSlN3I8QPudZbybrgRDVZYooDe1D+52\n5KLGqC2ajrIVOiDRTQIDAQAB\n-----END PUBLIC KEY-----"
}, },
"inbox": "https://fed.brid.gy/jk.nipponalba.scot/inbox", "inbox": "https://fed.brid.gy/jk.nipponalba.scot/inbox",

View file

@ -11,7 +11,7 @@
"toot": "http://joinmastodon.org/ns#", "toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji" "Emoji": "toot:Emoji"
}], }],
"id": "http://mastodon.example.org/users/admin", "id": "http://mastodon.example.org/users/relay",
"type": "Application", "type": "Application",
"invisible": true, "invisible": true,
"following": "http://mastodon.example.org/users/admin/following", "following": "http://mastodon.example.org/users/admin/following",
@ -24,8 +24,8 @@
"url": "http://mastodon.example.org/@admin", "url": "http://mastodon.example.org/@admin",
"manuallyApprovesFollowers": false, "manuallyApprovesFollowers": false,
"publicKey": { "publicKey": {
"id": "http://mastodon.example.org/users/admin#main-key", "id": "http://mastodon.example.org/users/relay#main-key",
"owner": "http://mastodon.example.org/users/admin", "owner": "http://mastodon.example.org/users/relay",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n" "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"
}, },
"attachment": [{ "attachment": [{

View file

@ -190,4 +190,66 @@ defmodule Pleroma.FilterTest do
{:error, _} = Filter.update(filter, changes) {:error, _} = Filter.update(filter, changes)
end end
describe "compose_regex/2" do
defp filters(phrases), do: Enum.map(phrases, &%Filter{phrase: &1})
defp pg_match?(regex, text) do
%{rows: [[match]]} = Repo.query!("SELECT $1::text ~* $2", [text, regex])
match
end
test "treats phrases as literal text in Postgres" do
regex = Filter.compose_regex(filters(["a.b", "(c"]))
assert pg_match?(regex, "see a.b here")
refute pg_match?(regex, "see axb here")
assert pg_match?(regex, "see (c here")
end
test "treats phrases as literal text in Elixir" do
regex = Filter.compose_regex(filters(["a.b", "(c"]), :re)
assert Regex.match?(regex, "see a.b here")
refute Regex.match?(regex, "see axb here")
assert Regex.match?(regex, "see (c here")
end
test "keeps word boundaries for every phrase in Postgres" do
regex = Filter.compose_regex(filters(["cat", "dog"]))
assert pg_match?(regex, "a dog barks")
refute pg_match?(regex, "a catalog")
refute pg_match?(regex, "a hotdog")
end
test "only adds word boundaries next to word characters" do
pg = Filter.compose_regex(filters(["#tag"]))
re = Filter.compose_regex(filters(["#tag"]), :re)
for {text, expected} <- [{"a #tag here", true}, {"a#tag", true}, {"some #tags", false}] do
assert pg_match?(pg, text) == expected
assert Regex.match?(re, text) == expected
end
end
test "matches inside words when whole_word is false" do
filters = [%Filter{phrase: "cat", whole_word: false}, %Filter{phrase: "dog"}]
pg = Filter.compose_regex(filters)
re = Filter.compose_regex(filters, :re)
for {text, expected} <- [{"a catalog", true}, {"a hotdog", false}, {"a dog", true}] do
assert pg_match?(pg, text) == expected
assert Regex.match?(re, text) == expected
end
end
test "keeps word boundaries for every phrase in Elixir" do
regex = Filter.compose_regex(filters(["cat", "dog"]), :re)
assert Regex.match?(regex, "a dog barks")
refute Regex.match?(regex, "a catalog")
refute Regex.match?(regex, "a hotdog")
end
end
end end

View file

@ -14,15 +14,15 @@
# #
# Pleroma cannot work around this: since 4.0 Hackney ignores `with_body` and # Pleroma cannot work around this: since 4.0 Hackney ignores `with_body` and
# always returns a fully read binary, so a synchronous request never yields a # always returns a fully read binary, so a synchronous request never yields a
# handle to close, and a truncated read still reports `{:ok, body}`. # handle to close. Up to 4.8.2 a truncated read also reported `{:ok, body}`;
# since 4.8.3 it is `{:error, {:closed, partial_body}}`.
# #
# Media proxy warming hits this whenever an attachment fetch is cut short and # Media proxy warming hits this whenever an attachment fetch is cut short and
# the outbound proxy is used as a CONNECT tunnel, since tunnels are never # the outbound proxy is used as a CONNECT tunnel, since tunnels are never
# pooled. That is how it was found. # pooled. That is how it was found.
# #
# Fixed upstream by https://github.com/benoitc/hackney/pull/919; mix.exs pins # Fixed upstream by https://github.com/benoitc/hackney/pull/919. Should this
# that commit until it lands in a release. Should this start failing, the pin # start failing, mix.exs has moved to a Hackney version without the fix.
# has been moved to a version without the fix.
# #
# https://github.com/benoitc/hackney/issues/918 # https://github.com/benoitc/hackney/issues/918
defmodule Pleroma.HTTP.HackneyConnLeakTest do defmodule Pleroma.HTTP.HackneyConnLeakTest do
@ -50,11 +50,9 @@ defmodule Pleroma.HTTP.HackneyConnLeakTest do
# pool: false takes hackney's connect_direct/4 path, the same unpooled shape # pool: false takes hackney's connect_direct/4 path, the same unpooled shape
# a CONNECT tunnel gets. A pooled connection is reaped by the pool's own # a CONNECT tunnel gets. A pooled connection is reaped by the pool's own
# grace timer and would not show the leak. # grace timer and would not show the leak.
assert {:ok, %Tesla.Env{status: 200, body: body}} = assert {:error, {:closed, body}} = request(server, pool: false)
request(server, pool: false)
# The short read still reports success, which is why nothing downstream # The short read hands back what did arrive.
# knows to clean up.
assert byte_size(body) == @sent_bytes assert byte_size(body) == @sent_bytes
leaked = wait_for_cleanup(before) leaked = wait_for_cleanup(before)
@ -70,7 +68,7 @@ defmodule Pleroma.HTTP.HackneyConnLeakTest do
test "a pooled request with a truncated body does not leak either", %{server: server} do test "a pooled request with a truncated body does not leak either", %{server: server} do
before = conn_pids() before = conn_pids()
assert {:ok, %Tesla.Env{status: 200}} = request(server, pool: :media) assert {:error, {:closed, _}} = request(server, pool: :media)
assert wait_for_cleanup(before) == [] assert wait_for_cleanup(before) == []
end end

View file

@ -4,8 +4,9 @@
# Hackney 4 ignores `with_body` and always returns a fully read binary, so # Hackney 4 ignores `with_body` and always returns a fully read binary, so
# `Tesla.Adapter.Hackney` never reaches the clause that honours `:max_body`. # `Tesla.Adapter.Hackney` never reaches the clause that honours `:max_body`.
# The limit is enforced by Pleroma.Tesla.Middleware.MaxBody instead; these # Hackney 4.9 enforces `max_body` itself while reading (CVE-2026-97200);
# exercise it against the real adapter. # Pleroma.Tesla.Middleware.MaxBody stays as the check for every adapter. These
# exercise both against the real adapter.
defmodule Pleroma.HTTP.HackneyMaxBodyRegressionTest do defmodule Pleroma.HTTP.HackneyMaxBodyRegressionTest do
use ExUnit.Case, async: false use ExUnit.Case, async: false
@ -43,8 +44,8 @@ defmodule Pleroma.HTTP.HackneyMaxBodyRegressionTest do
request("#{server.base_url}/big", max_body: 1024) request("#{server.base_url}/big", max_body: 1024)
end end
# Without a Content-Length the header pre-check cannot fire, so this is the # Without a Content-Length the header pre-check cannot fire, so only a size
# case that proves hackney's fully-read binary is measured. # check on the body can refuse this.
test "a chunked body over max_body is refused on size alone", %{server: server} do test "a chunked body over max_body is refused on size alone", %{server: server} do
assert {:error, :body_too_large} = assert {:error, :body_too_large} =
request("#{server.base_url}/chunked", max_body: 1024) request("#{server.base_url}/chunked", max_body: 1024)
@ -57,17 +58,16 @@ defmodule Pleroma.HTTP.HackneyMaxBodyRegressionTest do
assert byte_size(body) == @body_bytes assert byte_size(body) == @body_bytes
end end
test "without max_body the body is returned whatever its size", %{server: server} do test "without max_body the body is returned", %{server: server} do
assert {:ok, %Tesla.Env{status: 200, body: body}} = request("#{server.base_url}/big", []) assert {:ok, %Tesla.Env{status: 200, body: body}} = request("#{server.base_url}/big", [])
assert byte_size(body) == @body_bytes assert byte_size(body) == @body_bytes
end end
# Pins why the middleware is needed. Tesla.Adapter.Hackney only honours # Hackney 4.0 to 4.8 read the whole body whatever `max_body` said; 4.9
# :max_body on the branch where hackney hands back a ref, which 4.x never # stops reading once the body goes over it (CVE-2026-97200). Should this
# does for a synchronous request. Should this ever start failing, hackney or # start failing, the hackney pin has gone back to a version without the fix.
# Tesla have regained streaming and the middleware can go. test "the bare adapter enforces max_body", %{server: server} do
test "the bare adapter does not enforce max_body", %{server: server} do url = "#{server.base_url}/chunked"
url = "#{server.base_url}/big"
adapter_opts = adapter_opts =
HackneyAdapterHelper.options( HackneyAdapterHelper.options(
@ -77,10 +77,8 @@ defmodule Pleroma.HTTP.HackneyMaxBodyRegressionTest do
client = Tesla.client([], Tesla.Adapter.Hackney) client = Tesla.client([], Tesla.Adapter.Hackney)
assert {:ok, %Tesla.Env{status: 200, body: body}} = assert {:error, :body_too_large} =
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts]) Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
assert byte_size(body) == @body_bytes
end end
defp start_server do defp start_server do

View file

@ -29,6 +29,10 @@ defmodule Pleroma.HTTPTest do
%{method: :post, url: "http://example.com/world"} -> %{method: :post, url: "http://example.com/world"} ->
%Tesla.Env{status: 200, body: "world"} %Tesla.Env{status: 200, body: "world"}
# Hackney 4.9 hands back the bytes it read before the peer closed.
%{method: :get, url: "http://example.com/truncated"} ->
{:error, {:closed, "partial body"}}
%{method: :get, url: "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"} -> %{method: :get, url: "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"} ->
%Tesla.Env{status: 200, body: "emoji data"} %Tesla.Env{status: 200, body: "emoji data"}
@ -81,6 +85,12 @@ defmodule Pleroma.HTTPTest do
end end
end end
describe "get/1 with a truncated body" do
test "drops the partial body from the error" do
assert HTTP.get("http://example.com/truncated") == {:error, :closed}
end
end
describe "get/2 (with headers)" do describe "get/2 (with headers)" do
test "returns successfully result for json content-type" do test "returns successfully result for json content-type" do
assert HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) == assert HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) ==

View file

@ -7,8 +7,11 @@ defmodule Pleroma.NotificationTest do
import Pleroma.Factory import Pleroma.Factory
require Pleroma.Constants
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
@ -276,6 +279,22 @@ defmodule Pleroma.NotificationTest do
assert {:ok, []} == Notification.create_notifications(status) assert {:ok, []} == Notification.create_notifications(status)
end end
test "it creates notifications for objects without content when the user has filters" do
author = insert(:user)
user = insert(:user)
insert(:filter, user: user, phrase: "cofe", hide: true)
{:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"})
object = Object.normalize(status, fetch: false)
{:ok, _} =
object
|> Object.change(%{data: Map.delete(object.data, "content")})
|> Object.update_and_set_cache()
assert {:ok, [_]} = Notification.create_notifications(Pleroma.Activity.get_by_id(status.id))
end
test "it creates notifications if content matches with a not irreversible filter" do test "it creates notifications if content matches with a not irreversible filter" do
user = insert(:user) user = insert(:user)
subscriber = insert(:user) subscriber = insert(:user)
@ -611,6 +630,102 @@ defmodule Pleroma.NotificationTest do
assert other_user in enabled_receivers assert other_user in enabled_receivers
end end
defp incoming_note_tagging(author, to, tagged_ap_ids) do
tags = Enum.map(tagged_ap_ids, &%{"type" => "Mention", "href" => &1, "name" => &1})
{:ok, activity} =
Transmogrifier.handle_incoming(%{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"to" => to,
"actor" => author.ap_id,
"object" => %{
"type" => "Note",
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"to" => to,
"content" => "tagged",
"tag" => tags,
"attributedTo" => author.ap_id
}
})
activity
end
defp notified_ap_ids(activity),
do: activity |> Notification.get_notified_from_activity() |> Enum.map(& &1.ap_id)
test "it does not notify tagged users who cannot see a followers-only post" do
author = insert(:user)
tagged = insert(:user)
activity = incoming_note_tagging(author, [author.follower_address], [tagged.ap_id])
refute tagged.ap_id in notified_ap_ids(activity)
end
test "it notifies tagged followers of a followers-only post" do
author = insert(:user)
tagged = insert(:user)
{:ok, _, _} = Pleroma.User.follow(tagged, author)
activity = incoming_note_tagging(author, [author.follower_address], [tagged.ap_id])
assert tagged.ap_id in notified_ap_ids(activity)
end
test "it does not notify tagged users who are not addressed by a direct message" do
author = insert(:user)
addressed = insert(:user)
tagged = insert(:user)
activity = incoming_note_tagging(author, [addressed.ap_id], [tagged.ap_id])
receivers = notified_ap_ids(activity)
assert addressed.ap_id in receivers
refute tagged.ap_id in receivers
end
test "it notifies tagged users of a public post even if they are not addressed" do
author = insert(:user)
tagged = insert(:user)
activity =
incoming_note_tagging(author, [Pleroma.Constants.as_public()], [tagged.ap_id])
assert tagged.ap_id in notified_ap_ids(activity)
end
test "it does not notify tagged users with a pending follow request" do
author = insert(:user, is_locked: true)
tagged = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(author, tagged)
activity = incoming_note_tagging(author, [author.follower_address], [tagged.ap_id])
refute tagged.ap_id in notified_ap_ids(activity)
end
test "it ignores tags of unknown users in a followers-only post" do
author = insert(:user)
tagged = insert(:user)
follower = insert(:user)
{:ok, _, _} = Pleroma.User.follow(follower, author)
activity =
incoming_note_tagging(author, [author.follower_address], [
"https://unknown.example/users/nobody",
tagged.ap_id,
follower.ap_id
])
receivers = notified_ap_ids(activity)
refute tagged.ap_id in receivers
assert follower.ap_id in receivers
end
test "it does not send notifications to users who are only cc in new messages" do test "it does not send notifications to users who are only cc in new messages" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)

View file

@ -127,4 +127,56 @@ defmodule Pleroma.Object.ContainmentTest do
:ok = Containment.contain_child(data) :ok = Containment.contain_child(data)
end end
end end
describe "get_actor/1" do
test "returns nil for actor shapes without an id" do
for actor <- [%{"type" => "Person"}, %{"id" => 1}, 1, true] do
assert nil == Containment.get_actor(%{"actor" => actor})
assert nil == Containment.get_actor(%{"actor" => nil, "attributedTo" => actor})
end
assert nil == Containment.get_actor(%{})
end
test "contain_origin/2 rejects actor shapes without an id" do
assert :error ==
Containment.contain_origin("https://a.test/o/1", %{
"attributedTo" => %{"type" => "Person"}
})
end
end
describe "contain_id_to_fetch/2" do
test "matches the exact id" do
assert :ok =
Containment.contain_id_to_fetch("https://a.test/o/1", %{
"id" => "https://a.test/o/1"
})
end
test "ignores fragments, a trailing slash and the host case" do
assert :ok =
Containment.contain_id_to_fetch("https://A.test/o/1/", %{
"id" => "https://a.test/o/1#x"
})
end
test "rejects another id on the same host" do
assert :error =
Containment.contain_id_to_fetch("https://a.test/o/1", %{
"id" => "https://a.test/o/2"
})
end
test "rejects a different query" do
assert :error =
Containment.contain_id_to_fetch("https://a.test/o?page=1", %{
"id" => "https://a.test/o?page=2"
})
end
test "rejects documents without an id" do
assert :error = Containment.contain_id_to_fetch("https://a.test/o/1", %{})
end
end
end end

View file

@ -48,6 +48,8 @@ defmodule Pleroma.Object.FetcherTest do
%{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} -> %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} ->
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
# Pleroma redirects /notice/:id to the object id
url: "https://social.sakamoto.gq/objects/f20f2497-66d9-4a52-a2e1-1be2a39c32c1",
body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"), body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"),
headers: HttpRequestMock.activitypub_object_headers() headers: HttpRequestMock.activitypub_object_headers()
} }
@ -463,6 +465,78 @@ defmodule Pleroma.Object.FetcherTest do
%{object1: local_object1, object2: local_object2} %{object1: local_object1, object2: local_object2}
end end
test "it rejects a refetched object whose actor changed", %{object1: object1} do
insert(:user, ap_id: "https://mastodon.social/users/someone_else")
{:ok, o} = Object.create(object1)
mock(fn %{method: :get, url: "https://mastodon.social/1"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
object1
|> Map.merge(%{
"actor" => "https://mastodon.social/users/someone_else",
"attributedTo" => "https://mastodon.social/users/someone_else",
"content" => "hijacked"
})
|> Jason.encode!()
}
end)
assert {:error, _} = Fetcher.refetch_object(o)
assert Object.get_by_ap_id("https://mastodon.social/1").data["content"] == "test 1"
end
test "it rejects a refetched object whose author has no id", %{object1: object1} do
{:ok, o} = Object.create(object1)
mock(fn %{method: :get, url: "https://mastodon.social/1"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
object1
|> Map.delete("actor")
|> Map.put("attributedTo", %{"type" => "Person"})
|> Jason.encode!()
}
end)
assert {:error, _} = Fetcher.refetch_object(o)
end
test "it rejects a refetched object whose id changed", %{object1: object1} do
{:ok, o} = Object.create(object1)
mock(fn %{method: :get, url: "https://mastodon.social/1"} ->
%Tesla.Env{
status: 200,
url: "https://mastodon.social/2",
headers: [{"content-type", "application/activity+json"}],
body: object1 |> Map.put("id", "https://mastodon.social/2") |> Jason.encode!()
}
end)
assert {:error, _} = Fetcher.refetch_object(o)
end
test "it rejects a refetched object whose id differs only by a trailing slash", %{
object1: object1
} do
{:ok, o} = Object.create(object1)
mock(fn %{method: :get, url: "https://mastodon.social/1"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body: object1 |> Map.put("id", "https://mastodon.social/1/") |> Jason.encode!()
}
end)
assert {:error, _} = Fetcher.refetch_object(o)
end
test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
full_object1 = full_object1 =
object1 object1
@ -642,7 +716,23 @@ defmodule Pleroma.Object.FetcherTest do
}) })
} }
# No redirect - same domain # Same-domain redirect to the object id
%{method: :get, url: "https://original.test/@actor/redirect"} ->
%Tesla.Env{
status: 200,
url: "https://original.test/objects/redirect-target",
headers: [{"content-type", "application/activity+json"}],
body:
Jason.encode!(%{
"id" => "https://original.test/objects/redirect-target",
"type" => "Note",
"content" => "This has a same-domain redirect",
"actor" => "https://original.test/users/actor",
"attributedTo" => "https://original.test/users/actor"
})
}
# Same-domain redirect to a document claiming the original URL as id
%{method: :get, url: "https://original.test/objects/same-domain-redirect"} -> %{method: :get, url: "https://original.test/objects/same-domain-redirect"} ->
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
@ -695,12 +785,19 @@ defmodule Pleroma.Object.FetcherTest do
test "it accepts objects with same-domain redirects" do test "it accepts objects with same-domain redirects" do
assert {:ok, data} = assert {:ok, data} =
Fetcher.fetch_and_contain_remote_object_from_id( Fetcher.fetch_and_contain_remote_object_from_id(
"https://original.test/objects/same-domain-redirect" "https://original.test/@actor/redirect"
) )
assert data["content"] == "This has a same-domain redirect" assert data["content"] == "This has a same-domain redirect"
end end
test "it rejects redirect targets that claim the original URL as id" do
assert {:error, _} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://original.test/objects/same-domain-redirect"
)
end
test "it handles responses without URL field (common in tests)" do test "it handles responses without URL field (common in tests)" do
assert {:ok, data} = assert {:ok, data} =
Fetcher.fetch_and_contain_remote_object_from_id( Fetcher.fetch_and_contain_remote_object_from_id(
@ -711,6 +808,150 @@ defmodule Pleroma.Object.FetcherTest do
end end
end end
describe "id containment" do
setup do
note = fn id, content ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
Jason.encode!(%{
"id" => id,
"type" => "Note",
"content" => content,
"actor" => "https://host.test/users/actor",
"attributedTo" => "https://host.test/users/actor"
})
}
end
mock(fn
# display URL serving the canonical object
%{method: :get, url: "https://host.test/@actor/1"} ->
note.("https://host.test/objects/1", "display")
%{method: :get, url: "https://host.test/objects/1"} ->
note.("https://host.test/objects/1", "canonical")
# uploaded file claiming to be another object on the same host
%{method: :get, url: "https://host.test/media/evil.json"} ->
note.("https://host.test/objects/2", "spoofed")
%{method: :get, url: "https://host.test/objects/2"} ->
note.("https://host.test/objects/2", "real")
# canonical id does not serve itself
%{method: :get, url: "https://host.test/media/loop.json"} ->
note.("https://host.test/objects/3", "loop")
%{method: :get, url: "https://host.test/objects/3"} ->
note.("https://host.test/objects/4", "other")
# canonical id redirects
%{method: :get, url: "https://host.test/media/redirect.json"} ->
note.("https://host.test/objects/5", "redirect")
%{method: :get, url: "https://host.test/objects/5"} ->
%{note.("https://host.test/objects/5", "moved") | url: "https://host.test/media/x"}
%{method: :get, url: "https://host.test/objects/slash"} ->
note.("https://host.test/objects/slash/", "slash")
%{method: :get, url: "https://host.test/objects/no-id"} ->
note.(nil, "no id")
end)
:ok
end
test "it refetches display URLs from the canonical id" do
assert {:ok, %{"id" => "https://host.test/objects/1", "content" => "canonical"}} =
Fetcher.fetch_and_contain_remote_object_from_id("https://host.test/@actor/1")
end
test "it does not trust a document claiming another id on the same host" do
assert {:ok, %{"id" => "https://host.test/objects/2", "content" => "real"}} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://host.test/media/evil.json"
)
end
test "it refetches at most once" do
assert {:error, :id_mismatch} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://host.test/media/loop.json"
)
end
test "it does not follow redirects when refetching from the canonical id" do
assert {:error, _} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://host.test/media/redirect.json"
)
end
test "it ignores a trailing slash" do
assert {:ok, %{"content" => "slash"}} =
Fetcher.fetch_and_contain_remote_object_from_id("https://host.test/objects/slash")
end
test "it accepts ids that only differ by URL encoding" do
clear_config(:test_url_encoding, true)
mock(fn %{method: :get, url: url} ->
# the encoder sends uppercase escapes
assert url in [
"https://host.test/users/%E3%81%82",
"https://host.test/notes/%E3%81%82"
]
id =
if url =~ "users",
do: "https://host.test/users/あ",
else: "https://host.test/notes/%e3%81%82"
%Tesla.Env{
status: 200,
url: url,
headers: [{"content-type", "application/activity+json"}],
body: Jason.encode!(%{"id" => id, "type" => "Note"})
}
end)
assert {:ok, _} =
Fetcher.fetch_and_contain_remote_object_from_id("https://host.test/users/あ")
assert {:ok, _} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://host.test/notes/%e3%81%82"
)
end
test "it rejects objects whose author has no id" do
mock(fn %{method: :get, url: "https://host.test/objects/anon" = url} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
Jason.encode!(%{
"id" => url,
"type" => "Note",
"content" => "anon",
"attributedTo" => %{"type" => "Person", "name" => "nobody"}
})
}
end)
assert {:containment, :error} =
Fetcher.fetch_object_from_id("https://host.test/objects/anon")
end
test "it rejects documents without an id" do
assert {:error, _} =
Fetcher.fetch_and_contain_remote_object_from_id("https://host.test/objects/no-id")
end
end
describe "fetch with history" do describe "fetch with history" do
setup do setup do
object2 = %{ object2 = %{

View file

@ -1197,11 +1197,32 @@ defmodule Pleroma.UserTest do
res = User.get_followers(user) res = User.get_followers(user)
assert length(res) == 2
assert Enum.member?(res, follower_one) assert Enum.member?(res, follower_one)
assert Enum.member?(res, follower_two) assert Enum.member?(res, follower_two)
refute Enum.member?(res, not_follower) refute Enum.member?(res, not_follower)
end end
test "does not return deactivated followers" do
user = insert(:user)
follower_one = insert(:user)
follower_two = insert(:user)
not_follower = insert(:user)
{:ok, follower_one, user} = User.follow(follower_one, user)
{:ok, follower_two, user} = User.follow(follower_two, user)
User.delete(follower_two)
follower_two = User.get_cached_by_id(follower_two.id)
res = User.get_followers(user)
assert length(res) == 1
assert Enum.member?(res, follower_one)
refute Enum.member?(res, follower_two)
refute Enum.member?(res, not_follower)
end
test "gets all friends (followed users) for a given user" do test "gets all friends (followed users) for a given user" do
user = insert(:user) user = insert(:user)
followed_one = insert(:user) followed_one = insert(:user)
@ -1215,10 +1236,34 @@ defmodule Pleroma.UserTest do
followed_one = User.get_cached_by_ap_id(followed_one.ap_id) followed_one = User.get_cached_by_ap_id(followed_one.ap_id)
followed_two = User.get_cached_by_ap_id(followed_two.ap_id) followed_two = User.get_cached_by_ap_id(followed_two.ap_id)
assert length(res) == 2
assert Enum.member?(res, followed_one) assert Enum.member?(res, followed_one)
assert Enum.member?(res, followed_two) assert Enum.member?(res, followed_two)
refute Enum.member?(res, not_followed) refute Enum.member?(res, not_followed)
end end
test "does not return deactivated followed users (friends)" do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
not_followed = insert(:user)
{:ok, user, followed_one} = User.follow(user, followed_one)
{:ok, user, followed_two} = User.follow(user, followed_two)
User.delete(followed_two)
res = User.get_friends(user)
followed_one = User.get_cached_by_id(followed_one.id)
followed_two = User.get_cached_by_id(followed_two.id)
assert length(res) == 1
assert Enum.member?(res, followed_one)
refute Enum.member?(res, followed_two)
refute Enum.member?(res, not_followed)
end
end end
describe "updating note and follower count" do describe "updating note and follower count" do
@ -2964,6 +3009,18 @@ defmodule Pleroma.UserTest do
assert [%{id: ^id2}] = User.get_familiar_followers(user3, user1) assert [%{id: ^id2}] = User.get_familiar_followers(user3, user1)
end end
test "does not return deactivated users" do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
User.follow(user1, user2)
User.follow(user2, user3)
{:ok, _} = User.set_activation(user2, false)
assert [] = User.get_familiar_followers(user3, user1)
end
end end
describe "account endorsements" do describe "account endorsements" do

View file

@ -3104,4 +3104,125 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
obj_id in object_ids obj_id in object_ids
end) end)
end end
describe "OAuth scopes" do
setup do
Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Config)
[user: insert(:user)]
end
defp with_token(conn, user, scopes) do
token = insert(:oauth_token, user: user, scopes: scopes)
conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> put_req_header("accept", "application/activity+json")
end
test "GET /api/ap/whoami needs read:accounts", %{conn: conn, user: user} do
assert conn |> with_token(user, ["profile"]) |> get("/api/ap/whoami") |> json_response(403)
assert %{"id" => id} =
conn
|> with_token(user, ["read:accounts"])
|> get("/api/ap/whoami")
|> json_response(200)
assert id == user.ap_id
end
test "GET /users/:nickname/inbox needs read:statuses", %{conn: conn, user: user} do
inbox = "/users/#{user.nickname}/inbox"
assert conn |> with_token(user, ["read:accounts"]) |> get(inbox) |> json_response(403)
assert conn |> with_token(user, ["read:statuses"]) |> get(inbox) |> json_response(200)
end
test "POST /users/:nickname/outbox needs write", %{conn: conn, user: user} do
activity = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"object" => %{
"type" => "Note",
"content" => "scoped",
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => []
}
}
post = fn scopes ->
conn
|> with_token(user, scopes)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
end
assert post.(["profile", "write:media"]) |> json_response(403)
assert post.(["write"]) |> json_response(201)
end
test "POST /api/ap/upload_media is forbidden without write:media", %{conn: conn, user: user} do
assert conn
|> with_token(user, ["write:statuses"])
|> post("/api/ap/upload_media", %{})
|> json_response(403)
end
# majic's libmagic port is unavailable on local Darwin runs; Linux CI still runs this test.
@tag :skip_darwin
test "POST /api/ap/upload_media works with write:media", %{conn: conn, user: user} do
image = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
assert conn
|> with_token(user, ["write:media"])
|> post("/api/ap/upload_media", %{"file" => image})
|> json_response(201)
end
test "GET /users/:nickname/outbox shows private posts only with read:statuses",
%{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "secret", visibility: "private"})
outbox = "/users/#{user.nickname}/outbox?page=true"
ids = fn scopes ->
conn
|> with_token(user, scopes)
|> get(outbox)
|> json_response(200)
|> Map.fetch!("orderedItems")
|> Enum.map(& &1["id"])
end
refute activity.data["id"] in ids.(["profile"])
assert activity.data["id"] in ids.(["read:statuses"])
end
test "GET /users/:nickname/followers shows hidden followers only with read:accounts",
%{conn: conn} do
user = insert(:user, hide_followers: true)
User.follow(insert(:user), user)
followers = "/users/#{user.nickname}/followers?page=1"
assert conn |> with_token(user, ["profile"]) |> get(followers) |> response(403)
assert %{"orderedItems" => [_]} =
conn |> with_token(user, ["read:accounts"]) |> get(followers) |> json_response(200)
end
test "GET /users/:nickname/following shows hidden follows only with read:accounts",
%{conn: conn} do
user = insert(:user, hide_follows: true)
User.follow(user, insert(:user))
following = "/users/#{user.nickname}/following?page=1"
assert conn |> with_token(user, ["profile"]) |> get(following) |> response(403)
assert %{"orderedItems" => [_]} =
conn |> with_token(user, ["read:accounts"]) |> get(following) |> json_response(200)
end
end
end end

View file

@ -244,6 +244,58 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
} }
end end
test "rejects actors whose inbox is on another host" do
ap_id = "https://example.com/users/evil"
user_data =
"test/fixtures/users_mock/user.json"
|> File.read!()
|> String.replace("{{nickname}}", "evil")
|> Jason.decode!()
|> Map.put("inbox", "https://victim.example/inbox")
|> Map.put("featured", nil)
|> Jason.encode!()
Tesla.Mock.mock(fn
%{method: :get, url: ^ap_id} ->
%Tesla.Env{
status: 200,
body: user_data,
headers: [{"content-type", "application/activity+json"}]
}
end)
assert {:error, _} = ActivityPub.make_user_from_ap_id(ap_id)
refute User.get_by_ap_id(ap_id)
end
test "updates the known user when fetched from a display URL" do
ap_id = "https://example.com/users/lain"
display_url = "https://example.com/@lain"
user = insert(:user, local: false, ap_id: ap_id, nickname: "lain@example.com")
user_data =
"test/fixtures/users_mock/user.json"
|> File.read!()
|> String.replace("{{nickname}}", "lain")
|> Jason.decode!()
|> Map.merge(%{"featured" => nil, "name" => "new name"})
|> Jason.encode!()
Tesla.Mock.mock(fn %{method: :get, url: url} when url in [ap_id, display_url] ->
%Tesla.Env{
status: 200,
body: user_data,
headers: [{"content-type", "application/activity+json"}]
}
end)
assert {:ok, %User{id: id, name: "new name"}} =
ActivityPub.make_user_from_ap_id(display_url)
assert id == user.id
end
test "fetches user featured collection" do test "fetches user featured collection" do
ap_id = "https://example.com/users/lain" ap_id = "https://example.com/users/lain"
@ -516,7 +568,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end end
test "fetches user birthday information from misskey" do test "fetches user birthday information from misskey" do
user_id = "https://misskey.io/@mkljczk" user_id = "https://misskey.io/users/8dhi2ne167"
Tesla.Mock.mock(fn Tesla.Mock.mock(fn
%{ %{
@ -2033,7 +2085,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
json( json(
%{ %{
"@context" => "https://www.w3.org/ns/activitystreams", "@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://remote.org/users/masto_hidden_counters/followers" "id" => "https://remote.org/users/masto_hidden_counters/following"
}, },
headers: HttpRequestMock.activitypub_object_headers() headers: HttpRequestMock.activitypub_object_headers()
) )
@ -2045,7 +2097,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
json( json(
%{ %{
"@context" => "https://www.w3.org/ns/activitystreams", "@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://remote.org/users/masto_hidden_counters/following" "id" => "https://remote.org/users/masto_hidden_counters/followers"
}, },
headers: HttpRequestMock.activitypub_object_headers() headers: HttpRequestMock.activitypub_object_headers()
) )

View file

@ -8,6 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
require Pleroma.Constants
setup do: setup do:
clear_config(:mrf_object_age, clear_config(:mrf_object_age,
threshold: 172_800, threshold: 172_800,
@ -79,7 +81,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
{:ok, data} = ObjectAgePolicy.filter(data) {:ok, data} = ObjectAgePolicy.filter(data)
assert Visibility.get_visibility(%{data: data}) == "unlisted" assert Visibility.get_visibility(%{data: data}) == "direct"
end end
test "it delists an old post" do test "it delists an old post" do
@ -94,6 +96,90 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
assert Visibility.get_visibility(%{data: data}) == "unlisted" assert Visibility.get_visibility(%{data: data}) == "unlisted"
end end
test "it keeps an old followers-only post private" do
clear_config([:mrf_object_age, :actions], [:delist])
data = get_old_message()
{:ok, user} = User.get_or_fetch_by_ap_id(data["actor"])
data =
data
|> Map.put("to", [user.follower_address])
|> Map.put("cc", [])
|> put_in(["object", "to"], [user.follower_address])
|> put_in(["object", "cc"], [])
{:ok, data} = ObjectAgePolicy.filter(data)
assert data["to"] == [user.follower_address]
assert data["cc"] == []
assert Visibility.get_visibility(%{data: data}) == "private"
assert data["object"]["to"] == data["to"]
assert data["object"]["cc"] == data["cc"]
end
test "it keeps an old direct message direct" do
clear_config([:mrf_object_age, :actions], [:delist])
data = get_old_message()
{:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"])
recipient = "https://example.com/users/someone"
data =
data
|> Map.put("to", [recipient])
|> Map.put("cc", [])
|> put_in(["object", "to"], [recipient])
|> put_in(["object", "cc"], [])
{:ok, data} = ObjectAgePolicy.filter(data)
assert data["to"] == [recipient]
assert data["cc"] == []
assert Visibility.get_visibility(%{data: data}) == "direct"
end
test "it removes every copy of the follower address" do
clear_config([:mrf_object_age, :actions], [:strip_followers])
data = get_old_message()
{:ok, user} = User.get_or_fetch_by_ap_id(data["actor"])
public = Pleroma.Constants.as_public()
followers = [user.follower_address, user.follower_address]
data =
data
|> Map.put("to", [public])
|> Map.put("cc", followers)
|> put_in(["object", "to"], [public])
|> put_in(["object", "cc"], followers)
{:ok, data} = ObjectAgePolicy.filter(data)
assert data["to"] == [public]
assert data["cc"] == []
end
test "it does not duplicate addresses of an old unlisted post" do
clear_config([:mrf_object_age, :actions], [:delist, :strip_followers])
data = get_old_message()
{:ok, user} = User.get_or_fetch_by_ap_id(data["actor"])
public = Pleroma.Constants.as_public()
data =
data
|> Map.put("to", [user.follower_address])
|> Map.put("cc", [public])
|> put_in(["object", "to"], [user.follower_address])
|> put_in(["object", "cc"], [public])
{:ok, data} = ObjectAgePolicy.filter(data)
assert data["to"] == []
assert data["cc"] == [public]
end
test "it allows a new post" do test "it allows a new post" do
clear_config([:mrf_object_age, :actions], [:delist]) clear_config([:mrf_object_age, :actions], [:delist])

View file

@ -139,4 +139,37 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
type: :group type: :group
} }
end end
describe "instance_allowed?/1" do
setup do: clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
setup do: clear_config([:mrf_simple, :reject], [{"rejected.example", "spam"}])
setup do: clear_config([:mrf_simple, :accept], [])
test "is false for hosts rejected by SimplePolicy, including subdomains" do
clear_config([:mrf_simple, :reject], [{"*.rejected.example", "spam"}])
refute MRF.instance_allowed?("rejected.example")
refute MRF.instance_allowed?("sub.rejected.example")
assert MRF.instance_allowed?("fine.example")
end
test "only allows accepted hosts when SimplePolicy has an accept list" do
clear_config([:mrf_simple, :accept], [{"friend.example", ""}])
assert MRF.instance_allowed?("friend.example")
refute MRF.instance_allowed?("stranger.example")
end
test "ignores the SimplePolicy lists when the policy is not enabled" do
clear_config([:mrf, :policies], [])
clear_config([:mrf_simple, :accept], [{"friend.example", ""}])
assert MRF.instance_allowed?("rejected.example")
assert MRF.instance_allowed?("stranger.example")
end
test "is false without a host" do
refute MRF.instance_allowed?(nil)
end
end
end end

View file

@ -0,0 +1,75 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidatorTest do
use ExUnit.Case, async: true
alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator
defp actor do
"test/fixtures/users_mock/user.json"
|> File.read!()
|> String.replace("{{nickname}}", "lain")
|> Jason.decode!()
end
test "accepts an actor whose endpoints and collections are on its own host" do
data = actor()
assert {:ok, ^data, []} = UserValidator.validate(data, [])
end
test "accepts an actor without optional fields" do
data = Map.drop(actor(), ["endpoints", "featured", "followers", "following", "publicKey"])
assert {:ok, _, _} = UserValidator.validate(data, [])
end
test "accepts all actor fixtures" do
for path <- Path.wildcard("test/fixtures/**/*.json"),
{:ok, %{"inbox" => _, "id" => id} = data} when is_binary(id) <-
[Jason.decode(File.read!(path))] do
assert {{:ok, _, _}, ^path} = {UserValidator.validate(data, []), path}
end
end
test "treats empty and null fields as missing" do
data =
actor()
|> Map.merge(%{"inbox" => "", "followers" => nil, "following" => "", "featured" => ""})
|> put_in(["endpoints", "sharedInbox"], "")
|> put_in(["publicKey", "id"], "")
assert {:ok, _, _} = UserValidator.validate(data, [])
end
for field <- ["inbox", "followers", "following", "featured"] do
test "rejects an actor whose #{field} is on another host" do
data = Map.put(actor(), unquote(field), "https://victim.example/users/lain/x")
assert {:error, _} = UserValidator.validate(data, [])
end
end
test "rejects an actor whose sharedInbox is on another host" do
data = put_in(actor(), ["endpoints", "sharedInbox"], "https://victim.example/inbox")
assert {:error, _} = UserValidator.validate(data, [])
end
test "rejects an actor whose collection is an object on another host" do
data = Map.put(actor(), "followers", %{"id" => "https://victim.example/users/v/followers"})
assert {:error, _} = UserValidator.validate(data, [])
end
test "rejects an actor whose key id is on another host" do
data = put_in(actor(), ["publicKey", "id"], "https://victim.example/users/lain#main-key")
assert {:error, _} = UserValidator.validate(data, [])
end
test "rejects an actor whose key id has no host" do
data = put_in(actor(), ["publicKey", "id"], "lain")
assert {:error, _} = UserValidator.validate(data, [])
end
test "rejects an actor without an id" do
assert {:error, _} = UserValidator.validate(Map.delete(actor(), "id"), [])
end
end

View file

@ -32,8 +32,26 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
refute Publisher.should_federate?(nil, true) refute Publisher.should_federate?(nil, true)
end end
test "it returns true when public is true" do test "it returns true for public activities to quarantined instances" do
assert Publisher.should_federate?(false, true) clear_config([:instance, :quarantined_instances], [{"quarantined.example", ""}])
assert Publisher.should_federate?("https://quarantined.example/inbox", true)
refute Publisher.should_federate?("https://quarantined.example/inbox", false)
end
test "it returns false for instances rejected by SimplePolicy, even for public activities" do
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
clear_config([:mrf_simple, :reject], [{"rejected.example", ""}])
refute Publisher.should_federate?("https://rejected.example/inbox", true)
refute Publisher.should_federate?("https://rejected.example/inbox", false)
assert Publisher.should_federate?("https://fine.example/inbox", true)
end
test "it returns false for inboxes without a host" do
clear_config([:instance, :quarantined_instances], [{"quarantined.example", ""}])
refute Publisher.should_federate?("inbox", false)
end end
end end
@ -204,6 +222,32 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
) )
end end
test "doesn't publish a public activity to instances rejected by SimplePolicy" do
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
clear_config([:mrf_simple, :reject], [{"rejected.example", "spam"}])
actor = insert(:user)
for host <- ["rejected.example", "fine.example"] do
follower = insert(:user, local: false, inbox: "https://#{host}/users/nick/inbox")
{:ok, _, _} = Pleroma.User.follow(follower, actor)
end
actor = refresh_record(actor)
note_activity =
insert(:note_activity, user: actor, recipients: [@as_public, actor.follower_address])
assert :ok == Publisher.publish(actor, note_activity)
inboxes =
all_enqueued(worker: "Pleroma.Workers.PublisherWorker")
|> Enum.map(& &1.args["params"]["inbox"])
assert "https://fine.example/users/nick/inbox" in inboxes
refute "https://rejected.example/users/nick/inbox" in inboxes
end
test "Publishes a non-public activity to non-quarantined instances." do test "Publishes a non-public activity to non-quarantined instances." do
Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}]) Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}])

View file

@ -11,8 +11,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
import Pleroma.Factory import Pleroma.Factory
# matches the host of the actor in mastodon-update.json
@gargron "http://mastodon.example.org/users/gargron"
test "it works for incoming update activities" do test "it works for incoming update activities" do
user = insert(:user, local: false) user = insert(:user, local: false, ap_id: @gargron)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!() update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
@ -50,8 +53,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
assert user.bio == "<p>Some bio</p>" assert user.bio == "<p>Some bio</p>"
end end
test "it rejects updates that move the shared inbox to another host" do
user = insert(:user, local: false, ap_id: @gargron)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
update_data["object"]
|> Map.put("actor", user.ap_id)
|> Map.put("id", user.ap_id)
|> put_in(["endpoints", "sharedInbox"], "https://victim.example/inbox")
update_data =
update_data
|> Map.put("actor", user.ap_id)
|> Map.put("object", object)
assert {:error, _} = Transmogrifier.handle_incoming(update_data)
assert User.get_cached_by_ap_id(user.ap_id).shared_inbox == user.shared_inbox
end
test "it selects an actor type after an extension type" do test "it selects an actor type after an extension type" do
user = insert(:user, local: false) user = insert(:user, local: false, ap_id: @gargron)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!() update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object = object =
@ -70,7 +93,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
end end
test "it works with alsoKnownAs" do test "it works with alsoKnownAs" do
%{ap_id: actor} = insert(:user, local: false) %{ap_id: actor} = insert(:user, local: false, ap_id: @gargron)
assert User.get_cached_by_ap_id(actor).also_known_as == [] assert User.get_cached_by_ap_id(actor).also_known_as == []
@ -97,7 +120,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
end end
test "it works with custom profile fields" do test "it works with custom profile fields" do
user = insert(:user, local: false) user = insert(:user, local: false, ap_id: @gargron)
assert user.fields == [] assert user.fields == []
@ -155,7 +178,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
end end
test "it works for incoming update activities which lock the account" do test "it works for incoming update activities which lock the account" do
user = insert(:user, local: false) user = insert(:user, local: false, ap_id: @gargron)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!() update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()

View file

@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.SetLocalePlug alias Pleroma.Web.Plugs.SetLocalePlug
@ -2033,6 +2034,60 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
assert id == to_string(user.id) assert id == to_string(user.id)
end end
test "the profile scope only gets the public account" do
user = insert(:user, default_scope: "private", pleroma_settings_store: %{"fe" => %{}})
%{conn: conn} = oauth_access(["profile"], user: user)
response =
conn
|> get("/api/v1/accounts/verify_credentials")
|> json_response_and_validate_schema(200)
assert response ==
AccountView.render("show.json", user: user, skip_visibility_check: true)
|> Jason.encode!()
|> Jason.decode!()
refute Map.has_key?(response["source"], "privacy")
refute Map.has_key?(response, "follow_requests_count")
for key <- ~w(email settings_store chat_token notification_settings
unread_notifications_count unread_conversation_count allow_following_move) do
refute Map.has_key?(response["pleroma"], key), "#{key} must not be shown"
end
end
test "the profile scope with read:accounts gets the full account" do
%{conn: conn} = oauth_access(["profile", "read:accounts"])
response =
conn
|> get("/api/v1/accounts/verify_credentials")
|> json_response_and_validate_schema(200)
assert response["source"]["privacy"]
assert response["pleroma"]["chat_token"]
end
test "the profile scope works when unauthenticated users cannot see profiles" do
clear_config([:restrict_unauthenticated, :profiles, :local], true)
%{user: user, conn: conn} = oauth_access(["profile"])
assert %{"id" => id} =
conn
|> get("/api/v1/accounts/verify_credentials")
|> json_response_and_validate_schema(200)
assert id == to_string(user.id)
end
test "the profile scope does not give access to other endpoints" do
%{conn: conn} = oauth_access(["profile"])
assert conn |> get("/api/v1/timelines/home") |> json_response(403)
assert conn |> get("/api/v1/follow_requests") |> json_response(403)
end
end end
describe "user relationships" do describe "user relationships" do

View file

@ -345,7 +345,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
bookmark_folder: nil, bookmark_folder: nil,
list_id: nil list_id: nil
}, },
quotes_count: 0 quotes_count: 0,
quote: nil
} }
assert status == expected assert status == expected
@ -507,6 +508,63 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
refute status.pleroma.quote_visible refute status.pleroma.quote_visible
end end
describe "Mastodon quote entity" do
test "a quote has the quoted status, and quoted statuses only a shallow quote" do
post = insert(:note_activity)
user = insert(:user)
{:ok, quote_post} = CommonAPI.post(user, %{status: "he", quoted_status_id: post.id})
{:ok, quoted_quote_post} =
CommonAPI.post(user, %{status: "yo", quoted_status_id: quote_post.id})
status = StatusView.render("show.json", %{activity: quoted_quote_post})
assert %{state: "accepted", quoted_status: %{id: quoted_id} = quoted} = status.quote
assert quoted_id == to_string(quote_post.id)
assert quoted.quote == %{state: "accepted", quoted_status_id: to_string(post.id)}
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
end
test "a quote of a post the user cannot see is unauthorized" do
user = insert(:user)
private = insert(:followers_only_note_activity, user: user)
quote_private =
insert(:note_activity,
note: insert(:note, data: %{"quoteUrl" => Object.normalize(private).data["id"]})
)
status = StatusView.render("show.json", %{activity: quote_private})
assert status.quote == %{state: "unauthorized", quoted_status: nil}
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
follower = insert(:user)
CommonAPI.follow(user, follower)
status = StatusView.render("show.json", %{activity: quote_private, for: follower})
assert %{state: "accepted", quoted_status: %{id: id}} = status.quote
assert id == to_string(private.id)
end
test "a quote of a missing post is deleted" do
quote_missing =
insert(:note_activity,
note: insert(:note, data: %{"quoteUrl" => "https://example.com/objects/gone"})
)
status = StatusView.render("show.json", %{activity: quote_missing})
assert status.quote == %{state: "deleted", quoted_status: nil}
end
test "a post without a quote has no quote" do
status = StatusView.render("show.json", %{activity: insert(:note_activity)})
assert status.quote == nil
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
end
end
test "repost of quote post" do test "repost of quote post" do
post = insert(:note_activity) post = insert(:note_activity)
user = insert(:user) user = insert(:user)
@ -559,6 +617,46 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
assert mention.url == recipient_ap_id assert mention.url == recipient_ap_id
end end
test "inlined images are media proxied" do
ConfigMock |> stub_with(Pleroma.Test.StaticConfig)
clear_config([:media_proxy, :enabled], true)
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
content_type: "text/html",
status: "hii <img src=\"https://example.org/a.png\" />"
})
activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity)
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
assert status[:content] =~
~r/^hii <img src="https?:\/\/[^\/]+\/proxy\/[^\/]+\/aHR0cHM6Ly9leGFtcGxlLm9yZy9hLnBuZw\/a.png"/
end
test "inlined images using network-path ref are media proxied" do
ConfigMock |> stub_with(Pleroma.Test.StaticConfig)
clear_config([:media_proxy, :enabled], true)
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
content_type: "text/html",
status: "hii <img src=\"//example.org/a.png\" />"
})
activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity)
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
assert status[:content] =~
~r/^hii <img src="https?:\/\/[^\/]+\/proxy\/[^\/]+\/aHR0cHM6Ly9leGFtcGxlLm9yZy9hLnBuZw\/a.png"/
end
test "create mentions from the 'tag' field" do test "create mentions from the 'tag' field" do
recipient = insert(:user) recipient = insert(:user)
cc = insert_pair(:user) |> Enum.map(& &1.ap_id) cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
@ -597,6 +695,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
end end
describe "attachments" do describe "attachments" do
test "skips meta for zero dimensions" do
for {width, height} <- [{0, 0}, {200, 0}, {0, 100}] do
object = %{
"type" => "Image",
"url" => [
%{
"mediaType" => "image/png",
"href" => "someurl",
"width" => width,
"height" => height
}
]
}
refute Map.has_key?(StatusView.render("attachment.json", %{attachment: object}), :meta)
end
end
test "Complete Mastodon style" do test "Complete Mastodon style" do
object = %{ object = %{
"type" => "Image", "type" => "Image",

View file

@ -59,6 +59,20 @@ defmodule Pleroma.Web.MediaProxyTest do
assert decode_result(encoded) == url assert decode_result(encoded) == url
end end
test "encodes and decodes a network-path reference URL as HTTPS" do
url = "//example.org/static/logo.png"
encoded = MediaProxy.url(url)
assert String.starts_with?(
encoded,
Config.get([:media_proxy, :base_url], Pleroma.Web.Endpoint.url())
)
assert String.ends_with?(encoded, "/logo.png")
assert decode_result(encoded) == "https:" <> url
end
test "encodes and decodes URL without a path" do test "encodes and decodes URL without a path" do
url = "https://pleroma.soykaf.com" url = "https://pleroma.soykaf.com"
encoded = MediaProxy.url(url) encoded = MediaProxy.url(url)

View file

@ -52,36 +52,60 @@ defmodule Pleroma.Web.OAuth.TokenControllerTest do
end end
describe "GET /api/oauth_tokens" do describe "GET /api/oauth_tokens" do
setup do test "renders list" do
token = insert(:oauth_token) |> Repo.preload(:user) %{conn: conn} = oauth_access(["read:security"])
%{token: token}
end
test "renders list", %{token: token} do
response =
build_conn()
|> assign(:user, token.user)
|> get("/api/oauth_tokens")
keys = keys =
json_response(response, 200) conn
|> get("/api/oauth_tokens")
|> json_response(200)
|> hd() |> hd()
|> Map.keys() |> Map.keys()
assert Enum.sort(keys) == Enum.sort(["id", "app_name", "valid_until", "scopes"]) assert Enum.sort(keys) == Enum.sort(["id", "app_name", "valid_until", "scopes"])
end end
test "revoke token", %{token: token} do test "is allowed with the read scope" do
response = %{conn: conn} = oauth_access(["read"])
build_conn()
|> assign(:user, token.user)
|> delete("/api/oauth_tokens/#{token.id}")
tokens = Token.get_user_tokens(token.user) assert [_] = conn |> get("/api/oauth_tokens") |> json_response(200)
end
assert tokens == [] test "is forbidden without read:security" do
assert response.status == 201 %{conn: conn} = oauth_access(["profile", "read:accounts", "write:security"])
assert %{"error" => "Insufficient permissions: read:security."} =
conn |> get("/api/oauth_tokens") |> json_response(403)
end
end
describe "DELETE /api/oauth_tokens/:id" do
test "revokes the token" do
%{conn: conn, user: user, token: token} = oauth_access(["write:security"])
other = insert(:oauth_token, user: user)
conn = delete(conn, "/api/oauth_tokens/#{other.id}")
assert conn.status == 201
assert [%Token{id: id}] = Token.get_user_tokens(user)
assert id == token.id
end
test "is allowed with the write scope" do
%{conn: conn, user: user} = oauth_access(["write"])
other = insert(:oauth_token, user: user)
assert delete(conn, "/api/oauth_tokens/#{other.id}").status == 201
end
test "is forbidden without write:security" do
%{conn: conn, user: user} = oauth_access(["profile", "read", "write:accounts"])
other = insert(:oauth_token, user: user)
assert %{"error" => "Insufficient permissions: write:security."} =
conn |> delete("/api/oauth_tokens/#{other.id}") |> json_response(403)
assert length(Token.get_user_tokens(user)) == 2
end end
end end
end end

View file

@ -0,0 +1,38 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSignaturePlugMRFTest do
# Not async: the MRF settings are global config.
use Pleroma.Web.ConnCase
alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock
alias Pleroma.Web.Plugs.HTTPSignaturePlug
import Mox
import Phoenix.Controller, only: [put_format: 2]
import Plug.Conn
setup do: clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
setup do: clear_config([:mrf_simple, :reject], [{"mastodon.example.org", "no reason"}])
defp signed_get(actor) do
ConfigMock
|> expect(:get, fn [:activitypub, :authorized_fetch_mode], false -> true end)
|> expect(:get, fn [:instance, :rejected_instances] -> [] end)
HTTPSignaturesMock
|> expect(:validate_conn, fn _ -> true end)
build_conn(:get, "/doesntmattter", %{"actor" => actor})
|> put_req_header("signature", "keyId=\"#{actor}#main-key")
|> put_format("activity+json")
|> HTTPSignaturePlug.call(%{})
end
test "rejects signed requests from instances rejected by SimplePolicy" do
assert signed_get("http://mastodon.example.org/users/admin").halted
refute signed_get("http://allowed.example.org/users/admin").halted
end
end

View file

@ -50,7 +50,9 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do
test "it restricts based on config values" do test "it restricts based on config values" do
limiter_name = :test_plug_opts limiter_name = :test_plug_opts
scale = 80 # Long enough that the bucket cannot expire during the test on slow CI.
# The test expires it explicitly with expire_ttl/2.
scale = 60_000
limit = 5 limit = 5
clear_config([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1}) clear_config([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1})
@ -184,7 +186,8 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do
test "can have limits separate from unauthenticated connections" do test "can have limits separate from unauthenticated connections" do
limiter_name = :test_authenticated1 limiter_name = :test_authenticated1
scale = 50 # Long enough that the bucket cannot expire during the test on slow CI.
scale = 60_000
limit = 5 limit = 5
clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
clear_config([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}]) clear_config([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}])

View file

@ -153,4 +153,38 @@ defmodule Pleroma.Workers.PublisherWorkerTest do
assert {:cancel, :unreachable} = PublisherWorker.perform(job) assert {:cancel, :unreachable} = PublisherWorker.perform(job)
end end
end end
describe "Blocked instances:" do
defp publish_one_job(inbox, activity) do
%Oban.Job{
args: %{
"op" => "publish_one",
"params" => %{"inbox" => inbox, "activity_id" => activity.id}
},
attempt: 1,
max_attempts: 5
}
end
test "cancels queued jobs to instances rejected by SimplePolicy" do
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
clear_config([:mrf_simple, :reject], [{"rejected.example", ""}])
{:ok, activity} = CommonAPI.post(insert(:user), %{status: "public"})
assert {:cancel, :rejected} =
PublisherWorker.perform(
publish_one_job("https://rejected.example/inbox", activity)
)
end
test "cancels queued private jobs to quarantined instances" do
clear_config([:instance, :quarantined_instances], [{"quarantined.example", ""}])
{:ok, activity} = CommonAPI.post(insert(:user), %{status: "hi", visibility: "private"})
assert {:cancel, :rejected} =
PublisherWorker.perform(
publish_one_job("https://quarantined.example/inbox", activity)
)
end
end
end end

View file

@ -577,6 +577,16 @@ defmodule HttpRequestMock do
}} }}
end end
# the same status via its canonical id
def get("http://mastodon.example.org/users/admin/statuses/99541947525187367", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/mastodon-note-object.json"),
headers: activitypub_object_headers()
}}
end
def get("http://mastodon.example.org/@admin/99541947525187368", _, _, _) do def get("http://mastodon.example.org/@admin/99541947525187368", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{