Merge remote-tracking branch 'origin/develop' into shigusegubu-new
This commit is contained in:
commit
04b6aa2031
83 changed files with 1834 additions and 271 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
1
changelog.d/actor-containment.security
Normal file
1
changelog.d/actor-containment.security
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Reject remote actors whose inbox, shared inbox, followers, following or featured collection, or key id is on another host
|
||||||
1
changelog.d/ap-c2s-oauth-scopes.security
Normal file
1
changelog.d/ap-c2s-oauth-scopes.security
Normal 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
|
||||||
1
changelog.d/attachment-zero-size.fix
Normal file
1
changelog.d/attachment-zero-size.fix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Do not crash when rendering attachments with zero width or height
|
||||||
1
changelog.d/federation-reject-instances.security
Normal file
1
changelog.d/federation-reject-instances.security
Normal 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
|
||||||
1
changelog.d/fetch-id-containment.security
Normal file
1
changelog.d/fetch-id-containment.security
Normal 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
|
||||||
1
changelog.d/filter-regex-escape.fix
Normal file
1
changelog.d/filter-regex-escape.fix
Normal 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
|
||||||
1
changelog.d/filter-whole-word.fix
Normal file
1
changelog.d/filter-whole-word.fix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Filters with whole_word set to false now also match inside words
|
||||||
1
changelog.d/get-actor-crash.fix
Normal file
1
changelog.d/get-actor-crash.fix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Reject fetched objects whose author has no id instead of crashing
|
||||||
1
changelog.d/mastodon-quote-entity.add
Normal file
1
changelog.d/mastodon-quote-entity.add
Normal 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
|
||||||
1
changelog.d/media-proxy-network-path.security
Normal file
1
changelog.d/media-proxy-network-path.security
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Proxy media given as network-path URLs (//host/path), so remote HTML cannot load images directly
|
||||||
1
changelog.d/mention-notify-access.security
Normal file
1
changelog.d/mention-notify-access.security
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Do not notify tagged users who cannot see the post
|
||||||
1
changelog.d/notification-filter-nil-content.fix
Normal file
1
changelog.d/notification-filter-nil-content.fix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Do not crash when creating notifications for posts without content while the user has filters
|
||||||
1
changelog.d/oauth-profile-scope.add
Normal file
1
changelog.d/oauth-profile-scope.add
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Support the Mastodon `profile` OAuth scope, which only gives the public account from verify_credentials
|
||||||
1
changelog.d/oauth-tokens-scopes.security
Normal file
1
changelog.d/oauth-tokens-scopes.security
Normal 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)
|
||||||
1
changelog.d/object-age-non-public.security
Normal file
1
changelog.d/object-age-non-public.security
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ObjectAgePolicy no longer makes old DMs and followers-only posts public
|
||||||
1
changelog.d/refetch-containment.security
Normal file
1
changelog.d/refetch-containment.security
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Do not let a refetched object (such as a poll) change its id or author
|
||||||
0
changelog.d/release-2.11.1.skip
Normal file
0
changelog.d/release-2.11.1.skip
Normal file
0
changelog.d/remove-dead-pleroma-api-controllers.skip
Normal file
0
changelog.d/remove-dead-pleroma-api-controllers.skip
Normal file
1
changelog.d/user-fetch-display-url.fix
Normal file
1
changelog.d/user-fetch-display-url.fix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Update the known user instead of failing when an actor is fetched from a display URL
|
||||||
1
changelog.d/user-query-is-active.fix
Normal file
1
changelog.d/user-query-is-active.fix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fix deactivated users being counted in followers collections and getting admin emails, because User.Query ignored the is_active key
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
6
mix.exs
6
mix.exs
|
|
@ -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},
|
||||||
|
|
|
||||||
10
mix.lock
10
mix.lock
|
|
@ -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"},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
2
test/fixtures/bridgy/actor.json
vendored
2
test/fixtures/bridgy/actor.json
vendored
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": [{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"}]) ==
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 = %{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"}])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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!()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
38
test/pleroma/web/plugs/http_signature_plug_mrf_test.exs
Normal file
38
test/pleroma/web/plugs/http_signature_plug_mrf_test.exs
Normal 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
|
||||||
|
|
@ -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}])
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue