AP C2S: Implement proxyUrl endpoint

This commit is contained in:
Haelwenn (lanodan) Monnier 2020-10-06 17:17:56 +02:00
parent 4d852f3e78
commit e2bad1efc4
No known key found for this signature in database
GPG Key ID: D5B7A8E43C997DEE
7 changed files with 220 additions and 31 deletions

View File

@ -1770,7 +1770,7 @@ def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
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 @@ def get_or_fetch_by_ap_id(ap_id, opts \\ []) 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 @@ def get_mascot(%{mascot: mascot}) when is_nil(mascot) 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 @@ def ensure_keys_present(%User{} = 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,

View File

@ -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 @@ def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} =
|> 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

View File

@ -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 @@ def render("service.json", %{user: user}) 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 @@ def render("user.json", %{user: %User{nickname: nil} = user}),
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 @@ def render("user.json", %{user: user}) 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 @@ def render("user.json", %{user: user}) 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

View File

@ -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)

View File

@ -534,12 +534,15 @@ def get("https://social.stopwatchingus-heidelberg.de/.well-known/host-meta", _,
}}
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,

View File

@ -1546,5 +1546,133 @@ test "POST /api/ap/upload_media", %{conn: conn} 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

View File

@ -99,12 +99,12 @@ test "local users have a usable endpoints structure" 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