cherry-pick security from upstream
[akkoma] / lib / pleroma / web / activity_pub / activity_pub_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.ActivityPubController do
6 use Pleroma.Web, :controller
7
8 alias Pleroma.Activity
9 alias Pleroma.Delivery
10 alias Pleroma.Object
11 alias Pleroma.Object.Fetcher
12 alias Pleroma.User
13 alias Pleroma.Web.ActivityPub.ActivityPub
14 alias Pleroma.Web.ActivityPub.InternalFetchActor
15 alias Pleroma.Web.ActivityPub.ObjectView
16 alias Pleroma.Web.ActivityPub.Pipeline
17 alias Pleroma.Web.ActivityPub.Relay
18 alias Pleroma.Web.ActivityPub.Transmogrifier
19 alias Pleroma.Web.ActivityPub.UserView
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.ControllerHelper
23 alias Pleroma.Web.Endpoint
24 alias Pleroma.Web.Federator
25 alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
26 alias Pleroma.Web.Plugs.FederatingPlug
27
28 require Logger
29
30 action_fallback(:errors)
31
32 @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
33
34 plug(FederatingPlug when action in @federating_only_actions)
35
36 plug(
37 EnsureAuthenticatedPlug,
38 [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
39 )
40
41 # Note: :following and :followers must be served even without authentication (as via :api)
42 plug(
43 EnsureAuthenticatedPlug
44 when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
45 )
46
47 plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
48
49 plug(
50 Pleroma.Web.Plugs.Cache,
51 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
52 when action in [:activity, :object]
53 )
54
55 plug(:set_requester_reachable when action in [:inbox])
56 plug(:relay_active? when action in [:relay])
57
58 defp relay_active?(conn, _) do
59 if Pleroma.Config.get([:instance, :allow_relay]) do
60 conn
61 else
62 conn
63 |> render_error(:not_found, "not found")
64 |> halt()
65 end
66 end
67
68 def user(conn, %{"nickname" => nickname}) do
69 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
70 {:ok, user} <- User.ensure_keys_present(user) do
71 conn
72 |> put_resp_content_type("application/activity+json")
73 |> put_view(UserView)
74 |> render("user.json", %{user: user})
75 else
76 nil -> {:error, :not_found}
77 %{local: false} -> {:error, :not_found}
78 end
79 end
80
81 def object(%{assigns: assigns} = conn, _) do
82 with ap_id <- Endpoint.url() <> conn.request_path,
83 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
84 user <- Map.get(assigns, :user, nil),
85 {_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
86 conn
87 |> maybe_skip_cache(user)
88 |> assign(:tracking_fun_data, object.id)
89 |> set_cache_ttl_for(object)
90 |> put_resp_content_type("application/activity+json")
91 |> put_view(ObjectView)
92 |> render("object.json", object: object)
93 else
94 {:visible?, false} -> {:error, :not_found}
95 nil -> {:error, :not_found}
96 end
97 end
98
99 def track_object_fetch(conn, nil), do: conn
100
101 def track_object_fetch(conn, object_id) do
102 with %{assigns: %{user: %User{id: user_id}}} <- conn do
103 Delivery.create(object_id, user_id)
104 end
105
106 conn
107 end
108
109 def activity(%{assigns: assigns} = conn, _) do
110 with ap_id <- Endpoint.url() <> conn.request_path,
111 %Activity{} = activity <- Activity.normalize(ap_id),
112 {_, true} <- {:local?, activity.local},
113 user <- Map.get(assigns, :user, nil),
114 {_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
115 conn
116 |> maybe_skip_cache(user)
117 |> maybe_set_tracking_data(activity)
118 |> set_cache_ttl_for(activity)
119 |> put_resp_content_type("application/activity+json")
120 |> put_view(ObjectView)
121 |> render("object.json", object: activity)
122 else
123 {:visible?, false} -> {:error, :not_found}
124 {:local?, false} -> {:error, :not_found}
125 nil -> {:error, :not_found}
126 end
127 end
128
129 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
130 object_id = Object.normalize(activity, fetch: false).id
131 assign(conn, :tracking_fun_data, object_id)
132 end
133
134 defp maybe_set_tracking_data(conn, _activity), do: conn
135
136 defp set_cache_ttl_for(conn, %Activity{object: object}) do
137 set_cache_ttl_for(conn, object)
138 end
139
140 defp set_cache_ttl_for(conn, entity) do
141 ttl =
142 case entity do
143 %Object{data: %{"type" => "Question"}} ->
144 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
145
146 %Object{} ->
147 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
148
149 _ ->
150 nil
151 end
152
153 assign(conn, :cache_ttl, ttl)
154 end
155
156 def maybe_skip_cache(conn, user) do
157 if user do
158 conn
159 |> assign(:skip_cache, true)
160 else
161 conn
162 end
163 end
164
165 # GET /relay/following
166 def relay_following(conn, _params) do
167 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
168 conn
169 |> put_resp_content_type("application/activity+json")
170 |> put_view(UserView)
171 |> render("following.json", %{user: Relay.get_actor()})
172 end
173 end
174
175 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
176 with %User{} = user <- User.get_cached_by_nickname(nickname),
177 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
178 {:show_follows, true} <-
179 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
180 {page, _} = Integer.parse(page)
181
182 conn
183 |> put_resp_content_type("application/activity+json")
184 |> put_view(UserView)
185 |> render("following.json", %{user: user, page: page, for: for_user})
186 else
187 {:show_follows, _} ->
188 conn
189 |> put_resp_content_type("application/activity+json")
190 |> send_resp(403, "")
191 end
192 end
193
194 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
195 with %User{} = user <- User.get_cached_by_nickname(nickname),
196 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
197 conn
198 |> put_resp_content_type("application/activity+json")
199 |> put_view(UserView)
200 |> render("following.json", %{user: user, for: for_user})
201 end
202 end
203
204 # GET /relay/followers
205 def relay_followers(conn, _params) do
206 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
207 conn
208 |> put_resp_content_type("application/activity+json")
209 |> put_view(UserView)
210 |> render("followers.json", %{user: Relay.get_actor()})
211 end
212 end
213
214 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
215 with %User{} = user <- User.get_cached_by_nickname(nickname),
216 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
217 {:show_followers, true} <-
218 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
219 {page, _} = Integer.parse(page)
220
221 conn
222 |> put_resp_content_type("application/activity+json")
223 |> put_view(UserView)
224 |> render("followers.json", %{user: user, page: page, for: for_user})
225 else
226 {:show_followers, _} ->
227 conn
228 |> put_resp_content_type("application/activity+json")
229 |> send_resp(403, "")
230 end
231 end
232
233 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
234 with %User{} = user <- User.get_cached_by_nickname(nickname),
235 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
236 conn
237 |> put_resp_content_type("application/activity+json")
238 |> put_view(UserView)
239 |> render("followers.json", %{user: user, for: for_user})
240 end
241 end
242
243 def outbox(
244 %{assigns: %{user: for_user}} = conn,
245 %{"nickname" => nickname, "page" => page?} = params
246 )
247 when page? in [true, "true"] do
248 with %User{} = user <- User.get_cached_by_nickname(nickname),
249 {:ok, user} <- User.ensure_keys_present(user) do
250 # "include_poll_votes" is a hack because postgres generates inefficient
251 # queries when filtering by 'Answer', poll votes will be hidden by the
252 # visibility filter in this case anyway
253 params =
254 params
255 |> Map.drop(["nickname", "page"])
256 |> Map.put("include_poll_votes", true)
257 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
258
259 activities = ActivityPub.fetch_user_activities(user, for_user, params)
260
261 conn
262 |> put_resp_content_type("application/activity+json")
263 |> put_view(UserView)
264 |> render("activity_collection_page.json", %{
265 activities: activities,
266 pagination: ControllerHelper.get_pagination_fields(conn, activities),
267 iri: "#{user.ap_id}/outbox"
268 })
269 end
270 end
271
272 def outbox(conn, %{"nickname" => nickname}) do
273 with %User{} = user <- User.get_cached_by_nickname(nickname),
274 {:ok, user} <- User.ensure_keys_present(user) do
275 conn
276 |> put_resp_content_type("application/activity+json")
277 |> put_view(UserView)
278 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
279 end
280 end
281
282 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
283 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
284 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
285 true <- Utils.recipient_in_message(recipient, actor, params),
286 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
287 Federator.incoming_ap_doc(params)
288 json(conn, "ok")
289 end
290 end
291
292 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
293 Federator.incoming_ap_doc(params)
294 json(conn, "ok")
295 end
296
297 def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
298 conn
299 |> put_status(:bad_request)
300 |> json("Invalid HTTP Signature")
301 end
302
303 # POST /relay/inbox -or- POST /internal/fetch/inbox
304 def inbox(conn, %{"type" => "Create"} = params) do
305 if FederatingPlug.federating?() do
306 post_inbox_relayed_create(conn, params)
307 else
308 conn
309 |> put_status(:bad_request)
310 |> json("Not federating")
311 end
312 end
313
314 def inbox(conn, _params) do
315 conn
316 |> put_status(:bad_request)
317 |> json("error, missing HTTP Signature")
318 end
319
320 defp post_inbox_relayed_create(conn, params) do
321 Logger.debug(
322 "Signature missing or not from author, relayed Create message, fetching object from source"
323 )
324
325 Fetcher.fetch_object_from_id(params["object"]["id"])
326
327 json(conn, "ok")
328 end
329
330 defp represent_service_actor(%User{} = user, conn) do
331 with {:ok, user} <- User.ensure_keys_present(user) do
332 conn
333 |> put_resp_content_type("application/activity+json")
334 |> put_view(UserView)
335 |> render("user.json", %{user: user})
336 else
337 nil -> {:error, :not_found}
338 end
339 end
340
341 defp represent_service_actor(nil, _), do: {:error, :not_found}
342
343 def relay(conn, _params) do
344 Relay.get_actor()
345 |> represent_service_actor(conn)
346 end
347
348 def internal_fetch(conn, _params) do
349 InternalFetchActor.get_actor()
350 |> represent_service_actor(conn)
351 end
352
353 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
354 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
355 conn
356 |> put_resp_content_type("application/activity+json")
357 |> put_view(UserView)
358 |> render("user.json", %{user: user})
359 end
360
361 def read_inbox(
362 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
363 %{"nickname" => nickname, "page" => page?} = params
364 )
365 when page? in [true, "true"] do
366 params =
367 params
368 |> Map.drop(["nickname", "page"])
369 |> Map.put("blocking_user", user)
370 |> Map.put("user", user)
371 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
372
373 activities =
374 [user.ap_id | User.following(user)]
375 |> ActivityPub.fetch_activities(params)
376 |> Enum.reverse()
377
378 conn
379 |> put_resp_content_type("application/activity+json")
380 |> put_view(UserView)
381 |> render("activity_collection_page.json", %{
382 activities: activities,
383 pagination: ControllerHelper.get_pagination_fields(conn, activities),
384 iri: "#{user.ap_id}/inbox"
385 })
386 end
387
388 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
389 "nickname" => nickname
390 }) do
391 with {:ok, user} <- User.ensure_keys_present(user) do
392 conn
393 |> put_resp_content_type("application/activity+json")
394 |> put_view(UserView)
395 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
396 end
397 end
398
399 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
400 "nickname" => nickname
401 }) do
402 err =
403 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
404 nickname: nickname,
405 as_nickname: as_nickname
406 )
407
408 conn
409 |> put_status(:forbidden)
410 |> json(err)
411 end
412
413 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
414 when is_map(object) do
415 length =
416 [object["content"], object["summary"], object["name"]]
417 |> Enum.filter(&is_binary(&1))
418 |> Enum.join("")
419 |> String.length()
420
421 limit = Pleroma.Config.get([:instance, :limit])
422
423 if length < limit do
424 object =
425 object
426 |> Transmogrifier.strip_internal_fields()
427 |> Map.put("attributedTo", actor)
428 |> Map.put("actor", actor)
429 |> Map.put("id", Utils.generate_object_id())
430
431 {:ok, Map.put(activity, "object", object)}
432 else
433 {:error,
434 dgettext(
435 "errors",
436 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
437 limit: limit,
438 length: length
439 )}
440 end
441 end
442
443 defp fix_user_message(
444 %User{ap_id: actor} = user,
445 %{"type" => "Delete", "object" => object} = activity
446 ) do
447 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
448 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
449 {:ok, activity}
450 else
451 {:normalize, _} ->
452 {:error, "No such object found"}
453
454 {:permission, _} ->
455 {:forbidden, "You can't delete this object"}
456 end
457 end
458
459 defp fix_user_message(%User{}, activity) do
460 {:ok, activity}
461 end
462
463 def update_outbox(
464 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
465 %{"nickname" => nickname} = params
466 ) do
467 params =
468 params
469 |> Map.drop(["nickname"])
470 |> Map.put("id", Utils.generate_activity_id())
471 |> Map.put("actor", actor)
472
473 with {:ok, params} <- fix_user_message(user, params),
474 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
475 %Activity{data: activity_data} <- Activity.normalize(activity) do
476 conn
477 |> put_status(:created)
478 |> put_resp_header("location", activity_data["id"])
479 |> json(activity_data)
480 else
481 {:forbidden, message} ->
482 conn
483 |> put_status(:forbidden)
484 |> json(message)
485
486 {:error, message} ->
487 conn
488 |> put_status(:bad_request)
489 |> json(message)
490
491 e ->
492 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
493
494 conn
495 |> put_status(:bad_request)
496 |> json("Bad Request")
497 end
498 end
499
500 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
501 err =
502 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
503 nickname: nickname,
504 as_nickname: user.nickname
505 )
506
507 conn
508 |> put_status(:forbidden)
509 |> json(err)
510 end
511
512 defp errors(conn, {:error, :not_found}) do
513 conn
514 |> put_status(:not_found)
515 |> json(dgettext("errors", "Not found"))
516 end
517
518 defp errors(conn, _e) do
519 conn
520 |> put_status(:internal_server_error)
521 |> json(dgettext("errors", "error"))
522 end
523
524 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
525 with actor <- conn.params["actor"],
526 true <- is_binary(actor) do
527 Pleroma.Instances.set_reachable(actor)
528 end
529
530 conn
531 end
532
533 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
534 {:ok, new_user} = User.ensure_keys_present(user)
535
536 for_user =
537 if new_user != user and match?(%User{}, for_user) do
538 User.get_cached_by_nickname(for_user.nickname)
539 else
540 for_user
541 end
542
543 {new_user, for_user}
544 end
545
546 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
547 with {:ok, object} <-
548 ActivityPub.upload(
549 file,
550 actor: User.ap_id(user),
551 description: Map.get(data, "description")
552 ) do
553 Logger.debug(inspect(object))
554
555 conn
556 |> put_status(:created)
557 |> json(object.data)
558 end
559 end
560
561 def pinned(conn, %{"nickname" => nickname}) do
562 with %User{} = user <- User.get_cached_by_nickname(nickname) do
563 conn
564 |> put_resp_header("content-type", "application/activity+json")
565 |> json(UserView.render("featured.json", %{user: user}))
566 end
567 end
568 end