2019-07-10 05:13:23 +00:00
|
|
|
# Pleroma: A lightweight social networking server
|
2021-01-13 06:49:20 +00:00
|
|
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
2019-07-10 05:13:23 +00:00
|
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2018-12-01 23:03:03 +00:00
|
|
|
defmodule Pleroma.Object.FetcherTest do
|
2023-08-01 10:43:50 +00:00
|
|
|
use Pleroma.DataCase, async: false
|
2023-08-04 11:50:50 +00:00
|
|
|
@moduletag :mocked
|
2018-12-01 23:03:03 +00:00
|
|
|
|
2019-04-17 09:27:29 +00:00
|
|
|
alias Pleroma.Activity
|
2022-11-15 17:23:47 +00:00
|
|
|
alias Pleroma.Instances
|
2019-04-17 09:27:29 +00:00
|
|
|
alias Pleroma.Object
|
2018-12-01 23:03:03 +00:00
|
|
|
alias Pleroma.Object.Fetcher
|
2020-09-11 17:58:58 +00:00
|
|
|
|
2019-07-14 12:24:56 +00:00
|
|
|
import Mock
|
2020-09-11 17:58:58 +00:00
|
|
|
import Tesla.Mock
|
2019-04-17 11:21:39 +00:00
|
|
|
|
2024-03-13 21:04:31 +00:00
|
|
|
defp spoofed_object_with_ids(
|
|
|
|
id \\ "https://patch.cx/objects/spoof",
|
|
|
|
actor_id \\ "https://patch.cx/users/rin"
|
|
|
|
) do
|
|
|
|
File.read!("test/fixtures/spoofed-object.json")
|
|
|
|
|> Jason.decode!()
|
|
|
|
|> Map.put("id", id)
|
|
|
|
|> Map.put("actor", actor_id)
|
|
|
|
|> Jason.encode!()
|
|
|
|
end
|
|
|
|
|
2019-04-17 11:21:39 +00:00
|
|
|
setup do
|
2019-06-13 09:34:03 +00:00
|
|
|
mock(fn
|
|
|
|
%{method: :get, url: "https://mastodon.example.org/users/userisgone"} ->
|
|
|
|
%Tesla.Env{status: 410}
|
|
|
|
|
2019-06-13 10:13:35 +00:00
|
|
|
%{method: :get, url: "https://mastodon.example.org/users/userisgone404"} ->
|
|
|
|
%Tesla.Env{status: 404}
|
|
|
|
|
2024-03-13 21:04:31 +00:00
|
|
|
# Spoof: wrong Content-Type
|
2020-10-28 15:08:23 +00:00
|
|
|
%{
|
|
|
|
method: :get,
|
2024-03-13 21:04:31 +00:00
|
|
|
url: "https://patch.cx/objects/spoof_content_type.json"
|
2020-10-28 15:08:23 +00:00
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
2024-03-13 21:04:31 +00:00
|
|
|
url: "https://patch.cx/objects/spoof_content_type.json",
|
2020-10-28 15:08:23 +00:00
|
|
|
headers: [{"content-type", "application/json"}],
|
2024-03-13 21:04:31 +00:00
|
|
|
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_content_type.json")
|
|
|
|
}
|
|
|
|
|
|
|
|
# Spoof: no Content-Type
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://patch.cx/objects/spoof_content_type"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
url: "https://patch.cx/objects/spoof_content_type",
|
|
|
|
headers: [],
|
|
|
|
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_content_type")
|
2020-10-28 15:08:23 +00:00
|
|
|
}
|
|
|
|
|
2023-12-26 20:54:14 +00:00
|
|
|
%{method: :get, url: "https://octodon.social/users/cwebber/statuses/111647596861000656"} ->
|
|
|
|
%Tesla.Env{status: 403}
|
2024-04-12 19:33:33 +00:00
|
|
|
|
Only allow exact id matches
This protects us from falling for obvious spoofs as from the current
upload exploit (unfortunately we can’t reasonably do anything about
spoofs with exact matches as was possible via emoji and proxy).
Such objects being invalid is supported by the spec, sepcifically
sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id
Anonymous objects are not relevant here (they can only exists within
parent objects iiuc) and neither is client-to-server or transient objects
(as those cannot be fetched in the first place).
This leaves us with the requirement for `id` to (a) exist and
(b) be a publicly dereferencable URI from the originating server.
This alone does not yet demand strict equivalence, but the spec then
further explains objects ought to be fetchable _via their ID_.
Meaning an object not retrievable via its ID, is invalid.
This reading is supported by the fact, e.g. GoToSocial (recently) and
Mastodon (for 6+ years) do already implement such strict ID checks,
additionally proving this doesn’t cause federation issues in practice.
However, apart from canonical IDs there can also be additional display
URLs. *omas first redirect those to their canonical location, but *keys
and Mastodon directly serve the AP representation without redirects.
Mastodon and GTS deal with this in two different ways,
but both constitute an effective countermeasure:
- Mastodon:
Unless it already is a known AP id, two fetches occur.
The first fetch just reads the `id` property and then refetches from
the id. The last fetch requires the returned id to exactly match the
URL the content was fetched from. (This can be optimised by skipping
the second fetch if it already matches)
https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168
https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af
- GTS:
Only does a single fetch and then checks if _either_ the id
_or_ url property (which can be an object) match the original fetch
URL. This relies on implementations always including their display URL
as "url" if differing from the id. For actors this is true for all
investigated implementations, for posts only Mastodon includes an
"url", but it is also the only one with a differing display URL.
https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222
Albeit Mastodon’s refetch offers higher compatibility with theoretical
implmentations using either multiple different display URL or not
denoting any of them as "url" at all, for now we chose to adopt a
GTS-like refetch-free approach to avoid additional implementation
concerns wrt to whether redirects should be allowed when fetching a
canonical AP id and potential for accidentally loosening some checks
(e.g. cross-domain refetches) for one of the fetches.
This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
|
|
|
# Spoof: mismatching ids
|
|
|
|
# Variant 1: Non-exisitng fake id
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url:
|
|
|
|
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
url:
|
|
|
|
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json",
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: spoofed_object_with_ids()
|
|
|
|
}
|
|
|
|
|
|
|
|
%{method: :get, url: "https://patch.cx/objects/spoof"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 404,
|
|
|
|
url: "https://patch.cx/objects/spoof",
|
|
|
|
headers: [],
|
|
|
|
body: "Not found"
|
|
|
|
}
|
|
|
|
|
|
|
|
# Varaint 2: two-stage payload
|
|
|
|
%{method: :get, url: "https://patch.cx/media/spoof_stage1.json"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
url: "https://patch.cx/media/spoof_stage1.json",
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: spoofed_object_with_ids("https://patch.cx/media/spoof_stage2.json")
|
|
|
|
}
|
|
|
|
|
|
|
|
%{method: :get, url: "https://patch.cx/media/spoof_stage2.json"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
url: "https://patch.cx/media/spoof_stage2.json",
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: spoofed_object_with_ids("https://patch.cx/media/unpredictable.json")
|
|
|
|
}
|
|
|
|
|
2024-03-13 21:12:17 +00:00
|
|
|
# Spoof: cross-domain redirect with original domain id
|
|
|
|
%{method: :get, url: "https://patch.cx/objects/spoof_media_redirect1"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
url: "https://media.patch.cx/objects/spoof",
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_media_redirect1")
|
|
|
|
}
|
|
|
|
|
|
|
|
# Spoof: cross-domain redirect with final domain id
|
|
|
|
%{method: :get, url: "https://patch.cx/objects/spoof_media_redirect2"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
url: "https://media.patch.cx/objects/spoof_media_redirect2",
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: spoofed_object_with_ids("https://media.patch.cx/objects/spoof_media_redirect2")
|
|
|
|
}
|
|
|
|
|
|
|
|
# No-Spoof: same domain redirect
|
|
|
|
%{method: :get, url: "https://patch.cx/objects/spoof_redirect"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
Only allow exact id matches
This protects us from falling for obvious spoofs as from the current
upload exploit (unfortunately we can’t reasonably do anything about
spoofs with exact matches as was possible via emoji and proxy).
Such objects being invalid is supported by the spec, sepcifically
sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id
Anonymous objects are not relevant here (they can only exists within
parent objects iiuc) and neither is client-to-server or transient objects
(as those cannot be fetched in the first place).
This leaves us with the requirement for `id` to (a) exist and
(b) be a publicly dereferencable URI from the originating server.
This alone does not yet demand strict equivalence, but the spec then
further explains objects ought to be fetchable _via their ID_.
Meaning an object not retrievable via its ID, is invalid.
This reading is supported by the fact, e.g. GoToSocial (recently) and
Mastodon (for 6+ years) do already implement such strict ID checks,
additionally proving this doesn’t cause federation issues in practice.
However, apart from canonical IDs there can also be additional display
URLs. *omas first redirect those to their canonical location, but *keys
and Mastodon directly serve the AP representation without redirects.
Mastodon and GTS deal with this in two different ways,
but both constitute an effective countermeasure:
- Mastodon:
Unless it already is a known AP id, two fetches occur.
The first fetch just reads the `id` property and then refetches from
the id. The last fetch requires the returned id to exactly match the
URL the content was fetched from. (This can be optimised by skipping
the second fetch if it already matches)
https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168
https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af
- GTS:
Only does a single fetch and then checks if _either_ the id
_or_ url property (which can be an object) match the original fetch
URL. This relies on implementations always including their display URL
as "url" if differing from the id. For actors this is true for all
investigated implementations, for posts only Mastodon includes an
"url", but it is also the only one with a differing display URL.
https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222
Albeit Mastodon’s refetch offers higher compatibility with theoretical
implmentations using either multiple different display URL or not
denoting any of them as "url" at all, for now we chose to adopt a
GTS-like refetch-free approach to avoid additional implementation
concerns wrt to whether redirects should be allowed when fetching a
canonical AP id and potential for accidentally loosening some checks
(e.g. cross-domain refetches) for one of the fetches.
This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
|
|
|
url: "https://patch.cx/objects/spoof_redirect",
|
2024-03-13 21:12:17 +00:00
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_redirect")
|
|
|
|
}
|
|
|
|
|
2024-03-13 21:21:19 +00:00
|
|
|
# Spoof: Actor from another domain
|
|
|
|
%{method: :get, url: "https://patch.cx/objects/spoof_foreign_actor"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
url: "https://patch.cx/objects/spoof_foreign_actor",
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body:
|
|
|
|
spoofed_object_with_ids(
|
|
|
|
"https://patch.cx/objects/spoof_foreign_actor",
|
|
|
|
"https://not.patch.cx/users/rin"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-06-13 09:34:03 +00:00
|
|
|
env ->
|
|
|
|
apply(HttpRequestMock, :request, [env])
|
|
|
|
end)
|
|
|
|
|
2019-04-17 11:21:39 +00:00
|
|
|
:ok
|
|
|
|
end
|
2018-12-01 23:03:03 +00:00
|
|
|
|
2020-07-01 09:48:51 +00:00
|
|
|
describe "error cases" do
|
|
|
|
setup do
|
|
|
|
mock(fn
|
|
|
|
%{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
Only allow exact id matches
This protects us from falling for obvious spoofs as from the current
upload exploit (unfortunately we can’t reasonably do anything about
spoofs with exact matches as was possible via emoji and proxy).
Such objects being invalid is supported by the spec, sepcifically
sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id
Anonymous objects are not relevant here (they can only exists within
parent objects iiuc) and neither is client-to-server or transient objects
(as those cannot be fetched in the first place).
This leaves us with the requirement for `id` to (a) exist and
(b) be a publicly dereferencable URI from the originating server.
This alone does not yet demand strict equivalence, but the spec then
further explains objects ought to be fetchable _via their ID_.
Meaning an object not retrievable via its ID, is invalid.
This reading is supported by the fact, e.g. GoToSocial (recently) and
Mastodon (for 6+ years) do already implement such strict ID checks,
additionally proving this doesn’t cause federation issues in practice.
However, apart from canonical IDs there can also be additional display
URLs. *omas first redirect those to their canonical location, but *keys
and Mastodon directly serve the AP representation without redirects.
Mastodon and GTS deal with this in two different ways,
but both constitute an effective countermeasure:
- Mastodon:
Unless it already is a known AP id, two fetches occur.
The first fetch just reads the `id` property and then refetches from
the id. The last fetch requires the returned id to exactly match the
URL the content was fetched from. (This can be optimised by skipping
the second fetch if it already matches)
https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168
https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af
- GTS:
Only does a single fetch and then checks if _either_ the id
_or_ url property (which can be an object) match the original fetch
URL. This relies on implementations always including their display URL
as "url" if differing from the id. For actors this is true for all
investigated implementations, for posts only Mastodon includes an
"url", but it is also the only one with a differing display URL.
https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222
Albeit Mastodon’s refetch offers higher compatibility with theoretical
implmentations using either multiple different display URL or not
denoting any of them as "url" at all, for now we chose to adopt a
GTS-like refetch-free approach to avoid additional implementation
concerns wrt to whether redirects should be allowed when fetching a
canonical AP id and potential for accidentally loosening some checks
(e.g. cross-domain refetches) for one of the fetches.
This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
|
|
|
url: "https://social.sakamoto.gq/objects/f20f2497-66d9-4a52-a2e1-1be2a39c32c1",
|
2020-10-28 15:08:23 +00:00
|
|
|
body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"),
|
|
|
|
headers: HttpRequestMock.activitypub_object_headers()
|
2020-07-01 09:48:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
%{method: :get, url: "https://social.sakamoto.gq/users/eal"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
2020-10-28 15:08:23 +00:00
|
|
|
body: File.read!("test/fixtures/fetch_mocks/eal.json"),
|
|
|
|
headers: HttpRequestMock.activitypub_object_headers()
|
2020-07-01 09:48:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
%{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
2020-10-28 15:08:23 +00:00
|
|
|
body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json"),
|
|
|
|
headers: HttpRequestMock.activitypub_object_headers()
|
2020-07-01 09:48:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
%{method: :get, url: "https://busshi.moe/users/tuxcrafting"} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 500
|
|
|
|
}
|
2020-10-20 23:20:06 +00:00
|
|
|
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://stereophonic.space/objects/02997b83-3ea7-4b63-94af-ef3aa2d4ed17"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 500
|
|
|
|
}
|
2020-07-01 09:48:51 +00:00
|
|
|
end)
|
|
|
|
|
|
|
|
:ok
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it works when fetching the OP actor errors out" do
|
|
|
|
# Here we simulate a case where the author of the OP can't be read
|
|
|
|
assert {:ok, _} =
|
|
|
|
Fetcher.fetch_object_from_id(
|
|
|
|
"https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-02-15 17:41:38 +00:00
|
|
|
describe "max thread distance restriction" do
|
|
|
|
@ap_id "http://mastodon.example.org/@admin/99541947525187367"
|
2020-03-20 15:33:00 +00:00
|
|
|
setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
|
2020-02-15 17:41:38 +00:00
|
|
|
|
|
|
|
test "it returns thread depth exceeded error if thread depth is exceeded" do
|
2021-01-26 17:58:43 +00:00
|
|
|
clear_config([:instance, :federation_incoming_replies_max_depth], 0)
|
2020-02-15 17:41:38 +00:00
|
|
|
|
2023-12-28 03:28:41 +00:00
|
|
|
assert {:error, :allowed_depth} = Fetcher.fetch_object_from_id(@ap_id, depth: 1)
|
2020-02-15 17:41:38 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do
|
2021-01-26 17:58:43 +00:00
|
|
|
clear_config([:instance, :federation_incoming_replies_max_depth], 0)
|
2020-02-15 17:41:38 +00:00
|
|
|
|
|
|
|
assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it fetches object if requested depth does not exceed max thread depth" do
|
2021-01-26 17:58:43 +00:00
|
|
|
clear_config([:instance, :federation_incoming_replies_max_depth], 10)
|
2020-02-15 17:41:38 +00:00
|
|
|
|
|
|
|
assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-12-01 23:03:03 +00:00
|
|
|
describe "actor origin containment" do
|
2019-10-18 00:51:53 +00:00
|
|
|
test "it rejects objects with a bogus origin" do
|
2018-12-01 23:03:03 +00:00
|
|
|
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
|
|
|
|
end
|
|
|
|
|
2019-10-18 00:51:53 +00:00
|
|
|
test "it rejects objects when attributedTo is wrong (variant 1)" do
|
2018-12-01 23:03:03 +00:00
|
|
|
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
|
|
|
|
end
|
|
|
|
|
2019-10-18 00:51:53 +00:00
|
|
|
test "it rejects objects when attributedTo is wrong (variant 2)" do
|
2018-12-01 23:03:03 +00:00
|
|
|
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-03-13 21:04:31 +00:00
|
|
|
describe "fetcher security and auth checks" do
|
|
|
|
test "it does not fetch a spoofed object without content type" do
|
|
|
|
assert {:error, {:content_type, nil}} =
|
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://patch.cx/objects/spoof_content_type"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it does not fetch a spoofed object with wrong content type" do
|
|
|
|
assert {:error, {:content_type, _}} =
|
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://patch.cx/objects/spoof_content_type.json"
|
|
|
|
)
|
|
|
|
end
|
2024-03-13 21:12:17 +00:00
|
|
|
|
Only allow exact id matches
This protects us from falling for obvious spoofs as from the current
upload exploit (unfortunately we can’t reasonably do anything about
spoofs with exact matches as was possible via emoji and proxy).
Such objects being invalid is supported by the spec, sepcifically
sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id
Anonymous objects are not relevant here (they can only exists within
parent objects iiuc) and neither is client-to-server or transient objects
(as those cannot be fetched in the first place).
This leaves us with the requirement for `id` to (a) exist and
(b) be a publicly dereferencable URI from the originating server.
This alone does not yet demand strict equivalence, but the spec then
further explains objects ought to be fetchable _via their ID_.
Meaning an object not retrievable via its ID, is invalid.
This reading is supported by the fact, e.g. GoToSocial (recently) and
Mastodon (for 6+ years) do already implement such strict ID checks,
additionally proving this doesn’t cause federation issues in practice.
However, apart from canonical IDs there can also be additional display
URLs. *omas first redirect those to their canonical location, but *keys
and Mastodon directly serve the AP representation without redirects.
Mastodon and GTS deal with this in two different ways,
but both constitute an effective countermeasure:
- Mastodon:
Unless it already is a known AP id, two fetches occur.
The first fetch just reads the `id` property and then refetches from
the id. The last fetch requires the returned id to exactly match the
URL the content was fetched from. (This can be optimised by skipping
the second fetch if it already matches)
https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168
https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af
- GTS:
Only does a single fetch and then checks if _either_ the id
_or_ url property (which can be an object) match the original fetch
URL. This relies on implementations always including their display URL
as "url" if differing from the id. For actors this is true for all
investigated implementations, for posts only Mastodon includes an
"url", but it is also the only one with a differing display URL.
https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222
Albeit Mastodon’s refetch offers higher compatibility with theoretical
implmentations using either multiple different display URL or not
denoting any of them as "url" at all, for now we chose to adopt a
GTS-like refetch-free approach to avoid additional implementation
concerns wrt to whether redirects should be allowed when fetching a
canonical AP id and potential for accidentally loosening some checks
(e.g. cross-domain refetches) for one of the fetches.
This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
|
|
|
test "it does not fetch a spoofed object with id different from URL" do
|
2024-04-13 22:55:26 +00:00
|
|
|
assert {:error, :id_mismatch} =
|
Only allow exact id matches
This protects us from falling for obvious spoofs as from the current
upload exploit (unfortunately we can’t reasonably do anything about
spoofs with exact matches as was possible via emoji and proxy).
Such objects being invalid is supported by the spec, sepcifically
sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id
Anonymous objects are not relevant here (they can only exists within
parent objects iiuc) and neither is client-to-server or transient objects
(as those cannot be fetched in the first place).
This leaves us with the requirement for `id` to (a) exist and
(b) be a publicly dereferencable URI from the originating server.
This alone does not yet demand strict equivalence, but the spec then
further explains objects ought to be fetchable _via their ID_.
Meaning an object not retrievable via its ID, is invalid.
This reading is supported by the fact, e.g. GoToSocial (recently) and
Mastodon (for 6+ years) do already implement such strict ID checks,
additionally proving this doesn’t cause federation issues in practice.
However, apart from canonical IDs there can also be additional display
URLs. *omas first redirect those to their canonical location, but *keys
and Mastodon directly serve the AP representation without redirects.
Mastodon and GTS deal with this in two different ways,
but both constitute an effective countermeasure:
- Mastodon:
Unless it already is a known AP id, two fetches occur.
The first fetch just reads the `id` property and then refetches from
the id. The last fetch requires the returned id to exactly match the
URL the content was fetched from. (This can be optimised by skipping
the second fetch if it already matches)
https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168
https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af
- GTS:
Only does a single fetch and then checks if _either_ the id
_or_ url property (which can be an object) match the original fetch
URL. This relies on implementations always including their display URL
as "url" if differing from the id. For actors this is true for all
investigated implementations, for posts only Mastodon includes an
"url", but it is also the only one with a differing display URL.
https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222
Albeit Mastodon’s refetch offers higher compatibility with theoretical
implmentations using either multiple different display URL or not
denoting any of them as "url" at all, for now we chose to adopt a
GTS-like refetch-free approach to avoid additional implementation
concerns wrt to whether redirects should be allowed when fetching a
canonical AP id and potential for accidentally loosening some checks
(e.g. cross-domain refetches) for one of the fetches.
This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
|
|
|
|
)
|
|
|
|
|
2024-04-13 22:55:26 +00:00
|
|
|
assert {:error, :id_mismatch} =
|
Only allow exact id matches
This protects us from falling for obvious spoofs as from the current
upload exploit (unfortunately we can’t reasonably do anything about
spoofs with exact matches as was possible via emoji and proxy).
Such objects being invalid is supported by the spec, sepcifically
sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id
Anonymous objects are not relevant here (they can only exists within
parent objects iiuc) and neither is client-to-server or transient objects
(as those cannot be fetched in the first place).
This leaves us with the requirement for `id` to (a) exist and
(b) be a publicly dereferencable URI from the originating server.
This alone does not yet demand strict equivalence, but the spec then
further explains objects ought to be fetchable _via their ID_.
Meaning an object not retrievable via its ID, is invalid.
This reading is supported by the fact, e.g. GoToSocial (recently) and
Mastodon (for 6+ years) do already implement such strict ID checks,
additionally proving this doesn’t cause federation issues in practice.
However, apart from canonical IDs there can also be additional display
URLs. *omas first redirect those to their canonical location, but *keys
and Mastodon directly serve the AP representation without redirects.
Mastodon and GTS deal with this in two different ways,
but both constitute an effective countermeasure:
- Mastodon:
Unless it already is a known AP id, two fetches occur.
The first fetch just reads the `id` property and then refetches from
the id. The last fetch requires the returned id to exactly match the
URL the content was fetched from. (This can be optimised by skipping
the second fetch if it already matches)
https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168
https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af
- GTS:
Only does a single fetch and then checks if _either_ the id
_or_ url property (which can be an object) match the original fetch
URL. This relies on implementations always including their display URL
as "url" if differing from the id. For actors this is true for all
investigated implementations, for posts only Mastodon includes an
"url", but it is also the only one with a differing display URL.
https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222
Albeit Mastodon’s refetch offers higher compatibility with theoretical
implmentations using either multiple different display URL or not
denoting any of them as "url" at all, for now we chose to adopt a
GTS-like refetch-free approach to avoid additional implementation
concerns wrt to whether redirects should be allowed when fetching a
canonical AP id and potential for accidentally loosening some checks
(e.g. cross-domain refetches) for one of the fetches.
This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://patch.cx/media/spoof_stage1.json"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2024-03-13 21:12:17 +00:00
|
|
|
test "it does not fetch an object via cross-domain redirects (initial id)" do
|
|
|
|
assert {:error, {:cross_domain_redirect, true}} =
|
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://patch.cx/objects/spoof_media_redirect1"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it does not fetch an object via cross-domain redirects (final id)" do
|
|
|
|
assert {:error, {:cross_domain_redirect, true}} =
|
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://patch.cx/objects/spoof_media_redirect2"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it accepts same-domain redirects" do
|
|
|
|
assert {:ok, %{"id" => id} = _object} =
|
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://patch.cx/objects/spoof_redirect"
|
|
|
|
)
|
|
|
|
|
|
|
|
assert id == "https://patch.cx/objects/spoof_redirect"
|
|
|
|
end
|
2024-03-13 21:21:19 +00:00
|
|
|
|
|
|
|
test "it does not fetch a spoofed object with a foreign actor" do
|
2024-04-13 22:55:26 +00:00
|
|
|
assert {:error, _} =
|
2024-03-13 21:21:19 +00:00
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://patch.cx/objects/spoof_foreign_actor"
|
|
|
|
)
|
|
|
|
end
|
2024-03-13 22:00:23 +00:00
|
|
|
|
|
|
|
test "it does not fetch from localhost" do
|
2024-04-13 22:55:26 +00:00
|
|
|
assert {:error, :local_resource} =
|
2024-03-13 22:00:23 +00:00
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
Pleroma.Web.Endpoint.url() <> "/spoof_local"
|
|
|
|
)
|
|
|
|
end
|
2024-03-13 21:04:31 +00:00
|
|
|
end
|
|
|
|
|
2018-12-01 23:03:03 +00:00
|
|
|
describe "fetching an object" do
|
|
|
|
test "it fetches an object" do
|
|
|
|
{:ok, object} =
|
|
|
|
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
|
|
|
|
2021-03-25 09:17:26 +00:00
|
|
|
assert _activity = Activity.get_create_by_object_ap_id(object.data["id"])
|
2018-12-01 23:03:03 +00:00
|
|
|
|
|
|
|
{:ok, object_again} =
|
|
|
|
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
|
|
|
|
|
|
|
assert [attachment] = object.data["attachment"]
|
|
|
|
assert is_list(attachment["url"])
|
|
|
|
|
|
|
|
assert object == object_again
|
|
|
|
end
|
2020-09-11 17:58:58 +00:00
|
|
|
|
|
|
|
test "Return MRF reason when fetched status is rejected by one" do
|
|
|
|
clear_config([:mrf_keyword, :reject], ["yeah"])
|
|
|
|
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
|
|
|
|
|
|
|
|
assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
|
|
|
|
Fetcher.fetch_object_from_id(
|
|
|
|
"http://mastodon.example.org/@admin/99541947525187367"
|
|
|
|
)
|
|
|
|
end
|
2020-10-28 15:08:23 +00:00
|
|
|
|
2022-12-10 00:09:45 +00:00
|
|
|
test "does not fetch anything from a rejected instance" do
|
|
|
|
clear_config([:mrf_simple, :reject], [{"evil.example.org", "i said so"}])
|
|
|
|
|
|
|
|
assert {:reject, _} =
|
|
|
|
Fetcher.fetch_object_from_id("http://evil.example.org/@admin/99541947525187367")
|
|
|
|
end
|
|
|
|
|
|
|
|
test "does not fetch anything if mrf_simple accept is on" do
|
|
|
|
clear_config([:mrf_simple, :accept], [{"mastodon.example.org", "i said so"}])
|
|
|
|
clear_config([:mrf_simple, :reject], [])
|
|
|
|
|
|
|
|
assert {:reject, _} =
|
|
|
|
Fetcher.fetch_object_from_id(
|
|
|
|
"http://notlisted.example.org/@admin/99541947525187367"
|
|
|
|
)
|
|
|
|
|
|
|
|
assert {:ok, _object} =
|
|
|
|
Fetcher.fetch_object_from_id(
|
|
|
|
"http://mastodon.example.org/@admin/99541947525187367"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2022-11-15 17:23:47 +00:00
|
|
|
test "it resets instance reachability on successful fetch" do
|
|
|
|
id = "http://mastodon.example.org/@admin/99541947525187367"
|
|
|
|
Instances.set_consistently_unreachable(id)
|
|
|
|
refute Instances.reachable?(id)
|
|
|
|
|
2022-11-18 11:14:35 +00:00
|
|
|
{:ok, _object} =
|
2022-11-15 17:23:47 +00:00
|
|
|
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
|
|
|
|
|
|
|
assert Instances.reachable?(id)
|
|
|
|
end
|
2018-12-01 23:03:03 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
describe "implementation quirks" do
|
|
|
|
test "it can fetch plume articles" do
|
|
|
|
{:ok, object} =
|
|
|
|
Fetcher.fetch_object_from_id(
|
|
|
|
"https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
|
|
|
|
)
|
|
|
|
|
|
|
|
assert object
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it can fetch peertube videos" do
|
|
|
|
{:ok, object} =
|
|
|
|
Fetcher.fetch_object_from_id(
|
|
|
|
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
|
|
|
|
)
|
|
|
|
|
|
|
|
assert object
|
|
|
|
end
|
|
|
|
|
2019-12-17 15:16:21 +00:00
|
|
|
test "it can fetch Mobilizon events" do
|
|
|
|
{:ok, object} =
|
|
|
|
Fetcher.fetch_object_from_id(
|
|
|
|
"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
|
|
|
|
)
|
|
|
|
|
|
|
|
assert object
|
|
|
|
end
|
|
|
|
|
2019-07-24 19:28:21 +00:00
|
|
|
test "it can fetch wedistribute articles" do
|
|
|
|
{:ok, object} =
|
|
|
|
Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810")
|
|
|
|
|
|
|
|
assert object
|
|
|
|
end
|
|
|
|
|
2018-12-01 23:03:03 +00:00
|
|
|
test "all objects with fake directions are rejected by the object fetcher" do
|
2019-06-13 09:34:03 +00:00
|
|
|
assert {:error, _} =
|
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://info.pleroma.site/activity4.json"
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "handle HTTP 410 Gone response" do
|
2023-12-28 02:57:47 +00:00
|
|
|
assert {:error, :not_found} ==
|
2019-06-13 09:34:03 +00:00
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://mastodon.example.org/users/userisgone"
|
|
|
|
)
|
2018-12-01 23:03:03 +00:00
|
|
|
end
|
2019-06-13 10:13:35 +00:00
|
|
|
|
|
|
|
test "handle HTTP 404 response" do
|
2023-12-28 02:57:47 +00:00
|
|
|
assert {:error, :not_found} ==
|
2019-06-13 10:13:35 +00:00
|
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
|
|
"https://mastodon.example.org/users/userisgone404"
|
|
|
|
)
|
|
|
|
end
|
2020-06-23 03:30:34 +00:00
|
|
|
|
|
|
|
test "it can fetch pleroma polls with attachments" do
|
|
|
|
{:ok, object} =
|
|
|
|
Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment")
|
|
|
|
|
|
|
|
assert object
|
|
|
|
end
|
2018-12-01 23:03:03 +00:00
|
|
|
end
|
2019-05-21 00:41:58 +00:00
|
|
|
|
|
|
|
describe "pruning" do
|
|
|
|
test "it can refetch pruned objects" do
|
|
|
|
object_id = "http://mastodon.example.org/@admin/99541947525187367"
|
|
|
|
|
|
|
|
{:ok, object} = Fetcher.fetch_object_from_id(object_id)
|
|
|
|
|
|
|
|
assert object
|
|
|
|
|
|
|
|
{:ok, _object} = Object.prune(object)
|
|
|
|
|
|
|
|
refute Object.get_by_ap_id(object_id)
|
|
|
|
|
|
|
|
{:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id)
|
|
|
|
|
|
|
|
assert object.data["id"] == object_two.data["id"]
|
|
|
|
assert object.id != object_two.id
|
|
|
|
end
|
|
|
|
end
|
2019-07-17 22:58:52 +00:00
|
|
|
|
|
|
|
describe "signed fetches" do
|
2020-03-20 15:33:00 +00:00
|
|
|
setup do: clear_config([:activitypub, :sign_object_fetches])
|
2019-08-19 15:34:29 +00:00
|
|
|
|
2019-07-17 22:58:52 +00:00
|
|
|
test_with_mock "it signs fetches when configured to do so",
|
|
|
|
Pleroma.Signature,
|
|
|
|
[:passthrough],
|
|
|
|
[] do
|
2021-01-26 17:58:43 +00:00
|
|
|
clear_config([:activitypub, :sign_object_fetches], true)
|
2019-07-17 22:58:52 +00:00
|
|
|
|
|
|
|
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
|
|
|
|
|
|
|
assert called(Pleroma.Signature.sign(:_, :_))
|
|
|
|
end
|
|
|
|
|
|
|
|
test_with_mock "it doesn't sign fetches when not configured to do so",
|
|
|
|
Pleroma.Signature,
|
|
|
|
[:passthrough],
|
|
|
|
[] do
|
2021-01-26 17:58:43 +00:00
|
|
|
clear_config([:activitypub, :sign_object_fetches], false)
|
2019-07-17 22:58:52 +00:00
|
|
|
|
|
|
|
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
|
|
|
|
|
|
|
refute called(Pleroma.Signature.sign(:_, :_))
|
|
|
|
end
|
|
|
|
end
|
2022-09-06 19:24:02 +00:00
|
|
|
|
|
|
|
describe "refetching" do
|
|
|
|
setup do
|
|
|
|
object1 = %{
|
|
|
|
"id" => "https://mastodon.social/1",
|
|
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
|
|
"type" => "Note",
|
|
|
|
"content" => "test 1",
|
|
|
|
"bcc" => [],
|
|
|
|
"bto" => [],
|
|
|
|
"cc" => [],
|
|
|
|
"to" => [],
|
|
|
|
"summary" => ""
|
|
|
|
}
|
|
|
|
|
|
|
|
object2 = %{
|
|
|
|
"id" => "https://mastodon.social/2",
|
|
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
|
|
"type" => "Note",
|
|
|
|
"content" => "test 2",
|
|
|
|
"bcc" => [],
|
|
|
|
"bto" => [],
|
|
|
|
"cc" => [],
|
|
|
|
"to" => [],
|
|
|
|
"summary" => "",
|
|
|
|
"formerRepresentations" => %{
|
|
|
|
"type" => "OrderedCollection",
|
|
|
|
"orderedItems" => [
|
|
|
|
%{
|
|
|
|
"type" => "Note",
|
|
|
|
"content" => "orig 2",
|
|
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
|
|
"bcc" => [],
|
|
|
|
"bto" => [],
|
|
|
|
"cc" => [],
|
|
|
|
"to" => [],
|
|
|
|
"summary" => ""
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"totalItems" => 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mock(fn
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://mastodon.social/1"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: Jason.encode!(object1)
|
|
|
|
}
|
|
|
|
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://mastodon.social/2"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: Jason.encode!(object2)
|
|
|
|
}
|
|
|
|
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://mastodon.social/users/emelie/collections/featured"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body:
|
|
|
|
Jason.encode!(%{
|
|
|
|
"id" => "https://mastodon.social/users/emelie/collections/featured",
|
|
|
|
"type" => "OrderedCollection",
|
|
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
|
|
"orderedItems" => [],
|
|
|
|
"totalItems" => 0
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
env ->
|
|
|
|
apply(HttpRequestMock, :request, [env])
|
|
|
|
end)
|
|
|
|
|
|
|
|
%{object1: object1, object2: object2}
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
|
|
|
|
full_object1 =
|
|
|
|
object1
|
|
|
|
|> Map.merge(%{
|
|
|
|
"formerRepresentations" => %{
|
|
|
|
"type" => "OrderedCollection",
|
|
|
|
"orderedItems" => [
|
|
|
|
%{
|
|
|
|
"type" => "Note",
|
|
|
|
"content" => "orig 2",
|
|
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
|
|
"bcc" => [],
|
|
|
|
"bto" => [],
|
|
|
|
"cc" => [],
|
|
|
|
"to" => [],
|
|
|
|
"summary" => ""
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"totalItems" => 1
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
{:ok, o} = Object.create(full_object1)
|
|
|
|
|
|
|
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
|
|
|
|
|
|
|
assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
|
|
|
|
refetched.data
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it uses formerRepresentations from remote if possible", %{object2: object2} do
|
|
|
|
{:ok, o} = Object.create(object2)
|
|
|
|
|
|
|
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
|
|
|
|
|
|
|
assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
|
|
|
|
refetched.data
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it replaces formerRepresentations with the one from remote", %{object2: object2} do
|
|
|
|
full_object2 =
|
|
|
|
object2
|
|
|
|
|> Map.merge(%{
|
|
|
|
"content" => "mew mew #def",
|
|
|
|
"formerRepresentations" => %{
|
|
|
|
"type" => "OrderedCollection",
|
|
|
|
"orderedItems" => [
|
|
|
|
%{"type" => "Note", "content" => "mew mew 2"}
|
|
|
|
],
|
|
|
|
"totalItems" => 1
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
{:ok, o} = Object.create(full_object2)
|
|
|
|
|
|
|
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
|
|
|
|
|
|
|
assert %{
|
|
|
|
"content" => "test 2",
|
|
|
|
"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}
|
|
|
|
} = refetched.data
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it adds to formerRepresentations if the remote does not have one and the object has changed",
|
|
|
|
%{object1: object1} do
|
|
|
|
full_object1 =
|
|
|
|
object1
|
|
|
|
|> Map.merge(%{
|
|
|
|
"content" => "mew mew #def",
|
|
|
|
"formerRepresentations" => %{
|
|
|
|
"type" => "OrderedCollection",
|
|
|
|
"orderedItems" => [
|
|
|
|
%{"type" => "Note", "content" => "mew mew 1"}
|
|
|
|
],
|
|
|
|
"totalItems" => 1
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
{:ok, o} = Object.create(full_object1)
|
|
|
|
|
|
|
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
|
|
|
|
|
|
|
assert %{
|
|
|
|
"content" => "test 1",
|
|
|
|
"formerRepresentations" => %{
|
|
|
|
"orderedItems" => [
|
|
|
|
%{"content" => "mew mew #def"},
|
|
|
|
%{"content" => "mew mew 1"}
|
|
|
|
],
|
|
|
|
"totalItems" => 2
|
|
|
|
}
|
|
|
|
} = refetched.data
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "fetch with history" do
|
|
|
|
setup do
|
|
|
|
object2 = %{
|
|
|
|
"id" => "https://mastodon.social/2",
|
|
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
|
|
"type" => "Note",
|
|
|
|
"content" => "test 2",
|
|
|
|
"bcc" => [],
|
|
|
|
"bto" => [],
|
|
|
|
"cc" => ["https://mastodon.social/users/emelie/followers"],
|
|
|
|
"to" => [],
|
|
|
|
"summary" => "",
|
|
|
|
"formerRepresentations" => %{
|
|
|
|
"type" => "OrderedCollection",
|
|
|
|
"orderedItems" => [
|
|
|
|
%{
|
|
|
|
"type" => "Note",
|
|
|
|
"content" => "orig 2",
|
|
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
|
|
"bcc" => [],
|
|
|
|
"bto" => [],
|
|
|
|
"cc" => ["https://mastodon.social/users/emelie/followers"],
|
|
|
|
"to" => [],
|
|
|
|
"summary" => ""
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"totalItems" => 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mock(fn
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://mastodon.social/2"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: Jason.encode!(object2)
|
|
|
|
}
|
|
|
|
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://mastodon.social/users/emelie/collections/featured"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body:
|
|
|
|
Jason.encode!(%{
|
|
|
|
"id" => "https://mastodon.social/users/emelie/collections/featured",
|
|
|
|
"type" => "OrderedCollection",
|
|
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
|
|
"orderedItems" => [],
|
|
|
|
"totalItems" => 0
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
env ->
|
|
|
|
apply(HttpRequestMock, :request, [env])
|
|
|
|
end)
|
|
|
|
|
|
|
|
%{object2: object2}
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it gets history", %{object2: object2} do
|
|
|
|
{:ok, object} = Fetcher.fetch_object_from_id(object2["id"])
|
|
|
|
|
|
|
|
assert %{
|
|
|
|
"formerRepresentations" => %{
|
|
|
|
"type" => "OrderedCollection",
|
|
|
|
"orderedItems" => [%{}]
|
|
|
|
}
|
|
|
|
} = object.data
|
|
|
|
end
|
|
|
|
end
|
2022-12-12 19:06:04 +00:00
|
|
|
|
|
|
|
describe "get_object/1" do
|
|
|
|
test "should return ok if the content type is application/activity+json" do
|
|
|
|
Tesla.Mock.mock(fn
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://mastodon.social/2"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
2024-03-15 19:57:09 +00:00
|
|
|
url: "https://mastodon.social/2",
|
2022-12-12 19:06:04 +00:00
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: "{}"
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
|
2024-03-15 19:57:09 +00:00
|
|
|
assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2")
|
2022-12-12 19:06:04 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
test "should return ok if the content type is application/ld+json with a profile" do
|
|
|
|
Tesla.Mock.mock(fn
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://mastodon.social/2"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
2024-03-15 19:57:09 +00:00
|
|
|
url: "https://mastodon.social/2",
|
2022-12-12 19:06:04 +00:00
|
|
|
headers: [
|
|
|
|
{"content-type",
|
|
|
|
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""}
|
|
|
|
],
|
|
|
|
body: "{}"
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
|
2024-03-15 19:57:09 +00:00
|
|
|
assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2")
|
2022-12-12 19:06:04 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
test "should not return ok with other content types" do
|
|
|
|
Tesla.Mock.mock(fn
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://mastodon.social/2"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
2024-03-15 19:57:09 +00:00
|
|
|
url: "https://mastodon.social/2",
|
2022-12-12 19:06:04 +00:00
|
|
|
headers: [{"content-type", "application/json"}],
|
|
|
|
body: "{}"
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
|
|
|
|
assert {:error, {:content_type, "application/json"}} =
|
|
|
|
Fetcher.get_object("https://mastodon.social/2")
|
|
|
|
end
|
2024-03-15 19:57:09 +00:00
|
|
|
|
|
|
|
test "returns the url after redirects" do
|
|
|
|
Tesla.Mock.mock(fn
|
|
|
|
%{
|
|
|
|
method: :get,
|
|
|
|
url: "https://mastodon.social/5"
|
|
|
|
} ->
|
|
|
|
%Tesla.Env{
|
|
|
|
status: 200,
|
|
|
|
url: "https://mastodon.social/7",
|
|
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
|
|
body: "{}"
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
|
|
|
|
assert {:ok, "https://mastodon.social/7", "{}"} =
|
|
|
|
Fetcher.get_object("https://mastodon.social/5")
|
|
|
|
end
|
2022-12-12 19:06:04 +00:00
|
|
|
end
|
2018-12-01 23:03:03 +00:00
|
|
|
end
|