Merge remote-tracking branch 'origin/develop' into fork
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
acdda8d4df
27 changed files with 699 additions and 397 deletions
0
changelog.d/docs-fix.skip
Normal file
0
changelog.d/docs-fix.skip
Normal file
1
changelog.d/drop-unwanted.change
Normal file
1
changelog.d/drop-unwanted.change
Normal 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.
|
1
changelog.d/oauth-app.fix
Normal file
1
changelog.d/oauth-app.fix
Normal file
|
@ -0,0 +1 @@
|
|||
Prevent OAuth App flow from creating duplicate entries
|
1
changelog.d/oban-recevier-improvements.fix
Normal file
1
changelog.d/oban-recevier-improvements.fix
Normal 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.
|
1
changelog.d/rich-media-no-heads.change
Normal file
1
changelog.d/rich-media-no-heads.change
Normal 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
|
0
changelog.d/todo-cleanup.skip
Normal file
0
changelog.d/todo-cleanup.skip
Normal file
|
@ -433,7 +433,7 @@ Response:
|
|||
* On success: URL of the unfollowed relay
|
||||
|
||||
```json
|
||||
{"https://example.com/relay"}
|
||||
"https://example.com/relay"
|
||||
```
|
||||
|
||||
## `POST /api/v1/pleroma/admin/users/invite_token`
|
||||
|
@ -1193,20 +1193,23 @@ Loads json generated from `config/descriptions.exs`.
|
|||
- Response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1234,
|
||||
"data": {
|
||||
"actor": {
|
||||
"id": 1,
|
||||
"nickname": "lain"
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1234,
|
||||
"data": {
|
||||
"actor": {
|
||||
"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`
|
||||
|
|
|
@ -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
|
||||
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
|
||||
const(mime_regex,
|
||||
|
|
|
@ -52,6 +52,7 @@ defp adapter_helper do
|
|||
case adapter() do
|
||||
Tesla.Adapter.Gun -> AdapterHelper.Gun
|
||||
Tesla.Adapter.Hackney -> AdapterHelper.Hackney
|
||||
{Tesla.Adapter.Finch, _} -> AdapterHelper.Finch
|
||||
_ -> AdapterHelper.Default
|
||||
end
|
||||
end
|
||||
|
@ -118,4 +119,13 @@ def format_host(host) do
|
|||
host_charlist
|
||||
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
|
||||
|
|
33
lib/pleroma/http/adapter_helper/finch.ex
Normal file
33
lib/pleroma/http/adapter_helper/finch.ex
Normal 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
|
|
@ -32,6 +32,7 @@ def options(incoming_opts \\ [], %URI{} = uri) do
|
|||
|> AdapterHelper.maybe_add_proxy(proxy)
|
||||
|> Keyword.merge(incoming_opts)
|
||||
|> put_timeout()
|
||||
|> maybe_stream()
|
||||
end
|
||||
|
||||
defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
|
||||
|
@ -47,6 +48,14 @@ defp put_timeout(opts) do
|
|||
Keyword.put(opts, :timeout, recv_timeout)
|
||||
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()
|
||||
def pool_timeout(pool) do
|
||||
default = Config.get([:pools, :default, :recv_timeout], 5_000)
|
||||
|
|
|
@ -20,15 +20,13 @@ def safe_put_in(data, keys, value) when is_map(data) and is_list(keys) do
|
|||
end
|
||||
|
||||
def filter_empty_values(data) do
|
||||
# TODO: Change to Map.filter in Elixir 1.13+
|
||||
data
|
||||
|> Enum.filter(fn
|
||||
|> Map.filter(fn
|
||||
{_k, nil} -> false
|
||||
{_k, ""} -> false
|
||||
{_k, []} -> false
|
||||
{_k, %{} = v} -> Map.keys(v) != []
|
||||
{_k, _v} -> true
|
||||
end)
|
||||
|> Map.new()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -321,7 +321,7 @@ def inbox(conn, %{"type" => "Create"} = params) do
|
|||
post_inbox_relayed_create(conn, params)
|
||||
else
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> put_status(403)
|
||||
|> json("Not federating")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
# 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"]),
|
||||
{_, :ok} <-
|
||||
{: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)}")
|
||||
{:error, e}
|
||||
|
||||
{:error, {:validate_object, _}} = e ->
|
||||
Logger.error("Incoming AP doc validation error: #{inspect(e)}")
|
||||
Logger.debug(Jason.encode!(params, pretty: true))
|
||||
e
|
||||
|
||||
e ->
|
||||
# Just drop those for now
|
||||
Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end)
|
||||
|
|
|
@ -36,8 +36,7 @@ def create(%{body_params: params} = conn, _params) do
|
|||
|> Map.put(:scopes, scopes)
|
||||
|> Maps.put_if_present(:user_id, user_id)
|
||||
|
||||
with cs <- App.register_changeset(%App{}, app_attrs),
|
||||
{:ok, app} <- Repo.insert(cs) do
|
||||
with {:ok, app} <- App.get_or_make(app_attrs) do
|
||||
render(conn, "show.json", app: app)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -871,19 +871,7 @@ defp build_application(%{"type" => _type, "name" => name, "url" => url}),
|
|||
|
||||
defp build_application(_), do: nil
|
||||
|
||||
# Workaround for Elixir issue #10771
|
||||
# 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
|
||||
|
||||
@spec build_image_url(URI.t(), URI.t()) :: String.t()
|
||||
defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
|
||||
URI.merge(page_url_data, image_url_data) |> to_string
|
||||
end
|
||||
|
|
|
@ -67,35 +67,27 @@ def update(id, params) do
|
|||
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
|
||||
app
|
||||
|> changeset(params)
|
||||
|> validate_required([:scopes])
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets app by attrs or create new with attrs.
|
||||
And updates the scopes if need.
|
||||
Gets app by attrs or create new with attrs.
|
||||
Updates the attrs if needed.
|
||||
"""
|
||||
@spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
|
||||
def get_or_make(attrs, scopes) do
|
||||
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do
|
||||
update_scopes(app, scopes)
|
||||
@spec get_or_make(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
|
||||
def get_or_make(attrs) do
|
||||
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, client_name: attrs.client_name) do
|
||||
__MODULE__.update(app.id, Map.take(attrs, [:scopes, :website]))
|
||||
else
|
||||
_e ->
|
||||
%__MODULE__{}
|
||||
|> register_changeset(Map.put(attrs, :scopes, scopes))
|
||||
|> register_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
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()}
|
||||
def search(params) do
|
||||
query = from(a in __MODULE__)
|
||||
|
|
89
lib/pleroma/web/plugs/inbox_guard_plug.ex
Normal file
89
lib/pleroma/web/plugs/inbox_guard_plug.ex
Normal 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
|
|
@ -10,31 +10,40 @@ defmodule Pleroma.Web.RichMedia.Helpers do
|
|||
@type get_errors :: {:error, :body_too_large | :content_type | :head | :get}
|
||||
|
||||
@spec rich_media_get(String.t()) :: {:ok, String.t()} | get_errors()
|
||||
|
||||
defp 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}]
|
||||
def rich_media_get(url) do
|
||||
case Pleroma.HTTP.AdapterHelper.can_stream?() do
|
||||
true -> stream(url)
|
||||
false -> head_first(url)
|
||||
end
|
||||
|> handle_result(url)
|
||||
end
|
||||
|
||||
def rich_media_get(url) do
|
||||
headers = headers()
|
||||
defp stream(url) do
|
||||
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}}} <-
|
||||
{: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_length, check_content_length(headers)},
|
||||
{_, {: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}
|
||||
else
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_result(result, url) do
|
||||
case result do
|
||||
{:ok, body} ->
|
||||
{:ok, body}
|
||||
|
||||
{:head, _} ->
|
||||
Logger.debug("Rich media error for #{url}: HTTP HEAD failed")
|
||||
{:error, :head}
|
||||
|
@ -43,8 +52,12 @@ def rich_media_get(url) do
|
|||
Logger.debug("Rich media error for #{url}: content-type is #{type}")
|
||||
{:error, :content_type}
|
||||
|
||||
{:content_length, {_, length}} ->
|
||||
Logger.debug("Rich media error for #{url}: content-length is #{length}")
|
||||
{:content_length, :error} ->
|
||||
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}
|
||||
|
||||
{:get, _} ->
|
||||
|
@ -73,7 +86,7 @@ defp check_content_length(headers) do
|
|||
{_, maybe_content_length} ->
|
||||
case Integer.parse(maybe_content_length) do
|
||||
{content_length, ""} when content_length <= max_body -> :ok
|
||||
{_, ""} -> {:error, maybe_content_length}
|
||||
{_, ""} -> :error
|
||||
_ -> :ok
|
||||
end
|
||||
|
||||
|
@ -82,13 +95,46 @@ defp check_content_length(headers) do
|
|||
end
|
||||
end
|
||||
|
||||
defp http_options do
|
||||
timeout = Config.get!([:rich_media, :timeout])
|
||||
defp read_stream(stream) do
|
||||
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,
|
||||
max_body: Config.get([:rich_media, :max_body], 5_000_000),
|
||||
tesla_middleware: [{Tesla.Middleware.Timeout, timeout: timeout}]
|
||||
stream: true
|
||||
]
|
||||
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
|
||||
|
|
|
@ -217,6 +217,10 @@ defmodule Pleroma.Web.Router do
|
|||
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
|
||||
end
|
||||
|
||||
pipeline :inbox_guard do
|
||||
plug(Pleroma.Web.Plugs.InboxGuardPlug)
|
||||
end
|
||||
|
||||
pipeline :static_fe do
|
||||
plug(Pleroma.Web.Plugs.StaticFEPlug)
|
||||
end
|
||||
|
@ -1077,7 +1081,7 @@ defmodule Pleroma.Web.Router do
|
|||
end
|
||||
|
||||
scope "/", Pleroma.Web.ActivityPub do
|
||||
pipe_through(:activitypub)
|
||||
pipe_through([:activitypub, :inbox_guard])
|
||||
post("/inbox", ActivityPubController, :inbox)
|
||||
post("/users/:nickname/inbox", ActivityPubController, :inbox)
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ def perform(%Job{
|
|||
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),
|
||||
{:signature, true} <- {:signature, Signature.validate_signature(conn_data)},
|
||||
{: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)
|
||||
|
||||
defp process_errors({:error, {:error, _} = error}), do: process_errors(error)
|
||||
|
||||
defp process_errors(errors) do
|
||||
case errors do
|
||||
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
|
||||
{: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}
|
||||
# User fetch failures
|
||||
{: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
|
||||
e -> {:error, e}
|
||||
end
|
||||
|
|
117
test/fixtures/bastianallgeier.json
vendored
117
test/fixtures/bastianallgeier.json
vendored
|
@ -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 & developer. Creator of Kirby CMS</p>",
|
||||
"tag": [],
|
||||
"type": "Person",
|
||||
"url": "https://mastodon.social/@bastianallgeier"
|
||||
}
|
|
@ -1,62 +1,109 @@
|
|||
{
|
||||
"@context": [
|
||||
"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",
|
||||
"conversation": "ostatus:conversation",
|
||||
"ostatus": "http://ostatus.org#",
|
||||
"discoverable": "toot:discoverable",
|
||||
"focalPoint": {
|
||||
"@container": "@list",
|
||||
"@id": "toot:focalPoint"
|
||||
},
|
||||
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
||||
"ostatus": "http://ostatus.org#",
|
||||
"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#",
|
||||
"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",
|
||||
"attachment": [
|
||||
{
|
||||
"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",
|
||||
"actor": "https://phpc.social/users/denniskoch",
|
||||
"cc": [],
|
||||
"id": "https://phpc.social/users/denniskoch/statuses/112847382711461301/activity",
|
||||
"inReplyTo": null,
|
||||
"inReplyToAtomUri": null,
|
||||
"published": "2022-11-13T13:04:20Z",
|
||||
"replies": {
|
||||
"first": {
|
||||
"items": [],
|
||||
"next": "https://chaos.social/users/distantnative/statuses/109336635639931467/replies?only_other_accounts=true&page=true",
|
||||
"partOf": "https://chaos.social/users/distantnative/statuses/109336635639931467/replies",
|
||||
"type": "CollectionPage"
|
||||
"object": {
|
||||
"atomUri": "https://phpc.social/users/denniskoch/statuses/112847382711461301",
|
||||
"attachment": [],
|
||||
"attributedTo": "https://phpc.social/users/denniskoch",
|
||||
"cc": [],
|
||||
"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>"
|
||||
},
|
||||
"id": "https://chaos.social/users/distantnative/statuses/109336635639931467/replies",
|
||||
"type": "Collection"
|
||||
"conversation": "tag:mastodon.social,2024-07-25:objectId=760068442:objectType=Conversation",
|
||||
"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": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"type": "Note",
|
||||
"url": "https://chaos.social/@distantnative/109336635639931467"
|
||||
"type": "Create"
|
||||
}
|
||||
|
|
|
@ -675,7 +675,7 @@ test "accept follow activity", %{conn: conn} do
|
|||
end
|
||||
|
||||
test "without valid signature, " <>
|
||||
"it only accepts Create activities and requires enabled federation",
|
||||
"it accepts Create activities and requires enabled federation",
|
||||
%{conn: conn} do
|
||||
data = File.read!("test/fixtures/mastodon-post-activity.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)
|
||||
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
|
||||
object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"
|
||||
|
||||
|
|
|
@ -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 app.user_id == user.id
|
||||
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
|
||||
|
|
|
@ -12,20 +12,23 @@ defmodule Pleroma.Web.OAuth.AppTest do
|
|||
test "gets exist app" do
|
||||
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
|
||||
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
|
||||
end
|
||||
|
||||
test "make app" do
|
||||
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
|
||||
{:ok, %App{} = app} = App.get_or_make(attrs, ["write"])
|
||||
attrs = %{client_name: "Mastodon-Local", redirect_uris: ".", scopes: ["write"]}
|
||||
{:ok, %App{} = app} = App.get_or_make(attrs)
|
||||
assert app.scopes == ["write"]
|
||||
end
|
||||
|
||||
test "gets exist app and updates scopes" do
|
||||
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
|
||||
app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]}))
|
||||
{:ok, %App{} = exist_app} = App.get_or_make(attrs, ["read", "write", "follow", "push"])
|
||||
attrs = %{client_name: "Mastodon-Local", redirect_uris: ".", scopes: ["read", "write"]}
|
||||
app = insert(:oauth_app, attrs)
|
||||
|
||||
{:ok, %App{} = exist_app} =
|
||||
App.get_or_make(%{attrs | scopes: ["read", "write", "follow", "push"]})
|
||||
|
||||
assert exist_app.id == app.id
|
||||
assert exist_app.scopes == ["read", "write", "follow", "push"]
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
|||
import Mock
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Workers.ReceiverWorker
|
||||
|
||||
|
@ -51,25 +52,106 @@ test "it does not retry duplicates" do
|
|||
})
|
||||
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
|
||||
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"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
@ -86,136 +168,10 @@ test "it can validate the signature" do
|
|||
|> String.replace("{{domain}}", "phpc.social")
|
||||
|> 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)
|
||||
|
||||
params = %{
|
||||
"@context" => [
|
||||
"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"
|
||||
}
|
||||
params =
|
||||
File.read!("test/fixtures/receiver_worker_signature_activity.json") |> Jason.decode!()
|
||||
|
||||
req_headers = [
|
||||
["accept-encoding", "gzip"],
|
||||
|
@ -245,4 +201,46 @@ test "it can validate the signature" do
|
|||
|
||||
assert {:ok, %Pleroma.Activity{}} = ReceiverWorker.perform(oban_job)
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue