Merge remote-tracking branch 'origin/develop' into fork

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-09-02 00:26:35 +02:00
commit acdda8d4df
27 changed files with 699 additions and 397 deletions

View file

View file

@ -0,0 +1 @@
Restrict incoming activities from unknown actors to a subset that does not imply a previous relationship and early rejection of unrecognized activity types.

View file

@ -0,0 +1 @@
Prevent OAuth App flow from creating duplicate entries

View file

@ -0,0 +1 @@
ReceiverWorker will cancel processing jobs instead of retrying if the user cannot be fetched due to 403, 404, or 410 errors or if the account is disabled locally.

View file

@ -0,0 +1 @@
Rich Media preview fetching will skip making an HTTP HEAD request to check a URL for allowed content type and length if the Tesla adapter is Gun or Finch

View file

View file

@ -433,7 +433,7 @@ Response:
* On success: URL of the unfollowed relay * On success: URL of the unfollowed relay
```json ```json
{"https://example.com/relay"} "https://example.com/relay"
``` ```
## `POST /api/v1/pleroma/admin/users/invite_token` ## `POST /api/v1/pleroma/admin/users/invite_token`
@ -1193,20 +1193,23 @@ Loads json generated from `config/descriptions.exs`.
- Response: - Response:
```json ```json
[ {
{ "items": [
"id": 1234, {
"data": { "id": 1234,
"actor": { "data": {
"id": 1, "actor": {
"nickname": "lain" "id": 1,
"nickname": "lain"
},
"action": "relay_follow"
}, },
"action": "relay_follow" "time": 1502812026, // timestamp
}, "message": "[2017-08-15 15:47:06] @nick0 followed relay: https://example.org/relay" // log message
"time": 1502812026, // timestamp }
"message": "[2017-08-15 15:47:06] @nick0 followed relay: https://example.org/relay" // log message ],
} "total": 1
] }
``` ```
## `POST /api/v1/pleroma/admin/reload_emoji` ## `POST /api/v1/pleroma/admin/reload_emoji`

View file

@ -100,6 +100,36 @@ defmodule Pleroma.Constants do
] ]
) )
const(activity_types,
do: [
"Create",
"Update",
"Delete",
"Follow",
"Accept",
"Reject",
"Add",
"Remove",
"Like",
"Announce",
"Undo",
"Flag",
"EmojiReact"
]
)
const(allowed_activity_types_from_strangers,
do: [
"Block",
"Create",
"Flag",
"Follow",
"Like",
"EmojiReact",
"Announce"
]
)
# basic regex, just there to weed out potential mistakes # basic regex, just there to weed out potential mistakes
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1 # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
const(mime_regex, const(mime_regex,

View file

@ -52,6 +52,7 @@ defp adapter_helper do
case adapter() do case adapter() do
Tesla.Adapter.Gun -> AdapterHelper.Gun Tesla.Adapter.Gun -> AdapterHelper.Gun
Tesla.Adapter.Hackney -> AdapterHelper.Hackney Tesla.Adapter.Hackney -> AdapterHelper.Hackney
{Tesla.Adapter.Finch, _} -> AdapterHelper.Finch
_ -> AdapterHelper.Default _ -> AdapterHelper.Default
end end
end end
@ -118,4 +119,13 @@ def format_host(host) do
host_charlist host_charlist
end end
end end
@spec can_stream? :: bool()
def can_stream? do
case Application.get_env(:tesla, :adapter) do
Tesla.Adapter.Gun -> true
{Tesla.Adapter.Finch, _} -> true
_ -> false
end
end
end end

View file

@ -0,0 +1,33 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.AdapterHelper.Finch do
@behaviour Pleroma.HTTP.AdapterHelper
alias Pleroma.Config
alias Pleroma.HTTP.AdapterHelper
@spec options(keyword(), URI.t()) :: keyword()
def options(incoming_opts \\ [], %URI{} = _uri) do
proxy =
[:http, :proxy_url]
|> Config.get()
|> AdapterHelper.format_proxy()
config_opts = Config.get([:http, :adapter], [])
config_opts
|> Keyword.merge(incoming_opts)
|> AdapterHelper.maybe_add_proxy(proxy)
|> maybe_stream()
end
# Finch uses [response: :stream]
defp maybe_stream(opts) do
case Keyword.pop(opts, :stream, nil) do
{true, opts} -> Keyword.put(opts, :response, :stream)
{_, opts} -> opts
end
end
end

View file

@ -32,6 +32,7 @@ def options(incoming_opts \\ [], %URI{} = uri) do
|> AdapterHelper.maybe_add_proxy(proxy) |> AdapterHelper.maybe_add_proxy(proxy)
|> Keyword.merge(incoming_opts) |> Keyword.merge(incoming_opts)
|> put_timeout() |> put_timeout()
|> maybe_stream()
end end
defp add_scheme_opts(opts, %{scheme: "http"}), do: opts defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
@ -47,6 +48,14 @@ defp put_timeout(opts) do
Keyword.put(opts, :timeout, recv_timeout) Keyword.put(opts, :timeout, recv_timeout)
end end
# Gun uses [body_as: :stream]
defp maybe_stream(opts) do
case Keyword.pop(opts, :stream, nil) do
{true, opts} -> Keyword.put(opts, :body_as, :stream)
{_, opts} -> opts
end
end
@spec pool_timeout(pool()) :: non_neg_integer() @spec pool_timeout(pool()) :: non_neg_integer()
def pool_timeout(pool) do def pool_timeout(pool) do
default = Config.get([:pools, :default, :recv_timeout], 5_000) default = Config.get([:pools, :default, :recv_timeout], 5_000)

View file

@ -20,15 +20,13 @@ def safe_put_in(data, keys, value) when is_map(data) and is_list(keys) do
end end
def filter_empty_values(data) do def filter_empty_values(data) do
# TODO: Change to Map.filter in Elixir 1.13+
data data
|> Enum.filter(fn |> Map.filter(fn
{_k, nil} -> false {_k, nil} -> false
{_k, ""} -> false {_k, ""} -> false
{_k, []} -> false {_k, []} -> false
{_k, %{} = v} -> Map.keys(v) != [] {_k, %{} = v} -> Map.keys(v) != []
{_k, _v} -> true {_k, _v} -> true
end) end)
|> Map.new()
end end
end end

View file

@ -321,7 +321,7 @@ def inbox(conn, %{"type" => "Create"} = params) do
post_inbox_relayed_create(conn, params) post_inbox_relayed_create(conn, params)
else else
conn conn
|> put_status(:bad_request) |> put_status(403)
|> json("Not federating") |> json("Not federating")
end end
end end

View file

@ -102,7 +102,8 @@ def perform(:incoming_ap_doc, params) do
# NOTE: we use the actor ID to do the containment, this is fine because an # NOTE: we use the actor ID to do the containment, this is fine because an
# actor shouldn't be acting on objects outside their own AP server. # actor shouldn't be acting on objects outside their own AP server.
with {_, {:ok, _user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)}, with {_, {:ok, user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)},
{:user_active, true} <- {:user_active, match?(true, user.is_active)},
nil <- Activity.normalize(params["id"]), nil <- Activity.normalize(params["id"]),
{_, :ok} <- {_, :ok} <-
{:correct_origin?, Containment.contain_origin_from_id(actor, params)}, {:correct_origin?, Containment.contain_origin_from_id(actor, params)},
@ -121,11 +122,6 @@ def perform(:incoming_ap_doc, params) do
Logger.debug("Unhandled actor #{actor}, #{inspect(e)}") Logger.debug("Unhandled actor #{actor}, #{inspect(e)}")
{:error, e} {:error, e}
{:error, {:validate_object, _}} = e ->
Logger.error("Incoming AP doc validation error: #{inspect(e)}")
Logger.debug(Jason.encode!(params, pretty: true))
e
e -> e ->
# Just drop those for now # Just drop those for now
Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end) Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end)

