From cba2c5725fcec6b63bcfabf35da5b60027a06b45 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 15 Jun 2024 15:05:52 +0100 Subject: [PATCH 01/48] Filter emoji reaction accounts by domain blocks --- lib/pleroma/user.ex | 16 ++++++------ lib/pleroma/web/activity_pub/mrf.ex | 2 +- .../controllers/emoji_reaction_controller.ex | 15 ++++++++++- mix.lock | 4 +-- test/pleroma/user_test.exs | 25 +++++++++++++++++++ .../mastodon_api/views/status_view_test.exs | 11 +++++++- 6 files changed, 60 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 14414adc4..ee510b4b1 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1624,14 +1624,14 @@ def blocks_user?(%User{} = user, %User{} = target) do def blocks_user?(_, _), do: false - def blocks_domain?(%User{} = user, %User{} = target) do - %{host: host} = URI.parse(target.ap_id) - Enum.member?(user.domain_blocks, host) - # TODO: functionality should probably be changed such that subdomains block as well, - # but as it stands, this just hecks up the relationships endpoint - # domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) - # %{host: host} = URI.parse(target.ap_id) - # Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host) + def blocks_domain?(%User{} = user, %User{ap_id: ap_id}) do + blocks_domain?(user, ap_id) + end + + def blocks_domain?(%User{} = user, url) when is_binary(url) do + domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) + %{host: host} = URI.parse(url) + Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host) end def blocks_domain?(_, _), do: false diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index d03568444..02c0216a1 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -233,7 +233,7 @@ def config_descriptions(policies) do if function_exported?(policy, :config_description, 0) do description = @default_description - |> Map.merge(policy.config_description) + |> Map.merge(policy.config_description()) |> Map.put(:group, :pleroma) |> Map.put(:tab, :mrf) |> Map.put(:type, :group) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex index e762fcad8..49910df65 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -52,6 +52,15 @@ defp filter_allowed_user_by_ap_id(ap_ids, excluded_ap_ids) do end) end + defp filter_allowed_users_by_domain(ap_ids, %User{} = for_user) do + Enum.reject(ap_ids, fn ap_id -> + User.blocks_domain?(for_user, ap_id) + end) + end + + + defp filter_allowed_users_by_domain(ap_ids, nil), do: ap_ids + def filter_allowed_users(reactions, user, with_muted) do exclude_ap_ids = if is_nil(user) do @@ -62,12 +71,16 @@ def filter_allowed_users(reactions, user, with_muted) do end filter_emoji = fn emoji, users, url -> - case filter_allowed_user_by_ap_id(users, exclude_ap_ids) do + users + |> filter_allowed_user_by_ap_id(exclude_ap_ids) + |> filter_allowed_users_by_domain(user) + |> case do [] -> nil users -> {emoji, users, url} end end + reactions |> Stream.map(fn [emoji, users, url] when is_list(users) -> filter_emoji.(emoji, users, url) diff --git a/mix.lock b/mix.lock index a4e2ddc8c..9fce9c60e 100644 --- a/mix.lock +++ b/mix.lock @@ -18,7 +18,7 @@ "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, - "credo": {:hex, :credo, "1.7.6", "b8f14011a5443f2839b04def0b252300842ce7388f3af177157c86da18dfbeea", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "146f347fb9f8cbc5f7e39e3f22f70acbef51d441baa6d10169dd604bfbc55296"}, + "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, @@ -94,7 +94,7 @@ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index de71f4b95..38cbe824d 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -1149,6 +1149,18 @@ test "it blocks people" do assert User.blocks?(user, blocked_user) end + test "it blocks domains" do + user = insert(:user) + blocked_user = insert(:user) + + refute User.blocks_domain?(user, blocked_user) + + url = URI.parse(blocked_user.ap_id) + {:ok, user} = User.block_domain(user, url.host) + + assert User.blocks_domain?(user, blocked_user) + end + test "it unblocks users" do user = insert(:user) blocked_user = insert(:user) @@ -1159,6 +1171,19 @@ test "it unblocks users" do refute User.blocks?(user, blocked_user) end + test "it unblocks domains" do + user = insert(:user) + blocked_user = insert(:user) + + url = URI.parse(blocked_user.ap_id) + {:ok, user} = User.block_domain(user, url.host) + assert User.blocks_domain?(user, blocked_user) + + {:ok, user} = User.unblock_domain(user, url.host) + + refute User.blocks_domain?(user, blocked_user) + end + test "blocks tear down cyclical follow relationships" do blocker = insert(:user) blocked = insert(:user) diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index 7db3e3e61..41351a0fb 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -33,6 +33,10 @@ test "has an emoji reaction list" do user = insert(:user) other_user = insert(:user) third_user = insert(:user) + domain_blocked_user = insert(:user, %{ap_id: "https://blocked.com/@blocked"}) + + {:ok, user} = User.block_domain(user, "blocked.com") + {:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"}) {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕") @@ -40,6 +44,8 @@ test "has an emoji reaction list" do {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:") + # this should not show up when the user is viewing the status + {:ok, _} = CommonAPI.react_with_emoji(activity.id, domain_blocked_user, "😈") activity = Repo.get(Activity, activity.id) status = StatusView.render("show.json", activity: activity) @@ -55,7 +61,8 @@ test "has an emoji reaction list" do url: "http://localhost:4001/emoji/dino walking.gif", account_ids: [other_user.id, user.id] }, - %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} + %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}, + %{name: "😈", count: 1, me: false, url: nil, account_ids: [domain_blocked_user.id]} ] status = StatusView.render("show.json", activity: activity, for: user) @@ -73,6 +80,8 @@ test "has an emoji reaction list" do }, %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} ] + + refute Enum.any?(status[:pleroma][:emoji_reactions], fn reaction -> reaction[:name] == "😈" end) end test "works correctly with badly formatted emojis" do From 4b765b18864d4bca4bc1098d4373c2e126e28d49 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 15 Jun 2024 15:06:28 +0100 Subject: [PATCH 02/48] mix format --- .../web/pleroma_api/controllers/emoji_reaction_controller.ex | 4 +--- test/pleroma/web/mastodon_api/views/status_view_test.exs | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex index 49910df65..66cff7aaa 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -54,11 +54,10 @@ defp filter_allowed_user_by_ap_id(ap_ids, excluded_ap_ids) do defp filter_allowed_users_by_domain(ap_ids, %User{} = for_user) do Enum.reject(ap_ids, fn ap_id -> - User.blocks_domain?(for_user, ap_id) + User.blocks_domain?(for_user, ap_id) end) end - defp filter_allowed_users_by_domain(ap_ids, nil), do: ap_ids def filter_allowed_users(reactions, user, with_muted) do @@ -80,7 +79,6 @@ def filter_allowed_users(reactions, user, with_muted) do end end - reactions |> Stream.map(fn [emoji, users, url] when is_list(users) -> filter_emoji.(emoji, users, url) diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index 41351a0fb..6421df132 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -62,7 +62,7 @@ test "has an emoji reaction list" do account_ids: [other_user.id, user.id] }, %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}, - %{name: "😈", count: 1, me: false, url: nil, account_ids: [domain_blocked_user.id]} + %{name: "😈", count: 1, me: false, url: nil, account_ids: [domain_blocked_user.id]} ] status = StatusView.render("show.json", activity: activity, for: user) @@ -81,7 +81,7 @@ test "has an emoji reaction list" do %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} ] - refute Enum.any?(status[:pleroma][:emoji_reactions], fn reaction -> reaction[:name] == "😈" end) + refute Enum.any?(status[:pleroma][:emoji_reactions], fn reaction -> reaction[:name] == "😈" end) end test "works correctly with badly formatted emojis" do From c0b2bba55e295437a71ee18c669254d4926cf017 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 15 Jun 2024 15:14:42 +0100 Subject: [PATCH 03/48] revert subdomain change until i can look at why i did that --- lib/pleroma/user.ex | 5 ++--- test/pleroma/user_test.exs | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ee510b4b1..223b12223 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1629,9 +1629,8 @@ def blocks_domain?(%User{} = user, %User{ap_id: ap_id}) do end def blocks_domain?(%User{} = user, url) when is_binary(url) do - domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) - %{host: host} = URI.parse(url) - Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host) + %{host: host} = URI.parse(target.ap_id) + Enum.member?(user.domain_blocks, host) end def blocks_domain?(_, _), do: false diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 38cbe824d..e3f0bb415 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -1177,8 +1177,6 @@ test "it unblocks domains" do url = URI.parse(blocked_user.ap_id) {:ok, user} = User.block_domain(user, url.host) - assert User.blocks_domain?(user, blocked_user) - {:ok, user} = User.unblock_domain(user, url.host) refute User.blocks_domain?(user, blocked_user) From 3b197503d2bfa3ed63a2f7269db12a9c9f3fbbea Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 15 Jun 2024 15:30:02 +0100 Subject: [PATCH 04/48] me me stupid person --- lib/pleroma/user.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 223b12223..6b0d90147 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1629,7 +1629,7 @@ def blocks_domain?(%User{} = user, %User{ap_id: ap_id}) do end def blocks_domain?(%User{} = user, url) when is_binary(url) do - %{host: host} = URI.parse(target.ap_id) + %{host: host} = URI.parse(url) Enum.member?(user.domain_blocks, host) end From 4ff52930936699161223b12c4396d5a5ad6736d4 Mon Sep 17 00:00:00 2001 From: Oneric Date: Sun, 23 Jun 2024 20:46:58 +0200 Subject: [PATCH 05/48] Federate emoji as anonymous objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Usually an id should point to another AP object and the image file isn’t an AP object. We currently do not provide standalone AP objects for emoji and don't keep track of remote emoji at all. Thus just federate them as anonymous objects, i.e. objects only existing within a parent context and using an explicit null id. IceShrimp.NET previously adopted anonymous objects for remote emoji without any apparent issues. See: https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/commit/333611f65eb2a65b2779ece0435b5ba84bf60e99 Fixes: https://akkoma.dev/AkkomaGang/akkoma/issues/694 --- CHANGELOG.md | 2 ++ lib/pleroma/web/activity_pub/transmogrifier.ex | 2 +- .../web/activity_pub/transmogrifier/note_handling_test.exs | 2 +- test/pleroma/web/activity_pub/views/user_view_test.exs | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c743e5bd..c04ccec32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Fixed - Meilisearch: order of results returned from our REST API now actually matches how Meilisearch ranks results +- Emoji are now federated as anonymous objects, fixing issues with + some strict servers e.g. rejecting e.g. remote emoji reactions ## Changed - Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated. diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index ca5e85f2e..75c1f0f0c 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -951,7 +951,7 @@ defp build_emoji_tag({name, url}) do "name" => ":" <> name <> ":", "type" => "Emoji", "updated" => "1970-01-01T00:00:00Z", - "id" => url + "id" => nil } end diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs index 16ee31483..c8aa2a1ed 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -700,7 +700,7 @@ test "take_emoji_tags/1" do assert Transmogrifier.take_emoji_tags(user) == [ %{ "icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"}, - "id" => "https://example.org/firefox.png", + "id" => nil, "name" => ":firefox:", "type" => "Emoji", "updated" => "1970-01-01T00:00:00Z" diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs index abe91cdea..2a367b680 100644 --- a/test/pleroma/web/activity_pub/views/user_view_test.exs +++ b/test/pleroma/web/activity_pub/views/user_view_test.exs @@ -43,7 +43,7 @@ test "Renders with emoji tags" do "tag" => [ %{ "icon" => %{"type" => "Image", "url" => "/test"}, - "id" => "/test", + "id" => nil, "name" => ":bib:", "type" => "Emoji", "updated" => "1970-01-01T00:00:00Z" From 80a4e30be7f41142e2f64868d2af2c468d0bdf41 Mon Sep 17 00:00:00 2001 From: Oneric Date: Thu, 27 Jun 2024 18:27:18 +0200 Subject: [PATCH 06/48] Upgrade captach dep Fixes a deprecation warning showing up each mix call when using elixir 1.17 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 7ffc450e2..b147f2681 100644 --- a/mix.exs +++ b/mix.exs @@ -180,7 +180,7 @@ defp deps do {:remote_ip, "~> 1.1.0"}, {:captcha, git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", - ref: "90f6ce7672f70f56708792a98d98bd05176c9176"}, + ref: "6630c42aaaab124e697b4e513190c89d8b64e410"}, {:restarter, path: "./restarter"}, {:majic, git: "https://akkoma.dev/AkkomaGang/majic.git", From 7cd395415210f5a981d45e26bc6e21e078ee96cd Mon Sep 17 00:00:00 2001 From: Oneric Date: Thu, 20 Jun 2024 19:52:31 +0200 Subject: [PATCH 07/48] Remove superfluous actor key suffix Fragments are already always stripped anyway so listing one specific fragment here is unnecessary and potentially confusing. This effectively reverts 4457928e325cf370a0d2e028232dbd0a542547e0 but keeps the added bridgy testcase. --- lib/pleroma/signature.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index c4ac2c87e..3d33fcd62 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Signature do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - @known_suffixes ["/publickey", "/main-key", "#key"] + @known_suffixes ["/publickey", "/main-key"] def key_id_to_actor_id(key_id) do uri = From 95ed4931f8660df352a4f80f9b4767dba94f1eea Mon Sep 17 00:00:00 2001 From: Oneric Date: Thu, 20 Jun 2024 19:52:34 +0200 Subject: [PATCH 08/48] docs: note frontend tasks need to be run as akkoma user Fixes: https://akkoma.dev/AkkomaGang/akkoma/issues/748 --- docs/docs/installation/frontends.include | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/docs/installation/frontends.include b/docs/docs/installation/frontends.include index 6da4018a9..094cfe4bd 100644 --- a/docs/docs/installation/frontends.include +++ b/docs/docs/installation/frontends.include @@ -6,7 +6,9 @@ probably install frontends. These are no longer bundled with the distribution and need an extra command to install. -For most installations, the following will suffice: +You **must** run frontend management tasks as the akkoma user, +the same way you downloaded the build or cloned the git repo before. +But otherwise, for most installations, the following will suffice: === "OTP" ```sh @@ -28,4 +30,3 @@ For most installations, the following will suffice: ``` For more customised installations, refer to [Frontend Management](../../configuration/frontend_management) - From 0ab2f2ab457d16894c3a4affb77cd66a33901437 Mon Sep 17 00:00:00 2001 From: Oneric Date: Thu, 20 Jun 2024 19:52:39 +0200 Subject: [PATCH 09/48] ci: retry failed tasks once We have a bunch of mysterious sporadic failures which usually disappear when rerunning failed jobs only. Ideally we should locate and fix the cause of those psoradic failures, but until we figure this out retrying once makes CI status less useless. --- .woodpecker/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml index 81c779b50..45cf7f93d 100644 --- a/.woodpecker/test.yml +++ b/.woodpecker/test.yml @@ -87,5 +87,5 @@ steps: - mix ecto.create - mix ecto.migrate - mkdir -p test/tmp - - mix test --preload-modules --exclude erratic --exclude federated --exclude mocked - - mix test --preload-modules --only mocked + - mix test --preload-modules --exclude erratic --exclude federated --exclude mocked || mix test --failed + - mix test --preload-modules --only mocked || mix test --failed From 07539f78257c366536341063c547a63eee6ef160 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 18 Jun 2024 10:50:31 -0400 Subject: [PATCH 10/48] Hide logs during test unless a test fails Currently `mix test` prints a slew of logs in the terminal with messages from different tests intermsparsed. Globally enabling capture log hides log messages unless a test fails reducing noise and making it easier to anylse the important (from failed tests) messages. Compiler warnings and a few messages not printed via Logger still show up but its much more readable than before. Ported from: https://git.pleroma.social/pleroma/pleroma/-/commit/3aed111a42472018f01899714305d4f3a5cbc911 --- test/mix/tasks/pleroma/config_test.exs | 1 - test/pleroma/object/fetcher_test.exs | 1 - test/pleroma/user_test.exs | 2 -- .../web/activity_pub/activity_pub_controller_test.exs | 4 ---- test/pleroma/web/activity_pub/relay_test.exs | 1 - .../activity_pub/transmogrifier/announce_handling_test.exs | 2 -- .../activity_pub/transmogrifier/delete_handling_test.exs | 1 - .../web/activity_pub/transmogrifier/note_handling_test.exs | 2 -- test/pleroma/web/activity_pub/transmogrifier_test.exs | 1 - .../web/mastodon_api/controllers/search_controller_test.exs | 1 - .../mastodon_api/controllers/timeline_controller_test.exs | 2 -- test/pleroma/web/push/impl_test.exs | 2 -- test/pleroma/web/web_finger_test.exs | 1 - test/test_helper.exs | 6 +++++- 14 files changed, 5 insertions(+), 22 deletions(-) diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index 3b09f656b..75c67efb4 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -51,7 +51,6 @@ test "error if file with custom settings doesn't exist" do clear_config(:configurable_from_database, true) end - @tag capture_log: true test "config migration refused when deprecated settings are found" do clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"]) assert config_records() == [] diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs index 12154cb05..74f2cd31f 100644 --- a/test/pleroma/object/fetcher_test.exs +++ b/test/pleroma/object/fetcher_test.exs @@ -189,7 +189,6 @@ defp spoofed_object_with_ids( :ok end - @tag capture_log: true 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, _} = diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index de71f4b95..90cee1c65 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -814,7 +814,6 @@ test "gets an existing user by fully qualified nickname, case insensitive" do assert user == fetched_user end - @tag capture_log: true test "returns nil if no user could be fetched" do {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la") assert fetched_user == "not found nonexistant@social.heldscal.la" @@ -871,7 +870,6 @@ test "if nicknames clash, the old user gets a prefix with the old id to the nick assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org" end - @tag capture_log: true test "it returns the old user if stale, but unfetchable" do a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index b325bcb9a..4c134c8f2 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -573,7 +573,6 @@ test "it inserts an incoming activity into the database", %{conn: conn} do assert Activity.get_by_ap_id(data["id"]) end - @tag capture_log: true test "it inserts an incoming activity into the database" <> "even if we can't fetch the user but have it in our db", %{conn: conn} do @@ -1074,7 +1073,6 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da assert Instances.reachable?(sender_host) end - @tag capture_log: true test "it removes all follower collections but actor's", %{conn: conn} do [actor, recipient] = insert_pair(:user) @@ -1138,7 +1136,6 @@ test "it requires authentication", %{conn: conn} do assert json_response(ret_conn, 200) end - @tag capture_log: true test "forwarded report", %{conn: conn} do admin = insert(:user, is_admin: true) actor = insert(:user, local: false) @@ -1215,7 +1212,6 @@ test "forwarded report", %{conn: conn} do ) end - @tag capture_log: true test "forwarded report from mastodon", %{conn: conn} do admin = insert(:user, is_admin: true) actor = insert(:user, local: false) diff --git a/test/pleroma/web/activity_pub/relay_test.exs b/test/pleroma/web/activity_pub/relay_test.exs index 920aef9da..99cc2071e 100644 --- a/test/pleroma/web/activity_pub/relay_test.exs +++ b/test/pleroma/web/activity_pub/relay_test.exs @@ -114,7 +114,6 @@ test "returns error when activity not `Create` type" do assert Relay.publish(activity) == {:error, "Not implemented"} end - @tag capture_log: true test "returns error when activity not public" do activity = insert(:direct_note_activity) assert Relay.publish(activity) == {:error, false} diff --git a/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs index 524acddaf..88f42cada 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs @@ -83,7 +83,6 @@ test "it works for incoming announces, fetching the announced object" do assert(Activity.get_create_by_object_ap_id(data["object"])) end - @tag capture_log: true test "it works for incoming announces with an existing activity" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) @@ -136,7 +135,6 @@ test "it works for incoming announces with an inlined activity" do assert object.data["content"] == "this is a private toot" end - @tag capture_log: true test "it rejects incoming announces with an inlined activity from another origin" do Tesla.Mock.mock(fn %{method: :get} -> %Tesla.Env{status: 404, body: ""} diff --git a/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs index b7160bf58..e5d6f940a 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs @@ -86,7 +86,6 @@ test "it fails for incoming deletes with spoofed origin" do assert match?({:error, _}, Transmogrifier.handle_incoming(data)) end - @tag capture_log: true test "it works for incoming user deletes" do %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs index 16ee31483..6a7bd3a43 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -57,7 +57,6 @@ test "it ignores an incoming notice if we already have it" do assert activity == returned_activity end - @tag capture_log: true test "it fetches reply-to activities if we don't have them" do data = File.read!("test/fixtures/mastodon-post-activity.json") @@ -537,7 +536,6 @@ test "returns object with inReplyTo when denied incoming reply", %{data: data} d assert modified_object["inReplyTo"] == [] end - @tag capture_log: true test "returns modified object when allowed incoming reply", %{data: data} do object_with_reply = Map.put( diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index dd7977593..1be69317c 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -561,7 +561,6 @@ test "returns nil when cannot normalize object" do end) =~ ":valid_uri_scheme" end - @tag capture_log: true test "returns {:ok, %Object{}} for success case" do assert {:ok, %Object{}} = Transmogrifier.get_obj_helper( diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs index 6a0d25058..64364cfb3 100644 --- a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs @@ -79,7 +79,6 @@ test "search", %{conn: conn} do assert status["id"] == to_string(activity.id) end - @tag capture_log: true test "constructs hashtags from search query", %{conn: conn} do results = conn diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index 3e7b6730c..ccaf17fe4 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -152,7 +152,6 @@ test "filtering", %{conn: conn, user: user} do end describe "public" do - @tag capture_log: true test "the public timeline", %{conn: conn} do user = insert(:user) @@ -810,7 +809,6 @@ test "filtering", %{user: user, conn: conn} do describe "hashtag" do setup do: oauth_access(["n/a"]) - @tag capture_log: true test "hashtag timeline", %{conn: conn} do following = insert(:user) diff --git a/test/pleroma/web/push/impl_test.exs b/test/pleroma/web/push/impl_test.exs index a40c4ff13..f9936095f 100644 --- a/test/pleroma/web/push/impl_test.exs +++ b/test/pleroma/web/push/impl_test.exs @@ -67,7 +67,6 @@ test "performs sending notifications" do assert Impl.perform(notif) == {:ok, [:ok, :ok]} end - @tag capture_log: true test "returns error if notif does not match " do assert Impl.perform(%{}) == {:error, :unknown_type} end @@ -76,7 +75,6 @@ test "successful message sending" do assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok end - @tag capture_log: true test "fail message sending" do assert Impl.push_message( @message, diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index 2af084090..4fc2a8ffa 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -190,7 +190,6 @@ test "prevents spoofing" do end end - @tag capture_log: true test "prevents forgeries" do Tesla.Mock.mock(fn %{url: "https://bad.com/.well-known/webfinger?resource=acct:meanie@bad.com"} -> diff --git a/test/test_helper.exs b/test/test_helper.exs index dafa45099..6dcb87ff6 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,7 +3,11 @@ # SPDX-License-Identifier: AGPL-3.0-only os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: [] -ExUnit.start(exclude: [:federated, :erratic] ++ os_exclude) + +ExUnit.start( + capture_log: true, + exclude: [:federated, :erratic] ++ os_exclude +) Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) From cf19d4901f688016835e4b4f17a998a6b0816d56 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 18 Jun 2024 15:25:17 +0000 Subject: [PATCH 11/48] Disable Ecto query logging in tests The debug logs are very noisy and can be enabled during analysis of a specific error believed to be SQL-related -- Before log capturing those debug messages were still hidden, but with log capturing they show up in the output of failed tests unless disabled. Cherry-picked-from: https://git.pleroma.social/pleroma/pleroma/-/commit/e628d00a81ce18eaa54c6efb1e48a31d57216368 --- config/test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/test.exs b/config/test.exs index 1df360ec7..556b9e78c 100644 --- a/config/test.exs +++ b/config/test.exs @@ -51,7 +51,8 @@ hostname: System.get_env("DB_HOST") || "localhost", pool: Ecto.Adapters.SQL.Sandbox, pool_size: 50, - queue_target: 5000 + queue_target: 5000, + log: false config :pleroma, :dangerzone, override_repo_pool_size: true From 495a1a71e89f3a3b67f9948f5e241a8b195f5ecf Mon Sep 17 00:00:00 2001 From: Oneric Date: Fri, 21 Jun 2024 18:30:47 +0200 Subject: [PATCH 12/48] strip_metadata: skip BMP files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not _yet_ supported as of exiftool 12.87, though at first glance it seems like standard BMP files can't store any metadata besides colour profiles Fixes the specific case from https://akkoma.dev/AkkomaGang/akkoma-fe/issues/396 although the frontend shouldn’t get bricked regardless. --- lib/pleroma/upload/filter/exiftool/strip_metadata.ex | 1 + .../upload/filter/exiftool/strip_location_test.exs | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/lib/pleroma/upload/filter/exiftool/strip_metadata.ex b/lib/pleroma/upload/filter/exiftool/strip_metadata.ex index 912ff6a92..a2604a682 100644 --- a/lib/pleroma/upload/filter/exiftool/strip_metadata.ex +++ b/lib/pleroma/upload/filter/exiftool/strip_metadata.ex @@ -18,6 +18,7 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripMetadata do # Formats not compatible with exiftool at this time def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop} + def filter(%Pleroma.Upload{content_type: "image/bmp"}), do: {:ok, :noop} def filter(%Pleroma.Upload{content_type: "image/svg+xml"}), do: {:ok, :noop} def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do diff --git a/test/pleroma/upload/filter/exiftool/strip_location_test.exs b/test/pleroma/upload/filter/exiftool/strip_location_test.exs index 2e017cd7e..1f798556b 100644 --- a/test/pleroma/upload/filter/exiftool/strip_location_test.exs +++ b/test/pleroma/upload/filter/exiftool/strip_location_test.exs @@ -115,6 +115,15 @@ test "verify webp files are skipped" do assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :noop} end + test "verify bmp files are skipped" do + upload = %Pleroma.Upload{ + name: "sample.bmp", + content_type: "image/bmp" + } + + assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :noop} + end + test "verify svg files are skipped" do upload = %Pleroma.Upload{ name: "sample.svg", From ca182a0ae7cde1838b6f1c63b7348af7eb6b45b1 Mon Sep 17 00:00:00 2001 From: Oneric Date: Thu, 20 Jun 2024 19:52:43 +0200 Subject: [PATCH 13/48] Correctly parse content types with multiple profiles Multiple profiles can be specified as a space-separated list and the possibility of additional profiles is explicitly brought up in ActivityStream spec --- CHANGELOG.md | 1 + lib/pleroma/object/fetcher.ex | 8 ++++++-- test/pleroma/object/fetcher_test.exs | 22 +++++++++++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c743e5bd..d1e3a42c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Fixed - Meilisearch: order of results returned from our REST API now actually matches how Meilisearch ranks results +- AP objects with additional JSON-LD profiles beyond ActivityStreams can now be fetched ## Changed - Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated. diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 937026e04..54ea9e74e 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -369,8 +369,12 @@ def get_object(id) do {"activity+json", _} -> {:ok, final_id, body} - {"ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} -> - {:ok, final_id, body} + {"ld+json", %{"profile" => profiles}} -> + if "https://www.w3.org/ns/activitystreams" in String.split(profiles) do + {:ok, final_id, body} + else + {:error, {:content_type, content_type}} + end _ -> {:error, {:content_type, content_type}} diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs index 74f2cd31f..62dd64625 100644 --- a/test/pleroma/object/fetcher_test.exs +++ b/test/pleroma/object/fetcher_test.exs @@ -754,7 +754,7 @@ test "should return ok if the content type is application/activity+json" do assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2") end - test "should return ok if the content type is application/ld+json with a profile" do + test "should return ok if the content type is application/ld+json with the ActivityStream profile" do Tesla.Mock.mock(fn %{ method: :get, @@ -774,6 +774,26 @@ test "should return ok if the content type is application/ld+json with a profile assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2") end + test "should return ok if the content type is application/ld+json with several profiles" do + Tesla.Mock.mock(fn + %{ + method: :get, + url: "https://mastodon.social/2" + } -> + %Tesla.Env{ + status: 200, + url: "https://mastodon.social/2", + headers: [ + {"content-type", + "application/ld+json; profile=\"https://example.org/ns/superduperspec https://www.w3.org/ns/activitystreams\""} + ], + body: "{}" + } + end) + + assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2") + end + test "should not return ok with other content types" do Tesla.Mock.mock(fn %{ From d488cf476ea2ea662c5ec6acfa319331c3490d27 Mon Sep 17 00:00:00 2001 From: Oneric Date: Thu, 20 Jun 2024 19:52:47 +0200 Subject: [PATCH 14/48] Fix voters count field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mastodon API demands this be null unless it’s a multi-selection poll. Not abiding by this can mess up display in some clients. Fixes: https://akkoma.dev/AkkomaGang/akkoma/issues/190 --- CHANGELOG.md | 2 ++ lib/pleroma/web/api_spec/schemas/poll.ex | 4 +++- lib/pleroma/web/mastodon_api/views/poll_view.ex | 14 +++++++++++--- .../web/mastodon_api/views/poll_view_test.exs | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1e3a42c0..0d451dca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Fixed - Meilisearch: order of results returned from our REST API now actually matches how Meilisearch ranks results - AP objects with additional JSON-LD profiles beyond ActivityStreams can now be fetched +- Single-selection polls no longer expose the voter_count; MastoAPI demands it be null + and this confused some clients leading to vote distributions >100% ## Changed - Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated. diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex index 943ad8bd4..13a5e8be2 100644 --- a/lib/pleroma/web/api_spec/schemas/poll.ex +++ b/lib/pleroma/web/api_spec/schemas/poll.ex @@ -32,7 +32,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do }, voters_count: %Schema{ type: :integer, - description: "How many unique accounts have voted. Number." + nullable: true, + description: + "How many unique accounts have voted for a multi-selection poll. Number, or null if single-selection poll." }, voted: %Schema{ type: :boolean, diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index aa6443754..411cbd15a 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -19,7 +19,7 @@ def render("show.json", %{object: object, multiple: multiple, options: options} expired: expired, multiple: multiple, votes_count: votes_count, - voters_count: voters_count(object), + voters_count: voters_count(multiple, object), options: options, emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]) } @@ -68,11 +68,19 @@ defp options_and_votes_count(options) do end) end - defp voters_count(%{data: %{"voters" => voters}}) when is_list(voters) do + defp voters_count(false, _poll_data) do + # Mastodon always sets voter count to "null" unless multiple options were selectable + # Some clients may rely on this to detect multiple selection polls and it can mess + # up percentages for some clients if we never got a correct remote voter count and + # only count local voters here; see https://akkoma.dev/AkkomaGang/akkoma/issues/190 + nil + end + + defp voters_count(_multiple, %{data: %{"voters" => voters}}) when is_list(voters) do length(voters) end - defp voters_count(_), do: 0 + defp voters_count(_, _), do: 0 defp voted_and_own_votes(%{object: object} = params, options) do if params[:for] do diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs index 224b26cb9..91d95f229 100644 --- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs @@ -43,7 +43,7 @@ test "renders a poll" do %{title: "why are you even asking?", votes_count: 0} ], votes_count: 0, - voters_count: 0 + voters_count: nil } result = PollView.render("show.json", %{object: object}) From a3101a435bae8bba7da77bba52d80e06b85593a4 Mon Sep 17 00:00:00 2001 From: Oneric Date: Thu, 20 Jun 2024 19:53:03 +0200 Subject: [PATCH 15/48] Fix swagger-ui Ever since the browser frontend switcher was introduced in de64c6c54aaacc4123031f2e3d5bfb9fc9c517fe /akkoma counts as an API prefix and thus gets skipped by frontend plugs breaking the old swagger ui path of /akkoma/swagger-ui. Do the simple thing and change the frontend path to /pleroma/swaggerui which isn't an API path and can't collide with frontend user paths given pleroma is areserved nickname. Reported in https://meta.akkoma.dev/t/view-all-endpoints/269/7 https://meta.akkoma.dev/t/swagger-ui-not-loading/728 --- CHANGELOG.md | 1 + docs/docs/configuration/cheatsheet.md | 2 +- docs/docs/configuration/frontend_management.md | 2 +- lib/pleroma/web/endpoint.ex | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d451dca8..d2fcd752c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## BREAKING - Minimum PostgreSQL version is raised to 12 +- Swagger UI moved from `/akkoma/swaggerui/` to `/pleroma/swaggerui/` ## Added - Implement [FEP-67ff](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) (federation documentation) diff --git a/docs/docs/configuration/cheatsheet.md b/docs/docs/configuration/cheatsheet.md index 80f5c3577..916e1cc0c 100644 --- a/docs/docs/configuration/cheatsheet.md +++ b/docs/docs/configuration/cheatsheet.md @@ -338,7 +338,7 @@ config :pleroma, :frontends, * `:primary` - The frontend that will be served at `/` * `:admin` - The frontend that will be served at `/pleroma/admin` -* `:swagger` - Config for developers to act as an API reference to be served at `/akkoma/swaggerui/` (trailing slash _needed_). Disabled by default. +* `:swagger` - Config for developers to act as an API reference to be served at `/pleroma/swaggerui/` (trailing slash _needed_). Disabled by default. * `:mastodon` - The mastodon-fe configuration. This shouldn't need to be changed. This is served at `/web` when installed. ### :static\_fe diff --git a/docs/docs/configuration/frontend_management.md b/docs/docs/configuration/frontend_management.md index bc5344826..8875ee279 100644 --- a/docs/docs/configuration/frontend_management.md +++ b/docs/docs/configuration/frontend_management.md @@ -60,4 +60,4 @@ config :pleroma, :frontends, Then run the [pleroma.frontend cli task](../../administration/CLI_tasks/frontend) with the name of `swagger-ui` to install the distribution files. -You will now be able to view documentation at `/akkoma/swaggerui` +You will now be able to view documentation at `/pleroma/swaggerui` diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 6628fcaf3..3c9fcb48c 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -66,10 +66,10 @@ defmodule Pleroma.Web.Endpoint do } ) - plug(Plug.Static.IndexHtml, at: "/akkoma/swaggerui") + plug(Plug.Static.IndexHtml, at: "/pleroma/swaggerui/") plug(Pleroma.Web.Plugs.FrontendStatic, - at: "/akkoma/swaggerui", + at: "/pleroma/swaggerui", frontend_type: :swagger, gzip: true, if: &Pleroma.Web.Swagger.ui_enabled?/0, From 2c5c531c3528124d51dd58a94e7f0519825ae226 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Tue, 20 Aug 2024 11:05:36 +0100 Subject: [PATCH 16/48] readd comment about domain mutes --- lib/pleroma/user.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 6b0d90147..2bc3e9ace 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1631,6 +1631,11 @@ def blocks_domain?(%User{} = user, %User{ap_id: ap_id}) do def blocks_domain?(%User{} = user, url) when is_binary(url) do %{host: host} = URI.parse(url) Enum.member?(user.domain_blocks, host) + # TODO: functionality should probably be changed such that subdomains block as well, + # but as it stands, this just hecks up the relationships endpoint + # domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) + # %{host: host} = URI.parse(target.ap_id) + # Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host) end def blocks_domain?(_, _), do: false From b312edac4c9d83d6f3ad50e9ea3836359e7828cb Mon Sep 17 00:00:00 2001 From: cevado Date: Tue, 20 Aug 2024 19:29:11 -0300 Subject: [PATCH 17/48] Fix busywait on docker-entrypoint script --- docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 778ef08e2..ef5aa7d9e 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -11,4 +11,4 @@ echo "-- Running migrations..." mix ecto.migrate echo "-- Starting!" -mix phx.server +elixir --erl "+sbwt none +sbwtdcpu none +sbwtdio none" -S mix phx.server From bd14440386077d4254e9c24941f6577e8de34e36 Mon Sep 17 00:00:00 2001 From: Oneric Date: Tue, 24 Sep 2024 16:45:54 +0200 Subject: [PATCH 18/48] openrc: overhaul service file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pass env vars the proper™ way - write log to file - drop superfluous command_background - make settings easily overwritable via conf.d to avoid needing to edit the service file directly if e.g. Akkoma was installed to another location --- installation/init.d/akkoma | 56 ++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/installation/init.d/akkoma b/installation/init.d/akkoma index bd17516f2..cd7b70e7e 100755 --- a/installation/init.d/akkoma +++ b/installation/init.d/akkoma @@ -1,23 +1,40 @@ #!/sbin/openrc-run supervisor=supervise-daemon -command_user=akkoma:akkoma -command_background=1 -# Ask process to terminate within 30 seconds, otherwise kill it -retry="SIGTERM/30/SIGKILL/5" -pidfile="/var/run/akkoma.pid" -directory=/opt/akkoma -healthcheck_delay=60 -healthcheck_timer=30 no_new_privs="yes" +pidfile="/var/run/akkoma.pid" -: ${akkoma_port:-4000} +# Ask process first to terminate itself within 60s, otherwise kill it +retry="SIGTERM/60/SIGKILL/5" -# Needs OpenRC >= 0.42 -#respawn_max=0 -#respawn_delay=5 +# if you really want to use start-stop-daemon instead, +# also put the following in the config: +# command_background=1 + +# Adjust defaults as needed in /etc/conf.d/akkoma; +# no need to directly edit the service file +command_user="${command_user:-akkoma:akkoma}" +directory="${directory:-/var/lib/akkoma/akkoma}" +akkoma_port="${akkoma_port:-4000}" +# whether to allow connecting a remote exlixir shell to the running Akkoma instance +akkoma_console=${akkoma_console:-NO} + +output_log="${output_log:-/var/log/akkoma}" +error_log="${error_log:-/var/log/akkoma}" + +# 0 means unlimited restarts +respawn_max="${respawn_max:-0}" +respawn_delay="${respawn_delay:-5}" +# define respawn period to only count crashes within a +# sliding time window towards respawn_max, e.g.: +# respawn_period=2850 + +healthcheck_delay="${healthcheck_delay:-60}" +healthcheck_timer="${healthcheck_timer:-30}" + +MIX_ENV=prod +ERL_EPMD_ADDRESS="${ERL_EPMD_ADDRESS:-127.0.0.1}" +supervise_daemon_args="${supervise_daemon_args} --env MIX_ENV=${MIX_ENV} --env ERL_EPMD_ADDRESS=${ERL_EPMD_ADDRESS}" -# put akkoma_console=YES in /etc/conf.d/akkoma if you want to be able to -# connect to akkoma via an elixir console if yesno "${akkoma_console}"; then command=elixir command_args="--name akkoma@127.0.0.1 --erl '-kernel inet_dist_listen_min 9001 inet_dist_listen_max 9001 inet_dist_use_interface {127,0,0,1}' -S mix phx.server" @@ -31,13 +48,18 @@ else command_args="phx.server" fi -export MIX_ENV=prod -export ERL_EPMD_ADDRESS=127.0.0.1 - depend() { need nginx postgresql } +start_pre() { + # Ensure logfile ownership and perms are alright + checkpath --file --owner "$command_user" "$output_log" "$error_log" \ + || eerror "Logfile(s) not owned by $command_user, or not a file!" + checkpath --writable "$output_log" "$error_log" \ + || eerror "Logfile(s) not writable!" +} + healthcheck() { # put akkoma_health=YES in /etc/conf.d/akkoma if you want healthchecking # and make sure you have curl installed From 2901fda29c467ec333f692b50aa0bd4bdce205d4 Mon Sep 17 00:00:00 2001 From: Oneric Date: Tue, 24 Sep 2024 16:47:51 +0200 Subject: [PATCH 19/48] openrc: recompile with lower CPU prio --- installation/init.d/akkoma | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/installation/init.d/akkoma b/installation/init.d/akkoma index cd7b70e7e..747abc2c9 100755 --- a/installation/init.d/akkoma +++ b/installation/init.d/akkoma @@ -58,6 +58,12 @@ start_pre() { || eerror "Logfile(s) not owned by $command_user, or not a file!" checkpath --writable "$output_log" "$error_log" \ || eerror "Logfile(s) not writable!" + + # If a recompile is needed perform it with lowest prio + # (delaying the actual start) to avoid hogging too much + # CPU from other services + cd "$directory" + doas -u "${command_user%%:*}" env MIX_ENV="$MIX_ENV" nice -n 19 "$command" compile } healthcheck() { From a8a231c5b23ceadf30eb7ce212aaf30da2ef9074 Mon Sep 17 00:00:00 2001 From: Oneric Date: Tue, 24 Sep 2024 17:36:54 +0200 Subject: [PATCH 20/48] Don't busy wait in default from-source service files --- installation/akkoma.service | 3 +++ installation/init.d/akkoma | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/installation/akkoma.service b/installation/akkoma.service index 717693495..4945e108c 100644 --- a/installation/akkoma.service +++ b/installation/akkoma.service @@ -19,6 +19,9 @@ Environment="MIX_ENV=prod" ; Don't listen epmd on 0.0.0.0 Environment="ERL_EPMD_ADDRESS=127.0.0.1" +; Don't busy wait +Environment="ERL_AFLAGS=+sbwt none +sbwtdcpu none +sbwtdio none" + ; Make sure that all paths fit your installation. ; Path to the home directory of the user running the Akkoma service. Environment="HOME=/var/lib/akkoma" diff --git a/installation/init.d/akkoma b/installation/init.d/akkoma index 747abc2c9..a03b494c0 100755 --- a/installation/init.d/akkoma +++ b/installation/init.d/akkoma @@ -33,7 +33,10 @@ healthcheck_timer="${healthcheck_timer:-30}" MIX_ENV=prod ERL_EPMD_ADDRESS="${ERL_EPMD_ADDRESS:-127.0.0.1}" -supervise_daemon_args="${supervise_daemon_args} --env MIX_ENV=${MIX_ENV} --env ERL_EPMD_ADDRESS=${ERL_EPMD_ADDRESS}" +ERL_AFLAGS="${ERL_AFLAGS:-+sbwt none +sbwtdcpu none +sbwtdio none}" +supervise_daemon_args="${supervise_daemon_args} --env MIX_ENV=${MIX_ENV}" +supervise_daemon_args="${supervise_daemon_args} --env ERL_EPMD_ADDRESS=${ERL_EPMD_ADDRESS}" +supervise_daemon_args="${supervise_daemon_args} --env ERL_AFLAGS='${ERL_AFLAGS}'" if yesno "${akkoma_console}"; then command=elixir From 940792f8ba8eae33a2cb48b313986b3674aa3a20 Mon Sep 17 00:00:00 2001 From: Oneric Date: Sat, 13 Jul 2024 06:54:37 +0200 Subject: [PATCH 21/48] Refetch on AP ID mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As hinted at in the commit message when strict checking was added in 8684964c5d03f6c70f73730b3f1ad26784ffb004, refetching is more robust than display URL comparison but in exchange is harder to implement correctly. A similar refetch approach is also employed by e.g. Mastodon, IceShrimp and FireFish. To make sure no checks can be bypassed by forcing a refetch, id checking is placed at the very end. This will fix: - Peertube display URL arrays our transmogrifier fails to normalise - non-canonical display URLs from alternative frontends (theoretical; we didnt’t get any actual reports about this) It will also be helpful in the planned key handling overhaul. The modified user collision test was introduced in https://git.pleroma.social/pleroma/pleroma/-/merge_requests/461 and unfortunately the issues this fixes aren’t public. Afaict it was just meant to guard against someone serving faked data belonging to an unrelated domain. Since we now refetch and the id actually is mocked, lookup now succeeds but will use the real data from the authorative server making it unproblematic. Instead modify the fake data further and make sure we don’t end up using the spoofed version. --- lib/pleroma/object/containment.ex | 57 +++++++++++++++--------- lib/pleroma/object/fetcher.ex | 42 +++++++++++------ test/pleroma/object/containment_test.exs | 35 +++++++-------- test/pleroma/object/fetcher_test.exs | 2 +- test/support/http_request_mock.ex | 7 ++- 5 files changed, 87 insertions(+), 56 deletions(-) diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 37bc20e4d..7b1cc37bd 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -12,8 +12,6 @@ defmodule Pleroma.Object.Containment do spoofing, therefore removal of object containment functions is NOT recommended. """ - alias Pleroma.Web.ActivityPub.Transmogrifier - def get_actor(%{"actor" => actor}) when is_binary(actor) do actor end @@ -50,16 +48,39 @@ def get_object(_) do defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok defp compare_uris(_id_uri, _other_uri), do: :error - defp compare_uris_exact(uri, uri), do: :ok + defp uri_strip_slash(%URI{path: path} = uri) when is_binary(path), + do: %{uri | path: String.replace_suffix(path, "/", "")} - defp compare_uris_exact(%URI{} = id, %URI{} = other), - do: compare_uris_exact(URI.to_string(id), URI.to_string(other)) + defp uri_strip_slash(uri), do: uri - defp compare_uris_exact(id_uri, other_uri) - when is_binary(id_uri) and is_binary(other_uri) do - norm_id = String.replace_suffix(id_uri, "/", "") - norm_other = String.replace_suffix(other_uri, "/", "") - if norm_id == norm_other, do: :ok, else: :error + # domain names are case-insensitive per spec (other parts of URIs aren’t necessarily) + defp uri_normalise_host(%URI{host: host} = uri) when is_binary(host), + do: %{uri | host: String.downcase(host, :ascii)} + + defp uri_normalise_host(uri), do: uri + + defp compare_uri_identities(uri, uri), do: :ok + + defp compare_uri_identities(id_uri, other_uri) when is_binary(id_uri) and is_binary(other_uri), + do: compare_uri_identities(URI.parse(id_uri), URI.parse(other_uri)) + + defp compare_uri_identities(%URI{} = id, %URI{} = other) do + normid = + %{id | fragment: nil} + |> uri_strip_slash() + |> uri_normalise_host() + + normother = + %{other | fragment: nil} + |> uri_strip_slash() + |> uri_normalise_host() + + # Conversion back to binary avoids issues from non-normalised deprecated authority field + if URI.to_string(normid) == URI.to_string(normother) do + :ok + else + :error + end end @doc """ @@ -93,21 +114,13 @@ def contain_origin(id, %{"attributedTo" => actor} = params), def contain_origin(_id, _data), do: :ok @doc """ - Check whether the fetch URL (after redirects) exactly (sans tralining slash) matches either - the canonical ActivityPub id or the objects url field (for display URLs from *key and Mastodon) + Check whether the fetch URL (after redirects) is the + same location the canonical ActivityPub id points to. Since this is meant to be used for fetches, anonymous or transient objects are not accepted here. """ - def contain_id_to_fetch(url, %{"id" => id} = data) when is_binary(id) do - with {:id, :error} <- {:id, compare_uris_exact(id, url)}, - # "url" can be a "Link" object and this is checked before full normalisation - display_url <- Transmogrifier.fix_url(data)["url"], - true <- display_url != nil do - compare_uris_exact(display_url, url) - else - {:id, :ok} -> :ok - _ -> :error - end + def contain_id_to_fetch(url, %{"id" => id}) when is_binary(id) do + compare_uri_identities(url, id) end def contain_id_to_fetch(_url, _data), do: :error diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 937026e04..7f9a922aa 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -116,7 +116,7 @@ defp reinject_object(%Object{} = object, new_data) do @doc "Assumes object already is in our database and refetches from remote to update (e.g. for polls)" def refetch_object(%Object{data: %{"id" => id}} = object) do with {:local, false} <- {:local, Object.local?(object)}, - {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id), + {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id, true), {:id, true} <- {:id, new_data["id"] == id}, {:ok, object} <- reinject_object(object, new_data) do {:ok, object} @@ -253,14 +253,17 @@ defp maybe_date_fetch(headers, date) do end end - @doc "Fetches arbitrary remote object and performs basic safety and authenticity checks" - def fetch_and_contain_remote_object_from_id(id) + @doc """ + Fetches arbitrary remote object and performs basic safety and authenticity checks. + When the fetch URL is known to already be a canonical AP id, checks are stricter. + """ + def fetch_and_contain_remote_object_from_id(id, is_ap_id \\ false) - def fetch_and_contain_remote_object_from_id(%{"id" => id}), - do: fetch_and_contain_remote_object_from_id(id) + def fetch_and_contain_remote_object_from_id(%{"id" => id}, is_ap_id), + do: fetch_and_contain_remote_object_from_id(id, is_ap_id) - def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do - Logger.debug("Fetching object #{id} via AP") + def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do + Logger.debug("Fetching object #{id} via AP [ap_id=#{is_ap_id}]") with {:valid_uri_scheme, true} <- {:valid_uri_scheme, String.starts_with?(id, "http")}, %URI{} = uri <- URI.parse(id), @@ -270,18 +273,31 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do {:mrf_accept_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri)}, {:local_fetch, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)}, {:ok, final_id, body} <- get_object(id), + # a canonical ID shouldn't be a redirect + true <- !is_ap_id || final_id == id, {:ok, data} <- safe_json_decode(body), - {_, :ok} <- {:strict_id, Containment.contain_id_to_fetch(final_id, data)}, - {_, :ok} <- {:containment, Containment.contain_origin(final_id, data)} do + {_, :ok} <- {:containment, Containment.contain_origin(final_id, data)}, + {_, _, :ok} <- {:strict_id, data["id"], Containment.contain_id_to_fetch(final_id, data)} do unless Instances.reachable?(final_id) do Instances.set_reachable(final_id) end {:ok, data} else - {:strict_id, _} = e -> - log_fetch_error(id, e) - {:error, :id_mismatch} + # E.g. Mastodon and *key serve the AP object directly under their display URLs without + # redirecting to their canonical location first, thus ids will expectedly differ. + # Similarly keys, either use a fragment ID and are a subobjects or a distinct ID + # but for compatibility are still a subobject presenting their owning actors ID at the toplevel. + # Refetching _once_ from the listed id, should yield a strict match afterwards. + {:strict_id, ap_id, _} = e -> + case is_ap_id do + false -> + fetch_and_contain_remote_object_from_id(ap_id, true) + + true -> + log_fetch_error(id, e) + {:error, :id_mismatch} + end {:mrf_reject_check, _} = e -> log_fetch_error(id, e) @@ -311,7 +327,7 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do end end - def fetch_and_contain_remote_object_from_id(_id), + def fetch_and_contain_remote_object_from_id(_id, _is_ap_id), do: {:error, :invalid_id} defp check_crossdomain_redirect(final_host, original_url) diff --git a/test/pleroma/object/containment_test.exs b/test/pleroma/object/containment_test.exs index f8f40a3ac..1a1d01473 100644 --- a/test/pleroma/object/containment_test.exs +++ b/test/pleroma/object/containment_test.exs @@ -9,7 +9,6 @@ defmodule Pleroma.Object.ContainmentTest do alias Pleroma.User import Pleroma.Factory - import ExUnit.CaptureLog setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -136,23 +135,17 @@ test "contain_id_to_fetch() allows matching IDs" do ) end - test "contain_id_to_fetch() allows display URLs" do + test "contain_id_to_fetch() allows fragments and normalises domain casing" do data = %{ - "id" => "http://example.com/~alyssa/activities/1234.json", - "url" => "http://example.com/@alyssa/status/1234" + "id" => "http://example.com/users/capybara", + "url" => "http://example.com/@capybara" } - :ok = - Containment.contain_id_to_fetch( - "http://example.com/@alyssa/status/1234", - data - ) - - :ok = - Containment.contain_id_to_fetch( - "http://example.com/@alyssa/status/1234/", - data - ) + assert :ok == + Containment.contain_id_to_fetch( + "http://EXAMPLE.com/users/capybara#key", + data + ) end test "users cannot be collided through fake direction spoofing attempts" do @@ -164,10 +157,14 @@ test "users cannot be collided through fake direction spoofing attempts" do follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) }) - assert capture_log(fn -> - {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye") - end) =~ - "[error] Could not decode user at fetch https://n1u.moe/users/rye" + # Fetch from an attempted spoof id will suceed, but automatically retrieve + # the real data from the homeserver instead of naïvely using the spoof + {:ok, fetched_user} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye") + + refute fetched_user.name == "evil rye" + refute fetched_user.raw_bio == "boooo!" + assert fetched_user.name == "♡ rye ♡" + assert fetched_user.nickname == "rye@niu.moe" end test "contain_origin_from_id() gracefully handles cases where no ID is present" do diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs index 12154cb05..d2de0ccc5 100644 --- a/test/pleroma/object/fetcher_test.exs +++ b/test/pleroma/object/fetcher_test.exs @@ -252,7 +252,7 @@ test "it does not fetch a spoofed object with wrong content type" do end test "it does not fetch a spoofed object with id different from URL" do - assert {:error, :id_mismatch} = + assert {:error, :not_found} = Fetcher.fetch_and_contain_remote_object_from_id( "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json" ) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 6a01393e3..ea06c4966 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -263,7 +263,12 @@ def get("https://n1u.moe/users/rye", _, _, @activitypub_accept_headers) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/rye.json"), + body: + File.read!("test/fixtures/tesla_mock/rye.json") + |> Jason.decode!() + |> Map.put("name", "evil rye") + |> Map.put("bio", "boooo!") + |> Jason.encode!(), headers: activitypub_object_headers() }} end From d5b0720596b70240bb559a2084d3c3cdc76fe7b2 Mon Sep 17 00:00:00 2001 From: Oneric Date: Sat, 13 Jul 2024 06:54:45 +0200 Subject: [PATCH 22/48] Allow cross-domain redirects on AP requests Since we now remember the final location redirects lead to and use it for all further checks since 3e134b07fa4e382f1f4cfdbe90e74f8e73336a4e, these redirects can no longer be exploited to serve counterfeit objects. This fixes: - display URLs from independent webapp clients redirecting to the canonical domain - Peertube display URLs for remote content (acting like the above) --- lib/pleroma/object/fetcher.ex | 18 +--------------- test/pleroma/object/fetcher_test.exs | 32 ++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 7f9a922aa..2cecfec67 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -317,7 +317,7 @@ def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do {:containment, reason} -> log_fetch_error(id, reason) - {:error, reason} + {:error, {:containment, reason}} {:error, e} -> {:error, e} @@ -330,22 +330,10 @@ def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do def fetch_and_contain_remote_object_from_id(_id, _is_ap_id), do: {:error, :invalid_id} - defp check_crossdomain_redirect(final_host, original_url) - # HOPEFULLY TEMPORARY # Basically none of our Tesla mocks in tests set the (supposed to # exist for Tesla proper) url parameter for their responses # causing almost every fetch in test to fail otherwise - if @mix_env == :test do - defp check_crossdomain_redirect(nil, _) do - {:cross_domain_redirect, false} - end - end - - defp check_crossdomain_redirect(final_host, original_url) do - {:cross_domain_redirect, final_host != URI.parse(original_url).host} - end - if @mix_env == :test do defp get_final_id(nil, initial_url), do: initial_url defp get_final_id("", initial_url), do: initial_url @@ -371,10 +359,6 @@ def get_object(id) do with {:ok, %{body: body, status: code, headers: headers, url: final_url}} when code in 200..299 <- HTTP.Backoff.get(id, headers), - remote_host <- - URI.parse(final_url).host, - {:cross_domain_redirect, false} <- - check_crossdomain_redirect(remote_host, id), {:has_content_type, {_, content_type}} <- {:has_content_type, List.keyfind(headers, "content-type", 0)}, {:parse_content_type, {:ok, "application", subtype, type_params}} <- diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs index d2de0ccc5..84bf0aa05 100644 --- a/test/pleroma/object/fetcher_test.exs +++ b/test/pleroma/object/fetcher_test.exs @@ -22,6 +22,7 @@ defp spoofed_object_with_ids( |> Jason.decode!() |> Map.put("id", id) |> Map.put("actor", actor_id) + |> Map.put("attributedTo", actor_id) |> Jason.encode!() end @@ -109,7 +110,7 @@ defp spoofed_object_with_ids( body: spoofed_object_with_ids("https://patch.cx/objects/spoof_media_redirect1") } - # Spoof: cross-domain redirect with final domain id + # Spoof: cross-domain redirect with final domain id, but original id actor %{method: :get, url: "https://patch.cx/objects/spoof_media_redirect2"} -> %Tesla.Env{ status: 200, @@ -118,6 +119,19 @@ defp spoofed_object_with_ids( body: spoofed_object_with_ids("https://media.patch.cx/objects/spoof_media_redirect2") } + # No-Spoof: cross-domain redirect with id and actor from final domain + %{method: :get, url: "https://patch.cx/objects/spoof_media_redirect3"} -> + %Tesla.Env{ + status: 200, + url: "https://media.patch.cx/objects/spoof_media_redirect3", + headers: [{"content-type", "application/activity+json"}], + body: + spoofed_object_with_ids( + "https://media.patch.cx/objects/spoof_media_redirect3", + "https://media.patch.cx/users/rin" + ) + } + # No-Spoof: same domain redirect %{method: :get, url: "https://patch.cx/objects/spoof_redirect"} -> %Tesla.Env{ @@ -264,19 +278,29 @@ test "it does not fetch a spoofed object with id different from URL" do end test "it does not fetch an object via cross-domain redirects (initial id)" do - assert {:error, {:cross_domain_redirect, true}} = + assert {:error, {:containment, _}} = 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}} = + test "it does not fetch an object via cross-domain redirect if the actor is from the original domain" do + assert {:error, {:containment, :error}} = Fetcher.fetch_and_contain_remote_object_from_id( "https://patch.cx/objects/spoof_media_redirect2" ) end + test "it allows cross-domain redirects when id and author are from final domain" do + assert {:ok, %{"id" => id, "attributedTo" => author}} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://patch.cx/objects/spoof_media_redirect3" + ) + + assert URI.parse(id).host == "media.patch.cx" + assert URI.parse(author).host == "media.patch.cx" + end + test "it accepts same-domain redirects" do assert {:ok, %{"id" => id} = _object} = Fetcher.fetch_and_contain_remote_object_from_id( From 8b5aca96191e6741cd10fff594d7923d3ea7e9bb Mon Sep 17 00:00:00 2001 From: TudbuT Date: Fri, 18 Oct 2024 14:50:28 +0200 Subject: [PATCH 23/48] fix fs error while unpacking frontends --- lib/pleroma/frontend.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index dc9d55646..100490c70 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -79,6 +79,10 @@ def unzip(zip, dest) do new_file_path = Path.join(dest, path) + new_file_path + |> Path.dirname() + |> File.rm() + new_file_path |> Path.dirname() |> File.mkdir_p!() From 661b7fedb635eb23b106eca4730a89b2320d1e9f Mon Sep 17 00:00:00 2001 From: TudbuT Date: Fri, 18 Oct 2024 14:57:31 +0200 Subject: [PATCH 24/48] fix wrong type when importing emojis --- lib/mix/tasks/pleroma/emoji.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 12918dfff..aa8131254 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -111,7 +111,7 @@ def run(["get-packs" | args]) do {:ok, _} = :zip.unzip(binary_archive, - cwd: pack_path, + cwd: to_charlist(pack_path), file_list: files_to_unzip ) From cbd236aeb53b8e73adb220a242c2481efe76541f Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 26 Oct 2024 05:04:20 +0100 Subject: [PATCH 25/48] mix format --- lib/pleroma/frontend.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index 100490c70..a309d8467 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -82,7 +82,7 @@ def unzip(zip, dest) do new_file_path |> Path.dirname() |> File.rm() - + new_file_path |> Path.dirname() |> File.mkdir_p!() From fc99c694e65730bf6b391ce340670356857f363b Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Thu, 27 Jun 2024 05:05:56 +0100 Subject: [PATCH 26/48] Add signing key modules --- lib/pleroma/user/signing_key.ex | 190 ++++++++++++++++++ ...0240625213637_create_signing_key_table.exs | 15 ++ .../20240625220752_move_signing_keys.exs | 35 ++++ 3 files changed, 240 insertions(+) create mode 100644 lib/pleroma/user/signing_key.ex create mode 100644 priv/repo/migrations/20240625213637_create_signing_key_table.exs create mode 100644 priv/repo/migrations/20240625220752_move_signing_keys.exs diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex new file mode 100644 index 000000000..92ce7ae5d --- /dev/null +++ b/lib/pleroma/user/signing_key.ex @@ -0,0 +1,190 @@ +defmodule Pleroma.User.SigningKey do + use Ecto.Schema + import Ecto.Query + import Ecto.Changeset + alias Pleroma.User + alias Pleroma.Repo + alias Pleroma.HTTP + + require Logger + + @primary_key false + schema "signing_keys" do + belongs_to(:user, Pleroma.User, type: FlakeId.Ecto.CompatType) + field :public_key, :string + field :private_key, :string + # This is an arbitrary field given by the remote instance + field :key_id, :string, primary_key: true + timestamps() + end + + def key_id_of_local_user(%User{local: true, signing_key: %__MODULE__{key_id: key_id}}), + do: key_id + + @spec remote_changeset(__MODULE__, map) :: Changeset.t() + def remote_changeset(%__MODULE__{} = signing_key, attrs) do + signing_key + |> cast(attrs, [:public_key, :key_id]) + |> validate_required([:public_key, :key_id]) + end + + @spec key_id_to_user_id(String.t()) :: String.t() | nil + @doc """ + Given a key ID, return the user ID associated with that key. + Returns nil if the key ID is not found. + """ + def key_id_to_user_id(key_id) do + from(sk in __MODULE__, where: sk.key_id == ^key_id) + |> select([sk], sk.user_id) + |> Repo.one() + end + + @spec key_id_to_ap_id(String.t()) :: String.t() | nil + @doc """ + Given a key ID, return the AP ID associated with that key. + Returns nil if the key ID is not found. + """ + def key_id_to_ap_id(key_id) do + Logger.debug("Looking up key ID: #{key_id}") + + result = + from(sk in __MODULE__, where: sk.key_id == ^key_id) + |> join(:inner, [sk], u in User, on: sk.user_id == u.id) + |> select([sk, u], %{user: u}) + |> Repo.one() + + case result do + %{user: %User{ap_id: ap_id}} -> ap_id + _ -> nil + end + end + + @spec generate_rsa_pem() :: {:ok, binary()} + @doc """ + Generate a new RSA private key and return it as a PEM-encoded string. + """ + def generate_rsa_pem do + key = :public_key.generate_key({:rsa, 2048, 65_537}) + entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) + pem = :public_key.pem_encode([entry]) |> String.trim_trailing() + {:ok, pem} + end + + @spec generate_local_keys(String.t()) :: {:ok, Changeset.t()} | {:error, String.t()} + @doc """ + Generate a new RSA key pair and create a changeset for it + """ + def generate_local_keys(ap_id) do + {:ok, private_pem} = generate_rsa_pem() + {:ok, local_pem} = private_pem_to_public_pem(private_pem) + + %__MODULE__{} + |> change() + |> put_change(:public_key, local_pem) + |> put_change(:private_key, private_pem) + |> put_change(:key_id, ap_id <> "#main-key") + end + + @spec private_pem_to_public_pem(binary) :: {:ok, binary()} | {:error, String.t()} + @doc """ + Given a private key in PEM format, return the corresponding public key in PEM format. + """ + def private_pem_to_public_pem(private_pem) do + [private_key_code] = :public_key.pem_decode(private_pem) + private_key = :public_key.pem_entry_decode(private_key_code) + {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key + public_key = {:RSAPublicKey, modulus, exponent} + public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) + {:ok, :public_key.pem_encode([public_key])} + end + + @spec public_key(User.t()) :: {:ok, binary()} | {:error, String.t()} + @doc """ + Given a user, return the public key for that user in binary format. + """ + def public_key(%User{signing_key: %__MODULE__{public_key: public_key_pem}}) do + key = + public_key_pem + |> :public_key.pem_decode() + |> hd() + |> :public_key.pem_entry_decode() + + {:ok, key} + end + + def public_key(_), do: {:error, "key not found"} + + def public_key_pem(%User{signing_key: %__MODULE__{public_key: public_key_pem}}), + do: public_key_pem + + @spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()} + @doc """ + Given a user, return the private key for that user in binary format. + """ + def private_key(%User{signing_key: %__MODULE__{private_key: private_key_pem}}) do + key = + private_key_pem + |> :public_key.pem_decode() + |> hd() + |> :public_key.pem_entry_decode() + + {:ok, key} + end + + @spec fetch_remote_key(String.t()) :: {:ok, __MODULE__} | {:error, String.t()} + @doc """ + Fetch a remote key by key ID. + Will send a request to the remote instance to get the key ID. + This request should, at the very least, return a user ID and a public key object. + Though bear in mind that some implementations (looking at you, pleroma) may require a signature for this request. + This has the potential to create an infinite loop if the remote instance requires a signature to fetch the key... + So if we're rejected, we should probably just give up. + """ + def fetch_remote_key(key_id) do + resp = HTTP.Backoff.get(key_id) + + case handle_signature_response(resp) do + {:ok, ap_id, public_key_pem} -> + # fetch the user + user = User.get_or_fetch_by_ap_id(ap_id) + # store the key + key = %__MODULE__{ + user_id: user.id, + public_key: public_key_pem, + key_id: key_id + } + + Repo.insert(key) + + _ -> + {:error, "Could not fetch key"} + end + end + + # Take the response from the remote instance and extract the key details + # will check if the key ID matches the owner of the key, if not, error + defp extract_key_details(%{"id" => ap_id, "publicKey" => public_key}) do + if ap_id !== public_key["owner"] do + {:error, "Key ID does not match owner"} + else + %{"publicKeyPem" => public_key_pem} = public_key + {:ok, ap_id, public_key_pem} + end + end + + defp handle_signature_response({:ok, %{status: status, body: body}}) when status in 200..299 do + case Jason.decode(body) do + {:ok, %{"id" => _user_id, "publicKey" => _public_key} = body} -> + extract_key_details(body) + + {:ok, %{"error" => error}} -> + {:error, error} + + {:error, _} -> + {:error, "Could not parse key"} + end + end + + defp handle_signature_response({:error, e}), do: {:error, e} + defp handle_signature_response(other), do: {:error, "Could not fetch key: #{inspect(other)}"} +end diff --git a/priv/repo/migrations/20240625213637_create_signing_key_table.exs b/priv/repo/migrations/20240625213637_create_signing_key_table.exs new file mode 100644 index 000000000..34169f78c --- /dev/null +++ b/priv/repo/migrations/20240625213637_create_signing_key_table.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.CreateSigningKeyTable do + use Ecto.Migration + + def change do + create table(:signing_keys, primary_key: false) do + add :user_id, references(:users, type: :uuid, on_delete: :delete_all) + add :key_id, :text, primary_key: true + add :public_key, :text + add :private_key, :text + timestamps() + end + + create index(:signing_keys, [:key_id]) + end +end diff --git a/priv/repo/migrations/20240625220752_move_signing_keys.exs b/priv/repo/migrations/20240625220752_move_signing_keys.exs new file mode 100644 index 000000000..d9ef1acab --- /dev/null +++ b/priv/repo/migrations/20240625220752_move_signing_keys.exs @@ -0,0 +1,35 @@ +defmodule Pleroma.Repo.Migrations.MoveSigningKeys do + use Ecto.Migration + alias Pleroma.User + alias Pleroma.Repo + import Ecto.Query + + def up do + # we do not handle remote users here! + # because we want to store a key id -> user id mapping, and we don't + # currently store key ids for remote users... + query = + from(u in User) + |> where(local: true) + + Repo.stream(query, timeout: :infinity) + |> Enum.each(fn + %User{id: user_id, keys: private_key, local: true} -> + # we can precompute the public key here... + # we do use it on every user view which makes it a bit of a dos attack vector + # so we should probably cache it + {:ok, public_key} = User.SigningKey.private_pem_to_public_pem(private_key) + + key = %User.SigningKey{ + user_id: user_id, + public_key: public_key, + private_key: private_key + } + + {:ok, _} = Repo.insert(key) + end) + end + + # no need to rollback + def down, do: :ok +end From b0f7da9ce0a35fd08dc4d2101ecda0f4cded50dd Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Thu, 27 Jun 2024 05:06:04 +0100 Subject: [PATCH 27/48] remove now-unused Keys module --- lib/pleroma/keys.ex | 46 --------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 lib/pleroma/keys.ex diff --git a/lib/pleroma/keys.ex b/lib/pleroma/keys.ex deleted file mode 100644 index 413861b15..000000000 --- a/lib/pleroma/keys.ex +++ /dev/null @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Keys do - # Native generation of RSA keys is only available since OTP 20+ and in default build conditions - # We try at compile time to generate natively an RSA key otherwise we fallback on the old way. - try do - _ = :public_key.generate_key({:rsa, 2048, 65_537}) - - def generate_rsa_pem do - key = :public_key.generate_key({:rsa, 2048, 65_537}) - entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) - pem = :public_key.pem_encode([entry]) |> String.trim_trailing() - {:ok, pem} - end - rescue - _ -> - def generate_rsa_pem do - port = Port.open({:spawn, "openssl genrsa"}, [:binary]) - - {:ok, pem} = - receive do - {^port, {:data, pem}} -> {:ok, pem} - end - - Port.close(port) - - if Regex.match?(~r/RSA PRIVATE KEY/, pem) do - {:ok, pem} - else - :error - end - end - end - - def keys_from_pem(pem) do - with [private_key_code] <- :public_key.pem_decode(pem), - private_key <- :public_key.pem_entry_decode(private_key_code), - {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do - {:ok, private_key, {:RSAPublicKey, modulus, exponent}} - else - error -> {:error, error} - end - end -end From 9728e2f8f71e3a33f9f6ae60da79145c09b08e06 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Thu, 27 Jun 2024 05:06:27 +0100 Subject: [PATCH 28/48] adjust logic to use relation :signing_key --- lib/pleroma/object/fetcher.ex | 1 - lib/pleroma/signature.ex | 48 +++++++---------- lib/pleroma/user.ex | 33 ++++++------ lib/pleroma/web/activity_pub/activity_pub.ex | 26 ++++++++-- .../object_validators/user_validator.ex | 31 +---------- .../web/activity_pub/views/user_view.ex | 23 ++++++--- .../mapped_signature_to_identity_plug.ex | 30 +++++++---- mix.lock | 14 ++--- test/pleroma/keys_test.exs | 24 --------- test/pleroma/user_test.exs | 10 ++-- ...mapped_signature_to_identity_plug_test.exs | 51 +++++++++++-------- test/support/factory.ex | 18 +++++-- 12 files changed, 150 insertions(+), 159 deletions(-) delete mode 100644 test/pleroma/keys_test.exs diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 2cecfec67..897146bce 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -343,7 +343,6 @@ defp get_final_id(final_url, _intial_url) do final_url end - @doc "Do NOT use; only public for use in tests" def get_object(id) do date = Pleroma.Signature.signed_date() diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index c4ac2c87e..1374ac068 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -5,45 +5,33 @@ defmodule Pleroma.Signature do @behaviour HTTPSignatures.Adapter - alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.Keys alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - - @known_suffixes ["/publickey", "/main-key", "#key"] + alias Pleroma.User.SigningKey def key_id_to_actor_id(key_id) do - uri = - key_id - |> URI.parse() - |> Map.put(:fragment, nil) - |> Map.put(:query, nil) - |> remove_suffix(@known_suffixes) + # Given the key ID, first attempt to look it up in the signing keys table. + # If it's not found, then attempt to look it up via request to the remote instance. + case SigningKey.key_id_to_ap_id(key_id) do + nil -> + # this requires us to look up the url! + request_key_id_from_remote_instance(key_id) - maybe_ap_id = URI.to_string(uri) - - case ObjectValidators.ObjectID.cast(maybe_ap_id) do - {:ok, ap_id} -> - {:ok, ap_id} - - _ -> - case Pleroma.Web.WebFinger.finger(maybe_ap_id) do - {:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id} - _ -> {:error, maybe_ap_id} - end + key -> + {:ok, key} end end - defp remove_suffix(uri, [test | rest]) do - if not is_nil(uri.path) and String.ends_with?(uri.path, test) do - Map.put(uri, :path, String.replace(uri.path, test, "")) - else - remove_suffix(uri, rest) + def request_key_id_from_remote_instance(key_id) do + case SigningKey.fetch_remote_key(key_id) do + {:ok, key_id} -> + {:ok, key_id} + + {:error, _} -> + {:error, "Key ID not found"} end end - defp remove_suffix(uri, []), do: uri - def fetch_public_key(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), {:ok, actor_id} <- key_id_to_actor_id(kid), @@ -67,8 +55,8 @@ def refetch_public_key(conn) do end end - def sign(%User{keys: keys} = user, headers) do - with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do + def sign(%User{} = user, headers) do + with {:ok, private_key} <- SigningKey.private_key(user) do HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 2bc3e9ace..593173288 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -25,7 +25,6 @@ defmodule Pleroma.User do alias Pleroma.Hashtag alias Pleroma.User.HashtagFollow alias Pleroma.HTML - alias Pleroma.Keys alias Pleroma.MFA alias Pleroma.Notification alias Pleroma.Object @@ -43,6 +42,7 @@ defmodule Pleroma.User do alias Pleroma.Web.OAuth alias Pleroma.Web.RelMe alias Pleroma.Workers.BackgroundWorker + alias Pleroma.User.SigningKey use Pleroma.Web, :verified_routes @@ -222,6 +222,10 @@ defmodule Pleroma.User do on_replace: :delete ) + # FOR THE FUTURE: We might want to make this a one-to-many relationship + # it's entirely possible right now, but we don't have a use case for it + has_one(:signing_key, SigningKey, foreign_key: :user_id) + timestamps() end @@ -457,6 +461,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do |> fix_follower_address() struct + |> Repo.preload(:signing_key) |> cast( params, [ @@ -495,6 +500,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do |> validate_required([:ap_id]) |> validate_required([:name], trim: false) |> unique_constraint(:nickname) + |> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, required: false) |> validate_format(:nickname, @email_regex) |> validate_length(:bio, max: bio_limit) |> validate_length(:name, max: name_limit) @@ -570,6 +576,7 @@ def update_changeset(struct, params \\ %{}) do :pleroma_settings_store, &{:ok, Map.merge(struct.pleroma_settings_store, &1)} ) + |> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, requred: false) |> validate_fields(false, struct) end @@ -828,8 +835,10 @@ def put_following_and_follower_and_featured_address(changeset) do end defp put_private_key(changeset) do - {:ok, pem} = Keys.generate_rsa_pem() - put_change(changeset, :keys, pem) + ap_id = get_field(changeset, :ap_id) + + changeset + |> put_assoc(:signing_key, SigningKey.generate_local_keys(ap_id)) end defp autofollow_users(user) do @@ -2051,24 +2060,16 @@ defp create_service_actor(uri, nickname) do |> set_cache() end - def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do - key = - public_key_pem - |> :public_key.pem_decode() - |> hd() - |> :public_key.pem_entry_decode() - - {:ok, key} - end - - def public_key(_), do: {:error, "key not found"} + defdelegate public_key(user), to: SigningKey def get_public_key_for_ap_id(ap_id) do with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), - {:ok, public_key} <- public_key(user) do + {:ok, public_key} <- SigningKey.public_key(user) do {:ok, public_key} else - _ -> :error + e -> + Logger.error("Could not get public key for #{ap_id}.\n#{inspect(e)}") + {:error, e} end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index c87072300..9b28e64d9 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1547,6 +1547,17 @@ defp normalize_attachment(%{} = attachment), do: [attachment] defp normalize_attachment(attachment) when is_list(attachment), do: attachment defp normalize_attachment(_), do: [] + defp maybe_make_public_key_object(data) do + if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do + %{ + public_key: data["publicKey"]["publicKeyPem"], + key_id: data["publicKey"]["id"] + } + else + nil + end + end + defp object_to_user_data(data, additional) do fields = data @@ -1578,9 +1589,16 @@ defp object_to_user_data(data, additional) do featured_address = data["featured"] {:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address) - public_key = - if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do - data["publicKey"]["publicKeyPem"] + # first, check that the owner is correct + signing_key = + if data["id"] !== data["publicKey"]["owner"] do + Logger.error( + "Owner of the public key is not the same as the actor - not saving the public key." + ) + + nil + else + maybe_make_public_key_object(data) end shared_inbox = @@ -1624,7 +1642,7 @@ defp object_to_user_data(data, additional) do bio: data["summary"] || "", actor_type: actor_type, also_known_as: also_known_as, - public_key: public_key, + signing_key: signing_key, inbox: data["inbox"], shared_inbox: shared_inbox, pinned_objects: pinned_objects, diff --git a/lib/pleroma/web/activity_pub/object_validators/user_validator.ex b/lib/pleroma/web/activity_pub/object_validators/user_validator.ex index adb291a55..b80068e37 100644 --- a/lib/pleroma/web/activity_pub/object_validators/user_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/user_validator.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do @behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating alias Pleroma.Object.Containment - alias Pleroma.Signature require Pleroma.Constants @@ -23,8 +22,7 @@ def validate(object, meta) def validate(%{"type" => type, "id" => _id} = data, meta) when type in Pleroma.Constants.actor_types() do - with :ok <- validate_pubkey(data), - :ok <- validate_inbox(data), + with :ok <- validate_inbox(data), :ok <- contain_collection_origin(data) do {:ok, data, meta} else @@ -35,33 +33,6 @@ def validate(%{"type" => type, "id" => _id} = data, meta) def validate(_, _), do: {:error, "Not a user object"} - defp mabye_validate_owner(nil, _actor), do: :ok - defp mabye_validate_owner(actor, actor), do: :ok - defp mabye_validate_owner(_owner, _actor), do: :error - - defp validate_pubkey( - %{"id" => id, "publicKey" => %{"id" => pk_id, "publicKeyPem" => _key}} = data - ) - when id != nil do - with {_, {:ok, kactor}} <- {:key, Signature.key_id_to_actor_id(pk_id)}, - true <- id == kactor, - :ok <- mabye_validate_owner(Map.get(data, "owner"), id) do - :ok - else - {:key, _} -> - {:error, "Unable to determine actor id from key id"} - - false -> - {:error, "Key id does not relate to user id"} - - _ -> - {:error, "Actor does not own its public key"} - end - end - - # pubkey is optional atm - defp validate_pubkey(_data), do: :ok - defp validate_inbox(%{"id" => id, "inbox" => inbox}) do case Containment.same_origin(id, inbox) do :ok -> :ok diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 47b8e37e5..d855e1e40 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do use Pleroma.Web, :view - alias Pleroma.Keys alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -33,9 +32,7 @@ def render("endpoints.json", %{user: %User{local: true} = _user}) do def render("endpoints.json", _), do: %{} def render("service.json", %{user: user}) do - {:ok, _, public_key} = Keys.keys_from_pem(user.keys) - public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) - public_key = :public_key.pem_encode([public_key]) + public_key = User.SigningKey.public_key_pem(user) endpoints = render("endpoints.json", %{user: user}) @@ -70,9 +67,7 @@ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}), do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) def render("user.json", %{user: user}) do - {:ok, _, public_key} = Keys.keys_from_pem(user.keys) - public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) - public_key = :public_key.pem_encode([public_key]) + public_key = User.SigningKey.public_key_pem(user) user = User.sanitize_html(user) endpoints = render("endpoints.json", %{user: user}) @@ -116,6 +111,20 @@ def render("user.json", %{user: user}) do |> Map.merge(Utils.make_json_ld_header()) end + def render("keys.json", %{user: user}) do + public_key = User.SigningKey.public_key_pem(user) + + %{ + "id" => user.ap_id, + "publicKey" => %{ + "id" => User.SigningKey.key_id_of_local_user(user), + "owner" => user.ap_id, + "publicKeyPem" => public_key + } + } + |> Map.merge(Utils.make_json_ld_header()) + end + def render("following.json", %{user: user, page: page} = opts) do showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows showing_count = showing_items || !user.hide_follows_count diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex index a73def682..93d269187 100644 --- a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex +++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do alias Pleroma.Helpers.AuthHelper - alias Pleroma.Signature alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils @@ -33,7 +32,7 @@ def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = con |> assign(:valid_signature, false) # remove me once testsuite uses mapped capabilities instead of what we do now - {:user, nil} -> + {:user, _} -> Logger.debug("Failed to map identity from signature (lookup failure)") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}") @@ -93,22 +92,33 @@ defp only_permit_user_routes(conn) do end defp key_id_from_conn(conn) do - with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn), - {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do - ap_id - else + case HTTPSignatures.signature_for_conn(conn) do + %{"keyId" => key_id} when is_binary(key_id) -> + key_id + _ -> nil end end defp user_from_key_id(conn) do - with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn), - {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do + with {:key_id, key_id} when is_binary(key_id) <- {:key_id, key_id_from_conn(conn)}, + {:mapped_ap_id, ap_id} when is_binary(ap_id) <- + {:mapped_ap_id, User.SigningKey.key_id_to_ap_id(key_id)}, + {:user_fetch, {:ok, %User{} = user}} <- {:user_fetch, User.get_or_fetch_by_ap_id(ap_id)} do user else - _ -> - nil + {:key_id, nil} -> + Logger.debug("Failed to map identity from signature (no key ID)") + {:key_id, nil} + + {:mapped_ap_id, nil} -> + Logger.debug("Failed to map identity from signature (could not map key ID to AP ID)") + {:mapped_ap_id, nil} + + {:user_fetch, {:error, _}} -> + Logger.debug("Failed to map identity from signature (lookup failure)") + {:user_fetch, nil} end end diff --git a/mix.lock b/mix.lock index 09997f29d..92a704add 100644 --- a/mix.lock +++ b/mix.lock @@ -34,14 +34,14 @@ "elasticsearch": {:git, "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", "6cd946f75f6ab9042521a009d1d32d29a90113ca", [ref: "main"]}, "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "3.1.0", "4d6260486a8cce59e4bf3575fe2dd2a24766546ceeef9f93fcec6f7c62a2827a", [:mix], [{:erlsom, "~> 1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "8fe5f2e75f90bab07ee2161120c2dc038ebcae8135554f5582990f1c8c21f911"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "ex_aws": {:hex, :ex_aws, "2.5.4", "86c5bb870a49e0ab6f5aa5dd58cf505f09d2624ebe17530db3c1b61c88a673af", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e82bd0091bb9a5bb190139599f922ff3fc7aebcca4374d65c99c4e23aa6d1625"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, "ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"}, - "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, - "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, + "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, + "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_syslogger": {:hex, :ex_syslogger, "2.0.0", "de6de5c5472a9c4fdafb28fa6610e381ae79ebc17da6490b81d785d68bd124c9", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "a52b2fe71764e9e6ecd149ab66635812f68e39279cbeee27c52c0e35e8b8019e"}, "excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"}, "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, @@ -83,10 +83,10 @@ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "oban": {:hex, :oban, "2.17.10", "c3e5bd739b5c3fdc38eba1d43ab270a8c6ca4463bb779b7705c69400b0d87678", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4afd027b8e2bc3c399b54318b4f46ee8c40251fb55a285cb4e38b5363f0ee7c4"}, + "oban": {:hex, :oban, "2.17.11", "7a641f9f737b626030c3e2209b53df6db83740ac5537208bac7d3b9871c2d5e7", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c445c488151939d64265a5efea51973fa0b42ee4ebbb31aa83fac26543b8ac6d"}, "open_api_spex": {:hex, :open_api_spex, "3.19.1", "65ccb5d06e3d664d1eec7c5ea2af2289bd2f37897094a74d7219fb03fc2b5994", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "392895827ce2984a3459c91a484e70708132d8c2c6c5363972b4b91d6bbac3dd"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, + "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"}, "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"}, @@ -95,7 +95,7 @@ "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, - "plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, @@ -120,7 +120,7 @@ "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]}, - "tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"}, + "tesla": {:hex, :tesla, "1.11.0", "81b2b10213dddb27105ec6102d9eb0cc93d7097a918a0b1594f2dfd1a4601190", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b83ab5d4c2d202e1ea2b7e17a49f788d49a699513d7c4f08f2aef2c281be69db"}, "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, diff --git a/test/pleroma/keys_test.exs b/test/pleroma/keys_test.exs deleted file mode 100644 index 9a15bf06e..000000000 --- a/test/pleroma/keys_test.exs +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.KeysTest do - use Pleroma.DataCase, async: true - - alias Pleroma.Keys - - test "generates an RSA private key pem" do - {:ok, key} = Keys.generate_rsa_pem() - - assert is_binary(key) - assert Regex.match?(~r/RSA/, key) - end - - test "returns a public and private key from a pem" do - pem = File.read!("test/fixtures/private_key.pem") - {:ok, private, public} = Keys.keys_from_pem(pem) - - assert elem(private, 0) == :RSAPrivateKey - assert elem(public, 0) == :RSAPublicKey - end -end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index e3f0bb415..207e38993 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -639,11 +639,12 @@ test "it sets the password_hash, ap_id, private key and followers collection add changeset = User.register_changeset(%User{}, @full_user_data) assert changeset.valid? - assert is_binary(changeset.changes[:password_hash]) - assert is_binary(changeset.changes[:keys]) assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname}) - assert is_binary(changeset.changes[:keys]) + assert changeset.changes[:signing_key] + assert changeset.changes[:signing_key].valid? + assert is_binary(changeset.changes[:signing_key].changes.private_key) + assert is_binary(changeset.changes[:signing_key].changes.public_key) assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" end @@ -1665,7 +1666,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do name: "qqqqqqq", password_hash: "pdfk2$1b3n159001", keys: "RSA begin buplic key", - public_key: "--PRIVATE KEYE--", avatar: %{"a" => "b"}, tags: ["qqqqq"], banner: %{"a" => "b"}, @@ -1704,8 +1704,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do email: nil, name: nil, password_hash: nil, - keys: "RSA begin buplic key", - public_key: "--PRIVATE KEYE--", avatar: %{}, tags: [], last_refreshed_at: nil, diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index 086a27885..cfcccbf9e 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -8,52 +8,58 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do import Tesla.Mock import Plug.Conn + import Pleroma.Factory import Pleroma.Tests.Helpers, only: [clear_config: 2] setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok + user = insert(:user) + {:ok, %{user: user}} end - defp set_signature(conn, key_id) do + defp set_signature(conn, ap_id) do conn - |> put_req_header("signature", "keyId=\"#{key_id}\"") + |> put_req_header("signature", "keyId=\"#{ap_id}#main-key\"") |> assign(:valid_signature, true) end - test "it successfully maps a valid identity with a valid signature" do + test "it successfully maps a valid identity with a valid signature", %{user: user} do conn = build_conn(:get, "/doesntmattter") - |> set_signature("http://mastodon.example.org/users/admin") + |> set_signature(user.ap_id) |> MappedSignatureToIdentityPlug.call(%{}) refute is_nil(conn.assigns.user) end - test "it successfully maps a valid identity with a valid signature with payload" do + test "it successfully maps a valid identity with a valid signature with payload", %{user: user} do conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://mastodon.example.org/users/admin") + build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) + |> set_signature(user.ap_id) |> MappedSignatureToIdentityPlug.call(%{}) refute is_nil(conn.assigns.user) end - test "it considers a mapped identity to be invalid when it mismatches a payload" do + test "it considers a mapped identity to be invalid when it mismatches a payload", %{user: user} do conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) + build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) |> set_signature("https://niu.moe/users/rye") |> MappedSignatureToIdentityPlug.call(%{}) assert %{valid_signature: false} == conn.assigns end - test "it considers a mapped identity to be invalid when the associated instance is blocked" do + test "it considers a mapped identity to be invalid when the associated instance is blocked", %{ + user: user + } do clear_config([:activitypub, :authorized_fetch_mode], true) + # extract domain from user.ap_id + url = URI.parse(user.ap_id) clear_config([:mrf_simple, :reject], [ - {"mastodon.example.org", "anime is banned"} + {url.host, "anime is banned"} ]) on_exit(fn -> @@ -62,18 +68,20 @@ test "it considers a mapped identity to be invalid when the associated instance end) conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://mastodon.example.org/users/admin") + build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) + |> set_signature(user.ap_id) |> MappedSignatureToIdentityPlug.call(%{}) assert %{valid_signature: false} == conn.assigns end - test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed" do + test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed", + %{user: user} do clear_config([:activitypub, :authorized_fetch_mode], true) + url = URI.parse(user.ap_id) clear_config([:mrf_simple, :accept], [ - {"mastodon.example.org", "anime is allowed"} + {url.host, "anime is allowed"} ]) on_exit(fn -> @@ -82,15 +90,16 @@ test "allowlist federation: it considers a mapped identity to be valid when the end) conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://mastodon.example.org/users/admin") + build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) + |> set_signature(user.ap_id) |> MappedSignatureToIdentityPlug.call(%{}) assert conn.assigns[:valid_signature] refute is_nil(conn.assigns.user) end - test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed" do + test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed", + %{user: user} do clear_config([:activitypub, :authorized_fetch_mode], true) clear_config([:mrf_simple, :accept], [ @@ -103,8 +112,8 @@ test "allowlist federation: it considers a mapped identity to be invalid when th end) conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://mastodon.example.org/users/admin") + build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) + |> set_signature(user.ap_id) |> MappedSignatureToIdentityPlug.call(%{}) assert %{valid_signature: false} == conn.assigns diff --git a/test/support/factory.ex b/test/support/factory.ex index 2a73a4ae6..f648179d2 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -47,7 +47,6 @@ def instance_factory(attrs \\ %{}) do end def user_factory(attrs \\ %{}) do - pem = Enum.random(@rsa_keys) # Argon2.hash_pwd_salt("test") # it really eats CPU time, so we use a precomputed hash password_hash = @@ -64,8 +63,7 @@ def user_factory(attrs \\ %{}) do last_refreshed_at: NaiveDateTime.utc_now(), notification_settings: %Pleroma.User.NotificationSetting{}, multi_factor_authentication_settings: %Pleroma.MFA.Settings{}, - ap_enabled: true, - keys: pem + ap_enabled: true } urls = @@ -90,13 +88,27 @@ def user_factory(attrs \\ %{}) do end attrs = Map.delete(attrs, :domain) + signing_key = insert(:signing_key, %{key_id: urls[:ap_id] <> "#main-key"}) user |> Map.put(:raw_bio, user.bio) + |> Map.put(:signing_key, signing_key) |> Map.merge(urls) |> merge_attributes(attrs) end + def signing_key_factory(attrs \\ %{}) do + pem = Enum.random(@rsa_keys) + {:ok, public_key} = Pleroma.User.SigningKey.private_pem_to_public_pem(pem) + + %Pleroma.User.SigningKey{ + public_key: public_key, + private_key: pem, + key_id: attrs[:key_id] || "https://example.com/key" + } + |> merge_attributes(attrs) + end + def user_relationship_factory(attrs \\ %{}) do source = attrs[:source] || insert(:user) target = attrs[:target] || insert(:user) From 9c876cea21373e9d17742abc04f4657f73cd359e Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Thu, 27 Jun 2024 06:58:05 +0100 Subject: [PATCH 29/48] Fix some tests --- lib/pleroma/user.ex | 3 ++- lib/pleroma/user/signing_key.ex | 5 ++++ mix.exs | 2 +- .../activity_pub_controller_test.exs | 25 +++++++++++++------ ...mapped_signature_to_identity_plug_test.exs | 2 ++ test/support/factory.ex | 15 ++++++----- 6 files changed, 36 insertions(+), 16 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 593173288..05d3011cb 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1155,7 +1155,8 @@ def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do was_superuser_before_update = User.superuser?(user) with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do - set_cache(user) + user + |> set_cache() end |> maybe_remove_report_notifications(was_superuser_before_update) end diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index 92ce7ae5d..3b5f4c35d 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -18,6 +18,11 @@ defmodule Pleroma.User.SigningKey do timestamps() end + def load_key(%User{} = user) do + user + |> Repo.preload(:signing_key) + end + def key_id_of_local_user(%User{local: true, signing_key: %__MODULE__{key_id: key_id}}), do: key_id diff --git a/mix.exs b/mix.exs index 7ffc450e2..418db47a1 100644 --- a/mix.exs +++ b/mix.exs @@ -200,7 +200,7 @@ defp deps do ## dev & test {:ex_doc, "~> 0.30", only: :dev, runtime: false}, - {:ex_machina, "~> 2.7", only: :test}, + {:ex_machina, "~> 2.8", only: :test}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.8", only: :test}, {:excoveralls, "0.16.1", only: :test}, diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index b325bcb9a..26b6f8e47 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -584,6 +584,7 @@ test "it inserts an incoming activity into the database" <> local: false, last_refreshed_at: nil ) + |> with_signing_key() data = File.read!("test/fixtures/mastodon-post-activity.json") @@ -594,7 +595,7 @@ test "it inserts an incoming activity into the database" <> conn = conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{user.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) @@ -609,6 +610,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do sender_url = data["actor"] sender = insert(:user, ap_id: data["actor"]) + |> with_signing_key() Instances.set_consistently_unreachable(sender_url) refute Instances.reachable?(sender_url) @@ -616,7 +618,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do conn = conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) @@ -784,6 +786,7 @@ test "mastodon pin/unpin", %{conn: conn} do ap_id: actor, featured_address: "https://example.com/users/lain/collections/featured" ) + |> with_signing_key() Tesla.Mock.mock(fn %{ @@ -839,7 +842,7 @@ test "mastodon pin/unpin", %{conn: conn} do assert "ok" == conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -974,6 +977,7 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do {:ok, post} = CommonAPI.post(user, %{status: "hey"}) announcer = insert(:user, local: false) + |> with_signing_key() data = %{ "@context" => "https://www.w3.org/ns/activitystreams", @@ -988,7 +992,7 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do conn = conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{announcer.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{announcer.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1004,6 +1008,7 @@ test "it accepts messages from actors that are followed by the user", %{ } do recipient = insert(:user) actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) + |> with_signing_key() {:ok, recipient, actor} = User.follow(recipient, actor) @@ -1019,7 +1024,7 @@ test "it accepts messages from actors that are followed by the user", %{ conn = conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) @@ -1057,6 +1062,7 @@ test "it returns a note activity in a collection", %{conn: conn} do test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do user = insert(:user) + |> with_signing_key() data = Map.put(data, "bcc", [user.ap_id]) sender_host = URI.parse(data["actor"]).host @@ -1066,7 +1072,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da conn = conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"") + |> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1077,6 +1083,8 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da @tag capture_log: true test "it removes all follower collections but actor's", %{conn: conn} do [actor, recipient] = insert_pair(:user) + actor = with_signing_key(actor) + to = [ recipient.ap_id, @@ -1105,7 +1113,7 @@ test "it removes all follower collections but actor's", %{conn: conn} do conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) |> json_response(200) @@ -1142,6 +1150,7 @@ test "it requires authentication", %{conn: conn} do test "forwarded report", %{conn: conn} do admin = insert(:user, is_admin: true) actor = insert(:user, local: false) + |> with_signing_key() remote_domain = URI.parse(actor.ap_id).host reported_user = insert(:user) @@ -1198,7 +1207,7 @@ test "forwarded report", %{conn: conn} do conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index cfcccbf9e..77b14bbd2 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -58,6 +58,7 @@ test "it considers a mapped identity to be invalid when the associated instance # extract domain from user.ap_id url = URI.parse(user.ap_id) + clear_config([:mrf_simple, :reject], [ {url.host, "anime is banned"} ]) @@ -80,6 +81,7 @@ test "allowlist federation: it considers a mapped identity to be valid when the clear_config([:activitypub, :authorized_fetch_mode], true) url = URI.parse(user.ap_id) + clear_config([:mrf_simple, :accept], [ {url.host, "anime is allowed"} ]) diff --git a/test/support/factory.ex b/test/support/factory.ex index f648179d2..6689cc5d4 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -88,25 +88,28 @@ def user_factory(attrs \\ %{}) do end attrs = Map.delete(attrs, :domain) - signing_key = insert(:signing_key, %{key_id: urls[:ap_id] <> "#main-key"}) - user |> Map.put(:raw_bio, user.bio) - |> Map.put(:signing_key, signing_key) |> Map.merge(urls) |> merge_attributes(attrs) end + def with_signing_key(%User{} = user) do + signing_key = build(:signing_key, %{user: user, key_id: "#{user.ap_id}#main-key"}) + insert(signing_key) + %{user | signing_key: signing_key} + end + def signing_key_factory(attrs \\ %{}) do pem = Enum.random(@rsa_keys) + user = attrs[:user] || insert(:user) {:ok, public_key} = Pleroma.User.SigningKey.private_pem_to_public_pem(pem) - %Pleroma.User.SigningKey{ + user_id: user.id, public_key: public_key, private_key: pem, - key_id: attrs[:key_id] || "https://example.com/key" + key_id: attrs[:key_id] } - |> merge_attributes(attrs) end def user_relationship_factory(attrs \\ %{}) do From 8f322456a0eb3416fb8f5cf9c720c618c2c16f72 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Fri, 28 Jun 2024 03:00:10 +0100 Subject: [PATCH 30/48] Allow unsigned fetches of a user's public key --- lib/pleroma/user/signing_key.ex | 22 +++++++++--- .../activity_pub/activity_pub_controller.ex | 34 ++++++++++++++++++- .../web/activity_pub/views/user_view.ex | 6 ++-- .../web/plugs/ensure_user_public_key_plug.ex | 33 ++++++++++++++++++ lib/pleroma/web/router.ex | 9 ++++- 5 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 lib/pleroma/web/plugs/ensure_user_public_key_plug.ex diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index 3b5f4c35d..1568b5487 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -23,8 +23,12 @@ def load_key(%User{} = user) do |> Repo.preload(:signing_key) end - def key_id_of_local_user(%User{local: true, signing_key: %__MODULE__{key_id: key_id}}), - do: key_id + def key_id_of_local_user(%User{local: true} = user) do + case Repo.preload(user, :signing_key) do + %User{signing_key: %__MODULE__{key_id: key_id}} -> key_id + _ -> nil + end + end @spec remote_changeset(__MODULE__, map) :: Changeset.t() def remote_changeset(%__MODULE__{} = signing_key, attrs) do @@ -119,9 +123,16 @@ def public_key(%User{signing_key: %__MODULE__{public_key: public_key_pem}}) do def public_key(_), do: {:error, "key not found"} - def public_key_pem(%User{signing_key: %__MODULE__{public_key: public_key_pem}}), - do: public_key_pem + def public_key_pem(%User{} = user) do + case Repo.preload(user, :signing_key) do + %User{signing_key: %__MODULE__{public_key: public_key_pem}} -> {:ok, public_key_pem} + _ -> {:error, "key not found"} + end + end + def public_key_pem(e) do + {:error, "key not found"} + end @spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()} @doc """ Given a user, return the private key for that user in binary format. @@ -146,7 +157,8 @@ def private_key(%User{signing_key: %__MODULE__{private_key: private_key_pem}}) d So if we're rejected, we should probably just give up. """ def fetch_remote_key(key_id) do - resp = HTTP.Backoff.get(key_id) + # we should probably sign this, just in case + resp = Pleroma.Object.Fetcher.get_object(key_id) case handle_signature_response(resp) do {:ok, ap_id, public_key_pem} -> diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 6642f7771..dff3e1f8e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -60,7 +60,27 @@ defp relay_active?(conn, _) do end end - def user(conn, %{"nickname" => nickname}) do + + @doc """ + Render the user's AP data + WARNING: we cannot actually check if the request has a fragment! so let's play defensively + - IF we have a valid signature, serve full user + - IF we do not, and authorized_fetch_mode is enabled, serve the key only + - OTHERWISE, serve the full actor (since we don't need to worry about the signature) + """ + def user(%{assigns: %{valid_signature: true}} = conn, params) do + render_full_user(conn, params) + end + + def user(conn, params) do + if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do + render_key_only_user(conn, params) + else + render_full_user(conn, params) + end + end + + defp render_full_user(conn, %{"nickname" => nickname}) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do conn |> put_resp_content_type("application/activity+json") @@ -72,6 +92,18 @@ def user(conn, %{"nickname" => nickname}) do end end + def render_key_only_user(conn, %{"nickname" => nickname}) do + with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do + conn + |> put_resp_content_type("application/activity+json") + |> put_view(UserView) + |> render("keys.json", %{user: user}) + else + nil -> {:error, :not_found} + %{local: false} -> {:error, :not_found} + end + end + def object(%{assigns: assigns} = conn, _) do with ap_id <- Endpoint.url() <> conn.request_path, %Object{} = object <- Object.get_cached_by_ap_id(ap_id), diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index d855e1e40..f91810fb8 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -32,7 +32,7 @@ def render("endpoints.json", %{user: %User{local: true} = _user}) do def render("endpoints.json", _), do: %{} def render("service.json", %{user: user}) do - public_key = User.SigningKey.public_key_pem(user) + {:ok, public_key} = User.SigningKey.public_key_pem(user) endpoints = render("endpoints.json", %{user: user}) @@ -67,7 +67,7 @@ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}), do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) def render("user.json", %{user: user}) do - public_key = User.SigningKey.public_key_pem(user) + {:ok, public_key} = User.SigningKey.public_key_pem(user) user = User.sanitize_html(user) endpoints = render("endpoints.json", %{user: user}) @@ -112,7 +112,7 @@ def render("user.json", %{user: user}) do end def render("keys.json", %{user: user}) do - public_key = User.SigningKey.public_key_pem(user) + {:ok, public_key} = User.SigningKey.public_key_pem(user) %{ "id" => user.ap_id, diff --git a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex new file mode 100644 index 000000000..baebcec29 --- /dev/null +++ b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex @@ -0,0 +1,33 @@ +defmodule Pleroma.Web.Plugs.EnsureUserPublicKeyPlug do + @moduledoc """ + This plug will attempt to pull in a user's public key if we do not have it. + We _should_ be able to request the URL from the key URL... + """ + + import Plug.Conn + + alias Pleroma.User + + def init(options), do: options + + def call(conn, _opts) do + key_id = key_id_from_conn(conn) + unless is_nil(key_id) do + SigningKey.fetch_remote_key(key_id) + # now we SHOULD have the user that owns the key locally. maybe. + # if we don't, we'll error out when we try to validate. + end + + conn + end + + defp key_id_from_conn(conn) do + case HTTPSignatures.signature_for_conn(conn) do + %{"keyId" => key_id} when is_binary(key_id) -> + key_id + + _ -> + nil + end + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 49ab3540b..c227d0d4a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -144,7 +144,14 @@ defmodule Pleroma.Web.Router do }) end + pipeline :optional_http_signature do + plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug) + plug(Pleroma.Web.Plugs.HTTPSignaturePlug) + plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) + end + pipeline :http_signature do + plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug) plug(Pleroma.Web.Plugs.HTTPSignaturePlug) plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug) @@ -745,7 +752,7 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do # Note: html format is supported only if static FE is enabled # Note: http signature is only considered for json requests (no auth for non-json requests) - pipe_through([:accepts_html_xml_json, :http_signature, :static_fe]) + pipe_through([:accepts_html_xml_json, :optional_http_signature, :static_fe]) # Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed) From 6da783b84d628a4cc71408e26e6834d029d7299d Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Fri, 28 Jun 2024 03:22:11 +0100 Subject: [PATCH 31/48] Fix http signature plug tests --- lib/pleroma/user/signing_key.ex | 21 ++++++++---- .../activity_pub/activity_pub_controller.ex | 1 - .../web/plugs/ensure_user_public_key_plug.ex | 1 + lib/pleroma/web/plugs/http_signature_plug.ex | 13 +++++--- .../activity_pub_controller_test.exs | 32 ++++++++++++------- .../web/plugs/http_signature_plug_test.exs | 15 +++++++-- test/support/factory.ex | 7 +++- 7 files changed, 63 insertions(+), 27 deletions(-) diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index 1568b5487..3d9b476f0 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -111,14 +111,20 @@ def private_pem_to_public_pem(private_pem) do @doc """ Given a user, return the public key for that user in binary format. """ - def public_key(%User{signing_key: %__MODULE__{public_key: public_key_pem}}) do - key = - public_key_pem - |> :public_key.pem_decode() - |> hd() - |> :public_key.pem_entry_decode() + def public_key(%User{} = user) do + case Repo.preload(user, :signing_key) do + %User{signing_key: %__MODULE__{public_key: public_key_pem}} -> + key = + public_key_pem + |> :public_key.pem_decode() + |> hd() + |> :public_key.pem_entry_decode() - {:ok, key} + {:ok, key} + + _ -> + {:error, "key not found"} + end end def public_key(_), do: {:error, "key not found"} @@ -133,6 +139,7 @@ def public_key_pem(%User{} = user) do def public_key_pem(e) do {:error, "key not found"} end + @spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()} @doc """ Given a user, return the private key for that user in binary format. diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index dff3e1f8e..9c6b3655d 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -60,7 +60,6 @@ defp relay_active?(conn, _) do end end - @doc """ Render the user's AP data WARNING: we cannot actually check if the request has a fragment! so let's play defensively diff --git a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex index baebcec29..c59053854 100644 --- a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex +++ b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex @@ -12,6 +12,7 @@ def init(options), do: options def call(conn, _opts) do key_id = key_id_from_conn(conn) + unless is_nil(key_id) do SigningKey.fetch_remote_key(key_id) # now we SHOULD have the user that owns the key locally. maybe. diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex index e3ae99636..06527cada 100644 --- a/lib/pleroma/web/plugs/http_signature_plug.ex +++ b/lib/pleroma/web/plugs/http_signature_plug.ex @@ -139,12 +139,17 @@ defp maybe_require_signature( defp maybe_require_signature(conn), do: conn defp signature_host(conn) do - with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), - {:ok, actor_id} <- Signature.key_id_to_actor_id(kid) do + with {:key_id, %{"keyId" => kid}} <- {:key_id, HTTPSignatures.signature_for_conn(conn)}, + {:actor_id, {:ok, actor_id}} <- {:actor_id, Signature.key_id_to_actor_id(kid)} do actor_id else - e -> - {:error, e} + {:key_id, e} -> + Logger.error("Failed to extract key_id from signature: #{inspect(e)}") + nil + + {:actor_id, e} -> + Logger.error("Failed to extract actor_id from signature: #{inspect(e)}") + nil end end end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 26b6f8e47..d67b59ef2 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -609,8 +609,10 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() sender_url = data["actor"] - sender = insert(:user, ap_id: data["actor"]) - |> with_signing_key() + + sender = + insert(:user, ap_id: data["actor"]) + |> with_signing_key() Instances.set_consistently_unreachable(sender_url) refute Instances.reachable?(sender_url) @@ -976,8 +978,10 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "hey"}) - announcer = insert(:user, local: false) - |> with_signing_key() + + announcer = + insert(:user, local: false) + |> with_signing_key() data = %{ "@context" => "https://www.w3.org/ns/activitystreams", @@ -1007,8 +1011,10 @@ test "it accepts messages from actors that are followed by the user", %{ data: data } do recipient = insert(:user) - actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) - |> with_signing_key() + + actor = + insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) + |> with_signing_key() {:ok, recipient, actor} = User.follow(recipient, actor) @@ -1061,8 +1067,10 @@ test "it returns a note activity in a collection", %{conn: conn} do end test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do - user = insert(:user) - |> with_signing_key() + user = + insert(:user) + |> with_signing_key() + data = Map.put(data, "bcc", [user.ap_id]) sender_host = URI.parse(data["actor"]).host @@ -1085,7 +1093,6 @@ test "it removes all follower collections but actor's", %{conn: conn} do [actor, recipient] = insert_pair(:user) actor = with_signing_key(actor) - to = [ recipient.ap_id, recipient.follower_address, @@ -1149,8 +1156,11 @@ test "it requires authentication", %{conn: conn} do @tag capture_log: true test "forwarded report", %{conn: conn} do admin = insert(:user, is_admin: true) - actor = insert(:user, local: false) - |> with_signing_key() + + actor = + insert(:user, local: false) + |> with_signing_key() + remote_domain = URI.parse(actor.ap_id).host reported_user = insert(:user) diff --git a/test/pleroma/web/plugs/http_signature_plug_test.exs b/test/pleroma/web/plugs/http_signature_plug_test.exs index 0a602424d..7f623bfca 100644 --- a/test/pleroma/web/plugs/http_signature_plug_test.exs +++ b/test/pleroma/web/plugs/http_signature_plug_test.exs @@ -14,6 +14,15 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do import Phoenix.Controller, only: [put_format: 2] import Mock + setup do + user = + :user + |> insert(%{ ap_id: "http://mastodon.example.org/users/admin" }) + |> with_signing_key(%{ key_id: "http://mastodon.example.org/users/admin#main-key" }) + + {:ok, %{user: user}} + end + setup_with_mocks([ {HTTPSignatures, [], [ @@ -46,15 +55,15 @@ defp submit_to_plug(host, method, path) do |> HTTPSignaturePlug.call(%{}) end - test "it call HTTPSignatures to check validity if the actor signed it" do - params = %{"actor" => "http://mastodon.example.org/users/admin"} + test "it call HTTPSignatures to check validity if the actor signed it", %{user: user} do + params = %{"actor" => user.ap_id} conn = build_conn(:get, "/doesntmattter", params) conn = conn |> put_req_header( "signature", - "keyId=\"http://mastodon.example.org/users/admin#main-key" + "keyId=\"#{user.signing_key.key_id}\"" ) |> put_format("activity+json") |> HTTPSignaturePlug.call(%{}) diff --git a/test/support/factory.ex b/test/support/factory.ex index 6689cc5d4..1f54c5b7a 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -88,14 +88,18 @@ def user_factory(attrs \\ %{}) do end attrs = Map.delete(attrs, :domain) + user |> Map.put(:raw_bio, user.bio) |> Map.merge(urls) |> merge_attributes(attrs) end - def with_signing_key(%User{} = user) do + def with_signing_key(%User{} = user, attrs \\ %{}) do + signing_key = build(:signing_key, %{user: user, key_id: "#{user.ap_id}#main-key"}) + |> merge_attributes(attrs) + insert(signing_key) %{user | signing_key: signing_key} end @@ -104,6 +108,7 @@ def signing_key_factory(attrs \\ %{}) do pem = Enum.random(@rsa_keys) user = attrs[:user] || insert(:user) {:ok, public_key} = Pleroma.User.SigningKey.private_pem_to_public_pem(pem) + %Pleroma.User.SigningKey{ user_id: user.id, public_key: public_key, From ccf1007883e24b490608abdbdd4f13cba8921498 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sun, 30 Jun 2024 04:25:55 +0100 Subject: [PATCH 32/48] Fix about a million tests --- lib/pleroma/signature.ex | 20 ++--- lib/pleroma/user/signing_key.ex | 49 +++++++++--- .../web/activity_pub/views/user_view.ex | 7 +- .../web/plugs/ensure_user_public_key_plug.ex | 2 +- ...0240625213637_create_signing_key_table.exs | 2 +- test/mix/tasks/pleroma/database_test.exs | 3 +- test/pleroma/signature_test.exs | 75 ++++--------------- test/pleroma/user_search_test.exs | 2 +- .../activity_pub_controller_test.exs | 27 ++++++- .../web/activity_pub/publisher_test.exs | 10 +++ .../web/plugs/http_signature_plug_test.exs | 4 +- ...mapped_signature_to_identity_plug_test.exs | 6 +- test/support/factory.ex | 10 +-- test/support/helpers.ex | 1 + test/support/http_request_mock.ex | 27 +++++++ test/support/matching_helpers.ex | 9 +++ 16 files changed, 154 insertions(+), 100 deletions(-) create mode 100644 test/support/matching_helpers.ex diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index 1374ac068..cedfee2e5 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -8,32 +8,24 @@ defmodule Pleroma.Signature do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.User.SigningKey + require Logger def key_id_to_actor_id(key_id) do # Given the key ID, first attempt to look it up in the signing keys table. - # If it's not found, then attempt to look it up via request to the remote instance. case SigningKey.key_id_to_ap_id(key_id) do nil -> - # this requires us to look up the url! - request_key_id_from_remote_instance(key_id) + # hm, we SHOULD have gotten this in the pipeline before we hit here! + Logger.error("Could not figure out who owns the key #{key_id}") + {:error, :key_owner_not_found} key -> {:ok, key} end end - def request_key_id_from_remote_instance(key_id) do - case SigningKey.fetch_remote_key(key_id) do - {:ok, key_id} -> - {:ok, key_id} - - {:error, _} -> - {:error, "Key ID not found"} - end - end - def fetch_public_key(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), + {:ok, %SigningKey{}} <- SigningKey.get_or_fetch_by_key_id(kid), {:ok, actor_id} <- key_id_to_actor_id(kid), {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} @@ -45,8 +37,8 @@ def fetch_public_key(conn) do def refetch_public_key(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), + {:ok, %SigningKey{}} <- SigningKey.get_or_fetch_by_key_id(kid), {:ok, actor_id} <- key_id_to_actor_id(kid), - {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} else diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index 3d9b476f0..1fa574ae2 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -154,6 +154,21 @@ def private_key(%User{signing_key: %__MODULE__{private_key: private_key_pem}}) d {:ok, key} end + @spec get_or_fetch_by_key_id(String.t()) :: {:ok, __MODULE__} | {:error, String.t()} + @doc """ + Given a key ID, return the signing key associated with that key. + Will either return the key if it exists locally, or fetch it from the remote instance. + """ + def get_or_fetch_by_key_id(key_id) do + case key_id_to_user_id(key_id) do + nil -> + fetch_remote_key(key_id) + + user_id -> + {:ok, Repo.get_by(__MODULE__, user_id: user_id)} + end + end + @spec fetch_remote_key(String.t()) :: {:ok, __MODULE__} | {:error, String.t()} @doc """ Fetch a remote key by key ID. @@ -164,23 +179,33 @@ def private_key(%User{signing_key: %__MODULE__{private_key: private_key_pem}}) d So if we're rejected, we should probably just give up. """ def fetch_remote_key(key_id) do + Logger.debug("Fetching remote key: #{key_id}") # we should probably sign this, just in case resp = Pleroma.Object.Fetcher.get_object(key_id) - case handle_signature_response(resp) do - {:ok, ap_id, public_key_pem} -> - # fetch the user - user = User.get_or_fetch_by_ap_id(ap_id) - # store the key - key = %__MODULE__{ - user_id: user.id, - public_key: public_key_pem, - key_id: key_id - } + case resp do + {:ok, _original_url, body} -> + case handle_signature_response(resp) do + {:ok, ap_id, public_key_pem} -> + Logger.debug("Fetched remote key: #{ap_id}") + # fetch the user + {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) + # store the key + key = %__MODULE__{ + user_id: user.id, + public_key: public_key_pem, + key_id: key_id + } - Repo.insert(key) + Repo.insert(key, on_conflict: :replace_all, conflict_target: :key_id) + + e -> + Logger.debug("Failed to fetch remote key: #{inspect(e)}") + {:error, "Could not fetch key"} + end _ -> + Logger.debug("Failed to fetch remote key: #{inspect(resp)}") {:error, "Could not fetch key"} end end @@ -196,7 +221,7 @@ defp extract_key_details(%{"id" => ap_id, "publicKey" => public_key}) do end end - defp handle_signature_response({:ok, %{status: status, body: body}}) when status in 200..299 do + defp handle_signature_response({:ok, _original_url, body}) do case Jason.decode(body) do {:ok, %{"id" => _user_id, "publicKey" => _public_key} = body} -> extract_key_details(body) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index f91810fb8..ad6aeaff2 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -67,7 +67,12 @@ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}), do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) def render("user.json", %{user: user}) do - {:ok, public_key} = User.SigningKey.public_key_pem(user) + public_key = + case User.SigningKey.public_key_pem(user) do + {:ok, public_key} -> public_key + _ -> nil + end + user = User.sanitize_html(user) endpoints = render("endpoints.json", %{user: user}) diff --git a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex index c59053854..4da9215a3 100644 --- a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex +++ b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex @@ -14,7 +14,7 @@ def call(conn, _opts) do key_id = key_id_from_conn(conn) unless is_nil(key_id) do - SigningKey.fetch_remote_key(key_id) + User.SigningKey.fetch_remote_key(key_id) # now we SHOULD have the user that owns the key locally. maybe. # if we don't, we'll error out when we try to validate. end diff --git a/priv/repo/migrations/20240625213637_create_signing_key_table.exs b/priv/repo/migrations/20240625213637_create_signing_key_table.exs index 34169f78c..5bd9bbcf2 100644 --- a/priv/repo/migrations/20240625213637_create_signing_key_table.exs +++ b/priv/repo/migrations/20240625213637_create_signing_key_table.exs @@ -10,6 +10,6 @@ def change do timestamps() end - create index(:signing_keys, [:key_id]) + create unique_index(:signing_keys, [:key_id]) end end diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs index 20f113e6e..4f97a978a 100644 --- a/test/mix/tasks/pleroma/database_test.exs +++ b/test/mix/tasks/pleroma/database_test.exs @@ -353,7 +353,7 @@ test "with the --keep-threads option it keeps old threads with bookmarked posts" test "We don't have unexpected tables which may contain objects that are referenced by activities" do # We can delete orphaned activities. For that we look for the objects they reference in the 'objects', 'activities', and 'users' table. - # If someone adds another table with objects (idk, maybe with separate relations, or collections or w/e), then we need to make sure we + # If someone adds another table with objects (idk, maybe with separate relations, or collections or w/e), then we need to make sure we # add logic for that in the 'prune_objects' task so that we don't wrongly delete their corresponding activities. # So when someone adds (or removes) a table, this test will fail. # Either the table contains objects which can be referenced from the activities table @@ -401,6 +401,7 @@ test "We don't have unexpected tables which may contain objects that are referen ["rich_media_card"], ["scheduled_activities"], ["schema_migrations"], + ["signing_keys"], ["thread_mutes"], ["user_follows_hashtag"], ["user_frontend_setting_profiles"], diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs index 768c78f21..0e360d58d 100644 --- a/test/pleroma/signature_test.exs +++ b/test/pleroma/signature_test.exs @@ -35,25 +35,23 @@ defp make_fake_conn(key_id), do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}} describe "fetch_public_key/1" do - test "it returns key" do + test "it returns the key" do expected_result = {:ok, @rsa_public_key} - user = insert(:user, public_key: @public_key) + user = + insert(:user) + |> with_signing_key(public_key: @public_key) assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result end - test "it returns error when not found user" do - assert capture_log(fn -> - assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) == - {:error, :error} - end) =~ "[error] Could not decode user" - end - test "it returns error if public key is nil" do - user = insert(:user, public_key: nil) + # this actually needs the URL to be valid + user = insert(:user) + key_id = user.ap_id <> "#main-key" + Tesla.Mock.mock(fn %{url: ^key_id} -> {:ok, %{status: 404}} end) - assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error} + assert {:error, _} = Signature.fetch_public_key(make_fake_conn(user.ap_id)) end end @@ -63,12 +61,6 @@ test "it returns key" do assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key} end - - test "it returns error when not found user" do - assert capture_log(fn -> - {:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id")) - end) =~ "[error] Could not decode user" - end end defp split_signature(sig) do @@ -104,9 +96,9 @@ defp assert_part_equal(part_a, part_b) do test "it returns signature headers" do user = insert(:user, %{ - ap_id: "https://mastodon.social/users/lambadalambda", - keys: @private_key + ap_id: "https://mastodon.social/users/lambadalambda" }) + |> with_signing_key(private_key: @private_key) headers = %{ host: "test.test", @@ -121,50 +113,15 @@ test "it returns signature headers" do "keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\"" ) end - - test "it returns error" do - user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""}) - - assert Signature.sign( - user, - %{host: "test.test", "content-length": "100"} - ) == {:error, []} - end end describe "key_id_to_actor_id/1" do - test "it properly deduces the actor id for misskey" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") == - {:ok, "https://example.com/users/1234"} - end + test "it reverses the key id to actor id" do + user = + insert(:user) + |> with_signing_key() - test "it properly deduces the actor id for mastodon and pleroma" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") == - {:ok, "https://example.com/users/1234"} - end - - test "it deduces the actor id for gotoSocial" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234/main-key") == - {:ok, "https://example.com/users/1234"} - end - - test "it deduces the actor ID for streams" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234?operation=getkey") == - {:ok, "https://example.com/users/1234"} - end - - test "it deduces the actor ID for bridgy" do - assert Signature.key_id_to_actor_id("https://example.com/1234#key") == - {:ok, "https://example.com/1234"} - end - - test "it calls webfinger for 'acct:' accounts" do - with_mock(Pleroma.Web.WebFinger, - finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end - ) do - assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") == - {:ok, "https://gensokyo.2hu/users/raymoo"} - end + assert Signature.key_id_to_actor_id(user.signing_key.key_id) == {:ok, user.ap_id} end end diff --git a/test/pleroma/user_search_test.exs b/test/pleroma/user_search_test.exs index 2af19b6de..5d018a301 100644 --- a/test/pleroma/user_search_test.exs +++ b/test/pleroma/user_search_test.exs @@ -259,7 +259,7 @@ test "works with URIs" do |> Map.put(:multi_factor_authentication_settings, nil) |> Map.put(:notification_settings, nil) - assert user == expected + assert_user_match(user, expected) end test "excludes a blocked users from search result" do diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index d67b59ef2..120fb6d1b 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -645,7 +645,7 @@ test "accept follow activity", %{conn: conn} do assert "ok" == conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}#main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", accept) |> json_response(200) @@ -682,6 +682,7 @@ test "accepts Add/Remove activities", %{conn: conn} do |> String.replace("{{nickname}}", "lain") actor = "https://example.com/users/lain" + key_id = "#{actor}/main-key" insert(:user, ap_id: actor, @@ -709,6 +710,16 @@ test "accepts Add/Remove activities", %{conn: conn} do headers: [{"content-type", "application/activity+json"}] } + %{ + method: :get, + url: ^key_id + } -> + %Tesla.Env{ + status: 200, + body: user, + headers: [{"content-type", "application/activity+json"}] + } + %{method: :get, url: "https://example.com/users/lain/collections/featured"} -> %Tesla.Env{ status: 200, @@ -782,6 +793,7 @@ test "mastodon pin/unpin", %{conn: conn} do |> String.replace("{{nickname}}", "lain") actor = "https://example.com/users/lain" + key_id = "#{actor}/main-key" sender = insert(:user, @@ -811,6 +823,15 @@ test "mastodon pin/unpin", %{conn: conn} do headers: [{"content-type", "application/activity+json"}] } + %{ + method: :get, + url: ^key_id + } -> + %Tesla.Env{ + status: 200, + body: user, + headers: [{"content-type", "application/activity+json"}] + } %{method: :get, url: "https://example.com/users/lain/collections/featured"} -> %Tesla.Env{ status: 200, @@ -907,6 +928,7 @@ test "it inserts an incoming activity into the database", %{conn: conn, data: da test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do user = insert(:user) + |> with_signing_key() data = data @@ -952,6 +974,7 @@ test "it accepts messages with cc as string instead of array", %{conn: conn, dat test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do user = insert(:user) + |> with_signing_key() data = data @@ -1273,7 +1296,7 @@ test "forwarded report from mastodon", %{conn: conn} do conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{remote_actor}/main-key\"") + |> put_req_header("signature", "keyId=\"#{remote_actor}#main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 87930b7b1..726d85958 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -141,6 +141,7 @@ test "publish to url with with different ports" do end) actor = insert(:user) + |> with_signing_key() assert {:ok, %{body: "port 42"}} = Publisher.publish_one(%{ @@ -166,6 +167,7 @@ test "publish to url with with different ports" do [:passthrough], [] do actor = insert(:user) + |> with_signing_key() inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) @@ -177,6 +179,7 @@ test "publish to url with with different ports" do [:passthrough], [] do actor = insert(:user) + |> with_signing_key() inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = @@ -196,6 +199,7 @@ test "publish to url with with different ports" do [:passthrough], [] do actor = insert(:user) + |> with_signing_key() inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = @@ -215,6 +219,7 @@ test "publish to url with with different ports" do [:passthrough], [] do actor = insert(:user) + |> with_signing_key() inbox = "http://404.site/users/nick1/inbox" assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) @@ -227,6 +232,7 @@ test "publish to url with with different ports" do [:passthrough], [] do actor = insert(:user) + |> with_signing_key() inbox = "http://connrefused.site/users/nick1/inbox" assert capture_log(fn -> @@ -242,6 +248,7 @@ test "publish to url with with different ports" do [:passthrough], [] do actor = insert(:user) + |> with_signing_key() inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) @@ -254,6 +261,7 @@ test "publish to url with with different ports" do [:passthrough], [] do actor = insert(:user) + |> with_signing_key() inbox = "http://connrefused.site/users/nick1/inbox" assert capture_log(fn -> @@ -295,6 +303,7 @@ test "publish to url with with different ports" do }) actor = insert(:user, follower_address: follower.ap_id) + |> with_signing_key() {:ok, follower, actor} = Pleroma.User.follow(follower, actor) {:ok, _another_follower, actor} = Pleroma.User.follow(another_follower, actor) @@ -366,6 +375,7 @@ test "publish to url with with different ports" do }) actor = insert(:user, follower_address: follower.ap_id) + |> with_signing_key() {:ok, follower, actor} = Pleroma.User.follow(follower, actor) actor = refresh_record(actor) diff --git a/test/pleroma/web/plugs/http_signature_plug_test.exs b/test/pleroma/web/plugs/http_signature_plug_test.exs index 7f623bfca..5236b519e 100644 --- a/test/pleroma/web/plugs/http_signature_plug_test.exs +++ b/test/pleroma/web/plugs/http_signature_plug_test.exs @@ -17,8 +17,8 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do setup do user = :user - |> insert(%{ ap_id: "http://mastodon.example.org/users/admin" }) - |> with_signing_key(%{ key_id: "http://mastodon.example.org/users/admin#main-key" }) + |> insert(%{ap_id: "http://mastodon.example.org/users/admin"}) + |> with_signing_key(%{key_id: "http://mastodon.example.org/users/admin#main-key"}) {:ok, %{user: user}} end diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index 77b14bbd2..5789cd756 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -14,7 +14,11 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - user = insert(:user) + + user = + insert(:user) + |> with_signing_key() + {:ok, %{user: user}} end diff --git a/test/support/factory.ex b/test/support/factory.ex index 1f54c5b7a..54e5f91b7 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -96,9 +96,9 @@ def user_factory(attrs \\ %{}) do end def with_signing_key(%User{} = user, attrs \\ %{}) do - - signing_key = build(:signing_key, %{user: user, key_id: "#{user.ap_id}#main-key"}) - |> merge_attributes(attrs) + signing_key = + build(:signing_key, %{user: user, key_id: "#{user.ap_id}#main-key"}) + |> merge_attributes(attrs) insert(signing_key) %{user | signing_key: signing_key} @@ -111,8 +111,8 @@ def signing_key_factory(attrs \\ %{}) do %Pleroma.User.SigningKey{ user_id: user.id, - public_key: public_key, - private_key: pem, + public_key: attrs[:public_key] || public_key, + private_key: attrs[:private_key] || pem, key_id: attrs[:key_id] } end diff --git a/test/support/helpers.ex b/test/support/helpers.ex index 2dfff70a2..8d481b5f5 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -65,6 +65,7 @@ defmacro __using__(_opts) do clear_config: 1, clear_config: 2 ] + import Pleroma.Test.MatchingHelpers def time_travel(entity, seconds) do new_time = NaiveDateTime.add(entity.inserted_at, seconds) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index ea06c4966..d14434333 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -424,6 +424,15 @@ def get("http://mastodon.example.org/users/admin", _, _, _) do }} end + def get("http://mastodon.example.org/users/admin/main-key", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/admin@mastdon.example.org.json"), + headers: activitypub_object_headers() + }} + end + def get( "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true", _, @@ -958,6 +967,15 @@ def get("https://mastodon.social/users/lambadalambda", _, _, _) do }} end + def get("https://mastodon.social/users/lambadalambda#main-key", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/lambadalambda.json"), + headers: activitypub_object_headers() + }} + end + def get("https://mastodon.social/users/lambadalambda/collections/featured", _, _, _) do {:ok, %Tesla.Env{ @@ -1403,6 +1421,15 @@ def get("https://relay.mastodon.host/actor", _, _, _) do }} end + def get("https://relay.mastodon.host/actor#main-key", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/relay/relay.json"), + headers: activitypub_object_headers() + }} + end + def get("http://localhost:4001/", _, "", [{"accept", "text/html"}]) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}} end diff --git a/test/support/matching_helpers.ex b/test/support/matching_helpers.ex new file mode 100644 index 000000000..39963171f --- /dev/null +++ b/test/support/matching_helpers.ex @@ -0,0 +1,9 @@ +defmodule Pleroma.Test.MatchingHelpers do + import ExUnit.Assertions + @assoc_fields [ + :signing_key + ] + def assert_user_match(actor1, actor2) do + assert Ecto.reset_fields(actor1, @assoc_fields) == Ecto.reset_fields(actor2, @assoc_fields) + end +end From 430b376dede4847f0f63cc64be40995648e52dda Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Tue, 20 Aug 2024 11:05:17 +0100 Subject: [PATCH 33/48] mix format --- lib/pleroma/user/signing_key.ex | 20 ++++--- test/pleroma/user_search_test.exs | 2 +- .../activity_pub_controller_test.exs | 47 ++++++++------- .../web/activity_pub/publisher_test.exs | 57 ++++++++++++------- test/support/helpers.ex | 1 + test/support/matching_helpers.ex | 1 + 6 files changed, 78 insertions(+), 50 deletions(-) diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index 1fa574ae2..11786bbb0 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -144,14 +144,20 @@ def public_key_pem(e) do @doc """ Given a user, return the private key for that user in binary format. """ - def private_key(%User{signing_key: %__MODULE__{private_key: private_key_pem}}) do - key = - private_key_pem - |> :public_key.pem_decode() - |> hd() - |> :public_key.pem_entry_decode() + def private_key(%User{} = user) do + case Repo.preload(user, :signing_key) do + %{signing_key: %__MODULE__{private_key: private_key_pem}} -> + key = + private_key_pem + |> :public_key.pem_decode() + |> hd() + |> :public_key.pem_entry_decode() - {:ok, key} + {:ok, key} + + _ -> + {:error, "key not found"} + end end @spec get_or_fetch_by_key_id(String.t()) :: {:ok, __MODULE__} | {:error, String.t()} diff --git a/test/pleroma/user_search_test.exs b/test/pleroma/user_search_test.exs index 5d018a301..b499629c8 100644 --- a/test/pleroma/user_search_test.exs +++ b/test/pleroma/user_search_test.exs @@ -259,7 +259,7 @@ test "works with URIs" do |> Map.put(:multi_factor_authentication_settings, nil) |> Map.put(:notification_settings, nil) - assert_user_match(user, expected) + assert_user_match(user, expected) end test "excludes a blocked users from search result" do diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 120fb6d1b..183818159 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -710,15 +710,15 @@ test "accepts Add/Remove activities", %{conn: conn} do headers: [{"content-type", "application/activity+json"}] } - %{ - method: :get, - url: ^key_id - } -> - %Tesla.Env{ - status: 200, - body: user, - headers: [{"content-type", "application/activity+json"}] - } + %{ + method: :get, + url: ^key_id + } -> + %Tesla.Env{ + status: 200, + body: user, + headers: [{"content-type", "application/activity+json"}] + } %{method: :get, url: "https://example.com/users/lain/collections/featured"} -> %Tesla.Env{ @@ -823,15 +823,16 @@ test "mastodon pin/unpin", %{conn: conn} do headers: [{"content-type", "application/activity+json"}] } - %{ - method: :get, - url: ^key_id - } -> - %Tesla.Env{ - status: 200, - body: user, - headers: [{"content-type", "application/activity+json"}] - } + %{ + method: :get, + url: ^key_id + } -> + %Tesla.Env{ + status: 200, + body: user, + headers: [{"content-type", "application/activity+json"}] + } + %{method: :get, url: "https://example.com/users/lain/collections/featured"} -> %Tesla.Env{ status: 200, @@ -927,8 +928,9 @@ test "it inserts an incoming activity into the database", %{conn: conn, data: da end test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do - user = insert(:user) - |> with_signing_key() + user = + insert(:user) + |> with_signing_key() data = data @@ -973,8 +975,9 @@ test "it accepts messages with cc as string instead of array", %{conn: conn, dat end test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do - user = insert(:user) - |> with_signing_key() + user = + insert(:user) + |> with_signing_key() data = data diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 726d85958..eeec59cfb 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -140,8 +140,9 @@ test "publish to url with with different ports" do {:ok, %Tesla.Env{status: 200, body: "port 80"}} end) - actor = insert(:user) - |> with_signing_key() + actor = + insert(:user) + |> with_signing_key() assert {:ok, %{body: "port 42"}} = Publisher.publish_one(%{ @@ -166,8 +167,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) - |> with_signing_key() + actor = + insert(:user) + |> with_signing_key() + inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) @@ -178,8 +181,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) - |> with_signing_key() + actor = + insert(:user) + |> with_signing_key() + inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = @@ -198,8 +203,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) - |> with_signing_key() + actor = + insert(:user) + |> with_signing_key() + inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = @@ -218,8 +225,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) - |> with_signing_key() + actor = + insert(:user) + |> with_signing_key() + inbox = "http://404.site/users/nick1/inbox" assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) @@ -231,8 +240,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) - |> with_signing_key() + actor = + insert(:user) + |> with_signing_key() + inbox = "http://connrefused.site/users/nick1/inbox" assert capture_log(fn -> @@ -247,8 +258,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) - |> with_signing_key() + actor = + insert(:user) + |> with_signing_key() + inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) @@ -260,8 +273,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) - |> with_signing_key() + actor = + insert(:user) + |> with_signing_key() + inbox = "http://connrefused.site/users/nick1/inbox" assert capture_log(fn -> @@ -302,8 +317,9 @@ test "publish to url with with different ports" do ap_enabled: true }) - actor = insert(:user, follower_address: follower.ap_id) - |> with_signing_key() + actor = + insert(:user, follower_address: follower.ap_id) + |> with_signing_key() {:ok, follower, actor} = Pleroma.User.follow(follower, actor) {:ok, _another_follower, actor} = Pleroma.User.follow(another_follower, actor) @@ -374,8 +390,9 @@ test "publish to url with with different ports" do ap_enabled: true }) - actor = insert(:user, follower_address: follower.ap_id) - |> with_signing_key() + actor = + insert(:user, follower_address: follower.ap_id) + |> with_signing_key() {:ok, follower, actor} = Pleroma.User.follow(follower, actor) actor = refresh_record(actor) diff --git a/test/support/helpers.ex b/test/support/helpers.ex index 8d481b5f5..f0beae8ec 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -65,6 +65,7 @@ defmacro __using__(_opts) do clear_config: 1, clear_config: 2 ] + import Pleroma.Test.MatchingHelpers def time_travel(entity, seconds) do diff --git a/test/support/matching_helpers.ex b/test/support/matching_helpers.ex index 39963171f..c8411b907 100644 --- a/test/support/matching_helpers.ex +++ b/test/support/matching_helpers.ex @@ -1,5 +1,6 @@ defmodule Pleroma.Test.MatchingHelpers do import ExUnit.Assertions + @assoc_fields [ :signing_key ] From 13215f5f060a7c7da014aca79b6700654791856c Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 26 Oct 2024 05:28:55 +0100 Subject: [PATCH 34/48] remove public key field --- lib/pleroma/object/fetcher.ex | 1 + lib/pleroma/signature.ex | 1 - lib/pleroma/user.ex | 3 --- lib/pleroma/user/signing_key.ex | 2 +- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 897146bce..2cecfec67 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -343,6 +343,7 @@ defp get_final_id(final_url, _intial_url) do final_url end + @doc "Do NOT use; only public for use in tests" def get_object(id) do date = Pleroma.Signature.signed_date() diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index cedfee2e5..91e610fe1 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -11,7 +11,6 @@ defmodule Pleroma.Signature do require Logger def key_id_to_actor_id(key_id) do - # Given the key ID, first attempt to look it up in the signing keys table. case SigningKey.key_id_to_ap_id(key_id) do nil -> # hm, we SHOULD have gotten this in the pipeline before we hit here! diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 05d3011cb..bf8717ffb 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -101,7 +101,6 @@ defmodule Pleroma.User do field(:password, :string, virtual: true) field(:password_confirmation, :string, virtual: true) field(:keys, :string) - field(:public_key, :string) field(:ap_id, :string) field(:avatar, :map, default: %{}) field(:local, :boolean, default: true) @@ -471,7 +470,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :inbox, :shared_inbox, :nickname, - :public_key, :avatar, :ap_enabled, :banner, @@ -532,7 +530,6 @@ def update_changeset(struct, params \\ %{}) do :name, :emoji, :avatar, - :public_key, :inbox, :shared_inbox, :is_locked, diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index 11786bbb0..60f1edb7c 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -187,7 +187,7 @@ def get_or_fetch_by_key_id(key_id) do def fetch_remote_key(key_id) do Logger.debug("Fetching remote key: #{key_id}") # we should probably sign this, just in case - resp = Pleroma.Object.Fetcher.get_object(key_id) + resp = Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(key_id) case resp do {:ok, _original_url, body} -> From bee10eab5ebf6aa4d50238afd3d9c37a014dd745 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 26 Oct 2024 06:11:12 +0100 Subject: [PATCH 35/48] correct minor zip behaviour --- lib/pleroma/emoji/pack.ex | 2 +- test/pleroma/emoji/pack_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 142208854..e95320a07 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -125,7 +125,7 @@ def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} {:ok, _emoji_files} = :zip.unzip( to_charlist(file.path), - [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}] + [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, to_charlist(tmp_dir)}] ) {_, updated_pack} = diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs index f5d2e2eef..3843add13 100644 --- a/test/pleroma/emoji/pack_test.exs +++ b/test/pleroma/emoji/pack_test.exs @@ -64,7 +64,7 @@ test "returns error when zip file is bad", %{pack: pack} do path: Path.absname("test/instance_static/emoji/test_pack/blank.png") } - assert Pack.add_file(pack, nil, nil, file) == {:error, :einval} + assert Pack.add_file(pack, nil, nil, file) == {:error, :bad_eocd} end test "returns pack when zip file is empty", %{pack: pack} do From 58d5d9d7bf469f13300fc86d3ed9a14337d9eb3a Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 26 Oct 2024 06:58:47 +0100 Subject: [PATCH 36/48] fix tests, contain object --- lib/pleroma/signature.ex | 1 - lib/pleroma/user/signing_key.ex | 16 +++++-------- .../web/plugs/ensure_user_public_key_plug.ex | 2 +- .../activity_pub_controller_test.exs | 24 +++++++++++++------ .../web/activity_pub/views/user_view_test.exs | 21 ++++++++++++---- 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index 91e610fe1..25fad22f7 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -6,7 +6,6 @@ defmodule Pleroma.Signature do @behaviour HTTPSignatures.Adapter alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.User.SigningKey require Logger diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index 60f1edb7c..709ee2593 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -4,7 +4,6 @@ defmodule Pleroma.User.SigningKey do import Ecto.Changeset alias Pleroma.User alias Pleroma.Repo - alias Pleroma.HTTP require Logger @@ -136,7 +135,7 @@ def public_key_pem(%User{} = user) do end end - def public_key_pem(e) do + def public_key_pem(_e) do {:error, "key not found"} end @@ -190,7 +189,7 @@ def fetch_remote_key(key_id) do resp = Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(key_id) case resp do - {:ok, _original_url, body} -> + {:ok, _body} -> case handle_signature_response(resp) do {:ok, ap_id, public_key_pem} -> Logger.debug("Fetched remote key: #{ap_id}") @@ -227,16 +226,13 @@ defp extract_key_details(%{"id" => ap_id, "publicKey" => public_key}) do end end - defp handle_signature_response({:ok, _original_url, body}) do - case Jason.decode(body) do - {:ok, %{"id" => _user_id, "publicKey" => _public_key} = body} -> + defp handle_signature_response({:ok, body}) do + case body do + %{"id" => _user_id, "publicKey" => _public_key} -> extract_key_details(body) - {:ok, %{"error" => error}} -> + %{"error" => error} -> {:error, error} - - {:error, _} -> - {:error, "Could not parse key"} end end diff --git a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex index 4da9215a3..3eaa131e9 100644 --- a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex +++ b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex @@ -14,7 +14,7 @@ def call(conn, _opts) do key_id = key_id_from_conn(conn) unless is_nil(key_id) do - User.SigningKey.fetch_remote_key(key_id) + User.SigningKey.get_or_fetch_by_key_id(key_id) # now we SHOULD have the user that owns the key locally. maybe. # if we don't, we'll error out when we try to validate. end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 183818159..c41a19cc7 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -1094,7 +1094,7 @@ test "it returns a note activity in a collection", %{conn: conn} do test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do user = - insert(:user) + insert(:user, ap_id: data["actor"]) |> with_signing_key() data = Map.put(data, "bcc", [user.ap_id]) @@ -1277,12 +1277,22 @@ test "forwarded report from mastodon", %{conn: conn} do |> File.read!() |> String.replace("{{DOMAIN}}", remote_domain) - Tesla.Mock.mock(fn %{url: ^remote_actor} -> - %Tesla.Env{ - status: 200, - body: mock_json_body, - headers: [{"content-type", "application/activity+json"}] - } + key_url = "#{remote_actor}#main-key" + + Tesla.Mock.mock(fn + %{url: ^remote_actor} -> + %Tesla.Env{ + status: 200, + body: mock_json_body, + headers: [{"content-type", "application/activity+json"}] + } + + %{url: ^key_url} -> + %Tesla.Env{ + status: 200, + body: mock_json_body, + headers: [{"content-type", "application/activity+json"}] + } end) data = %{ diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs index 2a367b680..7e251e510 100644 --- a/test/pleroma/web/activity_pub/views/user_view_test.exs +++ b/test/pleroma/web/activity_pub/views/user_view_test.exs @@ -11,7 +11,9 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do alias Pleroma.Web.CommonAPI test "Renders a user, including the public key" do - user = insert(:user) + user = + insert(:user) + |> with_signing_key() result = UserView.render("user.json", %{user: user}) @@ -37,7 +39,9 @@ test "Renders profile fields" do end test "Renders with emoji tags" do - user = insert(:user, emoji: %{"bib" => "/test"}) + user = + insert(:user, emoji: %{"bib" => "/test"}) + |> with_signing_key() assert %{ "tag" => [ @@ -74,13 +78,18 @@ test "Does not add an avatar image if the user hasn't set one" do end test "renders an invisible user with the invisible property set to true" do - user = insert(:user, invisible: true) + user = + insert(:user, invisible: true) + |> with_signing_key() assert %{"invisible" => true} = UserView.render("service.json", %{user: user}) end test "service has a few essential fields" do - user = insert(:user) + user = + insert(:user) + |> with_signing_key() + result = UserView.render("service.json", %{user: user}) assert result["id"] assert result["type"] == "Application" @@ -120,7 +129,9 @@ test "remote users have an empty endpoints structure" do end test "instance users do not expose oAuth endpoints" do - user = insert(:user, nickname: nil, local: true) + user = + insert(:user, nickname: nil, local: true) + |> with_signing_key() result = UserView.render("user.json", %{user: user}) From c5a44a59db4366af611cf0d6c0f7246ea0612220 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 26 Oct 2024 07:00:38 +0100 Subject: [PATCH 37/48] remove unneeded index --- .../repo/migrations/20240625213637_create_signing_key_table.exs | 2 -- 1 file changed, 2 deletions(-) diff --git a/priv/repo/migrations/20240625213637_create_signing_key_table.exs b/priv/repo/migrations/20240625213637_create_signing_key_table.exs index 5bd9bbcf2..062edc66f 100644 --- a/priv/repo/migrations/20240625213637_create_signing_key_table.exs +++ b/priv/repo/migrations/20240625213637_create_signing_key_table.exs @@ -9,7 +9,5 @@ def change do add :private_key, :text timestamps() end - - create unique_index(:signing_keys, [:key_id]) end end From ac25b051ae5d5b7a5b9e39e58f43606bd49d1021 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 26 Oct 2024 07:28:28 +0100 Subject: [PATCH 38/48] remove previous "allow user routes" functionality --- .../web/plugs/mapped_signature_to_identity_plug.ex | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex index 93d269187..94ff17cc1 100644 --- a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex +++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex @@ -67,7 +67,8 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do Logger.debug("Failed to map identity from signature (lookup failure)") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}") - only_permit_user_routes(conn) + conn + |> assign(:valid_signature, false) _ -> Logger.debug("Failed to map identity from signature (no payload actor mismatch)") @@ -81,16 +82,6 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do # no signature at all def call(conn, _opts), do: conn - defp only_permit_user_routes(%{path_info: ["users", _]} = conn) do - conn - |> assign(:limited_ap, true) - end - - defp only_permit_user_routes(conn) do - conn - |> assign(:valid_signature, false) - end - defp key_id_from_conn(conn) do case HTTPSignatures.signature_for_conn(conn) do %{"keyId" => key_id} when is_binary(key_id) -> From 9d2c558f64fd7853f9317e74821d6687eb7a8886 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 26 Oct 2024 07:42:43 +0100 Subject: [PATCH 39/48] remove unused import --- .../web/plugs/ensure_user_public_key_plug.ex | 2 - .../mapped_signature_to_identity_plug.ex | 4 +- mix.lock | 57 ++++++++++--------- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex index 3eaa131e9..5b090f289 100644 --- a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex +++ b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex @@ -4,8 +4,6 @@ defmodule Pleroma.Web.Plugs.EnsureUserPublicKeyPlug do We _should_ be able to request the URL from the key URL... """ - import Plug.Conn - alias Pleroma.User def init(options), do: options diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex index 94ff17cc1..0d5cb9eab 100644 --- a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex +++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex @@ -67,8 +67,8 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do Logger.debug("Failed to map identity from signature (lookup failure)") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}") - conn - |> assign(:valid_signature, false) + conn + |> assign(:valid_signature, false) _ -> Logger.debug("Failed to map identity from signature (no payload actor mismatch)") diff --git a/mix.lock b/mix.lock index 92a704add..cdf99d7bc 100644 --- a/mix.lock +++ b/mix.lock @@ -8,28 +8,28 @@ "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "90f6ce7672f70f56708792a98d98bd05176c9176", [ref: "90f6ce7672f70f56708792a98d98bd05176c9176"]}, - "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, + "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, - "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, + "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"}, "concurrent_limiter": {:git, "https://akkoma.dev/AkkomaGang/concurrent-limiter.git", "a9e0b3d64574bdba761f429bb4fba0cf687b3338", [ref: "a9e0b3d64574bdba761f429bb4fba0cf687b3338"]}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, - "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, - "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, - "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, + "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"}, "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, - "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.0", "440719cd74f09b3f01c84455707a2c3972b725c513808e68eb6c5b0ab82bf523", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 0.18.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "f1512812dc196bcb932a96c82e55f69b543dc125e9d39f5e3631a9c4ec65ef12"}, + "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.2", "79350a53246ac5ec27326d208496aebceb77fa82a91744f66a9154560f0759d3", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0 and < 0.20.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "6149c1c4a5ba6602a76cb09ee7a269eb60dab9694a1dbbb797f032555212de75"}, "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, "elasticsearch": {:git, "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", "6cd946f75f6ab9042521a009d1d32d29a90113ca", [ref: "main"]}, "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, @@ -37,10 +37,10 @@ "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, - "ex_aws": {:hex, :ex_aws, "2.5.4", "86c5bb870a49e0ab6f5aa5dd58cf505f09d2624ebe17530db3c1b61c88a673af", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e82bd0091bb9a5bb190139599f922ff3fc7aebcca4374d65c99c4e23aa6d1625"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, + "ex_aws": {:hex, :ex_aws, "2.5.6", "6f642e0f82eff10a9b470044f084b81a791cf15b393d647ea5f3e65da2794e3d", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c69eec59e31fdd89d0beeb1d97e16518dd1b23ad95b3d5c9f1dcfec23d97f960"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.4", "87aaf4a2f24a48f516d7f5aaced9d128dd5d0f655c4431f9037a11a85c71109c", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "c06e7f68b33f7c0acba1361dbd951c79661a28f85aa2e0582990fccca4425355"}, "ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"}, - "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_syslogger": {:hex, :ex_syslogger, "2.0.0", "de6de5c5472a9c4fdafb28fa6610e381ae79ebc17da6490b81d785d68bd124c9", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "a52b2fe71764e9e6ecd149ab66635812f68e39279cbeee27c52c0e35e8b8019e"}, "excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"}, @@ -48,10 +48,10 @@ "fast_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"}, "fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"}, "file_ex": {:git, "https://akkoma.dev/AkkomaGang/file_ex.git", "cc7067c7d446c2526e9ecf91d40896b088851569", [ref: "cc7067c7d446c2526e9ecf91d40896b088851569"]}, - "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, - "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, + "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"}, "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, @@ -61,33 +61,34 @@ "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "joken": {:hex, :joken, "2.6.1", "2ca3d8d7f83bf7196296a3d9b2ecda421a404634bfc618159981a960020480a1", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "ab26122c400b3d254ce7d86ed066d6afad27e70416df947cdcb01e13a7382e68"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, "linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"}, - "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, + "mail": {:hex, :mail, "0.4.2", "959229676ef11dfdfb1269ce4a611622cb70415a1d6f925d79e547848bafc14d", [:mix], [], "hexpm", "08e5b70c72b8d1605cb88ef2df2c7e41d002210a621503ea1c13f1a7916b6bd3"}, "majic": {:git, "https://akkoma.dev/AkkomaGang/majic.git", "80540b36939ec83f48e76c61e5000e0fd67706f0", [ref: "80540b36939ec83f48e76c61e5000e0fd67706f0"]}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "b21ab7754024af096f2d14247574f55f0063295b", [ref: "b21ab7754024af096f2d14247574f55f0063295b"]}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, "mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"}, - "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, + "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "oban": {:hex, :oban, "2.17.11", "7a641f9f737b626030c3e2209b53df6db83740ac5537208bac7d3b9871c2d5e7", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c445c488151939d64265a5efea51973fa0b42ee4ebbb31aa83fac26543b8ac6d"}, - "open_api_spex": {:hex, :open_api_spex, "3.19.1", "65ccb5d06e3d664d1eec7c5ea2af2289bd2f37897094a74d7219fb03fc2b5994", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "392895827ce2984a3459c91a484e70708132d8c2c6c5363972b4b91d6bbac3dd"}, + "oban": {:hex, :oban, "2.17.12", "33fb0cbfb92b910d48dd91a908590fe3698bb85eacec8cd0d9bc6aa13dddd6d6", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7a647d6cd6bb300073db17faabce22d80ae135da3baf3180a064fa7c4fa046e3"}, + "open_api_spex": {:hex, :open_api_spex, "3.21.2", "6a704f3777761feeb5657340250d6d7332c545755116ca98f33d4b875777e1e5", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "f42ae6ed668b895ebba3e02773cfb4b41050df26f803f2ef634c72a7687dc387"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"}, "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"}, @@ -96,7 +97,7 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, @@ -104,7 +105,7 @@ "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, - "recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"}, + "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"}, "search_parser": {:git, "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git", "08971a81e68686f9ac465cfb6661d51c5e4e1e7f", [ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"]}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, @@ -114,22 +115,22 @@ "swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, "table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"}, "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]}, - "tesla": {:hex, :tesla, "1.11.0", "81b2b10213dddb27105ec6102d9eb0cc93d7097a918a0b1594f2dfd1a4601190", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b83ab5d4c2d202e1ea2b7e17a49f788d49a699513d7c4f08f2aef2c281be69db"}, + "tesla": {:hex, :tesla, "1.13.0", "24a068a48d107080dd7c943a593997eee265977a38020eb2ab657cca78a12502", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7b8fc8f6b0640fa0d090af7889d12eb396460e044b6f8688a8e55e30406a2200"}, "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, - "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, + "tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"}, "ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, "vex": {:hex, :vex, "0.9.2", "fe061acc9e0907d983d46b51bf35d58176f0fe6eb7ba3b33c9336401bf42b6d1", [:mix], [], "hexpm", "76e709a9762e98c6b462dfce92e9b5dfbf712839227f2da8add6dd11549b12cb"}, "web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, } From bd64d070823b8d729f0fa9d445def1a32fbc9af4 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 26 Oct 2024 07:51:41 +0100 Subject: [PATCH 40/48] ensure migration actually works --- priv/repo/migrations/20240625220752_move_signing_keys.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/priv/repo/migrations/20240625220752_move_signing_keys.exs b/priv/repo/migrations/20240625220752_move_signing_keys.exs index d9ef1acab..9531ceed8 100644 --- a/priv/repo/migrations/20240625220752_move_signing_keys.exs +++ b/priv/repo/migrations/20240625220752_move_signing_keys.exs @@ -14,7 +14,7 @@ def up do Repo.stream(query, timeout: :infinity) |> Enum.each(fn - %User{id: user_id, keys: private_key, local: true} -> + %User{id: user_id, keys: private_key, local: true, ap_id: ap_id} -> # we can precompute the public key here... # we do use it on every user view which makes it a bit of a dos attack vector # so we should probably cache it @@ -23,6 +23,7 @@ def up do key = %User.SigningKey{ user_id: user_id, public_key: public_key, + key_id: "#{ap_id}#main-key", private_key: private_key } From d330c57cda8d5d714688ef9dd677fa5b0d228867 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 26 Oct 2024 08:42:07 +0100 Subject: [PATCH 41/48] make sure we correctly match key objects --- lib/pleroma/user/signing_key.ex | 17 +++++++++++++++-- .../20240625220752_move_signing_keys.exs | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index 709ee2593..f88faf70f 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -2,6 +2,7 @@ defmodule Pleroma.User.SigningKey do use Ecto.Schema import Ecto.Query import Ecto.Changeset + require Pleroma.Constants alias Pleroma.User alias Pleroma.Repo @@ -185,7 +186,6 @@ def get_or_fetch_by_key_id(key_id) do """ def fetch_remote_key(key_id) do Logger.debug("Fetching remote key: #{key_id}") - # we should probably sign this, just in case resp = Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(key_id) case resp do @@ -228,7 +228,20 @@ defp extract_key_details(%{"id" => ap_id, "publicKey" => public_key}) do defp handle_signature_response({:ok, body}) do case body do - %{"id" => _user_id, "publicKey" => _public_key} -> + %{ + "type" => "CryptographicKey", + "publicKeyPem" => public_key_pem, + "owner" => ap_id + } -> + {:ok, ap_id, public_key_pem} + + # for when we get a subset of the user object + %{ + "id" => _user_id, + "publicKey" => _public_key, + "type" => actor_type + } + when actor_type in Pleroma.Constants.actor_types() -> extract_key_details(body) %{"error" => error} -> diff --git a/priv/repo/migrations/20240625220752_move_signing_keys.exs b/priv/repo/migrations/20240625220752_move_signing_keys.exs index 9531ceed8..4e8eef6c9 100644 --- a/priv/repo/migrations/20240625220752_move_signing_keys.exs +++ b/priv/repo/migrations/20240625220752_move_signing_keys.exs @@ -23,7 +23,7 @@ def up do key = %User.SigningKey{ user_id: user_id, public_key: public_key, - key_id: "#{ap_id}#main-key", + key_id: "#{ap_id}#main-key", private_key: private_key } From 180dc8b472fe401d7f9ffa1bdf71943405aacacc Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Sat, 26 Oct 2024 08:50:38 +0100 Subject: [PATCH 42/48] downgrade earmark --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 418db47a1..ff888fd35 100644 --- a/mix.exs +++ b/mix.exs @@ -144,7 +144,7 @@ defp deps do {:ex_aws, "~> 2.4"}, {:ex_aws_s3, "~> 2.4"}, {:sweet_xml, "~> 0.7"}, - {:earmark, "~> 1.4"}, + {:earmark, "1.4.46"}, {:bbcode_pleroma, "~> 0.2.0"}, {:argon2_elixir, "~> 3.1"}, {:cors_plug, "~> 3.0"}, diff --git a/mix.lock b/mix.lock index cdf99d7bc..04cc0aa0a 100644 --- a/mix.lock +++ b/mix.lock @@ -24,7 +24,7 @@ "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, - "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, + "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"}, "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, From 11c5838947815ff00c4fb8a8ebf01a6fe846e6b4 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Wed, 30 Oct 2024 12:44:01 +0000 Subject: [PATCH 43/48] standardise local key id generation --- lib/pleroma/signature.ex | 2 +- lib/pleroma/user/signing_key.ex | 10 +++++++++- lib/pleroma/web/activity_pub/views/user_view.ex | 4 ++-- .../migrations/20240625220752_move_signing_keys.exs | 3 ++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index 25fad22f7..bc3baf433 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -47,7 +47,7 @@ def refetch_public_key(conn) do def sign(%User{} = user, headers) do with {:ok, private_key} <- SigningKey.private_key(user) do - HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) + HTTPSignatures.sign(private_key, SigningKey.local_key_id(user.ap_id), headers) end end diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index f88faf70f..f25489068 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -91,7 +91,15 @@ def generate_local_keys(ap_id) do |> change() |> put_change(:public_key, local_pem) |> put_change(:private_key, private_pem) - |> put_change(:key_id, ap_id <> "#main-key") + |> put_change(:key_id, local_key_id(ap_id)) + end + + @spec local_key_id(String.t()) :: String.t() + @doc """ + Given an AP ID, return the key ID for the local user. + """ + def local_key_id(ap_id) do + ap_id <> "#main-key" end @spec private_pem_to_public_pem(binary) :: {:ok, binary()} | {:error, String.t()} diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index ad6aeaff2..2ca31fc3c 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -49,7 +49,7 @@ def render("service.json", %{user: user}) do "url" => user.ap_id, "manuallyApprovesFollowers" => false, "publicKey" => %{ - "id" => "#{user.ap_id}#main-key", + "id" => User.SigningKey.local_key_id(user.ap_id), "owner" => user.ap_id, "publicKeyPem" => public_key }, @@ -97,7 +97,7 @@ def render("user.json", %{user: user}) do "url" => user.ap_id, "manuallyApprovesFollowers" => user.is_locked, "publicKey" => %{ - "id" => "#{user.ap_id}#main-key", + "id" => User.SigningKey.local_key_id(user.ap_id), "owner" => user.ap_id, "publicKeyPem" => public_key }, diff --git a/priv/repo/migrations/20240625220752_move_signing_keys.exs b/priv/repo/migrations/20240625220752_move_signing_keys.exs index 4e8eef6c9..9104b7c29 100644 --- a/priv/repo/migrations/20240625220752_move_signing_keys.exs +++ b/priv/repo/migrations/20240625220752_move_signing_keys.exs @@ -15,6 +15,7 @@ def up do Repo.stream(query, timeout: :infinity) |> Enum.each(fn %User{id: user_id, keys: private_key, local: true, ap_id: ap_id} -> + IO.puts("Migrating user #{user_id}") # we can precompute the public key here... # we do use it on every user view which makes it a bit of a dos attack vector # so we should probably cache it @@ -23,7 +24,7 @@ def up do key = %User.SigningKey{ user_id: user_id, public_key: public_key, - key_id: "#{ap_id}#main-key", + key_id: User.SigningKey.local_key_id(ap_id), private_key: private_key } From c9b3fcc1d3371b637ddd56498ea2e8ae84c04f29 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Wed, 30 Oct 2024 14:43:18 +0000 Subject: [PATCH 44/48] allow for OTP code changes in :zip --- test/pleroma/emoji/pack_test.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs index 3843add13..049f27a92 100644 --- a/test/pleroma/emoji/pack_test.exs +++ b/test/pleroma/emoji/pack_test.exs @@ -64,7 +64,10 @@ test "returns error when zip file is bad", %{pack: pack} do path: Path.absname("test/instance_static/emoji/test_pack/blank.png") } - assert Pack.add_file(pack, nil, nil, file) == {:error, :bad_eocd} + # this varies by erlang OTP + possible_error_codes = [:bad_eocd, :einval] + {:error, code} = Pack.add_file(pack, nil, nil, file) + assert Enum.member?(possible_error_codes, code) end test "returns pack when zip file is empty", %{pack: pack} do From 2b1a252cc78dbb3ff8a34a8365b8c049c0b531fb Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 13 Aug 2024 20:06:01 +0200 Subject: [PATCH 45/48] User: truncate remote user fields instead of rejecting --- .../bugfix-truncate-remote-user-fields.fix | 1 + lib/pleroma/user.ex | 2 ++ test/pleroma/user_test.exs | 15 +++++++++++++++ .../transmogrifier/user_update_handling_test.exs | 4 ++-- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 changelog.d/bugfix-truncate-remote-user-fields.fix diff --git a/changelog.d/bugfix-truncate-remote-user-fields.fix b/changelog.d/bugfix-truncate-remote-user-fields.fix new file mode 100644 index 000000000..239a3c224 --- /dev/null +++ b/changelog.d/bugfix-truncate-remote-user-fields.fix @@ -0,0 +1 @@ +Truncate remote user fields, avoids them getting rejected diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index bf8717ffb..dfeab0410 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -443,6 +443,7 @@ defp fix_follower_address(params), do: params def remote_user_changeset(struct \\ %User{local: false}, params) do bio_limit = Config.get([:instance, :user_bio_length], 5000) name_limit = Config.get([:instance, :user_name_length], 100) + fields_limit = Config.get([:instance, :max_remote_account_fields], 0) name = case params[:name] do @@ -456,6 +457,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now()) |> truncate_if_exists(:name, name_limit) |> truncate_if_exists(:bio, bio_limit) + |> Map.update(:fields, [], &Enum.take(&1, fields_limit)) |> truncate_fields_param() |> fix_follower_address() diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index cf9cc7519..ac886aaf9 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -966,6 +966,21 @@ test "it is invalid given a local user" do refute cs.valid? end + + test "it truncates fields" do + clear_config([:instance, :max_remote_account_fields], 2) + + fields = [ + %{"name" => "One", "value" => "Uno"}, + %{"name" => "Two", "value" => "Dos"}, + %{"name" => "Three", "value" => "Tres"} + ] + + cs = User.remote_user_changeset(@valid_remote |> Map.put(:fields, fields)) + + assert [%{"name" => "One", "value" => "Uno"}, %{"name" => "Two", "value" => "Dos"}] == + Ecto.Changeset.get_field(cs, :fields) + end end describe "followers and friends" do diff --git a/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs index b1a064772..35a5fe03d 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs @@ -119,8 +119,8 @@ test "it works with custom profile fields" do user = User.get_cached_by_ap_id(user.ap_id) assert user.fields == [ - %{"name" => "foo", "value" => "updated"}, - %{"name" => "foo1", "value" => "updated"} + %{"name" => "foo", "value" => "bar"}, + %{"name" => "foo11", "value" => "bar11"} ] update_data = From d1d82782dbd93c1037a438526b92670464b19b61 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Tue, 26 Nov 2024 09:35:56 +0000 Subject: [PATCH 46/48] add signing key index --- .../migrations/20241126093029_add_signing_key_index.exs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 priv/repo/migrations/20241126093029_add_signing_key_index.exs diff --git a/priv/repo/migrations/20241126093029_add_signing_key_index.exs b/priv/repo/migrations/20241126093029_add_signing_key_index.exs new file mode 100644 index 000000000..25df1bb7f --- /dev/null +++ b/priv/repo/migrations/20241126093029_add_signing_key_index.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddSigningKeyIndex do + use Ecto.Migration + + def change do + create_if_not_exists(index(:signing_keys, [:user_id], name: :signing_keys_user_id_index)) + end +end From 79b282dea624a1f933a45b471753097231d2b680 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Tue, 26 Nov 2024 09:36:20 +0000 Subject: [PATCH 47/48] bump version --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 7afc7f16a..982876fcb 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("3.13.2"), + version: version("3.13.3"), elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), From 834edfcf96cd7149adffec6d81724ff4be3c18ac Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Tue, 26 Nov 2024 09:49:37 +0000 Subject: [PATCH 48/48] add changelog --- CHANGELOG.md | 2 +- changelog.d/bugfix-truncate-remote-user-fields.fix | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 changelog.d/bugfix-truncate-remote-user-fields.fix diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1644dad..74a925a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## UNRELEASED +## 3.13.3 ## BREAKING - Minimum PostgreSQL version is raised to 12 diff --git a/changelog.d/bugfix-truncate-remote-user-fields.fix b/changelog.d/bugfix-truncate-remote-user-fields.fix deleted file mode 100644 index 239a3c224..000000000 --- a/changelog.d/bugfix-truncate-remote-user-fields.fix +++ /dev/null @@ -1 +0,0 @@ -Truncate remote user fields, avoids them getting rejected