1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Object.Fetcher do
6 @behaviour HTTPSignatures.Adapter
8 alias Pleroma.Instances
12 alias Pleroma.Object.Containment
13 alias Pleroma.Signature
14 alias Pleroma.Web.ActivityPub.InternalFetchActor
15 alias Pleroma.Web.ActivityPub.MRF
16 alias Pleroma.Web.ActivityPub.ObjectValidator
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.ActivityPub.Transmogrifier
19 alias Pleroma.Web.Federator
22 require Pleroma.Constants
24 @spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
25 defp reinject_object(%Object{data: %{}} = object, new_data) do
26 Logger.debug("Reinjecting object #{new_data["id"]}")
28 with {:ok, new_data, _} <- ObjectValidator.validate(new_data, %{}),
29 {:ok, new_data} <- MRF.filter(new_data),
30 {:ok, new_object, _} <-
31 Object.Updater.do_update_and_invalidate_cache(
34 _touch_changeset? = true
39 Logger.error("Error while processing object: #{inspect(e)}")
44 defp reinject_object(_, new_data) do
45 with {:ok, object, _} <- Pipeline.common_pipeline(new_data, local: false) do
52 def refetch_object(%Object{data: %{"id" => id}} = object) do
53 with {:local, false} <- {:local, Object.local?(object)},
54 {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
55 {:ok, object} <- reinject_object(object, new_data) do
58 {:local, true} -> {:ok, object}
63 # Note: will create a Create activity, which we need internally at the moment.
64 def fetch_object_from_id(id, options \\ []) do
65 with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
66 {_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
67 {_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
68 {_, nil} <- {:normalize, Object.normalize(data, fetch: false)},
69 params <- prepare_activity_params(data),
70 {_, :ok} <- {:containment, Containment.contain_origin(id, params)},
71 {_, {:ok, activity}} <-
72 {:transmogrifier, Transmogrifier.handle_incoming(params, options)},
73 {_, _data, %Object{} = object} <-
74 {:object, data, Object.normalize(activity, fetch: false)} do
77 {:allowed_depth, false} = e ->
78 log_fetch_error(id, e)
79 {:error, :allowed_depth}
81 {:containment, reason} = e ->
82 log_fetch_error(id, e)
85 {:transmogrifier, {:error, {:reject, reason}}} = e ->
86 log_fetch_error(id, e)
89 {:transmogrifier, {:reject, reason}} = e ->
90 log_fetch_error(id, e)
93 {:transmogrifier, reason} = e ->
94 log_fetch_error(id, e)
97 {:object, data, nil} ->
98 reinject_object(%Object{}, data)
100 {:normalize, object = %Object{}} ->
103 {:fetch_object, %Object{} = object} ->
106 {:fetch, {:error, reason}} = e ->
107 log_fetch_error(id, e)
111 log_fetch_error(id, e)
116 defp log_fetch_error(id, error) do
117 Logger.metadata(object: id)
118 Logger.error("Object rejected while fetching #{id} #{inspect(error)}")
121 defp prepare_activity_params(data) do
124 # Should we seriously keep this attributedTo thing?
125 "actor" => data["actor"] || data["attributedTo"],
128 |> Maps.put_if_present("to", data["to"])
129 |> Maps.put_if_present("cc", data["cc"])
130 |> Maps.put_if_present("bto", data["bto"])
131 |> Maps.put_if_present("bcc", data["bcc"])
134 defp make_signature(id, date) do
137 spoofed_pem = Pleroma.Config.get([:activitypub, :spoofed_key])
138 # workaround for syntax shite disallowing me from defining signature in "if" block
139 spoofed_key = if Pleroma.Config.get([:activitypub, :spoof_object_fetch_signatures]) do
140 with {:ok, private_key, _} <- Keys.keys_from_pem(spoofed_pem) do
146 spoofed_instance = Pleroma.Config.get([:activitypub, :spoofed_instance])
148 signature = if Pleroma.Config.get([:activitypub, :spoof_object_fetch_signatures]) do
149 HTTPSignatures.sign(spoofed_key, spoofed_instance <> "/internal/fetch#main-key", %{
150 "(request-target)": "get #{uri.path}",
155 InternalFetchActor.get_actor()
157 "(request-target)": "get #{uri.path}",
163 {"signature", signature}
166 defp sign_fetch(headers, id, date) do
167 if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
168 [make_signature(id, date) | headers]
174 defp maybe_date_fetch(headers, date) do
175 if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
176 [{"date", date} | headers]
182 def fetch_and_contain_remote_object_from_id(id)
184 def fetch_and_contain_remote_object_from_id(%{"id" => id}),
185 do: fetch_and_contain_remote_object_from_id(id)
187 def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
188 Logger.debug("Fetching object #{id} via AP")
190 with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
191 {:ok, body} <- get_object(id),
192 {:ok, data} <- safe_json_decode(body),
193 :ok <- Containment.contain_origin_from_id(id, data) do
194 if not Instances.reachable?(id) do
195 Instances.set_reachable(id)
201 {:error, "Unsupported URI scheme"}
211 def fetch_and_contain_remote_object_from_id(_id),
212 do: {:error, "id must be a string"}
214 defp get_object(id) do
215 date = Pleroma.Signature.signed_date()
218 [{"accept", "application/activity+json"}]
219 |> maybe_date_fetch(date)
220 |> sign_fetch(id, date)
222 case HTTP.get(id, headers) do
223 {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
224 case List.keyfind(headers, "content-type", 0) do
226 case Plug.Conn.Utils.media_type(content_type) do
227 {:ok, "application", "activity+json", _} ->
230 {:ok, "application", "ld+json",
231 %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
235 {:error, {:content_type, content_type}}
239 {:error, {:content_type, nil}}
242 {:ok, %{status: code}} when code in [401, 403] ->
245 {:ok, %{status: code}} when code in [404, 410] ->
256 defp safe_json_decode(nil), do: {:ok, nil}
257 defp safe_json_decode(json), do: Jason.decode(json)