View file

@ -36,8 +36,7 @@ def create(%{body_params: params} = conn, _params) do
|> Map.put(:scopes, scopes) |> Map.put(:scopes, scopes)
|> Maps.put_if_present(:user_id, user_id) |> Maps.put_if_present(:user_id, user_id)
with cs <- App.register_changeset(%App{}, app_attrs), with {:ok, app} <- App.get_or_make(app_attrs) do
{:ok, app} <- Repo.insert(cs) do
render(conn, "show.json", app: app) render(conn, "show.json", app: app)
end end
end end

View file

@ -871,19 +871,7 @@ defp build_application(%{"type" => _type, "name" => name, "url" => url}),
defp build_application(_), do: nil defp build_application(_), do: nil
# Workaround for Elixir issue #10771 @spec build_image_url(URI.t(), URI.t()) :: String.t()
# Avoid applying URI.merge unless necessary
# TODO: revert to always attempting URI.merge(image_url_data, page_url_data)
# when Elixir 1.12 is the minimum supported version
@spec build_image_url(struct() | nil, struct()) :: String.t() | nil
defp build_image_url(
%URI{scheme: image_scheme, host: image_host} = image_url_data,
%URI{} = _page_url_data
)
when not is_nil(image_scheme) and not is_nil(image_host) do
image_url_data |> to_string
end
defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
URI.merge(page_url_data, image_url_data) |> to_string URI.merge(page_url_data, image_url_data) |> to_string
end end

View file

