diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bd41752cd..b848d3c98 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@ image: git.pleroma.social:5050/pleroma/pleroma/ci-base variables: &global_variables + # Only used for the release ELIXIR_VER: 1.12.3 POSTGRES_DB: pleroma_test POSTGRES_USER: postgres @@ -319,8 +320,9 @@ amd64: - deps variables: &release-variables MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS before_script: &before-release - - apt-get update && apt-get install -y cmake libmagic-dev + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev - echo "import Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force @@ -341,7 +343,7 @@ amd64-musl: cache: *release-cache variables: *release-variables before_script: &before-release-musl - - apk add git build-base cmake file-dev openssl + - apk add git build-base cmake file-dev openssl vips-dev - echo "import Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force diff --git a/changelog.d/authorize-interaction.add b/changelog.d/authorize-interaction.add new file mode 100644 index 000000000..8692209e1 --- /dev/null +++ b/changelog.d/authorize-interaction.add @@ -0,0 +1 @@ +Support /authorize-interaction route used by Mastodon \ No newline at end of file diff --git a/changelog.d/build-release-with-local-libvips.skip b/changelog.d/build-release-with-local-libvips.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/optimistic-inbox.change b/changelog.d/optimistic-inbox.change new file mode 100644 index 000000000..2cf1ce92c --- /dev/null +++ b/changelog.d/optimistic-inbox.change @@ -0,0 +1 @@ +Optimistic Inbox reduces the processing overhead of incoming activities without instantly verifiable signatures. diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ce125d608..0706f5607 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -2136,7 +2136,7 @@ defmodule Pleroma.User do def public_key(_), do: {:error, "key not found"} def get_public_key_for_ap_id(ap_id) do - with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), + with %User{} = user <- get_cached_by_ap_id(ap_id), {:ok, public_key} <- public_key(user) do {:ok, public_key} else diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 1357c379c..3b2193ca3 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -287,10 +287,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do json(conn, "ok") end - def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do - conn - |> put_status(:bad_request) - |> json("Invalid HTTP Signature") + def inbox(%{assigns: %{valid_signature: false}, req_headers: req_headers} = conn, params) do + Federator.incoming_ap_doc(%{req_headers: req_headers, params: params}) + json(conn, "ok") end # POST /relay/inbox -or- POST /internal/fetch/inbox diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index 84b77cda1..8621d984c 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -35,6 +35,17 @@ defmodule Pleroma.Web.Federator do end # Client API + def incoming_ap_doc(%{params: params, req_headers: req_headers}) do + ReceiverWorker.enqueue( + "incoming_ap_doc", + %{"req_headers" => req_headers, "params" => params, "timeout" => :timer.seconds(20)}, + priority: 2 + ) + end + + def incoming_ap_doc(%{"type" => "Delete"} = params) do + ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params}, priority: 3) + end def incoming_ap_doc(params) do ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params}) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index eb8576b02..38391cc8f 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -471,6 +471,8 @@ defmodule Pleroma.Web.Router do get("/main/ostatus", UtilController, :show_subscribe_form) get("/ostatus_subscribe", RemoteFollowController, :follow) post("/ostatus_subscribe", RemoteFollowController, :do_follow) + + get("/authorize_interaction", RemoteFollowController, :authorize_interaction) end scope "/api/pleroma", Pleroma.Web.TwitterAPI do diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex index 6229d5d05..178ad2b43 100644 --- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex @@ -121,6 +121,13 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."}) end + # GET /authorize_interaction + # + def authorize_interaction(conn, %{"uri" => uri}) do + conn + |> redirect(to: Routes.remote_follow_path(conn, :follow, %{acct: uri})) + end + defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee}) end diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index cf1bb62b4..1dddd8d2e 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -3,24 +3,56 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.ReceiverWorker do + alias Pleroma.Signature + alias Pleroma.User alias Pleroma.Web.Federator use Pleroma.Workers.WorkerHelper, queue: "federator_incoming" @impl Oban.Worker + + def perform(%Job{ + args: %{"op" => "incoming_ap_doc", "req_headers" => req_headers, "params" => params} + }) do + # Oban's serialization converts our tuple headers to lists. + # Revert it for the signature validation. + req_headers = Enum.into(req_headers, [], &List.to_tuple(&1)) + + conn_data = %{params: params, req_headers: req_headers} + + with {:ok, %User{} = _actor} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), + {:ok, _public_key} <- Signature.refetch_public_key(conn_data), + {:signature, true} <- {:signature, HTTPSignatures.validate_conn(conn_data)}, + {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + {:ok, res} + else + e -> process_errors(e) + end + end + def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do {:ok, res} else + e -> process_errors(e) + end + end + + @impl Oban.Worker + def timeout(%_{args: %{"timeout" => timeout}}), do: timeout + + def timeout(_job), do: :timer.seconds(5) + + defp process_errors(errors) do + case errors do {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} {:error, :already_present} -> {:cancel, :already_present} {:error, {:validate_object, reason}} -> {:cancel, reason} {:error, {:error, {:validate, reason}}} -> {:cancel, reason} {:error, {:reject, reason}} -> {:cancel, reason} + {:signature, false} -> {:cancel, :invalid_signature} + {:error, {:error, reason = "Object has been deleted"}} -> {:cancel, reason} e -> e end end - - @impl Oban.Worker - def timeout(_job), do: :timer.seconds(5) end diff --git a/mix.exs b/mix.exs index 99074b433..653e5694b 100644 --- a/mix.exs +++ b/mix.exs @@ -181,7 +181,7 @@ defmodule Pleroma.Mixfile do {:majic, "~> 1.0"}, {:open_api_spex, "~> 3.16"}, {:ecto_psql_extras, "~> 0.6"}, - {:vix, "~> 0.25.0"}, + {:vix, "~> 0.26.0"}, {:elixir_make, "~> 0.7.7", override: true}, {:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash}, diff --git a/mix.lock b/mix.lock index a5afb46d5..81a05687b 100644 --- a/mix.lock +++ b/mix.lock @@ -137,7 +137,7 @@ "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.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, - "vix": {:hex, :vix, "0.25.0", "b294ca3140c0357b262d86e9966949949844282b81923bb990668c1ee5a35337", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "be09c96982978bc2d0c501a73e0b65ba58ec94c1afb94e3617029d6ce7ae8c3f"}, + "vix": {:hex, :vix, "0.26.0", "027f10b6969b759318be84bd0bd8c88af877445e4e41cf96a0460392cea5399c", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "71b0a79ae7f199cacfc8e679b0e4ba25ee47dc02e182c5b9097efb29fbe14efd"}, "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.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [: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", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs index b849cbee7..f5a915fa8 100644 --- a/test/pleroma/signature_test.exs +++ b/test/pleroma/signature_test.exs @@ -43,10 +43,7 @@ defmodule Pleroma.SignatureTest do 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" + assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) == {:error, :error} end test "it returns error if public key is nil" do diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index b9df527a0..77ca9198b 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -1951,8 +1951,8 @@ defmodule Pleroma.UserTest do end end - test "get_public_key_for_ap_id fetches a user that's not in the db" do - assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin") + test "get_public_key_for_ap_id returns correctly for user that's not in the db" do + assert :error = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin") end describe "per-user rich-text filtering" do diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs index 6d6ac0f8e..6f4d24d9e 100644 --- a/test/pleroma/web/plugs/frontend_static_plug_test.exs +++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -89,6 +89,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do "api", "main", "ostatus_subscribe", + "authorize_interaction", "oauth", "objects", "activities", diff --git a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs b/test/pleroma/web/twitter_api/remote_follow_controller_test.exs index b0812667e..41f8ebcd7 100644 --- a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs +++ b/test/pleroma/web/twitter_api/remote_follow_controller_test.exs @@ -460,4 +460,38 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do assert avatar_url == "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png" end end + + describe "GET /authorize_interaction - authorize_interaction/2" do + test "redirects to /ostatus_subscribe", %{conn: conn} do + Tesla.Mock.mock(fn + %{method: :get, url: "https://mastodon.social/users/emelie"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: File.read!("test/fixtures/tesla_mock/emelie.json") + } + + %{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: + File.read!("test/fixtures/users_mock/masto_featured.json") + |> String.replace("{{domain}}", "mastodon.social") + |> String.replace("{{nickname}}", "emelie") + } + end) + + conn = + conn + |> get( + remote_follow_path(conn, :authorize_interaction, %{ + uri: "https://mastodon.social/users/emelie" + }) + ) + + assert redirected_to(conn) == + remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + end + end end