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