@ -67,35 +67,27 @@ def update(id, params) do
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
app app
|> changeset(params) |> changeset(params)
|> validate_required([:scopes])
|> Repo.update() |> Repo.update()
end end
end end
@doc """ @doc """
Gets app by attrs or create new with attrs. Gets app by attrs or create new with attrs.
And updates the scopes if need. Updates the attrs if needed.
""" """
@spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()} @spec get_or_make(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def get_or_make(attrs, scopes) do def get_or_make(attrs) do
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do with %__MODULE__{} = app <- Repo.get_by(__MODULE__, client_name: attrs.client_name) do
update_scopes(app, scopes) __MODULE__.update(app.id, Map.take(attrs, [:scopes, :website]))
else else
_e -> _e ->
%__MODULE__{} %__MODULE__{}
|> register_changeset(Map.put(attrs, :scopes, scopes)) |> register_changeset(attrs)
|> Repo.insert() |> Repo.insert()
end end
end end
defp update_scopes(%__MODULE__{} = app, []), do: {:ok, app}
defp update_scopes(%__MODULE__{scopes: scopes} = app, scopes), do: {:ok, app}
defp update_scopes(%__MODULE__{} = app, scopes) do
app
|> change(%{scopes: scopes})
|> Repo.update()
end
@spec search(map()) :: {:ok, [t()], non_neg_integer()} @spec search(map()) :: {:ok, [t()], non_neg_integer()}
def search(params) do def search(params) do
query = from(a in __MODULE__) query = from(a in __MODULE__)

View file

@ -0,0 +1,89 @@
# 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.InboxGuardPlug do
import Plug.Conn
import Pleroma.Constants, only: [activity_types: 0, allowed_activity_types_from_strangers: 0]
alias Pleroma.Config
alias Pleroma.User
def init(options) do
options
end
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
with {_, true} <- {:federating, Config.get!([:instance, :federating])} do
conn
|> filter_activity_types()
else
{:federating, false} ->
conn
|> json(403, "Not federating")
|> halt()
end
end
def call(conn, _opts) do
with {_, true} <- {:federating, Config.get!([:instance, :federating])},
conn = filter_activity_types(conn),
{:known, true} <- {:known, known_actor?(conn)} do
conn
else
{:federating, false} ->
conn
|> json(403, "Not federating")
|> halt()
{:known, false} ->
conn
|> filter_from_strangers()
end
end
# Early rejection of unrecognized types
defp filter_activity_types(%{body_params: %{"type" => type}} = conn) do
with true <- type in activity_types() do
conn
else
_ ->
conn
|> json(400, "Invalid activity type")
|> halt()
end
end
# If signature failed but we know this actor we should
# accept it as we may only need to refetch their public key
# during processing
defp known_actor?(%{body_params: data}) do
case Pleroma.Object.Containment.get_actor(data) |> User.get_cached_by_ap_id() do
%User{} -> true
_ -> false
end
end
# Only permit a subset of activity types from strangers
# or else it will add actors you've never interacted with
# to the database
defp filter_from_strangers(%{body_params: %{"type" => type}} = conn) do
with true <- type in allowed_activity_types_from_strangers() do
conn
else
_ ->
conn
|> json(400, "Invalid activity type for an unknown actor")
|> halt()
end
end
defp json(conn, status, resp) do
json_resp = Jason.encode!(resp)
conn
|> put_resp_content_type("application/json")
|> resp(status, json_resp)
|> halt()
end
end

View file

@ -10,31 +10,40 @@ defmodule Pleroma.Web.RichMedia.Helpers do
@type get_errors :: {:error, :body_too_large | :content_type | :head | :get} @type get_errors :: {:error, :body_too_large | :content_type | :head | :get}
@spec rich_media_get(String.t()) :: {:ok, String.t()} | get_errors() @spec rich_media_get(String.t()) :: {:ok, String.t()} | get_errors()
def rich_media_get(url) do
defp headers do case Pleroma.HTTP.AdapterHelper.can_stream?() do
user_agent = true -> stream(url)
case Pleroma.Config.get([:rich_media, :user_agent], :default) do false -> head_first(url)
:default -> end
Pleroma.Application.user_agent() <> "; Bot" |> handle_result(url)
custom ->
custom
end
[{"user-agent", user_agent}]
end end
def rich_media_get(url) do defp stream(url) do
headers = headers() with {_, {:ok, %Tesla.Env{status: 200, body: stream_body, headers: headers}}} <-
{:get, Pleroma.HTTP.get(url, req_headers(), http_options())},
{_, :ok} <- {:content_type, check_content_type(headers)},
{_, :ok} <- {:content_length, check_content_length(headers)},
{:read_stream, {:ok, body}} <- {:read_stream, read_stream(stream_body)} do
{:ok, body}
end
end
defp head_first(url) do
with {_, {:ok, %Tesla.Env{status: 200, headers: headers}}} <- with {_, {:ok, %Tesla.Env{status: 200, headers: headers}}} <-
{:head, Pleroma.HTTP.head(url, headers, http_options())}, {:head, Pleroma.HTTP.head(url, req_headers(), http_options())},
{_, :ok} <- {:content_type, check_content_type(headers)}, {_, :ok} <- {:content_type, check_content_type(headers)},
{_, :ok} <- {:content_length, check_content_length(headers)}, {_, :ok} <- {:content_length, check_content_length(headers)},
{_, {:ok, %Tesla.Env{status: 200, body: body}}} <- {_, {:ok, %Tesla.Env{status: 200, body: body}}} <-
{:get, Pleroma.HTTP.get(url, headers, http_options())} do {:get, Pleroma.HTTP.get(url, req_headers(), http_options())} do
{:ok, body} {:ok, body}
else end
end
defp handle_result(result, url) do
case result do
{:ok, body} ->
{:ok, body}
{:head, _} -> {:head, _} ->
Logger.debug("Rich media error for #{url}: HTTP HEAD failed") Logger.debug("Rich media error for #{url}: HTTP HEAD failed")
{:error, :head} {:error, :head}
@ -43,8 +52,12 @@ def rich_media_get(url) do
Logger.debug("Rich media error for #{url}: content-type is #{type}") Logger.debug("Rich media error for #{url}: content-type is #{type}")
{:error, :content_type} {:error, :content_type}
{:content_length, {_, length}} -> {:content_length, :error} ->
Logger.debug("Rich media error for #{url}: content-length is #{length}") Logger.debug("Rich media error for #{url}: content-length exceeded")
{:error, :body_too_large}
{:read_stream, :error} ->
Logger.debug("Rich media error for #{url}: content-length exceeded")
{:error, :body_too_large} {:error, :body_too_large}
{:get, _} -> {:get, _} ->
@ -73,7 +86,7 @@ defp check_content_length(headers) do
{_, maybe_content_length} -> {_, maybe_content_length} ->
case Integer.parse(maybe_content_length) do case Integer.parse(maybe_content_length) do
{content_length, ""} when content_length <= max_body -> :ok {content_length, ""} when content_length <= max_body -> :ok
{_, ""} -> {:error, maybe_content_length} {_, ""} -> :error
_ -> :ok _ -> :ok
end end
@ -82,13 +95,46 @@ defp check_content_length(headers) do
end end
end end
defp http_options do defp read_stream(stream) do
timeout = Config.get!([:rich_media, :timeout]) max_body = Keyword.get(http_options(), :max_body)
try do
result =
Stream.transform(stream, 0, fn chunk, total_bytes ->
new_total = total_bytes + byte_size(chunk)
if new_total > max_body do
raise("Exceeds max body limit of #{max_body}")
else
{[chunk], new_total}
end
end)
|> Enum.into(<<>>)
{:ok, result}
rescue
_ -> :error
end
end
defp http_options do
[ [
pool: :rich_media, pool: :rich_media,
max_body: Config.get([:rich_media, :max_body], 5_000_000), max_body: Config.get([:rich_media, :max_body], 5_000_000),
tesla_middleware: [{Tesla.Middleware.Timeout, timeout: timeout}] stream: true
] ]
end end
defp req_headers do
user_agent =
case Pleroma.Config.get([:rich_media, :user_agent], :default) do
:default ->
Pleroma.Application.user_agent() <> "; Bot"
custom ->
custom
end
[{"user-agent", user_agent}]
end
end end

View file

@ -217,6 +217,10 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end end
pipeline :inbox_guard do
plug(Pleroma.Web.Plugs.InboxGuardPlug)
end
pipeline :static_fe do pipeline :static_fe do
plug(Pleroma.Web.Plugs.StaticFEPlug) plug(Pleroma.Web.Plugs.StaticFEPlug)
end end
@ -1077,7 +1081,7 @@ defmodule Pleroma.Web.Router do
end end
scope "/", Pleroma.Web.ActivityPub do scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub) pipe_through([:activitypub, :inbox_guard])
post("/inbox", ActivityPubController, :inbox) post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox) post("/users/:nickname/inbox", ActivityPubController, :inbox)
end end

