HTTP signatures respect allowlist federation
[akkoma] / lib / pleroma / web / activity_pub / publisher.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.Publisher do
6 alias Pleroma.Activity
7 alias Pleroma.Config
8 alias Pleroma.Delivery
9 alias Pleroma.HTTP
10 alias Pleroma.Instances
11 alias Pleroma.Object
12 alias Pleroma.Repo
13 alias Pleroma.User
14 alias Pleroma.Web.ActivityPub.Relay
15 alias Pleroma.Web.ActivityPub.Transmogrifier
16
17 require Pleroma.Constants
18
19 import Pleroma.Web.ActivityPub.Visibility
20
21 @behaviour Pleroma.Web.Federator.Publisher
22
23 require Logger
24
25 @moduledoc """
26 ActivityPub outgoing federation module.
27 """
28
29 @doc """
30 Determine if an activity can be represented by running it through Transmogrifier.
31 """
32 def is_representable?(%Activity{} = activity) do
33 with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
34 true
35 else
36 _e ->
37 false
38 end
39 end
40
41 @doc """
42 Publish a single message to a peer. Takes a struct with the following
43 parameters set:
44
45 * `inbox`: the inbox to publish to
46 * `json`: the JSON message body representing the ActivityPub message
47 * `actor`: the actor which is signing the message
48 * `id`: the ActivityStreams URI of the message
49 """
50 def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
51 Logger.debug("Federating #{id} to #{inbox}")
52 uri = %{path: path} = URI.parse(inbox)
53 digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
54
55 date = Pleroma.Signature.signed_date()
56
57 signature =
58 Pleroma.Signature.sign(actor, %{
59 "(request-target)": "post #{path}",
60 host: signature_host(uri),
61 "content-length": byte_size(json),
62 digest: digest,
63 date: date
64 })
65
66 with {:ok, %{status: code}} = result when code in 200..299 <-
67 HTTP.post(
68 inbox,
69 json,
70 [
71 {"Content-Type", "application/activity+json"},
72 {"Date", date},
73 {"signature", signature},
74 {"digest", digest}
75 ]
76 ) do
77 if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
78 Instances.set_reachable(inbox)
79 end
80
81 result
82 else
83 {_post_result, response} ->
84 unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
85 {:error, response}
86 end
87 end
88
89 def publish_one(%{actor_id: actor_id} = params) do
90 actor = User.get_cached_by_id(actor_id)
91
92 params
93 |> Map.delete(:actor_id)
94 |> Map.put(:actor, actor)
95 |> publish_one()
96 end
97
98 defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
99 if port == URI.default_port(scheme) do
100 host
101 else
102 "#{host}:#{port}"
103 end
104 end
105
106 defp blocked_instances do
107 Config.get([:instance, :quarantined_instances], []) ++
108 Config.get([:mrf_simple, :reject], [])
109 end
110
111 defp allowed_instances do
112 Config.get([:mrf_simple, :accept])
113 end
114
115 def should_federate?(url) do
116 %{host: host} = URI.parse(url)
117
118 with allowed <- allowed_instances(),
119 false <- Enum.empty?(allowed) do
120 allowed
121 |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
122 |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
123 |> Pleroma.Web.ActivityPub.MRF.subdomain_match?(host)
124 else
125 _ ->
126 quarantined_instances =
127 blocked_instances()
128 |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
129 |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
130
131 not Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
132 end
133 end
134
135 @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
136 defp recipients(actor, activity) do
137 followers =
138 if actor.follower_address in activity.recipients do
139 User.get_external_followers(actor)
140 else
141 []
142 end
143
144 fetchers =
145 with %Activity{data: %{"type" => "Delete"}} <- activity,
146 %Object{id: object_id} <- Object.normalize(activity, fetch: false),
147 fetchers <- User.get_delivered_users_by_object_id(object_id),
148 _ <- Delivery.delete_all_by_object_id(object_id) do
149 fetchers
150 else
151 _ ->
152 []
153 end
154
155 Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
156 end
157
158 defp get_cc_ap_ids(ap_id, recipients) do
159 host = Map.get(URI.parse(ap_id), :host)
160
161 recipients
162 |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
163 |> Enum.map(& &1.ap_id)
164 end
165
166 defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
167 defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
168
169 @doc """
170 Determine a user inbox to use based on heuristics. These heuristics
171 are based on an approximation of the ``sharedInbox`` rules in the
172 [ActivityPub specification][ap-sharedinbox].
173
174 Please do not edit this function (or its children) without reading
175 the spec, as editing the code is likely to introduce some breakage
176 without some familiarity.
177
178 [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
179 """
180 def determine_inbox(
181 %Activity{data: activity_data},
182 %User{inbox: inbox} = user
183 ) do
184 to = activity_data["to"] || []
185 cc = activity_data["cc"] || []
186 type = activity_data["type"]
187
188 cond do
189 type == "Delete" ->
190 maybe_use_sharedinbox(user)
191
192 Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
193 maybe_use_sharedinbox(user)
194
195 length(to) + length(cc) > 1 ->
196 maybe_use_sharedinbox(user)
197
198 true ->
199 inbox
200 end
201 end
202
203 @doc """
204 Publishes an activity with BCC to all relevant peers.
205 """
206
207 def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
208 when is_list(bcc) and bcc != [] do
209 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
210
211 recipients = recipients(actor, activity)
212
213 inboxes =
214 recipients
215 |> Enum.filter(&User.ap_enabled?/1)
216 |> Enum.map(fn actor -> actor.inbox end)
217 |> Enum.filter(fn inbox -> should_federate?(inbox) end)
218 |> Instances.filter_reachable()
219
220 Repo.checkout(fn ->
221 Enum.each(inboxes, fn {inbox, unreachable_since} ->
222 %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
223
224 # Get all the recipients on the same host and add them to cc. Otherwise, a remote
225 # instance would only accept a first message for the first recipient and ignore the rest.
226 cc = get_cc_ap_ids(ap_id, recipients)
227
228 json =
229 data
230 |> Map.put("cc", cc)
231 |> Jason.encode!()
232
233 Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
234 inbox: inbox,
235 json: json,
236 actor_id: actor.id,
237 id: activity.data["id"],
238 unreachable_since: unreachable_since
239 })
240 end)
241 end)
242 end
243
244 # Publishes an activity to all relevant peers.
245 def publish(%User{} = actor, %Activity{} = activity) do
246 public = is_public?(activity)
247
248 if public && Config.get([:instance, :allow_relay]) do
249 Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
250 Relay.publish(activity)
251 end
252
253 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
254 json = Jason.encode!(data)
255
256 recipients(actor, activity)
257 |> Enum.filter(fn user -> User.ap_enabled?(user) end)
258 |> Enum.map(fn %User{} = user ->
259 determine_inbox(activity, user)
260 end)
261 |> Enum.uniq()
262 |> Enum.filter(fn inbox -> should_federate?(inbox) end)
263 |> Instances.filter_reachable()
264 |> Enum.each(fn {inbox, unreachable_since} ->
265 Pleroma.Web.Federator.Publisher.enqueue_one(
266 __MODULE__,
267 %{
268 inbox: inbox,
269 json: json,
270 actor_id: actor.id,
271 id: activity.data["id"],
272 unreachable_since: unreachable_since
273 }
274 )
275 end)
276 end
277
278 def gather_webfinger_links(%User{} = user) do
279 [
280 %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
281 %{
282 "rel" => "self",
283 "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
284 "href" => user.ap_id
285 },
286 %{
287 "rel" => "http://ostatus.org/schema/1.0/subscribe",
288 "template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}"
289 }
290 ]
291 end
292
293 def gather_nodeinfo_protocol_names, do: ["activitypub"]
294 end