Merge branch 'develop' into feature/matstodon-statuses-by-name
[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(_), do: :noop
174
175 @supported_activities [
176 "Create",
177 "Follow",
178 "Like",
179 "Announce",
180 "Undo",
181 "Delete"
182 ]
183
184 def is_representable?(%Activity{data: %{"type" => type}} = activity)
185 when type in @supported_activities,
186 do: Visibility.is_public?(activity)
187
188 def is_representable?(_), do: false
189
190 @doc """
191 Publishes an activity to remote accounts
192 """
193 @spec publish(User.t(), Pleroma.Activity.t()) :: none
194 def publish(user, activity)
195
196 def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity)
197 when type in @supported_activities do
198 feed = ActivityRepresenter.to_simple_form(activity, user, true)
199
200 if feed do
201 feed =
202 ActivityRepresenter.wrap_with_entry(feed)
203 |> :xmerl.export_simple(:xmerl_xml)
204 |> to_string
205
206 {:ok, private, _} = Keys.keys_from_pem(keys)
207 {:ok, feed} = encode(private, feed)
208
209 remote_users = remote_users(user, activity)
210
211 salmon_urls = Enum.map(remote_users, & &1.info.salmon)
212 reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
213 reachable_urls = Map.keys(reachable_urls_metadata)
214
215 remote_users
216 |> Enum.filter(&(&1.info.salmon in reachable_urls))
217 |> Enum.each(fn remote_user ->
218 Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
219
220 Publisher.enqueue_one(__MODULE__, %{
221 recipient: remote_user,
222 feed: feed,
223 unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
224 })
225 end)
226 end
227 end
228
229 def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
230
231 def gather_webfinger_links(%User{} = user) do
232 {:ok, _private, public} = Keys.keys_from_pem(user.info.keys)
233 magic_key = encode_key(public)
234
235 [
236 %{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
237 %{
238 "rel" => "magic-public-key",
239 "href" => "data:application/magic-public-key,#{magic_key}"
240 }
241 ]
242 end
243
244 def gather_nodeinfo_protocol_names, do: []
245 end