View file

@ -33,7 +33,7 @@ def perform(%Job{
query_string: query_string query_string: query_string
} }
with {:ok, %User{} = _actor} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]),
{:ok, _public_key} <- Signature.refetch_public_key(conn_data), {:ok, _public_key} <- Signature.refetch_public_key(conn_data),
{:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, {:signature, true} <- {:signature, Signature.validate_signature(conn_data)},
{:ok, res} <- Federator.perform(:incoming_ap_doc, params) do {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
@ -56,17 +56,29 @@ def timeout(%_{args: %{"timeout" => timeout}}), do: timeout
def timeout(_job), do: :timer.seconds(5) def timeout(_job), do: :timer.seconds(5)
defp process_errors({:error, {:error, _} = error}), do: process_errors(error)
defp process_errors(errors) do defp process_errors(errors) do
case errors do case errors do
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} # User fetch failures
{:error, :already_present} -> {:cancel, :already_present}
{:error, {:validate_object, _} = reason} -> {:cancel, reason}
{:error, {:error, {:validate, {:error, _changeset} = reason}}} -> {:cancel, reason}
{:error, {:reject, _} = reason} -> {:cancel, reason}
{:signature, false} -> {:cancel, :invalid_signature}
{:error, "Object has been deleted"} = reason -> {:cancel, reason}
{:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
{:error, :not_found} = reason -> {:cancel, reason} {:error, :not_found} = reason -> {:cancel, reason}
{:error, :forbidden} = reason -> {:cancel, reason}
# Inactive user
{:error, {:user_active, false} = reason} -> {:cancel, reason}
# Validator will error and return a changeset error
# e.g., duplicate activities or if the object was deleted
{:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason}
# Duplicate detection during Normalization
{:error, :already_present} -> {:cancel, :already_present}
# MRFs will return a reject
{:error, {:reject, _} = reason} -> {:cancel, reason}
# HTTP Sigs
{:signature, false} -> {:cancel, :invalid_signature}
# Origin / URL validation failed somewhere possibly due to spoofing
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
# Unclear if this can be reached
{:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
# Catchall
{:error, _} = e -> e {:error, _} = e -> e
e -> {:error, e} e -> {:error, e}
end end

View file

@ -1,117 +0,0 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"Curve25519Key": "toot:Curve25519Key",
"Device": "toot:Device",
"Ed25519Key": "toot:Ed25519Key",
"Ed25519Signature": "toot:Ed25519Signature",
"EncryptedMessage": "toot:EncryptedMessage",
"PropertyValue": "schema:PropertyValue",
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"cipherText": "toot:cipherText",
"claim": {
"@id": "toot:claim",
"@type": "@id"
},
"deviceId": "toot:deviceId",
"devices": {
"@id": "toot:devices",
"@type": "@id"
},
"discoverable": "toot:discoverable",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"featuredTags": {
"@id": "toot:featuredTags",
"@type": "@id"
},
"fingerprintKey": {
"@id": "toot:fingerprintKey",
"@type": "@id"
},
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
},
"identityKey": {
"@id": "toot:identityKey",
"@type": "@id"
},
"indexable": "toot:indexable",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"memorial": "toot:memorial",
"messageFranking": "toot:messageFranking",
"messageType": "toot:messageType",
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"publicKeyBase64": "toot:publicKeyBase64",
"schema": "http://schema.org#",
"suspended": "toot:suspended",
"toot": "http://joinmastodon.org/ns#",
"value": "schema:value"
}
],
"attachment": [
{
"name": "Website",
"type": "PropertyValue",
"value": "<a href=\"https://bastianallgeier.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"\">bastianallgeier.com</span><span class=\"invisible\"></span></a>"
},
{
"name": "Project",
"type": "PropertyValue",
"value": "<a href=\"https://getkirby.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"\">getkirby.com</span><span class=\"invisible\"></span></a>"
},
{
"name": "Github",
"type": "PropertyValue",
"value": "<a href=\"https://github.com/bastianallgeier\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"\">github.com/bastianallgeier</span><span class=\"invisible\"></span></a>"
}
],
"devices": "https://mastodon.social/users/bastianallgeier/collections/devices",
"discoverable": true,
"endpoints": {
"sharedInbox": "https://mastodon.social/inbox"
},
"featured": "https://mastodon.social/users/bastianallgeier/collections/featured",
"featuredTags": "https://mastodon.social/users/bastianallgeier/collections/tags",
"followers": "https://mastodon.social/users/bastianallgeier/followers",
"following": "https://mastodon.social/users/bastianallgeier/following",
"icon": {
"mediaType": "image/jpeg",
"type": "Image",
"url": "https://files.mastodon.social/accounts/avatars/000/007/393/original/0180a20079617c71.jpg"
},
"id": "https://mastodon.social/users/bastianallgeier",
"image": {
"mediaType": "image/jpeg",
"type": "Image",
"url": "https://files.mastodon.social/accounts/headers/000/007/393/original/13d644ab46d50478.jpeg"
},
"inbox": "https://mastodon.social/users/bastianallgeier/inbox",
"indexable": false,
"manuallyApprovesFollowers": false,
"memorial": false,
"name": "Bastian Allgeier",
"outbox": "https://mastodon.social/users/bastianallgeier/outbox",
"preferredUsername": "bastianallgeier",
"publicKey": {
"id": "https://mastodon.social/users/bastianallgeier#main-key",
"owner": "https://mastodon.social/users/bastianallgeier",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3fz+hpgVztO9z6HUhyzv\nwP++ERBBoIwSLKf1TyIM8bvzGFm2YXaO5uxu1HvumYFTYc3ACr3q4j8VUb7NMxkQ\nlzu4QwPjOFJ43O+fY+HSPORXEDW5fXDGC5DGpox4+i08LxRmx7L6YPRUSUuPN8nI\nWyq1Qsq1zOQrNY/rohMXkBdSXxqC3yIRqvtLt4otCgay/5tMogJWkkS6ZKyFhb9z\nwVVy1fsbV10c9C+SHy4NH26CKaTtpTYLRBMjhTCS8bX8iDSjGIf2aZgYs1ir7gEz\n9wf5CvLiENmVWGwm64t6KSEAkA4NJ1hzgHUZPCjPHZE2SmhO/oHaxokTzqtbbENJ\n1QIDAQAB\n-----END PUBLIC KEY-----\n"
},
"published": "2016-11-01T00:00:00Z",
"summary": "<p>Designer &amp; developer. Creator of Kirby CMS</p>",
"tag": [],
"type": "Person",
"url": "https://mastodon.social/@bastianallgeier"
}

