diff --git a/lib/pleroma/web/akkoma_api/controllers/protocol_handler_controller.ex b/lib/pleroma/web/akkoma_api/controllers/protocol_handler_controller.ex index a0a521f5b..40e039e9a 100644 --- a/lib/pleroma/web/akkoma_api/controllers/protocol_handler_controller.ex +++ b/lib/pleroma/web/akkoma_api/controllers/protocol_handler_controller.ex @@ -3,6 +3,11 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerController do + @moduledoc """ + Handles web-based protocol requests, in particular web+ap: which reference ActivityPub URIs. + see https://datatracker.ietf.org/doc/draft-soni-protocol-handler-well-known-uri/ + A web+ap: URI should be handled like an https: URI, redirecting to the local representation of the remote or local object. + """ use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, only: [json_response: 3] @@ -17,7 +22,6 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerController do # Note: (requires read:search) plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated} when action in @oauth_search_actions) - # Protocol definition: https://datatracker.ietf.org/doc/draft-soni-protocol-handler-well-known-uri/ def reroute(conn, %{"target" => target_param}) do conn |> redirect(to: "/api/v1/akkoma/protocol-handler?#{URI.encode_query([target: target_param])}") end @@ -38,12 +42,13 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerController do def handle(conn, _), do: conn |> json_response(:bad_request, "Could not handle protocol URL") + @spec find_and_redirect(Plug.Conn.t(), String.t()) :: Plug.Conn.t() defp find_and_redirect(%{assigns: %{user: user}} = conn, identifier) do # Remove userinfo if present (username:password@) cleaned = URI.parse("https:" <> identifier) |> Map.merge(%{ userinfo: nil }) |> URI.to_string() with {:error, _err} <- User.get_or_fetch(cleaned), [] <- DatabaseSearch.maybe_fetch([], user, cleaned), - [] <- exact_search(cleaned, user) do + [] <- exact_user_search(cleaned, user) do conn |> json_response(:not_found, "Not Found - #{cleaned}") else {:ok, %User{} = found_user} -> conn |> redirect(to: "/users/#{found_user.id}") @@ -54,7 +59,7 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerController do end end - defp exact_search(identifier, user) do + defp exact_user_search(identifier, user) do case User.search(identifier, limit: 1, for_user: user) do [%User{:ap_id => ^identifier} = found_user] -> [found_user] [%User{:uri => ^identifier} = found_user] -> [found_user] diff --git a/test/pleroma/web/akkoma_api/protocol_handler_controller_test.exs b/test/pleroma/web/akkoma_api/protocol_handler_controller_test.exs index 5857dece8..ca0466804 100644 --- a/test/pleroma/web/akkoma_api/protocol_handler_controller_test.exs +++ b/test/pleroma/web/akkoma_api/protocol_handler_controller_test.exs @@ -6,38 +6,6 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerControllerTest do import Pleroma.Factory - setup do - Tesla.Mock.mock(fn - %{method: :get, url: "https://mastodon.social/users/emelie/statuses/101849165031453009"} -> - %Tesla.Env{ - status: 200, - headers: [{"content-type", "application/activity+json"}], - body: File.read!("test/fixtures/tesla_mock/status.emelie.json") - } - %{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/@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) - end - describe "GET /.well-known/protocol-handler" do test "should return bad_request when missing `target`" do %{conn: conn} = oauth_access([]) @@ -63,6 +31,42 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerControllerTest do end describe "GET /api/v1/akkoma/protocol-handler" do + setup do + clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com") + Tesla.Mock.mock(fn + %{method: :get, url: "https://mastodon.social/users/emelie/statuses/101849165031453009"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: File.read!("test/fixtures/tesla_mock/status.emelie.json") + } + %{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/@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") + } + _ -> %Tesla.Env{ + status: 404, + } + end) + end + test "should return bad_request when target prefix has unknown protocol" do %{conn: conn} = oauth_access([]) @@ -75,7 +79,6 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerControllerTest do end test "should return forbidden for unauthed user when target is remote" do - clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com") %{conn: conn} = oauth_access([]) resp = @@ -87,7 +90,6 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerControllerTest do end test "should return redirect for unauthed user when target is local AP ID for user" do - clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com") %{conn: conn} = oauth_access([]) local_user = insert(:user, %{nickname: "akkoma@sub.example.com", local: true, ap_id: "https://sub.example.com/users/akkoma"}) @@ -100,23 +102,54 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerControllerTest do assert resp =~ "" end - test "should return redirect for unauthed user when target is local AP ID for note activity" do - clear_config([Pleroma.Web.Endpoint, :url, :host], "mastodon.social") + test "should return not_found for unauthed user when target is local AP ID for DM note activity" do + %{conn: conn} = oauth_access([]) + local_user = insert(:user, %{nickname: "akkoma@sub.example.com", local: true, ap_id: "https://sub.example.com/users/akkoma"}) + note = insert(:note, %{ + id: "AYAsX3ZRH6NJAzZmEa", + data: %{ + "cc" => [], + "to" => [], + "actor" => local_user.ap_id, + "id" => "https://sub.example.com/notice/AYAsX3ZRH6NJAzZmEa", + "summary" => "", + "content" => "Pleroma's really cool!", + "directMessage" => true, + } + }) + insert(:note_activity, note: note, user: local_user) - clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com") - %{conn: conn} = oauth_access(["read:search"]) + conn + |> get("/api/v1/akkoma/protocol-handler?target=web%2Bap%3A%2F%2Fsub.example.com/notice/AYAsX3ZRH6NJAzZmEa") + |> json_response(404) + end + + test "should return not_found for unauthed user when target is local AP ID for public note activity" do + %{conn: conn} = oauth_access([]) + local_user = insert(:user, %{nickname: "akkoma@sub.example.com", local: true, ap_id: "https://sub.example.com/users/akkoma"}) + note = insert(:note, %{ + id: "AYAsX3ZRH6NJAzZmPa", + data: %{ + "cc" => [], + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "actor" => local_user.ap_id, + "id" => "https://sub.example.com/notice/AYAsX3ZRH6NJAzZmPa", + "summary" => "", + "content" => "Pleroma's really cool!", + } + }) + activity = insert(:note_activity, note: note, user: local_user, visibility: "direct") resp = conn - |> get("/api/v1/akkoma/protocol-handler?target=web%2Bap%3A%2F%2Fmastodon.social/users/emelie/statuses/101849165031453009") + |> get("/api/v1/akkoma/protocol-handler?target=web%2Bap%3A%2F%2Fsub.example.com/notice/AYAsX3ZRH6NJAzZmPa") |> html_response(302) - assert activity = Activity.get_by_object_ap_id_with_object("https://mastodon.social/users/emelie/statuses/101849165031453009") - assert resp =~ "You are being" - assert resp =~ "" + assert resp =~ "You are being" + assert resp =~ "" end - test "should return redirect for authed user when target is AP ID for user" do + test "should return redirect for authed user when target is AP ID for remote user" do %{conn: conn} = oauth_access(["read:search"]) remote_user = insert(:user, %{nickname: "akkoma@ihatebeinga.live", local: false, ap_id: "https://ihatebeinga.live/users/akkoma"}) @@ -129,7 +162,7 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerControllerTest do assert resp =~ "" end - test "should return redirect for authed user when target is URL for user" do + test "should return redirect for authed user when target is URI for remote user" do %{conn: conn} = oauth_access(["read:search"]) remote_user = insert(:user, %{ nickname: "emelie@mastodon.social", @@ -160,7 +193,7 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerControllerTest do assert resp =~ "" end - test "should return redirect for authed user when target is AP ID for note activity" do + test "should return redirect for authed user when target is AP ID for remote note activity" do %{conn: conn} = oauth_access(["read:search"]) resp =