Merge branch 'feature/optimize_rich_media_parser' into 'develop'
[akkoma] / lib / pleroma / web / salmon / salmon.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.Salmon do
6 @behaviour Pleroma.Web.Federator.Publisher
7
8 use Bitwise
9
10 alias Pleroma.Activity
11 alias Pleroma.HTTP
12 alias Pleroma.Instances
13 alias Pleroma.Keys
14 alias Pleroma.User
15 alias Pleroma.Web.ActivityPub.Visibility
16 alias Pleroma.Web.Federator.Publisher
17 alias Pleroma.Web.OStatus
18 alias Pleroma.Web.OStatus.ActivityRepresenter
19 alias Pleroma.Web.XML
20
21 require Logger
22
23 def decode(salmon) do
24 doc = XML.parse_document(salmon)
25
26 {:xmlObj, :string, data} = :xmerl_xpath.string('string(//me:data[1])', doc)
27 {:xmlObj, :string, sig} = :xmerl_xpath.string('string(//me:sig[1])', doc)
28 {:xmlObj, :string, alg} = :xmerl_xpath.string('string(//me:alg[1])', doc)
29 {:xmlObj, :string, encoding} = :xmerl_xpath.string('string(//me:encoding[1])', doc)
30 {:xmlObj, :string, type} = :xmerl_xpath.string('string(//me:data[1]/@type)', doc)
31
32 {:ok, data} = Base.url_decode64(to_string(data), ignore: :whitespace)
33 {:ok, sig} = Base.url_decode64(to_string(sig), ignore: :whitespace)
34 alg = to_string(alg)
35 encoding = to_string(encoding)
36 type = to_string(type)
37
38 [data, type, encoding, alg, sig]
39 end
40
41 def fetch_magic_key(salmon) do
42 with [data, _, _, _, _] <- decode(salmon),
43 doc <- XML.parse_document(data),
44 uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
45 {:ok, public_key} <- User.get_public_key_for_ap_id(uri),
46 magic_key <- encode_key(public_key) do
47 {:ok, magic_key}
48 end
49 end
50
51 def decode_and_validate(magickey, salmon) do
52 [data, type, encoding, alg, sig] = decode(salmon)
53
54 signed_text =
55 [data, type, encoding, alg]
56 |> Enum.map(&Base.url_encode64/1)
57 |> Enum.join(".")
58
59 key = decode_key(magickey)
60
61 verify = :public_key.verify(signed_text, :sha256, sig, key)
62
63 if verify do
64 {:ok, data}
65 else
66 :error
67 end
68 end
69
70 def decode_key("RSA." <> magickey) do
71 make_integer = fn bin ->
72 list = :erlang.binary_to_list(bin)
73 Enum.reduce(list, 0, fn el, acc -> acc <<< 8 ||| el end)
74 end
75
76 [modulus, exponent] =
77 magickey
78 |> String.split(".")
79 |> Enum.map(fn n -> Base.url_decode64!(n, padding: false) end)
80 |> Enum.map(make_integer)
81
82 {:RSAPublicKey, modulus, exponent}
83 end
84
85 def encode_key({:RSAPublicKey, modulus, exponent}) do
86 modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64()
87 exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64()
88
89 "RSA.#{modulus_enc}.#{exponent_enc}"
90 end
91
92 def encode(private_key, doc) do
93 type = "application/atom+xml"
94 encoding = "base64url"
95 alg = "RSA-SHA256"
96
97 signed_text =
98 [doc, type, encoding, alg]
99 |> Enum.map(&Base.url_encode64/1)
100 |> Enum.join(".")
101
102 signature =
103 signed_text
104 |> :public_key.sign(:sha256, private_key)
105 |> to_string
106 |> Base.url_encode64()
107
108 doc_base64 =
109 doc
110 |> Base.url_encode64()
111
112 # Don't need proper xml building, these strings are safe to leave unescaped
113 salmon = """
114 <?xml version="1.0" encoding="UTF-8"?>
115 <me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
116 <me:data type="application/atom+xml">#{doc_base64}</me:data>
117 <me:encoding>#{encoding}</me:encoding>
118 <me:alg>#{alg}</me:alg>
119 <me:sig>#{signature}</me:sig>
120 </me:env>
121 """
122
123 {:ok, salmon}
124 end
125
126 def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
127 cc = Map.get(data, "cc", [])
128
129 bcc =
130 data
131 |> Map.get("bcc", [])
132 |> Enum.reduce([], fn ap_id, bcc ->
133 case Pleroma.List.get_by_ap_id(ap_id) do
134 %Pleroma.List{user_id: ^user_id} = list ->
135 {:ok, following} = Pleroma.List.get_following(list)
136 bcc ++ Enum.map(following, & &1.ap_id)
137
138 _ ->
139 bcc
140 end
141 end)
142
143 [to, cc, bcc]
144 |> Enum.concat()
145 |> Enum.map(&User.get_cached_by_ap_id/1)
146 |> Enum.filter(fn user -> user && !user.local end)
147 end
148
149 @doc "Pushes an activity to remote account."
150 def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
151 do: publish_one(Map.put(params, :recipient, salmon))
152
153 def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
154 with {:ok, %{status: code}} when code in 200..299 <-
155 HTTP.post(
156 url,
157 feed,
158 [{"Content-Type", "application/magic-envelope+xml"}]
159 ) do
160 if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
161 do: Instances.set_reachable(url)
162
163 Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
164 {:ok, code}
165 else
166 e ->
167 unless params[:unreachable_since], do: Instances.set_reachable(url)
168 Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
169 {:error, "Unreachable instance"}
170 end
171 end
172
173 def publish_one(%{recipient_id: recipient_id} = params) do
174 recipient = User.get_cached_by_id(recipient_id)
175
176 params
177 |> Map.delete(:recipient_id)
178 |> Map.put(:recipient, recipient)
179 |> publish_one()
180 end
181
182 def publish_one(_), do: :noop
183
184 @supported_activities [
185 "Create",
186 "Follow",
187 "Like",
188 "Announce",
189 "Undo",
190 "Delete"
191 ]
192
193 def is_representable?(%Activity{data: %{"type" => type}} = activity)
194 when type in @supported_activities,
195 do: Visibility.is_public?(activity)
196
197 def is_representable?(_), do: false
198
199 @doc """
200 Publishes an activity to remote accounts
201 """
202 @spec publish(User.t(), Pleroma.Activity.t()) :: none
203 def publish(user, activity)
204
205 def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity)
206 when type in @supported_activities do
207 feed = ActivityRepresenter.to_simple_form(activity, user, true)
208
209 if feed do
210 feed =
211 ActivityRepresenter.wrap_with_entry(feed)
212 |> :xmerl.export_simple(:xmerl_xml)
213 |> to_string
214
215 {:ok, private, _} = Keys.keys_from_pem(keys)
216 {:ok, feed} = encode(private, feed)
217
218 remote_users = remote_users(user, activity)
219
220 salmon_urls = Enum.map(remote_users, & &1.info.salmon)
221 reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
222 reachable_urls = Map.keys(reachable_urls_metadata)
223
224 remote_users
225 |> Enum.filter(&(&1.info.salmon in reachable_urls))
226 |> Enum.each(fn remote_user ->
227 Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
228
229 Publisher.enqueue_one(__MODULE__, %{
230 recipient_id: remote_user.id,
231 feed: feed,
232 unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
233 })
234 end)
235 end
236 end
237
238 def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
239
240 def gather_webfinger_links(%User{} = user) do
241 {:ok, _private, public} = Keys.keys_from_pem(user.info.keys)
242 magic_key = encode_key(public)
243
244 [
245 %{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
246 %{
247 "rel" => "magic-public-key",
248 "href" => "data:application/magic-public-key,#{magic_key}"
249 }
250 ]
251 end
252
253 def gather_nodeinfo_protocol_names, do: []
254 end