View file

@ -1,62 +1,109 @@
{ {
"@context": [ "@context": [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{ {
"claim": {
"@id": "toot:claim",
"@type": "@id"
},
"memorial": "toot:memorial",
"atomUri": "ostatus:atomUri", "atomUri": "ostatus:atomUri",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"blurhash": "toot:blurhash", "blurhash": "toot:blurhash",
"conversation": "ostatus:conversation", "ostatus": "http://ostatus.org#",
"discoverable": "toot:discoverable",
"focalPoint": { "focalPoint": {
"@container": "@list", "@container": "@list",
"@id": "toot:focalPoint" "@id": "toot:focalPoint"
}, },
"inReplyToAtomUri": "ostatus:inReplyToAtomUri", "votersCount": "toot:votersCount",
"ostatus": "http://ostatus.org#", "Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji",
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"sensitive": "as:sensitive", "sensitive": "as:sensitive",
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"Device": "toot:Device",
"schema": "http://schema.org#",
"toot": "http://joinmastodon.org/ns#", "toot": "http://joinmastodon.org/ns#",
"votersCount": "toot:votersCount" "cipherText": "toot:cipherText",
"suspended": "toot:suspended",
"messageType": "toot:messageType",
"featuredTags": {
"@id": "toot:featuredTags",
"@type": "@id"
},
"Curve25519Key": "toot:Curve25519Key",
"deviceId": "toot:deviceId",
"Ed25519Signature": "toot:Ed25519Signature",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"devices": {
"@id": "toot:devices",
"@type": "@id"
},
"value": "schema:value",
"PropertyValue": "schema:PropertyValue",
"messageFranking": "toot:messageFranking",
"publicKeyBase64": "toot:publicKeyBase64",
"identityKey": {
"@id": "toot:identityKey",
"@type": "@id"
},
"Ed25519Key": "toot:Ed25519Key",
"indexable": "toot:indexable",
"EncryptedMessage": "toot:EncryptedMessage",
"fingerprintKey": {
"@id": "toot:fingerprintKey",
"@type": "@id"
}
} }
], ],
"atomUri": "https://chaos.social/users/distantnative/statuses/109336635639931467", "actor": "https://phpc.social/users/denniskoch",
"attachment": [ "cc": [],
{ "id": "https://phpc.social/users/denniskoch/statuses/112847382711461301/activity",
"blurhash": "UAK1zS00OXIUxuMxIUM{?b-:-;W:Di?b%2M{",
"height": 960,
"mediaType": "image/jpeg",
"name": null,
"type": "Document",
"url": "https://assets.chaos.social/media_attachments/files/109/336/634/286/114/657/original/2e6122063d8bfb26.jpeg",
"width": 346
}
],
"attributedTo": "https://chaos.social/users/distantnative",
"cc": [
"https://chaos.social/users/distantnative/followers"
],
"content": "<p>Favorite piece of anthropology meta discourse.</p>",
"contentMap": {
"en": "<p>Favorite piece of anthropology meta discourse.</p>"
},
"conversation": "tag:chaos.social,2022-11-13:objectId=71843781:objectType=Conversation",
"id": "https://chaos.social/users/distantnative/statuses/109336635639931467",
"inReplyTo": null, "inReplyTo": null,
"inReplyToAtomUri": null, "inReplyToAtomUri": null,
"published": "2022-11-13T13:04:20Z", "object": {
"replies": { "atomUri": "https://phpc.social/users/denniskoch/statuses/112847382711461301",
"first": { "attachment": [],
"items": [], "attributedTo": "https://phpc.social/users/denniskoch",
"next": "https://chaos.social/users/distantnative/statuses/109336635639931467/replies?only_other_accounts=true&page=true", "cc": [],
"partOf": "https://chaos.social/users/distantnative/statuses/109336635639931467/replies", "content": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://mastodon.social/@bastianallgeier\" class=\"u-url mention\">@<span>bastianallgeier</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://chaos.social/@distantnative\" class=\"u-url mention\">@<span>distantnative</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://fosstodon.org/@kev\" class=\"u-url mention\">@<span>kev</span></a></span> Another main argument: Discord is popular. Many people have an account, so you can just join an server quickly. Also you know the app and how to get around.</p>",
"type": "CollectionPage" "contentMap": {
"en": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://mastodon.social/@bastianallgeier\" class=\"u-url mention\">@<span>bastianallgeier</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://chaos.social/@distantnative\" class=\"u-url mention\">@<span>distantnative</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://fosstodon.org/@kev\" class=\"u-url mention\">@<span>kev</span></a></span> Another main argument: Discord is popular. Many people have an account, so you can just join an server quickly. Also you know the app and how to get around.</p>"
}, },
"id": "https://chaos.social/users/distantnative/statuses/109336635639931467/replies", "conversation": "tag:mastodon.social,2024-07-25:objectId=760068442:objectType=Conversation",
"type": "Collection" "id": "https://phpc.social/users/denniskoch/statuses/112847382711461301",
"published": "2024-07-25T13:33:29Z",
"replies": null,
"sensitive": false,
"tag": [],
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Note",
"url": "https://phpc.social/@denniskoch/112847382711461301"
},
"published": "2024-07-25T13:33:29Z",
"signature": {
"created": "2024-07-25T13:33:29Z",
"creator": "https://phpc.social/users/denniskoch#main-key",
"signatureValue": "slz9BKJzd2n1S44wdXGOU+bV/wsskdgAaUpwxj8R16mYOL8+DTpE6VnfSKoZGsBBJT8uG5gnVfVEz1YsTUYtymeUgLMh7cvd8VnJnZPS+oixbmBRVky/Myf91TEgQQE7G4vDmTdB4ii54hZrHcOOYYf5FKPNRSkMXboKA6LMqNtekhbI+JTUJYIB02WBBK6PUyo15f6B1RJ6HGWVgud9NE0y1EZXfrkqUt682p8/9D49ORf7AwjXUJibKic2RbPvhEBj70qUGfBm4vvgdWhSUn1IG46xh+U0+NrTSUED82j1ZVOeua/2k/igkGs8cSBkY35quXTkPz6gbqCCH66CuA==",
"type": "RsaSignature2017"
}, },
"sensitive": false,
"summary": null,
"tag": [],
"to": [ "to": [
"https://www.w3.org/ns/activitystreams#Public" "https://www.w3.org/ns/activitystreams#Public"
], ],
"type": "Note", "type": "Create"
"url": "https://chaos.social/@distantnative/109336635639931467"
} }

