diff options
author | Haelwenn (lanodan) Monnier <contact@hacktivis.me> | 2020-10-06 17:17:56 +0200 |
---|---|---|
committer | Haelwenn (lanodan) Monnier <contact@hacktivis.me> | 2020-10-06 17:29:01 +0200 |
commit | e2bad1efc49e9abd0fb35a1d3fe64cd2e6aa4074 (patch) | |
tree | 21eb84da9be6e9671104bfa97435687e322b6436 | |
parent | 4d852f3e78d6fcfe7bb2ef8b8d3a2f29359dd20c (diff) | |
download | pleroma-features/ap_c2s_proxyUrl.tar.gz |
AP C2S: Implement proxyUrl endpointfeatures/ap_c2s_proxyUrl
-rw-r--r-- | lib/pleroma/user.ex | 15 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub_controller.ex | 31 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/views/user_view.ex | 57 | ||||
-rw-r--r-- | lib/pleroma/web/router.ex | 1 | ||||
-rw-r--r-- | test/support/http_request_mock.ex | 15 | ||||
-rw-r--r-- | test/web/activity_pub/activity_pub_controller_test.exs | 128 | ||||
-rw-r--r-- | test/web/activity_pub/views/user_view_test.exs | 4 |
7 files changed, 220 insertions, 31 deletions
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 09ea80793..8115fb0fa 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1770,7 +1770,7 @@ defmodule Pleroma.User do def fetch_by_ap_id(ap_id, opts \\ []), do: ActivityPub.make_user_from_ap_id(ap_id, opts) - def get_or_fetch_by_ap_id(ap_id, opts \\ []) do + def get_or_fetch_by_ap_id(ap_id, opts \\ []) when is_binary(ap_id) do cached_user = get_cached_by_ap_id(ap_id) maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id, opts) @@ -1787,6 +1787,13 @@ defmodule Pleroma.User do end end + def get_or_fetch_by_ap_id!(ap_id, opts \\ []) when is_binary(ap_id) do + case get_or_fetch_by_ap_id(ap_id, opts) do + {:ok, user} -> user + _ -> nil + end + end + @doc """ Creates an internal service actor by URI if missing. Optionally takes nickname for addressing. @@ -2097,9 +2104,9 @@ defmodule Pleroma.User do } end - def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user} + def ensure_keys_present(%User{keys: keys} = user) when not is_nil(keys), do: {:ok, user} - def ensure_keys_present(%User{} = user) do + def ensure_keys_present(%User{local: true} = user) do with {:ok, pem} <- Keys.generate_rsa_pem() do user |> cast(%{keys: pem}, [:keys]) @@ -2108,6 +2115,8 @@ defmodule Pleroma.User do end end + def ensure_keys_present(%User{local: false}), do: {:error, :none} + def get_ap_ids_by_nicknames(nicknames) do from(u in User, where: u.nickname in ^nicknames, diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 732c44271..07692221a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -42,7 +42,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do # Note: :following and :followers must be served even without authentication (as via :api) plug( EnsureAuthenticatedPlug - when action in [:read_inbox, :update_outbox, :whoami, :upload_media] + when action in [:read_inbox, :update_outbox, :whoami, :upload_media, :proxy_url] ) plug( @@ -550,4 +550,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> json(object.data) end end + + def proxy_url(%{assigns: %{user: %User{}}} = conn, %{"id" => id}) when is_binary(id) do + cond do + object = Object.normalize(id, true) -> + conn + |> maybe_set_tracking_data(object) + |> set_cache_ttl_for(object) + |> put_resp_content_type("application/activity+json") + |> put_view(ObjectView) + |> render("object.json", object: object) + + object = Activity.get_by_ap_id_with_object(id) -> + conn + |> maybe_set_tracking_data(object) + |> set_cache_ttl_for(object) + |> put_resp_content_type("application/activity+json") + |> put_view(ObjectView) + |> render("object.json", object: object) + + user = User.get_or_fetch_by_ap_id!(id) -> + conn + |> put_resp_content_type("application/activity+json") + |> put_view(UserView) + |> render("user.json", %{user: user}) + + true -> + {:error, :not_found} + end + end end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 3a4564912..715dc4c62 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do use Pleroma.Web, :view alias Pleroma.Keys + alias Pleroma.Maps alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.Transmogrifier @@ -15,21 +16,27 @@ defmodule Pleroma.Web.ActivityPub.UserView do import Ecto.Query - def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do + def render("endpoints.json", %{user: %User{nickname: nil, local: true}}) do %{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)} end - def render("endpoints.json", %{user: %User{local: true} = _user}) do + def render("endpoints.json", %{user: %User{local: true}}) do %{ "oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize), "oauthRegistrationEndpoint" => Helpers.app_url(Endpoint, :create), "oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange), "sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox), - "uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media) + "uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media), + "proxyUrl" => Helpers.activity_pub_url(Endpoint, :proxy_url) } end - def render("endpoints.json", _), do: %{} + def render("endpoints.json", %{user: %User{shared_inbox: shared_inbox}}) + when is_binary(shared_inbox) do + %{"sharedInbox" => shared_inbox} + end + + def render("endpoints.json", _), do: nil def render("service.json", %{user: user}) do {:ok, user} = User.ensure_keys_present(user) @@ -59,6 +66,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do "invisible" => User.invisible?(user) } |> Map.merge(Utils.make_json_ld_header()) + |> Maps.put_if_present("endpoints", endpoints) end # the instance itself is not a Person, but instead an Application @@ -68,11 +76,22 @@ defmodule Pleroma.Web.ActivityPub.UserView do 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, user} = User.ensure_keys_present(user) - {: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]) + def render("user.json", %{user: %User{} = user}) do + public_key = + with {:ok, user} <- User.ensure_keys_present(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]) + + %{ + "id" => "#{user.ap_id}#main-key", + "owner" => user.ap_id, + "publicKeyPem" => public_key + } + else + _ -> nil + end + user = User.sanitize_html(user) endpoints = render("endpoints.json", %{user: user}) @@ -90,24 +109,22 @@ defmodule Pleroma.Web.ActivityPub.UserView do %{} end + # FIXME: user.outbox + inbox = if user.local, do: "#{user.ap_id}/inbox", else: user.inbox + outbox = if user.local, do: "#{user.ap_id}/outbox", else: nil + %{ "id" => user.ap_id, "type" => user.actor_type, - "following" => "#{user.ap_id}/following", - "followers" => "#{user.ap_id}/followers", - "inbox" => "#{user.ap_id}/inbox", - "outbox" => "#{user.ap_id}/outbox", + "following" => User.ap_following(user), + "followers" => User.ap_followers(user), + "inbox" => inbox, + "outbox" => outbox, "preferredUsername" => user.nickname, "name" => user.name, "summary" => user.bio, "url" => user.ap_id, "manuallyApprovesFollowers" => user.locked, - "publicKey" => %{ - "id" => "#{user.ap_id}#main-key", - "owner" => user.ap_id, - "publicKeyPem" => public_key - }, - "endpoints" => endpoints, "attachment" => fields, "tag" => emoji_tags, "discoverable" => user.discoverable, @@ -116,6 +133,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) |> Map.merge(Utils.make_json_ld_header()) + |> Maps.put_if_present("endpoints", endpoints) + |> Maps.put_if_present("publicKey", public_key) end def render("following.json", %{user: user, page: page} = opts) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e22b31b4c..2a435c38c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -625,6 +625,7 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/outbox", ActivityPubController, :outbox) post("/users/:nickname/outbox", ActivityPubController, :update_outbox) post("/api/ap/upload_media", ActivityPubController, :upload_media) + post("/api/ap/proxy_url", ActivityPubController, :proxy_url) # The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`: get("/users/:nickname/followers", ActivityPubController, :followers) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index cb022333f..ba88aff90 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -534,12 +534,15 @@ defmodule HttpRequestMock do }} end - def get( - "http://mastodon.example.org/@admin/99541947525187367", - _, - _, - _ - ) do + def get("http://mastodon.example.org/users/admin/statuses/99541947525187367", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/mastodon-note-object.json") + }} + end + + def get("http://mastodon.example.org/@admin/99541947525187367", _, _, _) do {:ok, %Tesla.Env{ status: 200, diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 0517571f2..bd242dd53 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -1546,5 +1546,133 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) |> json_response(403) end + + test "POST /api/ap/proxy_url, get actor", %{conn: conn} do + user = insert(:user) + + expected = %{ + "@context" => [ + "https://www.w3.org/ns/activitystreams", + "http://localhost:4001/schemas/litepub-0.1.jsonld", + %{"@language" => "und"} + ], + "attachment" => [ + %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}, + %{"name" => "foo1", "type" => "PropertyValue", "value" => "bar1"} + ], + "capabilities" => %{"acceptsChatMessages" => true}, + "discoverable" => true, + "endpoints" => %{"sharedInbox" => "http://mastodon.example.org/inbox"}, + "followers" => "http://mastodon.example.org/users/admin/followers", + "following" => "http://mastodon.example.org/users/admin/following", + "icon" => %{ + "type" => "Image", + "url" => + "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" + }, + "id" => "http://mastodon.example.org/users/admin", + "image" => %{ + "type" => "Image", + "url" => + "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" + }, + "inbox" => "http://mastodon.example.org/users/admin/inbox", + "manuallyApprovesFollowers" => false, + "name" => "admin@mastodon.example.org", + "outbox" => nil, + "preferredUsername" => "admin@mastodon.example.org", + "summary" => "<p></p>", + "tag" => [], + "type" => "Person", + "url" => "http://mastodon.example.org/users/admin" + } + + assert conn + |> assign(:user, user) + |> post("/api/ap/proxy_url", %{"id" => "http://mastodon.example.org/users/admin"}) + |> json_response(200) == expected + + assert conn + |> post("/api/ap/proxy_url", %{"id" => "http://mastodon.example.org/users/admin"}) + |> json_response(403) == %{"error" => "Invalid credentials."} + end + + test "POST /api/ap/proxy_url, get object", %{conn: conn} do + user = insert(:user) + + expected = %{ + "@context" => [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + %{ + "Emoji" => "toot:Emoji", + "Hashtag" => "as:Hashtag", + "atomUri" => "ostatus:atomUri", + "conversation" => "ostatus:conversation", + "inReplyToAtomUri" => "ostatus:inReplyToAtomUri", + "manuallyApprovesFollowers" => "as:manuallyApprovesFollowers", + "movedTo" => "as:movedTo", + "ostatus" => "http://ostatus.org#", + "sensitive" => "as:sensitive", + "toot" => "http://joinmastodon.org/ns#" + } + ], + "actor" => "http://mastodon.example.org/users/admin", + "atomUri" => "http://mastodon.example.org/users/admin/statuses/99541947525187367", + "attachment" => [ + %{ + "mediaType" => "image/jpeg", + "name" => nil, + "type" => "Document", + "url" => + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg" + } + ], + "attributedTo" => "http://mastodon.example.org/users/admin", + "bcc" => [], + "bto" => [], + "cc" => ["http://mastodon.example.org/users/admin/followers"], + "content" => "<p>yeah.</p>", + "context" => "tag:mastodon.example.org,2018-02-17:objectId=59:objectType=Conversation", + "conversation" => + "tag:mastodon.example.org,2018-02-17:objectId=59:objectType=Conversation", + "id" => "http://mastodon.example.org/users/admin/statuses/99541947525187367", + "inReplyTo" => nil, + "inReplyToAtomUri" => nil, + "published" => "2018-02-17T17:46:20Z", + "sensitive" => false, + "summary" => "", + "tag" => [], + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Note", + "url" => "http://mastodon.example.org/@admin/99541947525187367" + } + + assert conn + |> assign(:user, user) + |> post("/api/ap/proxy_url", %{ + "id" => "http://mastodon.example.org/users/admin/statuses/99541947525187367" + }) + |> json_response(200) == expected + + assert conn + |> post("/api/ap/proxy_url", %{ + "id" => "http://mastodon.example.org/users/admin/statuses/99541947525187367" + }) + |> json_response(403) == %{"error" => "Invalid credentials."} + + assert conn + |> assign(:user, user) + |> post("/api/ap/proxy_url", %{ + "id" => "http://mastodon.example.org/@admin/99541947525187367" + }) + |> json_response(200) == expected + + assert conn + |> post("/api/ap/proxy_url", %{ + "id" => "http://mastodon.example.org/@admin/99541947525187367" + }) + |> json_response(403) == %{"error" => "Invalid credentials."} + end end end diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index 98c7c9d09..b3af3d3f6 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -99,12 +99,12 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do test "remote users have an empty endpoints structure" do user = insert(:user, local: false) - {:ok, user} = User.ensure_keys_present(user) + {:error, :none} = User.ensure_keys_present(user) result = UserView.render("user.json", %{user: user}) assert result["id"] == user.ap_id - assert result["endpoints"] == %{} + assert result["endpoints"] == nil end test "instance users do not expose oAuth endpoints" do |