Docs + test for DM privacy
Some checks are pending
ci/woodpecker/pr/build-amd64 Pipeline is pending
ci/woodpecker/pr/build-arm64 Pipeline is pending
ci/woodpecker/pr/docs Pipeline is pending
ci/woodpecker/pr/lint Pipeline is pending
ci/woodpecker/pr/test Pipeline is pending

This commit is contained in:
smitten 2023-07-29 00:08:29 -04:00
parent 208d2a6e0d
commit 5469a1268e
Signed by untrusted user: smitten
GPG key ID: 1DDD22F13552A07A
2 changed files with 86 additions and 48 deletions

View file

@ -3,6 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerController do 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 use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@ -17,7 +22,6 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerController do
# Note: (requires read:search) # Note: (requires read:search)
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated} when action in @oauth_search_actions) 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 def reroute(conn, %{"target" => target_param}) do
conn |> redirect(to: "/api/v1/akkoma/protocol-handler?#{URI.encode_query([target: target_param])}") conn |> redirect(to: "/api/v1/akkoma/protocol-handler?#{URI.encode_query([target: target_param])}")
end end
@ -38,12 +42,13 @@ def handle(%{assigns: %{user: user}} = conn, %{"target" => "web+ap:" <> identifi
def handle(conn, _), do: conn |> json_response(:bad_request, "Could not handle protocol URL") 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 defp find_and_redirect(%{assigns: %{user: user}} = conn, identifier) do
# Remove userinfo if present (username:password@) # Remove userinfo if present (username:password@)
cleaned = URI.parse("https:" <> identifier) |> Map.merge(%{ userinfo: nil }) |> URI.to_string() cleaned = URI.parse("https:" <> identifier) |> Map.merge(%{ userinfo: nil }) |> URI.to_string()
with {:error, _err} <- User.get_or_fetch(cleaned), with {:error, _err} <- User.get_or_fetch(cleaned),
[] <- DatabaseSearch.maybe_fetch([], user, 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}") conn |> json_response(:not_found, "Not Found - #{cleaned}")
else else
{:ok, %User{} = found_user} -> conn |> redirect(to: "/users/#{found_user.id}") {:ok, %User{} = found_user} -> conn |> redirect(to: "/users/#{found_user.id}")
@ -54,7 +59,7 @@ defp find_and_redirect(%{assigns: %{user: user}} = conn, identifier) do
end end
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 case User.search(identifier, limit: 1, for_user: user) do
[%User{:ap_id => ^identifier} = found_user] -> [found_user] [%User{:ap_id => ^identifier} = found_user] -> [found_user]
[%User{:uri => ^identifier} = found_user] -> [found_user] [%User{:uri => ^identifier} = found_user] -> [found_user]

View file

@ -6,38 +6,6 @@ defmodule Pleroma.Web.AkkomaAPI.ProtocolHandlerControllerTest do
import Pleroma.Factory 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 describe "GET /.well-known/protocol-handler" do
test "should return bad_request when missing `target`" do test "should return bad_request when missing `target`" do
%{conn: conn} = oauth_access([]) %{conn: conn} = oauth_access([])
@ -63,6 +31,42 @@ test "should return redirect when target parameter is present" do
end end
describe "GET /api/v1/akkoma/protocol-handler" do 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 test "should return bad_request when target prefix has unknown protocol" do
%{conn: conn} = oauth_access([]) %{conn: conn} = oauth_access([])
@ -75,7 +79,6 @@ test "should return bad_request when target prefix has unknown protocol" do
end end
test "should return forbidden for unauthed user when target is remote" do 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([]) %{conn: conn} = oauth_access([])
resp = resp =
@ -87,7 +90,6 @@ test "should return forbidden for unauthed user when target is remote" do
end end
test "should return redirect for unauthed user when target is local AP ID for user" do 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([]) %{conn: conn} = oauth_access([])
local_user = insert(:user, %{nickname: "akkoma@sub.example.com", local: true, ap_id: "https://sub.example.com/users/akkoma"}) local_user = insert(:user, %{nickname: "akkoma@sub.example.com", local: true, ap_id: "https://sub.example.com/users/akkoma"})
@ -100,23 +102,54 @@ test "should return redirect for unauthed user when target is local AP ID for us
assert resp =~ "<a href=\"/users/#{local_user.id}\">" assert resp =~ "<a href=\"/users/#{local_user.id}\">"
end end
test "should return redirect for unauthed user when target is local AP ID for note activity" do test "should return not_found for unauthed user when target is local AP ID for DM note activity" do
clear_config([Pleroma.Web.Endpoint, :url, :host], "mastodon.social") %{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: conn} = oauth_access(["read:search"]) |> 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 = resp =
conn 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) |> 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 =~ "You are being" assert resp =~ "<a href=\"/notice/#{activity.id}\">"
assert resp =~ "<a href=\"/notice/#{activity.id}\">"
end 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"]) %{conn: conn} = oauth_access(["read:search"])
remote_user = insert(:user, %{nickname: "akkoma@ihatebeinga.live", local: false, ap_id: "https://ihatebeinga.live/users/akkoma"}) remote_user = insert(:user, %{nickname: "akkoma@ihatebeinga.live", local: false, ap_id: "https://ihatebeinga.live/users/akkoma"})
@ -129,7 +162,7 @@ test "should return redirect for authed user when target is AP ID for user" do
assert resp =~ "<a href=\"/users/#{remote_user.id}\">" assert resp =~ "<a href=\"/users/#{remote_user.id}\">"
end 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"]) %{conn: conn} = oauth_access(["read:search"])
remote_user = insert(:user, %{ remote_user = insert(:user, %{
nickname: "emelie@mastodon.social", nickname: "emelie@mastodon.social",
@ -160,7 +193,7 @@ test "should return redirect for authed user when target is AP ID for user, stri
assert resp =~ "<a href=\"/users/#{remote_user.id}\">" assert resp =~ "<a href=\"/users/#{remote_user.id}\">"
end 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"]) %{conn: conn} = oauth_access(["read:search"])
resp = resp =