View file

@ -675,7 +675,7 @@ test "accept follow activity", %{conn: conn} do
end end
test "without valid signature, " <> test "without valid signature, " <>
"it only accepts Create activities and requires enabled federation", "it accepts Create activities and requires enabled federation",
%{conn: conn} do %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!() non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
@ -702,6 +702,54 @@ test "without valid signature, " <>
|> json_response(400) |> json_response(400)
end end
# When activity is delivered to the inbox and we cannot immediately verify signature
# we capture all the params and process it later in the Oban job.
# Once we begin processing it through Oban we risk fetching the actor to validate the
# activity which just leads to inserting a new user to process a Delete not relevant to us.
test "Activities of certain types from an unknown actor are discarded", %{conn: conn} do
example_bad_types =
Pleroma.Constants.activity_types() --
Pleroma.Constants.allowed_activity_types_from_strangers()
Enum.each(example_bad_types, fn bad_type ->
params =
%{
"type" => bad_type,
"actor" => "https://unknown.mastodon.instance/users/somebody"
}
|> Jason.encode!()
conn
|> assign(:valid_signature, false)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", params)
|> json_response(400)
assert all_enqueued() == []
end)
end
test "Unknown activity types are discarded", %{conn: conn} do
unknown_types = ["Poke", "Read", "Dazzle"]
Enum.each(unknown_types, fn bad_type ->
params =
%{
"type" => bad_type,
"actor" => "https://unknown.mastodon.instance/users/somebody"
}
|> Jason.encode!()
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", params)
|> json_response(400)
assert all_enqueued() == []
end)
end
test "accepts Add/Remove activities", %{conn: conn} do test "accepts Add/Remove activities", %{conn: conn} do
object_id = "c61d6733-e256-4fe1-ab13-1e369789423f" object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"

View file

@ -89,4 +89,114 @@ test "creates an oauth app with a user", %{conn: conn} do
assert expected == json_response_and_validate_schema(conn, 200) assert expected == json_response_and_validate_schema(conn, 200)
assert app.user_id == user.id assert app.user_id == user.id
end end
test "creates an oauth app without a user", %{conn: conn} do
app_attrs = build(:oauth_app)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: app_attrs.client_name,
redirect_uris: app_attrs.redirect_uris
})
[app] = Repo.all(App)
expected = %{
"name" => app.client_name,
"website" => app.website,
"client_id" => app.client_id,
"client_secret" => app.client_secret,
"id" => app.id |> to_string(),
"redirect_uri" => app.redirect_uris,
"vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
}
assert expected == json_response_and_validate_schema(conn, 200)
end
test "does not duplicate apps with the same client name", %{conn: conn} do
client_name = "BleromaSE"
redirect_uris = "https://bleroma.app/oauth-callback"
for _i <- 1..3 do
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: client_name,
redirect_uris: redirect_uris
})
|> json_response_and_validate_schema(200)
end
apps = Repo.all(App)
assert length(apps) == 1
assert List.first(apps).client_name == client_name
assert List.first(apps).redirect_uris == redirect_uris
end
test "app scopes can be updated", %{conn: conn} do
client_name = "BleromaSE"
redirect_uris = "https://bleroma.app/oauth-callback"
website = "https://bleromase.com"
scopes = "read write"
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: client_name,
redirect_uris: redirect_uris,
website: website,
scopes: scopes
})
|> json_response_and_validate_schema(200)
assert List.first(Repo.all(App)).scopes == String.split(scopes, " ")
updated_scopes = "read write push"
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: client_name,
redirect_uris: redirect_uris,
website: website,
scopes: updated_scopes
})
|> json_response_and_validate_schema(200)
assert List.first(Repo.all(App)).scopes == String.split(updated_scopes, " ")
end
test "app website URL can be updated", %{conn: conn} do
client_name = "BleromaSE"
redirect_uris = "https://bleroma.app/oauth-callback"
website = "https://bleromase.com"
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: client_name,
redirect_uris: redirect_uris,
website: website
})
|> json_response_and_validate_schema(200)
assert List.first(Repo.all(App)).website == website
updated_website = "https://bleromase2ultimateedition.com"
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: client_name,
redirect_uris: redirect_uris,
website: updated_website
})
|> json_response_and_validate_schema(200)
assert List.first(Repo.all(App)).website == updated_website
end
end end

View file

@ -12,20 +12,23 @@ defmodule Pleroma.Web.OAuth.AppTest do
test "gets exist app" do test "gets exist app" do
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]}))
{:ok, %App{} = exist_app} = App.get_or_make(attrs, []) {:ok, %App{} = exist_app} = App.get_or_make(attrs)
assert exist_app == app assert exist_app == app
end end
test "make app" do test "make app" do
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} attrs = %{client_name: "Mastodon-Local", redirect_uris: ".", scopes: ["write"]}
{:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) {:ok, %App{} = app} = App.get_or_make(attrs)
assert app.scopes == ["write"] assert app.scopes == ["write"]
end end
test "gets exist app and updates scopes" do test "gets exist app and updates scopes" do
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} attrs = %{client_name: "Mastodon-Local", redirect_uris: ".", scopes: ["read", "write"]}
app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) app = insert(:oauth_app, attrs)
{:ok, %App{} = exist_app} = App.get_or_make(attrs, ["read", "write", "follow", "push"])
{:ok, %App{} = exist_app} =
App.get_or_make(%{attrs | scopes: ["read", "write", "follow", "push"]})
assert exist_app.id == app.id assert exist_app.id == app.id
assert exist_app.scopes == ["read", "write", "follow", "push"] assert exist_app.scopes == ["read", "write", "follow", "push"]
end end

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
import Mock import Mock
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.User
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
alias Pleroma.Workers.ReceiverWorker alias Pleroma.Workers.ReceiverWorker
@ -51,25 +52,106 @@ test "it does not retry duplicates" do
}) })
end end
describe "cancels on a failed user fetch" do
setup do
Tesla.Mock.mock(fn
%{url: "https://springfield.social/users/bart"} ->
%Tesla.Env{
status: 403,
body: ""
}
%{url: "https://springfield.social/users/troymcclure"} ->
%Tesla.Env{
status: 404,
body: ""
}
%{url: "https://springfield.social/users/hankscorpio"} ->
%Tesla.Env{
status: 410,
body: ""
}
end)
end
test "when request returns a 403" do
params =
insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/bart")
{:ok, oban_job} =
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, {:error, :forbidden}} = ReceiverWorker.perform(oban_job)
end
test "when request returns a 404" do
params =
insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/troymcclure")
{:ok, oban_job} =
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job)
end
test "when request returns a 410" do
params =
insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/hankscorpio")
{:ok, oban_job} =
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job)
end
test "when user account is disabled" do
user = insert(:user)
fake_activity = URI.parse(user.ap_id) |> Map.put(:path, "/fake-activity") |> to_string
params =
insert(:note_activity, user: user).data
|> Map.put("id", fake_activity)
{:ok, %User{}} = User.set_activation(user, false)
{:ok, oban_job} =
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, {:user_active, false}} = ReceiverWorker.perform(oban_job)
end
end
test "it can validate the signature" do test "it can validate the signature" do
Tesla.Mock.mock(fn Tesla.Mock.mock(fn
%{url: "https://mastodon.social/users/bastianallgeier"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/bastianallgeier.json"),
headers: [{"content-type", "application/activity+json"}]
}
%{url: "https://mastodon.social/users/bastianallgeier/collections/featured"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "mastodon.social")
|> String.replace("{{nickname}}", "bastianallgeier")
}
%{url: "https://phpc.social/users/denniskoch"} -> %{url: "https://phpc.social/users/denniskoch"} ->
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
@ -86,136 +168,10 @@ test "it can validate the signature" do
|> String.replace("{{domain}}", "phpc.social") |> String.replace("{{domain}}", "phpc.social")
|> String.replace("{{nickname}}", "denniskoch") |> String.replace("{{nickname}}", "denniskoch")
} }
%{url: "https://mastodon.social/users/bastianallgeier/statuses/112846516276907281"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body: File.read!("test/fixtures/receiver_worker_signature_activity.json")
}
end) end)
params = %{ params =
"@context" => [ File.read!("test/fixtures/receiver_worker_signature_activity.json") |> Jason.decode!()
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
%{
"claim" => %{"@id" => "toot:claim", "@type" => "@id"},
"memorial" => "toot:memorial",
"atomUri" => "ostatus:atomUri",
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
"blurhash" => "toot:blurhash",
"ostatus" => "http://ostatus.org#",
"discoverable" => "toot:discoverable",
"focalPoint" => %{"@container" => "@list", "@id" => "toot:focalPoint"},
"votersCount" => "toot:votersCount",
"Hashtag" => "as:Hashtag",
"Emoji" => "toot:Emoji",
"alsoKnownAs" => %{"@id" => "as:alsoKnownAs", "@type" => "@id"},
"sensitive" => "as:sensitive",
"movedTo" => %{"@id" => "as:movedTo", "@type" => "@id"},
"inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
"conversation" => "ostatus:conversation",
"Device" => "toot:Device",
"schema" => "http://schema.org#",
"toot" => "http://joinmastodon.org/ns#",
"cipherText" => "toot:cipherText",
"suspended" => "toot:suspended",
"messageType" => "toot:messageType",
"featuredTags" => %{"@id" => "toot:featuredTags", "@type" => "@id"},
"Curve25519Key" => "toot:Curve25519Key",
"deviceId" => "toot:deviceId",
"Ed25519Signature" => "toot:Ed25519Signature",
"featured" => %{"@id" => "toot:featured", "@type" => "@id"},
"devices" => %{"@id" => "toot:devices", "@type" => "@id"},
"value" => "schema:value",
"PropertyValue" => "schema:PropertyValue",
"messageFranking" => "toot:messageFranking",
"publicKeyBase64" => "toot:publicKeyBase64",
"identityKey" => %{"@id" => "toot:identityKey", "@type" => "@id"},
"Ed25519Key" => "toot:Ed25519Key",
"indexable" => "toot:indexable",
"EncryptedMessage" => "toot:EncryptedMessage",
"fingerprintKey" => %{"@id" => "toot:fingerprintKey", "@type" => "@id"}
}
],
"actor" => "https://phpc.social/users/denniskoch",
"cc" => [
"https://phpc.social/users/denniskoch/followers",
"https://mastodon.social/users/bastianallgeier",
"https://chaos.social/users/distantnative",
"https://fosstodon.org/users/kev"
],
"id" => "https://phpc.social/users/denniskoch/statuses/112847382711461301/activity",
"object" => %{
"atomUri" => "https://phpc.social/users/denniskoch/statuses/112847382711461301",
"attachment" => [],
"attributedTo" => "https://phpc.social/users/denniskoch",
"cc" => [
"https://phpc.social/users/denniskoch/followers",
"https://mastodon.social/users/bastianallgeier",
"https://chaos.social/users/distantnative",
"https://fosstodon.org/users/kev"
],
"content" =>
"<p><span class=\"h-card\" translate=\"no\"><a href=\"https://mastodon.social/@bastianallgeier\" class=\"u-url mention\">@<span>bastianallgeier</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://chaos.social/@distantnative\" class=\"u-url mention\">@<span>distantnative</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://fosstodon.org/@kev\" class=\"u-url mention\">@<span>kev</span></a></span> Another main argument: Discord is popular. Many people have an account, so you can just join an server quickly. Also you know the app and how to get around.</p>",
"contentMap" => %{
"en" =>
"<p><span class=\"h-card\" translate=\"no\"><a href=\"https://mastodon.social/@bastianallgeier\" class=\"u-url mention\">@<span>bastianallgeier</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://chaos.social/@distantnative\" class=\"u-url mention\">@<span>distantnative</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://fosstodon.org/@kev\" class=\"u-url mention\">@<span>kev</span></a></span> Another main argument: Discord is popular. Many people have an account, so you can just join an server quickly. Also you know the app and how to get around.</p>"
},
"conversation" =>
"tag:mastodon.social,2024-07-25:objectId=760068442:objectType=Conversation",
"id" => "https://phpc.social/users/denniskoch/statuses/112847382711461301",
"inReplyTo" =>
"https://mastodon.social/users/bastianallgeier/statuses/112846516276907281",
"inReplyToAtomUri" =>
"https://mastodon.social/users/bastianallgeier/statuses/112846516276907281",
"published" => "2024-07-25T13:33:29Z",
"replies" => %{
"first" => %{
"items" => [],
"next" =>
"https://phpc.social/users/denniskoch/statuses/112847382711461301/replies?only_other_accounts=true&page=true",
"partOf" =>
"https://phpc.social/users/denniskoch/statuses/112847382711461301/replies",
"type" => "CollectionPage"
},
"id" => "https://phpc.social/users/denniskoch/statuses/112847382711461301/replies",
"type" => "Collection"
},
"sensitive" => false,
"tag" => [
%{
"href" => "https://mastodon.social/users/bastianallgeier",
"name" => "@bastianallgeier@mastodon.social",
"type" => "Mention"
},
%{
"href" => "https://chaos.social/users/distantnative",
"name" => "@distantnative@chaos.social",
"type" => "Mention"
},
%{
"href" => "https://fosstodon.org/users/kev",
"name" => "@kev@fosstodon.org",
"type" => "Mention"
}
],
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Note",
"url" => "https://phpc.social/@denniskoch/112847382711461301"
},
"published" => "2024-07-25T13:33:29Z",
"signature" => %{
"created" => "2024-07-25T13:33:29Z",
"creator" => "https://phpc.social/users/denniskoch#main-key",
"signatureValue" =>
"slz9BKJzd2n1S44wdXGOU+bV/wsskdgAaUpwxj8R16mYOL8+DTpE6VnfSKoZGsBBJT8uG5gnVfVEz1YsTUYtymeUgLMh7cvd8VnJnZPS+oixbmBRVky/Myf91TEgQQE7G4vDmTdB4ii54hZrHcOOYYf5FKPNRSkMXboKA6LMqNtekhbI+JTUJYIB02WBBK6PUyo15f6B1RJ6HGWVgud9NE0y1EZXfrkqUt682p8/9D49ORf7AwjXUJibKic2RbPvhEBj70qUGfBm4vvgdWhSUn1IG46xh+U0+NrTSUED82j1ZVOeua/2k/igkGs8cSBkY35quXTkPz6gbqCCH66CuA==",
"type" => "RsaSignature2017"
},
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Create"
}
req_headers = [ req_headers = [
["accept-encoding", "gzip"], ["accept-encoding", "gzip"],
@ -245,4 +201,46 @@ test "it can validate the signature" do
assert {:ok, %Pleroma.Activity{}} = ReceiverWorker.perform(oban_job) assert {:ok, %Pleroma.Activity{}} = ReceiverWorker.perform(oban_job)
end end
test "cancels due to origin containment" do
params =
insert(:note_activity).data
|> Map.put("id", "https://notorigindomain.com/activity")
{:ok, oban_job} =
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, :origin_containment_failed} = ReceiverWorker.perform(oban_job)
end
test "canceled due to deleted object" do
params =
insert(:announce_activity).data
|> Map.put("object", "http://localhost:4001/deleted")
Tesla.Mock.mock(fn
%{url: "http://localhost:4001/deleted"} ->
%Tesla.Env{
status: 404,
body: ""
}
end)
{:ok, oban_job} =
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, _} = ReceiverWorker.perform(oban_job)
end
end end